jquery.seat-charts.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. /*!
  2. * jQuery-Seat-Charts v1.1.5
  3. * https://github.com/mateuszmarkowski/jQuery-Seat-Charts
  4. *
  5. * Copyright 2013, 2016 Mateusz Markowski
  6. * Released under the MIT license
  7. */
  8. (function($) {
  9. //'use strict';
  10. $.fn.seatCharts = function (setup) {
  11. //if there's seatCharts object associated with the current element, return it
  12. if (this.data('seatCharts')) {
  13. return this.data('seatCharts');
  14. }
  15. var fn = this,
  16. seats = {},
  17. seatIds = [],
  18. legend,
  19. settings = {
  20. animate : false, //requires jQuery UI
  21. naming : {
  22. top : true,
  23. left : true,
  24. getId : function(character, row, column) {
  25. return row + '_' + column;
  26. },
  27. getLabel : function (character, row, column) {
  28. return column;
  29. }
  30. },
  31. legend : {
  32. node : null,
  33. items : []
  34. },
  35. click : function() {
  36. if (this.status() == 'available') {
  37. return 'selected';
  38. } else if (this.status() == 'selected') {
  39. return 'available';
  40. } else {
  41. return this.style();
  42. }
  43. },
  44. focus : function() {
  45. if (this.status() == 'available') {
  46. return 'focused';
  47. } else {
  48. return this.style();
  49. }
  50. },
  51. blur : function() {
  52. return this.status();
  53. },
  54. seats : {}
  55. },
  56. //seat will be basically a seat object which we'll when generating the map
  57. seat = (function(seatCharts, seatChartsSettings) {
  58. return function (setup) {
  59. var fn = this;
  60. fn.settings = $.extend({
  61. status : 'available', //available, unavailable, selected
  62. style : 'available',
  63. //make sure there's an empty hash if user doesn't pass anything
  64. data : seatChartsSettings.seats[setup.character] || {}
  65. //anything goes here?
  66. }, setup);
  67. fn.settings.$node = $('<div></div>');
  68. fn.settings.$node
  69. .attr({
  70. id : fn.settings.id,
  71. role : 'checkbox',
  72. 'aria-checked' : false,
  73. focusable : true,
  74. tabIndex : -1 //manual focus
  75. })
  76. .text(fn.settings.label)
  77. .addClass(['seatCharts-seat', 'seatCharts-cell', 'available'].concat(
  78. //let's merge custom user defined classes with standard JSC ones
  79. fn.settings.classes,
  80. typeof seatChartsSettings.seats[fn.settings.character] == "undefined" ?
  81. [] : seatChartsSettings.seats[fn.settings.character].classes
  82. ).join(' '));
  83. //basically a wrapper function
  84. fn.data = function() {
  85. return fn.settings.data;
  86. };
  87. fn.char = function() {
  88. return fn.settings.character;
  89. };
  90. fn.node = function() {
  91. return fn.settings.$node;
  92. };
  93. /*
  94. * Can either set or return status depending on arguments.
  95. *
  96. * If there's no argument, it will return the current style.
  97. *
  98. * If you pass an argument, it will update seat's style
  99. */
  100. fn.style = function() {
  101. return arguments.length == 1 ?
  102. (function(newStyle) {
  103. var oldStyle = fn.settings.style;
  104. //if nothing changes, do nothing
  105. if (newStyle == oldStyle) {
  106. return oldStyle;
  107. }
  108. //focused is a special style which is not associated with status
  109. fn.settings.status = newStyle != 'focused' ? newStyle : fn.settings.status;
  110. fn.settings.$node
  111. .attr('aria-checked', newStyle == 'selected');
  112. //if user wants to animate status changes, let him do this
  113. seatChartsSettings.animate ?
  114. fn.settings.$node.switchClass(oldStyle, newStyle, 200) :
  115. fn.settings.$node.removeClass(oldStyle).addClass(newStyle);
  116. return fn.settings.style = newStyle;
  117. })(arguments[0]) : fn.settings.style;
  118. };
  119. //either set or retrieve
  120. fn.status = function() {
  121. return fn.settings.status = arguments.length == 1 ?
  122. fn.style(arguments[0]) : fn.settings.status;
  123. };
  124. //using immediate function to convienietly get shortcut variables
  125. (function(seatSettings, character, seat) {
  126. //attach event handlers
  127. $.each(['click', 'focus', 'blur'], function(index, callback) {
  128. //we want to be able to call the functions for each seat object
  129. fn[callback] = function() {
  130. if (callback == 'focus') {
  131. //if there's already a focused element, we have to remove focus from it first
  132. if (seatCharts.attr('aria-activedescendant') !== undefined) {
  133. seats[seatCharts.attr('aria-activedescendant')].blur();
  134. }
  135. seatCharts.attr('aria-activedescendant', seat.settings.id);
  136. seat.node().focus();
  137. }
  138. /*
  139. * User can pass his own callback function, so we have to first check if it exists
  140. * and if not, use our default callback.
  141. *
  142. * Each callback function is executed in the current seat context.
  143. */
  144. return fn.style(typeof seatSettings[character][callback] === 'function' ?
  145. seatSettings[character][callback].apply(seat) : seatChartsSettings[callback].apply(seat));
  146. };
  147. });
  148. //the below will become seatSettings, character, seat thanks to the immediate function
  149. })(seatChartsSettings.seats, fn.settings.character, fn);
  150. fn.node()
  151. //the first three mouse events are simple
  152. .on('click', fn.click)
  153. .on('mouseenter', fn.focus)
  154. .on('mouseleave', fn.blur)
  155. //keydown requires quite a lot of logic, because we have to know where to move the focus
  156. .on('keydown', (function(seat, $seat) {
  157. return function (e) {
  158. var $newSeat;
  159. //everything depends on the pressed key
  160. switch (e.which) {
  161. //spacebar will just trigger the same event mouse click does
  162. case 32:
  163. e.preventDefault();
  164. seat.click();
  165. break;
  166. //UP & DOWN
  167. case 40:
  168. case 38:
  169. e.preventDefault();
  170. /*
  171. * This is a recursive, immediate function which searches for the first "focusable" row.
  172. *
  173. * We're using immediate function because we want a convenient access to some DOM elements
  174. * We're using recursion because sometimes we may hit an empty space rather than a seat.
  175. *
  176. */
  177. $newSeat = (function findAvailable($rows, $seats, $currentRow) {
  178. var $newRow;
  179. //let's determine which row should we move to
  180. if (!$rows.index($currentRow) && e.which == 38) {
  181. //if this is the first row and user has pressed up arrow, move to the last row
  182. $newRow = $rows.last();
  183. } else if ($rows.index($currentRow) == $rows.length-1 && e.which == 40) {
  184. //if this is the last row and user has pressed down arrow, move to the first row
  185. $newRow = $rows.first();
  186. } else {
  187. //using eq to get an element at the desired index position
  188. $newRow = $rows.eq(
  189. //if up arrow, then decrement the index, if down increment it
  190. $rows.index($currentRow) + (e.which == 38 ? (-1) : (+1))
  191. );
  192. }
  193. //now that we know the row, let's get the seat using the current column position
  194. $newSeat = $newRow.find('.seatCharts-seat,.seatCharts-space').eq($seats.index($seat));
  195. //if the seat we found is a space, keep looking further
  196. return $newSeat.hasClass('seatCharts-space') ?
  197. findAvailable($rows, $seats, $newRow) : $newSeat;
  198. })($seat
  199. //get a reference to the parent container and then select all rows but the header
  200. .parents('.seatCharts-container')
  201. .find('.seatCharts-row:not(.seatCharts-header)'),
  202. $seat
  203. //get a reference to the parent row and then find all seat cells (both seats & spaces)
  204. .parents('.seatCharts-row:first')
  205. .find('.seatCharts-seat,.seatCharts-space'),
  206. //get a reference to the current row
  207. $seat.parents('.seatCharts-row:not(.seatCharts-header)')
  208. );
  209. //we couldn't determine the new seat, so we better give up
  210. if (!$newSeat.length) {
  211. return;
  212. }
  213. //remove focus from the old seat and put it on the new one
  214. seat.blur();
  215. seats[$newSeat.attr('id')].focus();
  216. $newSeat.focus();
  217. //update our "aria" reference with the new seat id
  218. seatCharts.attr('aria-activedescendant', $newSeat.attr('id'));
  219. break;
  220. //LEFT & RIGHT
  221. case 37:
  222. case 39:
  223. e.preventDefault();
  224. /*
  225. * The logic here is slightly different from the one for up/down arrows.
  226. * User will be able to browse the whole map using just left/right arrow, because
  227. * it will move to the next row when we reach the right/left-most seat.
  228. */
  229. $newSeat = (function($seats) {
  230. if (!$seats.index($seat) && e.which == 37) {
  231. //user has pressed left arrow and we're currently on the left-most seat
  232. return $seats.last();
  233. } else if ($seats.index($seat) == $seats.length -1 && e.which == 39) {
  234. //user has pressed right arrow and we're currently on the right-most seat
  235. return $seats.first();
  236. } else {
  237. //simply move one seat left or right depending on the key
  238. return $seats.eq($seats.index($seat) + (e.which == 37 ? (-1) : (+1)));
  239. }
  240. })($seat
  241. .parents('.seatCharts-container:first')
  242. .find('.seatCharts-seat:not(.seatCharts-space)'));
  243. if (!$newSeat.length) {
  244. return;
  245. }
  246. //handle focus
  247. seat.blur();
  248. seats[$newSeat.attr('id')].focus();
  249. $newSeat.focus();
  250. //update our "aria" reference with the new seat id
  251. seatCharts.attr('aria-activedescendant', $newSeat.attr('id'));
  252. break;
  253. default:
  254. break;
  255. }
  256. };
  257. })(fn, fn.node()));
  258. //.appendTo(seatCharts.find('.' + row));
  259. }
  260. })(fn, settings);
  261. fn.addClass('seatCharts-container');
  262. //true -> deep copy!
  263. $.extend(true, settings, setup);
  264. //Generate default row ids unless user passed his own
  265. settings.naming.rows = settings.naming.rows || (function(length) {
  266. var rows = [];
  267. for (var i = 1; i <= length; i++) {
  268. rows.push(i);
  269. }
  270. return rows;
  271. })(settings.map.length);
  272. //Generate default column ids unless user passed his own
  273. settings.naming.columns = settings.naming.columns || (function(length) {
  274. var columns = [];
  275. for (var i = 1; i <= length; i++) {
  276. columns.push(i);
  277. }
  278. return columns;
  279. })(settings.map[0].split('').length);
  280. if (settings.naming.top) {
  281. var $headerRow = $('<div></div>')
  282. .addClass('seatCharts-row seatCharts-header');
  283. if (settings.naming.left) {
  284. $headerRow.append($('<div></div>').addClass('seatCharts-cell'));
  285. }
  286. $.each(settings.naming.columns, function(index, value) {
  287. $headerRow.append(
  288. $('<div></div>')
  289. .addClass('seatCharts-cell')
  290. .text(value)
  291. );
  292. });
  293. }
  294. fn.append($headerRow);
  295. //do this for each map row
  296. $.each(settings.map, function(row, characters) {
  297. var $row = $('<div></div>').addClass('seatCharts-row');
  298. if (settings.naming.left) {
  299. $row.append(
  300. $('<div></div>')
  301. .addClass('seatCharts-cell seatCharts-space')
  302. .text(settings.naming.rows[row])
  303. );
  304. }
  305. /*
  306. * Do this for each seat (letter)
  307. *
  308. * Now users will be able to pass custom ID and label which overwrite the one that seat would be assigned by getId and
  309. * getLabel
  310. *
  311. * New format is like this:
  312. * a[ID,label]a[ID]aaaaa
  313. *
  314. * So you can overwrite the ID or label (or both) even for just one seat.
  315. * Basically ID should be first, so if you want to overwrite just label write it as follows:
  316. * a[,LABEL]
  317. *
  318. * Allowed characters in IDs areL 0-9, a-z, A-Z, _
  319. * Allowed characters in labels are: 0-9, a-z, A-Z, _, ' ' (space)
  320. *
  321. */
  322. $.each(characters.match(/[a-z_]{1}(\[[0-9a-z_]{0,}(,[0-9a-z_ ]+)?\])?/gi), function (column, characterParams) {
  323. var matches = characterParams.match(/([a-z_]{1})(\[([0-9a-z_ ,]+)\])?/i),
  324. //no matter if user specifies [] params, the character should be in the second element
  325. character = matches[1],
  326. //check if user has passed some additional params to override id or label
  327. params = typeof matches[3] !== 'undefined' ? matches[3].split(',') : [],
  328. //id param should be first
  329. overrideId = params.length ? params[0] : null,
  330. //label param should be second
  331. overrideLabel = params.length === 2 ? params[1] : null;
  332. $row.append(character != '_' ?
  333. //if the character is not an underscore (empty space)
  334. (function(naming) {
  335. //so users don't have to specify empty objects
  336. settings.seats[character] = character in settings.seats ? settings.seats[character] : {};
  337. var id = overrideId ? overrideId : naming.getId(character, naming.rows[row], naming.columns[column]);
  338. seats[id] = new seat({
  339. id : id,
  340. label : overrideLabel ?
  341. overrideLabel : naming.getLabel(character, naming.rows[row], naming.columns[column]),
  342. row : row,
  343. column : column,
  344. character : character
  345. });
  346. seatIds.push(id);
  347. return seats[id].node();
  348. })(settings.naming) :
  349. //this is just an empty space (_)
  350. $('<div></div>').addClass('seatCharts-cell seatCharts-space')
  351. );
  352. });
  353. fn.append($row);
  354. });
  355. //if there're any legend items to be rendered
  356. settings.legend.items.length ? (function(legend) {
  357. //either use user-defined container or create our own and insert it right after the seat chart div
  358. var $container = (legend.node || $('<div></div>').insertAfter(fn))
  359. .addClass('seatCharts-legend');
  360. var $ul = $('<ul></ul>')
  361. .addClass('seatCharts-legendList')
  362. .appendTo($container);
  363. $.each(legend.items, function(index, item) {
  364. $ul.append(
  365. $('<li></li>')
  366. .addClass('seatCharts-legendItem')
  367. .append(
  368. $('<div></div>')
  369. //merge user defined classes with our standard ones
  370. .addClass(['seatCharts-seat', 'seatCharts-cell', item[1]].concat(
  371. settings.classes,
  372. typeof settings.seats[item[0]] == "undefined" ? [] : settings.seats[item[0]].classes).join(' ')
  373. )
  374. )
  375. .append(
  376. $('<span></span>')
  377. .addClass('seatCharts-legendDescription')
  378. .text(item[2])
  379. )
  380. );
  381. });
  382. return $container;
  383. })(settings.legend) : null;
  384. fn.attr({
  385. tabIndex : 0
  386. });
  387. //when container's focused, move focus to the first seat
  388. fn.focus(function() {
  389. if (fn.attr('aria-activedescendant')) {
  390. seats[fn.attr('aria-activedescendant')].blur();
  391. }
  392. fn.find('.seatCharts-seat:not(.seatCharts-space):first').focus();
  393. seats[seatIds[0]].focus();
  394. });
  395. //public methods of seatCharts
  396. fn.data('seatCharts', {
  397. seats : seats,
  398. seatIds : seatIds,
  399. //set for one, set for many, get for one
  400. status: function() {
  401. var fn = this;
  402. return arguments.length == 1 ? fn.seats[arguments[0]].status() : (function(seatsIds, newStatus) {
  403. return typeof seatsIds == 'string' ? fn.seats[seatsIds].status(newStatus) : (function() {
  404. $.each(seatsIds, function(index, seatId) {
  405. fn.seats[seatId].status(newStatus);
  406. });
  407. })();
  408. })(arguments[0], arguments[1]);
  409. },
  410. each : function(callback) {
  411. var fn = this;
  412. for (var seatId in fn.seats) {
  413. if (false === callback.call(fn.seats[seatId], seatId)) {
  414. return seatId;//return last checked
  415. }
  416. }
  417. return true;
  418. },
  419. node : function() {
  420. var fn = this;
  421. //basically create a CSS query to get all seats by their DOM ids
  422. return $('#' + fn.seatIds.join(',#'));
  423. },
  424. find : function(query) {//D, a.available, unavailable
  425. var fn = this;
  426. var seatSet = fn.set();
  427. //is RegExp
  428. return query instanceof RegExp ?
  429. (function () {
  430. fn.each(function (id) {
  431. if (id.match(query)) {
  432. seatSet.push(id, this);
  433. }
  434. });
  435. return seatSet;
  436. })() :
  437. (query.length == 1 ?
  438. (function (character) {
  439. //user searches just for a particual character
  440. fn.each(function () {
  441. if (this.char() == character) {
  442. seatSet.push(this.settings.id, this);
  443. }
  444. });
  445. return seatSet;
  446. })(query) :
  447. (function () {
  448. //user runs a more sophisticated query, so let's see if there's a dot
  449. return query.indexOf('.') > -1 ?
  450. (function () {
  451. //there's a dot which separates character and the status
  452. var parts = query.split('.');
  453. fn.each(function (seatId) {
  454. if (this.char() == parts[0] && this.status() == parts[1]) {
  455. seatSet.push(this.settings.id, this);
  456. }
  457. });
  458. return seatSet;
  459. })() :
  460. (function () {
  461. fn.each(function () {
  462. if (this.status() == query) {
  463. seatSet.push(this.settings.id, this);
  464. }
  465. });
  466. return seatSet;
  467. })();
  468. })()
  469. );
  470. },
  471. set : function set() {//inherits some methods
  472. var fn = this;
  473. return {
  474. seats : [],
  475. seatIds : [],
  476. length : 0,
  477. status : function() {
  478. var args = arguments,
  479. that = this;
  480. //if there's just one seat in the set and user didn't pass any params, return current status
  481. return this.length == 1 && args.length == 0 ? this.seats[0].status() : (function() {
  482. //otherwise call status function for each of the seats in the set
  483. $.each(that.seats, function() {
  484. this.status.apply(this, args);
  485. });
  486. })();
  487. },
  488. node : function() {
  489. return fn.node.call(this);
  490. },
  491. each : function() {
  492. return fn.each.call(this, arguments[0]);
  493. },
  494. get : function() {
  495. return fn.get.call(this, arguments[0]);
  496. },
  497. find : function() {
  498. return fn.find.call(this, arguments[0]);
  499. },
  500. set : function() {
  501. return set.call(fn);
  502. },
  503. push : function(id, seat) {
  504. this.seats.push(seat);
  505. this.seatIds.push(id);
  506. ++this.length;
  507. }
  508. };
  509. },
  510. //get one object or a set of objects
  511. get : function(seatsIds) {
  512. var fn = this;
  513. return typeof seatsIds == 'string' ?
  514. fn.seats[seatsIds] : (function() {
  515. var seatSet = fn.set();
  516. $.each(seatsIds, function(index, seatId) {
  517. if (typeof fn.seats[seatId] === 'object') {
  518. seatSet.push(seatId, fn.seats[seatId]);
  519. }
  520. });
  521. return seatSet;
  522. })();
  523. }
  524. });
  525. return fn.data('seatCharts');
  526. }
  527. })(jQuery);