lity.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. /*! Lity - v2.4.0 - 2019-08-10
  2. * http://sorgalla.com/lity/
  3. * Copyright (c) 2015-2019 Jan Sorgalla; Licensed MIT */
  4. (function(window, factory) {
  5. if (typeof define === 'function' && define.amd) {
  6. define(['jquery'], function($) {
  7. return factory(window, $);
  8. });
  9. } else if (typeof module === 'object' && typeof module.exports === 'object') {
  10. module.exports = factory(window, require('jquery'));
  11. } else {
  12. window.lity = factory(window, window.jQuery || window.Zepto);
  13. }
  14. }(typeof window !== "undefined" ? window : this, function(window, $) {
  15. 'use strict';
  16. var document = window.document;
  17. var _win = $(window);
  18. var _deferred = $.Deferred;
  19. var _html = $('html');
  20. var _instances = [];
  21. var _attrAriaHidden = 'aria-hidden';
  22. var _dataAriaHidden = 'lity-' + _attrAriaHidden;
  23. var _focusableElementsSelector = 'a[href],area[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),button:not([disabled]),iframe,object,embed,[contenteditable],[tabindex]:not([tabindex^="-"])';
  24. var _defaultOptions = {
  25. esc: true,
  26. handler: null,
  27. handlers: {
  28. image: imageHandler,
  29. inline: inlineHandler,
  30. youtube: youtubeHandler,
  31. vimeo: vimeoHandler,
  32. googlemaps: googlemapsHandler,
  33. facebookvideo: facebookvideoHandler,
  34. iframe: iframeHandler
  35. },
  36. template: '<div class="lity" role="dialog" aria-label="Dialog Window (Press escape to close)" tabindex="-1"><div class="lity-wrap" data-lity-close role="document"><div class="lity-loader" aria-hidden="true">Loading...</div><div class="lity-container"><div class="lity-content"></div><button class="lity-close" type="button" aria-label="Close (Press escape to close)" data-lity-close>&times;</button></div></div></div>'
  37. };
  38. var _imageRegexp = /(^data:image\/)|(\.(png|jpe?g|gif|svg|webp|bmp|ico|tiff?)(\?\S*)?$)/i;
  39. var _youtubeRegex = /(youtube(-nocookie)?\.com|youtu\.be)\/(watch\?v=|v\/|u\/|embed\/?)?([\w-]{11})(.*)?/i;
  40. var _vimeoRegex = /(vimeo(pro)?.com)\/(?:[^\d]+)?(\d+)\??(.*)?$/;
  41. var _googlemapsRegex = /((maps|www)\.)?google\.([^\/\?]+)\/?((maps\/?)?\?)(.*)/i;
  42. var _facebookvideoRegex = /(facebook\.com)\/([a-z0-9_-]*)\/videos\/([0-9]*)(.*)?$/i;
  43. var _transitionEndEvent = (function() {
  44. var el = document.createElement('div');
  45. var transEndEventNames = {
  46. WebkitTransition: 'webkitTransitionEnd',
  47. MozTransition: 'transitionend',
  48. OTransition: 'oTransitionEnd otransitionend',
  49. transition: 'transitionend'
  50. };
  51. for (var name in transEndEventNames) {
  52. if (el.style[name] !== undefined) {
  53. return transEndEventNames[name];
  54. }
  55. }
  56. return false;
  57. })();
  58. function transitionEnd(element) {
  59. var deferred = _deferred();
  60. if (!_transitionEndEvent || !element.length) {
  61. deferred.resolve();
  62. } else {
  63. element.one(_transitionEndEvent, deferred.resolve);
  64. setTimeout(deferred.resolve, 500);
  65. }
  66. return deferred.promise();
  67. }
  68. function settings(currSettings, key, value) {
  69. if (arguments.length === 1) {
  70. return $.extend({}, currSettings);
  71. }
  72. if (typeof key === 'string') {
  73. if (typeof value === 'undefined') {
  74. return typeof currSettings[key] === 'undefined'
  75. ? null
  76. : currSettings[key];
  77. }
  78. currSettings[key] = value;
  79. } else {
  80. $.extend(currSettings, key);
  81. }
  82. return this;
  83. }
  84. function parseQueryParams(params) {
  85. var pairs = decodeURI(params.split('#')[0]).split('&');
  86. var obj = {}, p;
  87. for (var i = 0, n = pairs.length; i < n; i++) {
  88. if (!pairs[i]) {
  89. continue;
  90. }
  91. p = pairs[i].split('=');
  92. obj[p[0]] = p[1];
  93. }
  94. return obj;
  95. }
  96. function appendQueryParams(url, params) {
  97. return url + (url.indexOf('?') > -1 ? '&' : '?') + $.param(params);
  98. }
  99. function transferHash(originalUrl, newUrl) {
  100. var pos = originalUrl.indexOf('#');
  101. if (-1 === pos) {
  102. return newUrl;
  103. }
  104. if (pos > 0) {
  105. originalUrl = originalUrl.substr(pos);
  106. }
  107. return newUrl + originalUrl;
  108. }
  109. function error(msg) {
  110. return $('<span class="lity-error"/>').append(msg);
  111. }
  112. function imageHandler(target, instance) {
  113. var desc = (instance.opener() && instance.opener().data('lity-desc')) || 'Image with no description';
  114. var img = $('<img src="' + target + '" alt="' + desc + '"/>');
  115. var deferred = _deferred();
  116. var failed = function() {
  117. deferred.reject(error('Failed loading image'));
  118. };
  119. img
  120. .on('load', function() {
  121. if (this.naturalWidth === 0) {
  122. return failed();
  123. }
  124. deferred.resolve(img);
  125. })
  126. .on('error', failed)
  127. ;
  128. return deferred.promise();
  129. }
  130. imageHandler.test = function(target) {
  131. return _imageRegexp.test(target);
  132. };
  133. function inlineHandler(target, instance) {
  134. var el, placeholder, hasHideClass;
  135. try {
  136. el = $(target);
  137. } catch (e) {
  138. return false;
  139. }
  140. if (!el.length) {
  141. return false;
  142. }
  143. placeholder = $('<i style="display:none !important"/>');
  144. hasHideClass = el.hasClass('lity-hide');
  145. instance
  146. .element()
  147. .one('lity:remove', function() {
  148. placeholder
  149. .before(el)
  150. .remove()
  151. ;
  152. if (hasHideClass && !el.closest('.lity-content').length) {
  153. el.addClass('lity-hide');
  154. }
  155. })
  156. ;
  157. return el
  158. .removeClass('lity-hide')
  159. .after(placeholder)
  160. ;
  161. }
  162. function youtubeHandler(target) {
  163. var matches = _youtubeRegex.exec(target);
  164. if (!matches) {
  165. return false;
  166. }
  167. return iframeHandler(
  168. transferHash(
  169. target,
  170. appendQueryParams(
  171. 'https://www.youtube' + (matches[2] || '') + '.com/embed/' + matches[4],
  172. $.extend(
  173. {
  174. autoplay: 1
  175. },
  176. parseQueryParams(matches[5] || '')
  177. )
  178. )
  179. )
  180. );
  181. }
  182. function vimeoHandler(target) {
  183. var matches = _vimeoRegex.exec(target);
  184. if (!matches) {
  185. return false;
  186. }
  187. return iframeHandler(
  188. transferHash(
  189. target,
  190. appendQueryParams(
  191. 'https://player.vimeo.com/video/' + matches[3],
  192. $.extend(
  193. {
  194. autoplay: 1
  195. },
  196. parseQueryParams(matches[4] || '')
  197. )
  198. )
  199. )
  200. );
  201. }
  202. function facebookvideoHandler(target) {
  203. var matches = _facebookvideoRegex.exec(target);
  204. if (!matches) {
  205. return false;
  206. }
  207. if (0 !== target.indexOf('http')) {
  208. target = 'https:' + target;
  209. }
  210. return iframeHandler(
  211. transferHash(
  212. target,
  213. appendQueryParams(
  214. 'https://www.facebook.com/plugins/video.php?href=' + target,
  215. $.extend(
  216. {
  217. autoplay: 1
  218. },
  219. parseQueryParams(matches[4] || '')
  220. )
  221. )
  222. )
  223. );
  224. }
  225. function googlemapsHandler(target) {
  226. var matches = _googlemapsRegex.exec(target);
  227. if (!matches) {
  228. return false;
  229. }
  230. return iframeHandler(
  231. transferHash(
  232. target,
  233. appendQueryParams(
  234. 'https://www.google.' + matches[3] + '/maps?' + matches[6],
  235. {
  236. output: matches[6].indexOf('layer=c') > 0 ? 'svembed' : 'embed'
  237. }
  238. )
  239. )
  240. );
  241. }
  242. function iframeHandler(target) {
  243. return '<div class="lity-iframe-container"><iframe frameborder="0" allowfullscreen allow="autoplay; fullscreen" src="' + target + '"/></div>';
  244. }
  245. function winHeight() {
  246. return document.documentElement.clientHeight
  247. ? document.documentElement.clientHeight
  248. : Math.round(_win.height());
  249. }
  250. function keydown(e) {
  251. var current = currentInstance();
  252. if (!current) {
  253. return;
  254. }
  255. // ESC key
  256. if (e.keyCode === 27 && !!current.options('esc')) {
  257. current.close();
  258. }
  259. // TAB key
  260. if (e.keyCode === 9) {
  261. handleTabKey(e, current);
  262. }
  263. }
  264. function handleTabKey(e, instance) {
  265. var focusableElements = instance.element().find(_focusableElementsSelector);
  266. var focusedIndex = focusableElements.index(document.activeElement);
  267. if (e.shiftKey && focusedIndex <= 0) {
  268. focusableElements.get(focusableElements.length - 1).focus();
  269. e.preventDefault();
  270. } else if (!e.shiftKey && focusedIndex === focusableElements.length - 1) {
  271. focusableElements.get(0).focus();
  272. e.preventDefault();
  273. }
  274. }
  275. function resize() {
  276. $.each(_instances, function(i, instance) {
  277. instance.resize();
  278. });
  279. }
  280. function registerInstance(instanceToRegister) {
  281. if (1 === _instances.unshift(instanceToRegister)) {
  282. _html.addClass('lity-active');
  283. _win
  284. .on({
  285. resize: resize,
  286. keydown: keydown
  287. })
  288. ;
  289. }
  290. $('body > *').not(instanceToRegister.element())
  291. .addClass('lity-hidden')
  292. .each(function() {
  293. var el = $(this);
  294. if (undefined !== el.data(_dataAriaHidden)) {
  295. return;
  296. }
  297. el.data(_dataAriaHidden, el.attr(_attrAriaHidden) || null);
  298. })
  299. .attr(_attrAriaHidden, 'true')
  300. ;
  301. }
  302. function removeInstance(instanceToRemove) {
  303. var show;
  304. instanceToRemove
  305. .element()
  306. .attr(_attrAriaHidden, 'true')
  307. ;
  308. if (1 === _instances.length) {
  309. _html.removeClass('lity-active');
  310. _win
  311. .off({
  312. resize: resize,
  313. keydown: keydown
  314. })
  315. ;
  316. }
  317. _instances = $.grep(_instances, function(instance) {
  318. return instanceToRemove !== instance;
  319. });
  320. if (!!_instances.length) {
  321. show = _instances[0].element();
  322. } else {
  323. show = $('.lity-hidden');
  324. }
  325. show
  326. .removeClass('lity-hidden')
  327. .each(function() {
  328. var el = $(this), oldAttr = el.data(_dataAriaHidden);
  329. if (!oldAttr) {
  330. el.removeAttr(_attrAriaHidden);
  331. } else {
  332. el.attr(_attrAriaHidden, oldAttr);
  333. }
  334. el.removeData(_dataAriaHidden);
  335. })
  336. ;
  337. }
  338. function currentInstance() {
  339. if (0 === _instances.length) {
  340. return null;
  341. }
  342. return _instances[0];
  343. }
  344. function factory(target, instance, handlers, preferredHandler) {
  345. var handler = 'inline', content;
  346. var currentHandlers = $.extend({}, handlers);
  347. if (preferredHandler && currentHandlers[preferredHandler]) {
  348. content = currentHandlers[preferredHandler](target, instance);
  349. handler = preferredHandler;
  350. } else {
  351. // Run inline and iframe handlers after all other handlers
  352. $.each(['inline', 'iframe'], function(i, name) {
  353. delete currentHandlers[name];
  354. currentHandlers[name] = handlers[name];
  355. });
  356. $.each(currentHandlers, function(name, currentHandler) {
  357. // Handler might be "removed" by setting callback to null
  358. if (!currentHandler) {
  359. return true;
  360. }
  361. if (
  362. currentHandler.test &&
  363. !currentHandler.test(target, instance)
  364. ) {
  365. return true;
  366. }
  367. content = currentHandler(target, instance);
  368. if (false !== content) {
  369. handler = name;
  370. return false;
  371. }
  372. });
  373. }
  374. return {handler: handler, content: content || ''};
  375. }
  376. function Lity(target, options, opener, activeElement) {
  377. var self = this;
  378. var result;
  379. var isReady = false;
  380. var isClosed = false;
  381. var element;
  382. var content;
  383. options = $.extend(
  384. {},
  385. _defaultOptions,
  386. options
  387. );
  388. element = $(options.template);
  389. // -- API --
  390. self.element = function() {
  391. return element;
  392. };
  393. self.opener = function() {
  394. return opener;
  395. };
  396. self.options = $.proxy(settings, self, options);
  397. self.handlers = $.proxy(settings, self, options.handlers);
  398. self.resize = function() {
  399. if (!isReady || isClosed) {
  400. return;
  401. }
  402. content
  403. .css('max-height', winHeight() + 'px')
  404. .trigger('lity:resize', [self])
  405. ;
  406. };
  407. self.close = function() {
  408. if (!isReady || isClosed) {
  409. return;
  410. }
  411. isClosed = true;
  412. removeInstance(self);
  413. var deferred = _deferred();
  414. // We return focus only if the current focus is inside this instance
  415. if (
  416. activeElement &&
  417. (
  418. document.activeElement === element[0] ||
  419. $.contains(element[0], document.activeElement)
  420. )
  421. ) {
  422. try {
  423. activeElement.focus();
  424. } catch (e) {
  425. // Ignore exceptions, eg. for SVG elements which can't be
  426. // focused in IE11
  427. }
  428. }
  429. content.trigger('lity:close', [self]);
  430. element
  431. .removeClass('lity-opened')
  432. .addClass('lity-closed')
  433. ;
  434. transitionEnd(content.add(element))
  435. .always(function() {
  436. content.trigger('lity:remove', [self]);
  437. element.remove();
  438. element = undefined;
  439. deferred.resolve();
  440. })
  441. ;
  442. return deferred.promise();
  443. };
  444. // -- Initialization --
  445. result = factory(target, self, options.handlers, options.handler);
  446. element
  447. .attr(_attrAriaHidden, 'false')
  448. .addClass('lity-loading lity-opened lity-' + result.handler)
  449. .appendTo('body')
  450. .focus()
  451. .on('click', '[data-lity-close]', function(e) {
  452. if ($(e.target).is('[data-lity-close]')) {
  453. self.close();
  454. }
  455. })
  456. .trigger('lity:open', [self])
  457. ;
  458. registerInstance(self);
  459. $.when(result.content)
  460. .always(ready)
  461. ;
  462. function ready(result) {
  463. content = $(result)
  464. .css('max-height', winHeight() + 'px')
  465. ;
  466. element
  467. .find('.lity-loader')
  468. .each(function() {
  469. var loader = $(this);
  470. transitionEnd(loader)
  471. .always(function() {
  472. loader.remove();
  473. })
  474. ;
  475. })
  476. ;
  477. element
  478. .removeClass('lity-loading')
  479. .find('.lity-content')
  480. .empty()
  481. .append(content)
  482. ;
  483. isReady = true;
  484. content
  485. .trigger('lity:ready', [self])
  486. ;
  487. }
  488. }
  489. function lity(target, options, opener) {
  490. if (!target.preventDefault) {
  491. opener = $(opener);
  492. } else {
  493. target.preventDefault();
  494. opener = $(this);
  495. target = opener.data('lity-target') || opener.attr('href') || opener.attr('src');
  496. }
  497. var instance = new Lity(
  498. target,
  499. $.extend(
  500. {},
  501. opener.data('lity-options') || opener.data('lity'),
  502. options
  503. ),
  504. opener,
  505. document.activeElement
  506. );
  507. if (!target.preventDefault) {
  508. return instance;
  509. }
  510. }
  511. lity.version = '2.4.0';
  512. lity.options = $.proxy(settings, lity, _defaultOptions);
  513. lity.handlers = $.proxy(settings, lity, _defaultOptions.handlers);
  514. lity.current = currentInstance;
  515. $(document).on('click.lity', '[data-lity]', lity);
  516. return lity;
  517. }));