util.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. (function($) {
  2. /**
  3. * Generate an indented list of links from a nav. Meant for use with panel().
  4. * @return {jQuery} jQuery object.
  5. */
  6. $.fn.navList = function() {
  7. var $this = $(this);
  8. $a = $this.find('a'),
  9. b = [];
  10. $a.each(function() {
  11. var $this = $(this),
  12. indent = Math.max(0, $this.parents('li').length - 1),
  13. href = $this.attr('href'),
  14. target = $this.attr('target');
  15. b.push(
  16. '<a ' +
  17. 'class="link depth-' + indent + '"' +
  18. ( (typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') +
  19. ( (typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') +
  20. '>' +
  21. '<span class="indent-' + indent + '"></span>' +
  22. $this.text() +
  23. '</a>'
  24. );
  25. });
  26. return b.join('');
  27. };
  28. /**
  29. * Panel-ify an element.
  30. * @param {object} userConfig User config.
  31. * @return {jQuery} jQuery object.
  32. */
  33. $.fn.panel = function(userConfig) {
  34. // No elements?
  35. if (this.length == 0)
  36. return $this;
  37. // Multiple elements?
  38. if (this.length > 1) {
  39. for (var i=0; i < this.length; i++)
  40. $(this[i]).panel(userConfig);
  41. return $this;
  42. }
  43. // Vars.
  44. var $this = $(this),
  45. $body = $('body'),
  46. $window = $(window),
  47. id = $this.attr('id'),
  48. config;
  49. // Config.
  50. config = $.extend({
  51. // Delay.
  52. delay: 0,
  53. // Hide panel on link click.
  54. hideOnClick: false,
  55. // Hide panel on escape keypress.
  56. hideOnEscape: false,
  57. // Hide panel on swipe.
  58. hideOnSwipe: false,
  59. // Reset scroll position on hide.
  60. resetScroll: false,
  61. // Reset forms on hide.
  62. resetForms: false,
  63. // Side of viewport the panel will appear.
  64. side: null,
  65. // Target element for "class".
  66. target: $this,
  67. // Class to toggle.
  68. visibleClass: 'visible'
  69. }, userConfig);
  70. // Expand "target" if it's not a jQuery object already.
  71. if (typeof config.target != 'jQuery')
  72. config.target = $(config.target);
  73. // Panel.
  74. // Methods.
  75. $this._hide = function(event) {
  76. // Already hidden? Bail.
  77. if (!config.target.hasClass(config.visibleClass))
  78. return;
  79. // If an event was provided, cancel it.
  80. if (event) {
  81. event.preventDefault();
  82. event.stopPropagation();
  83. }
  84. // Hide.
  85. config.target.removeClass(config.visibleClass);
  86. // Post-hide stuff.
  87. window.setTimeout(function() {
  88. // Reset scroll position.
  89. if (config.resetScroll)
  90. $this.scrollTop(0);
  91. // Reset forms.
  92. if (config.resetForms)
  93. $this.find('form').each(function() {
  94. this.reset();
  95. });
  96. }, config.delay);
  97. };
  98. // Vendor fixes.
  99. $this
  100. .css('-ms-overflow-style', '-ms-autohiding-scrollbar')
  101. .css('-webkit-overflow-scrolling', 'touch');
  102. // Hide on click.
  103. if (config.hideOnClick) {
  104. $this.find('a')
  105. .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');
  106. $this
  107. .on('click', 'a', function(event) {
  108. var $a = $(this),
  109. href = $a.attr('href'),
  110. target = $a.attr('target');
  111. if (!href || href == '#' || href == '' || href == '#' + id)
  112. return;
  113. // Cancel original event.
  114. event.preventDefault();
  115. event.stopPropagation();
  116. // Hide panel.
  117. $this._hide();
  118. // Redirect to href.
  119. window.setTimeout(function() {
  120. if (target == '_blank')
  121. window.open(href);
  122. else
  123. window.location.href = href;
  124. }, config.delay + 10);
  125. });
  126. }
  127. // Event: Touch stuff.
  128. $this.on('touchstart', function(event) {
  129. $this.touchPosX = event.originalEvent.touches[0].pageX;
  130. $this.touchPosY = event.originalEvent.touches[0].pageY;
  131. })
  132. $this.on('touchmove', function(event) {
  133. if ($this.touchPosX === null
  134. || $this.touchPosY === null)
  135. return;
  136. var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX,
  137. diffY = $this.touchPosY - event.originalEvent.touches[0].pageY,
  138. th = $this.outerHeight(),
  139. ts = ($this.get(0).scrollHeight - $this.scrollTop());
  140. // Hide on swipe?
  141. if (config.hideOnSwipe) {
  142. var result = false,
  143. boundary = 20,
  144. delta = 50;
  145. switch (config.side) {
  146. case 'left':
  147. result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta);
  148. break;
  149. case 'right':
  150. result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta));
  151. break;
  152. case 'top':
  153. result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta);
  154. break;
  155. case 'bottom':
  156. result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta));
  157. break;
  158. default:
  159. break;
  160. }
  161. if (result) {
  162. $this.touchPosX = null;
  163. $this.touchPosY = null;
  164. $this._hide();
  165. return false;
  166. }
  167. }
  168. // Prevent vertical scrolling past the top or bottom.
  169. if (($this.scrollTop() < 0 && diffY < 0)
  170. || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
  171. event.preventDefault();
  172. event.stopPropagation();
  173. }
  174. });
  175. // Event: Prevent certain events inside the panel from bubbling.
  176. $this.on('click touchend touchstart touchmove', function(event) {
  177. event.stopPropagation();
  178. });
  179. // Event: Hide panel if a child anchor tag pointing to its ID is clicked.
  180. $this.on('click', 'a[href="#' + id + '"]', function(event) {
  181. event.preventDefault();
  182. event.stopPropagation();
  183. config.target.removeClass(config.visibleClass);
  184. });
  185. // Body.
  186. // Event: Hide panel on body click/tap.
  187. $body.on('click touchend', function(event) {
  188. $this._hide(event);
  189. });
  190. // Event: Toggle.
  191. $body.on('click', 'a[href="#' + id + '"]', function(event) {
  192. event.preventDefault();
  193. event.stopPropagation();
  194. config.target.toggleClass(config.visibleClass);
  195. });
  196. // Window.
  197. // Event: Hide on ESC.
  198. if (config.hideOnEscape)
  199. $window.on('keydown', function(event) {
  200. if (event.keyCode == 27)
  201. $this._hide(event);
  202. });
  203. return $this;
  204. };
  205. /**
  206. * Apply "placeholder" attribute polyfill to one or more forms.
  207. * @return {jQuery} jQuery object.
  208. */
  209. $.fn.placeholder = function() {
  210. // Browser natively supports placeholders? Bail.
  211. if (typeof (document.createElement('input')).placeholder != 'undefined')
  212. return $(this);
  213. // No elements?
  214. if (this.length == 0)
  215. return $this;
  216. // Multiple elements?
  217. if (this.length > 1) {
  218. for (var i=0; i < this.length; i++)
  219. $(this[i]).placeholder();
  220. return $this;
  221. }
  222. // Vars.
  223. var $this = $(this);
  224. // Text, TextArea.
  225. $this.find('input[type=text],textarea')
  226. .each(function() {
  227. var i = $(this);
  228. if (i.val() == ''
  229. || i.val() == i.attr('placeholder'))
  230. i
  231. .addClass('polyfill-placeholder')
  232. .val(i.attr('placeholder'));
  233. })
  234. .on('blur', function() {
  235. var i = $(this);
  236. if (i.attr('name').match(/-polyfill-field$/))
  237. return;
  238. if (i.val() == '')
  239. i
  240. .addClass('polyfill-placeholder')
  241. .val(i.attr('placeholder'));
  242. })
  243. .on('focus', function() {
  244. var i = $(this);
  245. if (i.attr('name').match(/-polyfill-field$/))
  246. return;
  247. if (i.val() == i.attr('placeholder'))
  248. i
  249. .removeClass('polyfill-placeholder')
  250. .val('');
  251. });
  252. // Password.
  253. $this.find('input[type=password]')
  254. .each(function() {
  255. var i = $(this);
  256. var x = $(
  257. $('<div>')
  258. .append(i.clone())
  259. .remove()
  260. .html()
  261. .replace(/type="password"/i, 'type="text"')
  262. .replace(/type=password/i, 'type=text')
  263. );
  264. if (i.attr('id') != '')
  265. x.attr('id', i.attr('id') + '-polyfill-field');
  266. if (i.attr('name') != '')
  267. x.attr('name', i.attr('name') + '-polyfill-field');
  268. x.addClass('polyfill-placeholder')
  269. .val(x.attr('placeholder')).insertAfter(i);
  270. if (i.val() == '')
  271. i.hide();
  272. else
  273. x.hide();
  274. i
  275. .on('blur', function(event) {
  276. event.preventDefault();
  277. var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
  278. if (i.val() == '') {
  279. i.hide();
  280. x.show();
  281. }
  282. });
  283. x
  284. .on('focus', function(event) {
  285. event.preventDefault();
  286. var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']');
  287. x.hide();
  288. i
  289. .show()
  290. .focus();
  291. })
  292. .on('keypress', function(event) {
  293. event.preventDefault();
  294. x.val('');
  295. });
  296. });
  297. // Events.
  298. $this
  299. .on('submit', function() {
  300. $this.find('input[type=text],input[type=password],textarea')
  301. .each(function(event) {
  302. var i = $(this);
  303. if (i.attr('name').match(/-polyfill-field$/))
  304. i.attr('name', '');
  305. if (i.val() == i.attr('placeholder')) {
  306. i.removeClass('polyfill-placeholder');
  307. i.val('');
  308. }
  309. });
  310. })
  311. .on('reset', function(event) {
  312. event.preventDefault();
  313. $this.find('select')
  314. .val($('option:first').val());
  315. $this.find('input,textarea')
  316. .each(function() {
  317. var i = $(this),
  318. x;
  319. i.removeClass('polyfill-placeholder');
  320. switch (this.type) {
  321. case 'submit':
  322. case 'reset':
  323. break;
  324. case 'password':
  325. i.val(i.attr('defaultValue'));
  326. x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
  327. if (i.val() == '') {
  328. i.hide();
  329. x.show();
  330. }
  331. else {
  332. i.show();
  333. x.hide();
  334. }
  335. break;
  336. case 'checkbox':
  337. case 'radio':
  338. i.attr('checked', i.attr('defaultValue'));
  339. break;
  340. case 'text':
  341. case 'textarea':
  342. i.val(i.attr('defaultValue'));
  343. if (i.val() == '') {
  344. i.addClass('polyfill-placeholder');
  345. i.val(i.attr('placeholder'));
  346. }
  347. break;
  348. default:
  349. i.val(i.attr('defaultValue'));
  350. break;
  351. }
  352. });
  353. });
  354. return $this;
  355. };
  356. /**
  357. * Moves elements to/from the first positions of their respective parents.
  358. * @param {jQuery} $elements Elements (or selector) to move.
  359. * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations.
  360. */
  361. $.prioritize = function($elements, condition) {
  362. var key = '__prioritize';
  363. // Expand $elements if it's not already a jQuery object.
  364. if (typeof $elements != 'jQuery')
  365. $elements = $($elements);
  366. // Step through elements.
  367. $elements.each(function() {
  368. var $e = $(this), $p,
  369. $parent = $e.parent();
  370. // No parent? Bail.
  371. if ($parent.length == 0)
  372. return;
  373. // Not moved? Move it.
  374. if (!$e.data(key)) {
  375. // Condition is false? Bail.
  376. if (!condition)
  377. return;
  378. // Get placeholder (which will serve as our point of reference for when this element needs to move back).
  379. $p = $e.prev();
  380. // Couldn't find anything? Means this element's already at the top, so bail.
  381. if ($p.length == 0)
  382. return;
  383. // Move element to top of parent.
  384. $e.prependTo($parent);
  385. // Mark element as moved.
  386. $e.data(key, $p);
  387. }
  388. // Moved already?
  389. else {
  390. // Condition is true? Bail.
  391. if (condition)
  392. return;
  393. $p = $e.data(key);
  394. // Move element back to its original location (using our placeholder).
  395. $e.insertAfter($p);
  396. // Unmark element as moved.
  397. $e.removeData(key);
  398. }
  399. });
  400. };
  401. })(jQuery);