util.js (12433B)
1 (function($) { 2 3 /** 4 * Generate an indented list of links from a nav. Meant for use with panel(). 5 * @return {jQuery} jQuery object. 6 */ 7 $.fn.navList = function() { 8 9 var $this = $(this); 10 $a = $this.find('a'), 11 b = []; 12 13 $a.each(function() { 14 15 var $this = $(this), 16 indent = Math.max(0, $this.parents('li').length - 1), 17 href = $this.attr('href'), 18 target = $this.attr('target'); 19 20 b.push( 21 '<a ' + 22 'class="link depth-' + indent + '"' + 23 ( (typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') + 24 ( (typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') + 25 '>' + 26 '<span class="indent-' + indent + '"></span>' + 27 $this.text() + 28 '</a>' 29 ); 30 31 }); 32 33 return b.join(''); 34 35 }; 36 37 /** 38 * Panel-ify an element. 39 * @param {object} userConfig User config. 40 * @return {jQuery} jQuery object. 41 */ 42 $.fn.panel = function(userConfig) { 43 44 // No elements? 45 if (this.length == 0) 46 return $this; 47 48 // Multiple elements? 49 if (this.length > 1) { 50 51 for (var i=0; i < this.length; i++) 52 $(this[i]).panel(userConfig); 53 54 return $this; 55 56 } 57 58 // Vars. 59 var $this = $(this), 60 $body = $('body'), 61 $window = $(window), 62 id = $this.attr('id'), 63 config; 64 65 // Config. 66 config = $.extend({ 67 68 // Delay. 69 delay: 0, 70 71 // Hide panel on link click. 72 hideOnClick: false, 73 74 // Hide panel on escape keypress. 75 hideOnEscape: false, 76 77 // Hide panel on swipe. 78 hideOnSwipe: false, 79 80 // Reset scroll position on hide. 81 resetScroll: false, 82 83 // Reset forms on hide. 84 resetForms: false, 85 86 // Side of viewport the panel will appear. 87 side: null, 88 89 // Target element for "class". 90 target: $this, 91 92 // Class to toggle. 93 visibleClass: 'visible' 94 95 }, userConfig); 96 97 // Expand "target" if it's not a jQuery object already. 98 if (typeof config.target != 'jQuery') 99 config.target = $(config.target); 100 101 // Panel. 102 103 // Methods. 104 $this._hide = function(event) { 105 106 // Already hidden? Bail. 107 if (!config.target.hasClass(config.visibleClass)) 108 return; 109 110 // If an event was provided, cancel it. 111 if (event) { 112 113 event.preventDefault(); 114 event.stopPropagation(); 115 116 } 117 118 // Hide. 119 config.target.removeClass(config.visibleClass); 120 121 // Post-hide stuff. 122 window.setTimeout(function() { 123 124 // Reset scroll position. 125 if (config.resetScroll) 126 $this.scrollTop(0); 127 128 // Reset forms. 129 if (config.resetForms) 130 $this.find('form').each(function() { 131 this.reset(); 132 }); 133 134 }, config.delay); 135 136 }; 137 138 // Vendor fixes. 139 $this 140 .css('-ms-overflow-style', '-ms-autohiding-scrollbar') 141 .css('-webkit-overflow-scrolling', 'touch'); 142 143 // Hide on click. 144 if (config.hideOnClick) { 145 146 $this.find('a') 147 .css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)'); 148 149 $this 150 .on('click', 'a', function(event) { 151 152 var $a = $(this), 153 href = $a.attr('href'), 154 target = $a.attr('target'); 155 156 if (!href || href == '#' || href == '' || href == '#' + id) 157 return; 158 159 // Cancel original event. 160 event.preventDefault(); 161 event.stopPropagation(); 162 163 // Hide panel. 164 $this._hide(); 165 166 // Redirect to href. 167 window.setTimeout(function() { 168 169 if (target == '_blank') 170 window.open(href); 171 else 172 window.location.href = href; 173 174 }, config.delay + 10); 175 176 }); 177 178 } 179 180 // Event: Touch stuff. 181 $this.on('touchstart', function(event) { 182 183 $this.touchPosX = event.originalEvent.touches[0].pageX; 184 $this.touchPosY = event.originalEvent.touches[0].pageY; 185 186 }) 187 188 $this.on('touchmove', function(event) { 189 190 if ($this.touchPosX === null 191 || $this.touchPosY === null) 192 return; 193 194 var diffX = $this.touchPosX - event.originalEvent.touches[0].pageX, 195 diffY = $this.touchPosY - event.originalEvent.touches[0].pageY, 196 th = $this.outerHeight(), 197 ts = ($this.get(0).scrollHeight - $this.scrollTop()); 198 199 // Hide on swipe? 200 if (config.hideOnSwipe) { 201 202 var result = false, 203 boundary = 20, 204 delta = 50; 205 206 switch (config.side) { 207 208 case 'left': 209 result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta); 210 break; 211 212 case 'right': 213 result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta)); 214 break; 215 216 case 'top': 217 result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta); 218 break; 219 220 case 'bottom': 221 result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta)); 222 break; 223 224 default: 225 break; 226 227 } 228 229 if (result) { 230 231 $this.touchPosX = null; 232 $this.touchPosY = null; 233 $this._hide(); 234 235 return false; 236 237 } 238 239 } 240 241 // Prevent vertical scrolling past the top or bottom. 242 if (($this.scrollTop() < 0 && diffY < 0) 243 || (ts > (th - 2) && ts < (th + 2) && diffY > 0)) { 244 245 event.preventDefault(); 246 event.stopPropagation(); 247 248 } 249 250 }); 251 252 // Event: Prevent certain events inside the panel from bubbling. 253 $this.on('click touchend touchstart touchmove', function(event) { 254 event.stopPropagation(); 255 }); 256 257 // Event: Hide panel if a child anchor tag pointing to its ID is clicked. 258 $this.on('click', 'a[href="#' + id + '"]', function(event) { 259 260 event.preventDefault(); 261 event.stopPropagation(); 262 263 config.target.removeClass(config.visibleClass); 264 265 }); 266 267 // Body. 268 269 // Event: Hide panel on body click/tap. 270 $body.on('click touchend', function(event) { 271 $this._hide(event); 272 }); 273 274 // Event: Toggle. 275 $body.on('click', 'a[href="#' + id + '"]', function(event) { 276 277 event.preventDefault(); 278 event.stopPropagation(); 279 280 config.target.toggleClass(config.visibleClass); 281 282 }); 283 284 // Window. 285 286 // Event: Hide on ESC. 287 if (config.hideOnEscape) 288 $window.on('keydown', function(event) { 289 290 if (event.keyCode == 27) 291 $this._hide(event); 292 293 }); 294 295 return $this; 296 297 }; 298 299 /** 300 * Apply "placeholder" attribute polyfill to one or more forms. 301 * @return {jQuery} jQuery object. 302 */ 303 $.fn.placeholder = function() { 304 305 // Browser natively supports placeholders? Bail. 306 if (typeof (document.createElement('input')).placeholder != 'undefined') 307 return $(this); 308 309 // No elements? 310 if (this.length == 0) 311 return $this; 312 313 // Multiple elements? 314 if (this.length > 1) { 315 316 for (var i=0; i < this.length; i++) 317 $(this[i]).placeholder(); 318 319 return $this; 320 321 } 322 323 // Vars. 324 var $this = $(this); 325 326 // Text, TextArea. 327 $this.find('input[type=text],textarea') 328 .each(function() { 329 330 var i = $(this); 331 332 if (i.val() == '' 333 || i.val() == i.attr('placeholder')) 334 i 335 .addClass('polyfill-placeholder') 336 .val(i.attr('placeholder')); 337 338 }) 339 .on('blur', function() { 340 341 var i = $(this); 342 343 if (i.attr('name').match(/-polyfill-field$/)) 344 return; 345 346 if (i.val() == '') 347 i 348 .addClass('polyfill-placeholder') 349 .val(i.attr('placeholder')); 350 351 }) 352 .on('focus', function() { 353 354 var i = $(this); 355 356 if (i.attr('name').match(/-polyfill-field$/)) 357 return; 358 359 if (i.val() == i.attr('placeholder')) 360 i 361 .removeClass('polyfill-placeholder') 362 .val(''); 363 364 }); 365 366 // Password. 367 $this.find('input[type=password]') 368 .each(function() { 369 370 var i = $(this); 371 var x = $( 372 $('<div>') 373 .append(i.clone()) 374 .remove() 375 .html() 376 .replace(/type="password"/i, 'type="text"') 377 .replace(/type=password/i, 'type=text') 378 ); 379 380 if (i.attr('id') != '') 381 x.attr('id', i.attr('id') + '-polyfill-field'); 382 383 if (i.attr('name') != '') 384 x.attr('name', i.attr('name') + '-polyfill-field'); 385 386 x.addClass('polyfill-placeholder') 387 .val(x.attr('placeholder')).insertAfter(i); 388 389 if (i.val() == '') 390 i.hide(); 391 else 392 x.hide(); 393 394 i 395 .on('blur', function(event) { 396 397 event.preventDefault(); 398 399 var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); 400 401 if (i.val() == '') { 402 403 i.hide(); 404 x.show(); 405 406 } 407 408 }); 409 410 x 411 .on('focus', function(event) { 412 413 event.preventDefault(); 414 415 var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']'); 416 417 x.hide(); 418 419 i 420 .show() 421 .focus(); 422 423 }) 424 .on('keypress', function(event) { 425 426 event.preventDefault(); 427 x.val(''); 428 429 }); 430 431 }); 432 433 // Events. 434 $this 435 .on('submit', function() { 436 437 $this.find('input[type=text],input[type=password],textarea') 438 .each(function(event) { 439 440 var i = $(this); 441 442 if (i.attr('name').match(/-polyfill-field$/)) 443 i.attr('name', ''); 444 445 if (i.val() == i.attr('placeholder')) { 446 447 i.removeClass('polyfill-placeholder'); 448 i.val(''); 449 450 } 451 452 }); 453 454 }) 455 .on('reset', function(event) { 456 457 event.preventDefault(); 458 459 $this.find('select') 460 .val($('option:first').val()); 461 462 $this.find('input,textarea') 463 .each(function() { 464 465 var i = $(this), 466 x; 467 468 i.removeClass('polyfill-placeholder'); 469 470 switch (this.type) { 471 472 case 'submit': 473 case 'reset': 474 break; 475 476 case 'password': 477 i.val(i.attr('defaultValue')); 478 479 x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]'); 480 481 if (i.val() == '') { 482 i.hide(); 483 x.show(); 484 } 485 else { 486 i.show(); 487 x.hide(); 488 } 489 490 break; 491 492 case 'checkbox': 493 case 'radio': 494 i.attr('checked', i.attr('defaultValue')); 495 break; 496 497 case 'text': 498 case 'textarea': 499 i.val(i.attr('defaultValue')); 500 501 if (i.val() == '') { 502 i.addClass('polyfill-placeholder'); 503 i.val(i.attr('placeholder')); 504 } 505 506 break; 507 508 default: 509 i.val(i.attr('defaultValue')); 510 break; 511 512 } 513 }); 514 515 }); 516 517 return $this; 518 519 }; 520 521 /** 522 * Moves elements to/from the first positions of their respective parents. 523 * @param {jQuery} $elements Elements (or selector) to move. 524 * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations. 525 */ 526 $.prioritize = function($elements, condition) { 527 528 var key = '__prioritize'; 529 530 // Expand $elements if it's not already a jQuery object. 531 if (typeof $elements != 'jQuery') 532 $elements = $($elements); 533 534 // Step through elements. 535 $elements.each(function() { 536 537 var $e = $(this), $p, 538 $parent = $e.parent(); 539 540 // No parent? Bail. 541 if ($parent.length == 0) 542 return; 543 544 // Not moved? Move it. 545 if (!$e.data(key)) { 546 547 // Condition is false? Bail. 548 if (!condition) 549 return; 550 551 // Get placeholder (which will serve as our point of reference for when this element needs to move back). 552 $p = $e.prev(); 553 554 // Couldn't find anything? Means this element's already at the top, so bail. 555 if ($p.length == 0) 556 return; 557 558 // Move element to top of parent. 559 $e.prependTo($parent); 560 561 // Mark element as moved. 562 $e.data(key, $p); 563 564 } 565 566 // Moved already? 567 else { 568 569 // Condition is true? Bail. 570 if (condition) 571 return; 572 573 $p = $e.data(key); 574 575 // Move element back to its original location (using our placeholder). 576 $e.insertAfter($p); 577 578 // Unmark element as moved. 579 $e.removeData(key); 580 581 } 582 583 }); 584 585 }; 586 587 })(jQuery);