/*! * jQuery-Seat-Charts v1.1.5 * https://github.com/mateuszmarkowski/jQuery-Seat-Charts * * Copyright 2013, 2016 Mateusz Markowski * Released under the MIT license */ (function($) { //'use strict'; $.fn.seatCharts = function (setup) { //if there's seatCharts object associated with the current element, return it if (this.data('seatCharts')) { return this.data('seatCharts'); } var fn = this, seats = {}, seatIds = [], legend, settings = { animate : false, //requires jQuery UI naming : { top : true, left : true, getId : function(character, row, column) { return row + '_' + column; }, getLabel : function (character, row, column) { return column; } }, legend : { node : null, items : [] }, click : function() { if (this.status() == 'available') { return 'selected'; } else if (this.status() == 'selected') { return 'available'; } else { return this.style(); } }, focus : function() { if (this.status() == 'available') { return 'focused'; } else { return this.style(); } }, blur : function() { return this.status(); }, seats : {} }, //seat will be basically a seat object which we'll when generating the map seat = (function(seatCharts, seatChartsSettings) { return function (setup) { var fn = this; fn.settings = $.extend({ status : 'available', //available, unavailable, selected style : 'available', //make sure there's an empty hash if user doesn't pass anything data : seatChartsSettings.seats[setup.character] || {} //anything goes here? }, setup); fn.settings.$node = $('
'); fn.settings.$node .attr({ id : fn.settings.id, role : 'checkbox', 'aria-checked' : false, focusable : true, tabIndex : -1 //manual focus }) .text(fn.settings.label) .addClass(['seatCharts-seat', 'seatCharts-cell', 'available'].concat( //let's merge custom user defined classes with standard JSC ones fn.settings.classes, typeof seatChartsSettings.seats[fn.settings.character] == "undefined" ? [] : seatChartsSettings.seats[fn.settings.character].classes ).join(' ')); //basically a wrapper function fn.data = function() { return fn.settings.data; }; fn.char = function() { return fn.settings.character; }; fn.node = function() { return fn.settings.$node; }; /* * Can either set or return status depending on arguments. * * If there's no argument, it will return the current style. * * If you pass an argument, it will update seat's style */ fn.style = function() { return arguments.length == 1 ? (function(newStyle) { var oldStyle = fn.settings.style; //if nothing changes, do nothing if (newStyle == oldStyle) { return oldStyle; } //focused is a special style which is not associated with status fn.settings.status = newStyle != 'focused' ? newStyle : fn.settings.status; fn.settings.$node .attr('aria-checked', newStyle == 'selected'); //if user wants to animate status changes, let him do this seatChartsSettings.animate ? fn.settings.$node.switchClass(oldStyle, newStyle, 200) : fn.settings.$node.removeClass(oldStyle).addClass(newStyle); return fn.settings.style = newStyle; })(arguments[0]) : fn.settings.style; }; //either set or retrieve fn.status = function() { return fn.settings.status = arguments.length == 1 ? fn.style(arguments[0]) : fn.settings.status; }; //using immediate function to convienietly get shortcut variables (function(seatSettings, character, seat) { //attach event handlers $.each(['click', 'focus', 'blur'], function(index, callback) { //we want to be able to call the functions for each seat object fn[callback] = function() { if (callback == 'focus') { //if there's already a focused element, we have to remove focus from it first if (seatCharts.attr('aria-activedescendant') !== undefined) { seats[seatCharts.attr('aria-activedescendant')].blur(); } seatCharts.attr('aria-activedescendant', seat.settings.id); seat.node().focus(); } /* * User can pass his own callback function, so we have to first check if it exists * and if not, use our default callback. * * Each callback function is executed in the current seat context. */ return fn.style(typeof seatSettings[character][callback] === 'function' ? seatSettings[character][callback].apply(seat) : seatChartsSettings[callback].apply(seat)); }; }); //the below will become seatSettings, character, seat thanks to the immediate function })(seatChartsSettings.seats, fn.settings.character, fn); fn.node() //the first three mouse events are simple .on('click', fn.click) .on('mouseenter', fn.focus) .on('mouseleave', fn.blur) //keydown requires quite a lot of logic, because we have to know where to move the focus .on('keydown', (function(seat, $seat) { return function (e) { var $newSeat; //everything depends on the pressed key switch (e.which) { //spacebar will just trigger the same event mouse click does case 32: e.preventDefault(); seat.click(); break; //UP & DOWN case 40: case 38: e.preventDefault(); /* * This is a recursive, immediate function which searches for the first "focusable" row. * * We're using immediate function because we want a convenient access to some DOM elements * We're using recursion because sometimes we may hit an empty space rather than a seat. * */ $newSeat = (function findAvailable($rows, $seats, $currentRow) { var $newRow; //let's determine which row should we move to if (!$rows.index($currentRow) && e.which == 38) { //if this is the first row and user has pressed up arrow, move to the last row $newRow = $rows.last(); } else if ($rows.index($currentRow) == $rows.length-1 && e.which == 40) { //if this is the last row and user has pressed down arrow, move to the first row $newRow = $rows.first(); } else { //using eq to get an element at the desired index position $newRow = $rows.eq( //if up arrow, then decrement the index, if down increment it $rows.index($currentRow) + (e.which == 38 ? (-1) : (+1)) ); } //now that we know the row, let's get the seat using the current column position $newSeat = $newRow.find('.seatCharts-seat,.seatCharts-space').eq($seats.index($seat)); //if the seat we found is a space, keep looking further return $newSeat.hasClass('seatCharts-space') ? findAvailable($rows, $seats, $newRow) : $newSeat; })($seat //get a reference to the parent container and then select all rows but the header .parents('.seatCharts-container') .find('.seatCharts-row:not(.seatCharts-header)'), $seat //get a reference to the parent row and then find all seat cells (both seats & spaces) .parents('.seatCharts-row:first') .find('.seatCharts-seat,.seatCharts-space'), //get a reference to the current row $seat.parents('.seatCharts-row:not(.seatCharts-header)') ); //we couldn't determine the new seat, so we better give up if (!$newSeat.length) { return; } //remove focus from the old seat and put it on the new one seat.blur(); seats[$newSeat.attr('id')].focus(); $newSeat.focus(); //update our "aria" reference with the new seat id seatCharts.attr('aria-activedescendant', $newSeat.attr('id')); break; //LEFT & RIGHT case 37: case 39: e.preventDefault(); /* * The logic here is slightly different from the one for up/down arrows. * User will be able to browse the whole map using just left/right arrow, because * it will move to the next row when we reach the right/left-most seat. */ $newSeat = (function($seats) { if (!$seats.index($seat) && e.which == 37) { //user has pressed left arrow and we're currently on the left-most seat return $seats.last(); } else if ($seats.index($seat) == $seats.length -1 && e.which == 39) { //user has pressed right arrow and we're currently on the right-most seat return $seats.first(); } else { //simply move one seat left or right depending on the key return $seats.eq($seats.index($seat) + (e.which == 37 ? (-1) : (+1))); } })($seat .parents('.seatCharts-container:first') .find('.seatCharts-seat:not(.seatCharts-space)')); if (!$newSeat.length) { return; } //handle focus seat.blur(); seats[$newSeat.attr('id')].focus(); $newSeat.focus(); //update our "aria" reference with the new seat id seatCharts.attr('aria-activedescendant', $newSeat.attr('id')); break; default: break; } }; })(fn, fn.node())); //.appendTo(seatCharts.find('.' + row)); } })(fn, settings); fn.addClass('seatCharts-container'); //true -> deep copy! $.extend(true, settings, setup); //Generate default row ids unless user passed his own settings.naming.rows = settings.naming.rows || (function(length) { var rows = []; for (var i = 1; i <= length; i++) { rows.push(i); } return rows; })(settings.map.length); //Generate default column ids unless user passed his own settings.naming.columns = settings.naming.columns || (function(length) { var columns = []; for (var i = 1; i <= length; i++) { columns.push(i); } return columns; })(settings.map[0].split('').length); if (settings.naming.top) { var $headerRow = $('
') .addClass('seatCharts-row seatCharts-header'); if (settings.naming.left) { $headerRow.append($('
').addClass('seatCharts-cell')); } $.each(settings.naming.columns, function(index, value) { $headerRow.append( $('
') .addClass('seatCharts-cell') .text(value) ); }); } fn.append($headerRow); //do this for each map row $.each(settings.map, function(row, characters) { var $row = $('
').addClass('seatCharts-row'); if (settings.naming.left) { $row.append( $('
') .addClass('seatCharts-cell seatCharts-space') .text(settings.naming.rows[row]) ); } /* * Do this for each seat (letter) * * Now users will be able to pass custom ID and label which overwrite the one that seat would be assigned by getId and * getLabel * * New format is like this: * a[ID,label]a[ID]aaaaa * * So you can overwrite the ID or label (or both) even for just one seat. * Basically ID should be first, so if you want to overwrite just label write it as follows: * a[,LABEL] * * Allowed characters in IDs areL 0-9, a-z, A-Z, _ * Allowed characters in labels are: 0-9, a-z, A-Z, _, ' ' (space) * */ $.each(characters.match(/[a-z_]{1}(\[[0-9a-z_]{0,}(,[0-9a-z_ ]+)?\])?/gi), function (column, characterParams) { var matches = characterParams.match(/([a-z_]{1})(\[([0-9a-z_ ,]+)\])?/i), //no matter if user specifies [] params, the character should be in the second element character = matches[1], //check if user has passed some additional params to override id or label params = typeof matches[3] !== 'undefined' ? matches[3].split(',') : [], //id param should be first overrideId = params.length ? params[0] : null, //label param should be second overrideLabel = params.length === 2 ? params[1] : null; $row.append(character != '_' ? //if the character is not an underscore (empty space) (function(naming) { //so users don't have to specify empty objects settings.seats[character] = character in settings.seats ? settings.seats[character] : {}; var id = overrideId ? overrideId : naming.getId(character, naming.rows[row], naming.columns[column]); seats[id] = new seat({ id : id, label : overrideLabel ? overrideLabel : naming.getLabel(character, naming.rows[row], naming.columns[column]), row : row, column : column, character : character }); seatIds.push(id); return seats[id].node(); })(settings.naming) : //this is just an empty space (_) $('
').addClass('seatCharts-cell seatCharts-space') ); }); fn.append($row); }); //if there're any legend items to be rendered settings.legend.items.length ? (function(legend) { //either use user-defined container or create our own and insert it right after the seat chart div var $container = (legend.node || $('
').insertAfter(fn)) .addClass('seatCharts-legend'); var $ul = $('') .addClass('seatCharts-legendList') .appendTo($container); $.each(legend.items, function(index, item) { $ul.append( $('
  • ') .addClass('seatCharts-legendItem') .append( $('
    ') //merge user defined classes with our standard ones .addClass(['seatCharts-seat', 'seatCharts-cell', item[1]].concat( settings.classes, typeof settings.seats[item[0]] == "undefined" ? [] : settings.seats[item[0]].classes).join(' ') ) ) .append( $('') .addClass('seatCharts-legendDescription') .text(item[2]) ) ); }); return $container; })(settings.legend) : null; fn.attr({ tabIndex : 0 }); //when container's focused, move focus to the first seat fn.focus(function() { if (fn.attr('aria-activedescendant')) { seats[fn.attr('aria-activedescendant')].blur(); } fn.find('.seatCharts-seat:not(.seatCharts-space):first').focus(); seats[seatIds[0]].focus(); }); //public methods of seatCharts fn.data('seatCharts', { seats : seats, seatIds : seatIds, //set for one, set for many, get for one status: function() { var fn = this; return arguments.length == 1 ? fn.seats[arguments[0]].status() : (function(seatsIds, newStatus) { return typeof seatsIds == 'string' ? fn.seats[seatsIds].status(newStatus) : (function() { $.each(seatsIds, function(index, seatId) { fn.seats[seatId].status(newStatus); }); })(); })(arguments[0], arguments[1]); }, each : function(callback) { var fn = this; for (var seatId in fn.seats) { if (false === callback.call(fn.seats[seatId], seatId)) { return seatId;//return last checked } } return true; }, node : function() { var fn = this; //basically create a CSS query to get all seats by their DOM ids return $('#' + fn.seatIds.join(',#')); }, find : function(query) {//D, a.available, unavailable var fn = this; var seatSet = fn.set(); //is RegExp return query instanceof RegExp ? (function () { fn.each(function (id) { if (id.match(query)) { seatSet.push(id, this); } }); return seatSet; })() : (query.length == 1 ? (function (character) { //user searches just for a particual character fn.each(function () { if (this.char() == character) { seatSet.push(this.settings.id, this); } }); return seatSet; })(query) : (function () { //user runs a more sophisticated query, so let's see if there's a dot return query.indexOf('.') > -1 ? (function () { //there's a dot which separates character and the status var parts = query.split('.'); fn.each(function (seatId) { if (this.char() == parts[0] && this.status() == parts[1]) { seatSet.push(this.settings.id, this); } }); return seatSet; })() : (function () { fn.each(function () { if (this.status() == query) { seatSet.push(this.settings.id, this); } }); return seatSet; })(); })() ); }, set : function set() {//inherits some methods var fn = this; return { seats : [], seatIds : [], length : 0, status : function() { var args = arguments, that = this; //if there's just one seat in the set and user didn't pass any params, return current status return this.length == 1 && args.length == 0 ? this.seats[0].status() : (function() { //otherwise call status function for each of the seats in the set $.each(that.seats, function() { this.status.apply(this, args); }); })(); }, node : function() { return fn.node.call(this); }, each : function() { return fn.each.call(this, arguments[0]); }, get : function() { return fn.get.call(this, arguments[0]); }, find : function() { return fn.find.call(this, arguments[0]); }, set : function() { return set.call(fn); }, push : function(id, seat) { this.seats.push(seat); this.seatIds.push(id); ++this.length; } }; }, //get one object or a set of objects get : function(seatsIds) { var fn = this; return typeof seatsIds == 'string' ? fn.seats[seatsIds] : (function() { var seatSet = fn.set(); $.each(seatsIds, function(index, seatId) { if (typeof fn.seats[seatId] === 'object') { seatSet.push(seatId, fn.seats[seatId]); } }); return seatSet; })(); } }); return fn.data('seatCharts'); } })(jQuery);