1/*! Snap.js v2.0.0-rc1 */ 2(function(win, doc) { 3 4 'use strict'; 5 6 // Our export 7 var Namespace = 'Snap'; 8 9 // Our main toolbelt 10 var utils = { 11 12 /** 13 * Deeply extends two objects 14 * @param {Object} destination The destination object 15 * @param {Object} source The custom options to extend destination by 16 * @return {Object} The desination object 17 */ 18 extend: function(destination, source) { 19 var property; 20 for (property in source) { 21 if (source[property] && source[property].constructor && source[property].constructor === Object) { 22 destination[property] = destination[property] || {}; 23 utils.extend(destination[property], source[property]); 24 } else { 25 destination[property] = source[property]; 26 } 27 } 28 return destination; 29 } 30 }; 31 32 /** 33 * Our Snap global that initializes our instance 34 * @param {Object} opts The custom Snap.js options 35 */ 36 var Core = function( opts ) { 37 38 var self = this; 39 40 /** 41 * Our default settings for a Snap instance 42 * @type {Object} 43 */ 44 var settings = self.settings = { 45 element: null, 46 dragger: null, 47 disable: 'none', 48 addBodyClasses: true, 49 hyperextensible: true, 50 resistance: 0.5, 51 flickThreshold: 50, 52 transitionSpeed: 0.3, 53 easing: 'ease', 54 maxPosition: 266, 55 minPosition: -266, 56 tapToClose: true, 57 touchToDrag: true, 58 clickToDrag: true, 59 slideIntent: 40, // degrees 60 minDragDistance: 5 61 }; 62 63 /** 64 * Stores internally global data 65 * @type {Object} 66 */ 67 var cache = self.cache = { 68 isDragging: false, 69 simpleStates: { 70 opening: null, 71 towards: null, 72 hyperExtending: null, 73 halfway: null, 74 flick: null, 75 translation: { 76 absolute: 0, 77 relative: 0, 78 sinceDirectionChange: 0, 79 percentage: 0 80 } 81 } 82 }; 83 84 var eventList = self.eventList = {}; 85 86 utils.extend(utils, { 87 88 /** 89 * Determines if we are interacting with a touch device 90 * @type {Boolean} 91 */ 92 hasTouch: ('ontouchstart' in doc.documentElement || win.navigator.msPointerEnabled), 93 94 /** 95 * Returns the appropriate event type based on whether we are a touch device or not 96 * @param {String} action The "action" event you're looking for: up, down, move, out 97 * @return {String} The browsers supported event name 98 */ 99 eventType: function(action) { 100 var eventTypes = { 101 down: (utils.hasTouch ? 'touchstart' : settings.clickToDrag ? 'mousedown' : ''), 102 move: (utils.hasTouch ? 'touchmove' : settings.clickToDrag ? 'mousemove' : ''), 103 up: (utils.hasTouch ? 'touchend' : settings.clickToDrag ? 'mouseup': ''), 104 out: (utils.hasTouch ? 'touchcancel' : settings.clickToDrag ? 'mouseout' : '') 105 }; 106 return eventTypes[action]; 107 }, 108 109 /** 110 * Returns the correct "cursor" position on both browser and mobile 111 * @param {String} t The coordinate to retrieve, either "X" or "Y" 112 * @param {Object} e The event object being triggered 113 * @return {Number} The desired coordiante for the events interaction 114 */ 115 page: function(t, e){ 116 return (utils.hasTouch && e.touches.length && e.touches[0]) ? e.touches[0]['page'+t] : e['page'+t]; 117 }, 118 119 120 klass: { 121 122 /** 123 * Checks if an element has a class name 124 * @param {Object} el The element to check 125 * @param {String} name The class name to search for 126 * @return {Boolean} Returns true if the class exists 127 */ 128 has: function(el, name){ 129 return (el.className).indexOf(name) !== -1; 130 }, 131 132 /** 133 * Adds a class name to an element 134 * @param {Object} el The element to add to 135 * @param {String} name The class name to add 136 */ 137 add: function(el, name){ 138 if(!utils.klass.has(el, name) && settings.addBodyClasses){ 139 el.className += " "+name; 140 } 141 }, 142 143 /** 144 * Removes a class name 145 * @param {Object} el The element to remove from 146 * @param {String} name The class name to remove 147 */ 148 remove: function(el, name){ 149 if(utils.klass.has(el, name) && settings.addBodyClasses){ 150 el.className = (el.className).replace(name, "").replace(/^\s+|\s+$/g, ''); 151 } 152 } 153 }, 154 155 /**
156 * Dispatch a custom Snap.js event 157 * @param {String} type The event name 158 */ 159 dispatchEvent: function(type) { 160 if( typeof eventList[type] === 'function') { 161 return eventList[type].apply(); 162 } 163 }, 164 165 /** 166 * Determines the browsers vendor prefix for CSS3 167 * @return {String} The browsers vendor prefix 168 */ 169 vendor: function(){ 170 var tmp = doc.createElement("div"), 171 prefixes = 'webkit Moz O ms'.split(' '), 172 i; 173 for (i in prefixes) { 174 if (typeof tmp.style[prefixes[i] + 'Transition'] !== 'undefined') { 175 return prefixes[i]; 176 } 177 } 178 }, 179 180 /** 181 * Determines the browsers vendor prefix for transition callback events 182 * @return {String} The event name 183 */ 184 transitionCallback: function(){ 185 return (cache.vendor==='Moz' || cache.vendor==='ms') ? 'transitionend' : cache.vendor+'TransitionEnd'; 186 }, 187 188 /** 189 * Determines if the users browser supports CSS3 transformations 190 * @return {[type]} [description] 191 */ 192 canTransform: function(){ 193 return typeof settings.element.style[cache.vendor+'Transform'] !== 'undefined'; 194 }, 195 196 /** 197 * Determines an angle between two points 198 * @param {Number} x The X coordinate 199 * @param {Number} y The Y coordinate 200 * @return {Number} The number of degrees between the two points 201 */ 202 angleOfDrag: function(x, y) { 203 var degrees, theta; 204 // Calc Theta 205 theta = Math.atan2(-(cache.startDragY - y), (cache.startDragX - x)); 206 if (theta < 0) { 207 theta += 2 * Math.PI; 208 } 209 // Calc Degrees 210 degrees = Math.floor(theta * (180 / Math.PI) - 180); 211 if (degrees < 0 && degrees > -180) { 212 degrees = 360 - Math.abs(degrees); 213 } 214 return Math.abs(degrees); 215 }, 216 217 218 events: { 219 220 /** 221 * Adds an event to an element 222 * @param {Object} element Element to add event to 223 * @param {String} eventName The event name 224 * @param {Function} func Callback function 225 */ 226 addEvent: function addEvent(element, eventName, func) { 227 if (element.addEventListener) { 228 return element.addEventListener(eventName, func, false); 229 } else if (element.attachEvent) { 230 return element.attachEvent("on" + eventName, func); 231 } 232 }, 233 234 /** 235 * Removes an event to an element 236 * @param {Object} element Element to remove event from 237 * @param {String} eventName The event name 238 * @param {Function} func Callback function 239 */ 240 removeEvent: function addEvent(element, eventName, func) { 241 if (element.addEventListener) { 242 return element.removeEventListener(eventName, func, false); 243 } else if (element.attachEvent) { 244 return element.detachEvent("on" + eventName, func); 245 } 246 }, 247 248 /** 249 * Prevents the default event 250 * @param {Object} e The event object 251 */ 252 prevent: function(e) { 253 if (e.preventDefault) { 254 e.preventDefault(); 255 } else { 256 e.returnValue = false; 257 } 258 } 259 }, 260 261 /** 262 * Searches the parent element until a specified attribute has been matched 263 * @param {Object} el The element to search from 264 * @param {String} attr The attribute to search for 265 * @return {Object|null} Returns a matched element if it exists, else, null 266 */ 267 parentUntil: function(el, attr) { 268 var isStr = typeof attr === 'string'; 269 while (el.parentNode) { 270 if (isStr && el.getAttribute && el.getAttribute(attr)){ 271 return el; 272 } else if(!isStr && el === attr){ 273 return el; 274 } 275 el = el.parentNode; 276 } 277 return null; 278 } 279 }); 280 281 282 var action = self.action = { 283 284 /** 285 * Handles translating the elements position 286 * @type {Object} 287 */ 288 translate: { 289 get: { 290 291 /** 292 * Returns the amount an element is translated 293 * @param {Number} index The index desired from the CSS3 values of translate3d 294 * @return {Number} The amount of pixels an element is translated 295 */ 296 matrix: function(index) { 297 298 if( !cache.canTransform ){ 299 return parseInt(settings.element.style.left, 10); 300 } else { 301 var matrix = win.getComputedStyle(settings.element)[cache.vendor+'Transform'].match(/\((.*)\)/), 302 ieOffset = 8; 303 if (matrix) { 304 matrix = matrix[1].split(','); 305 306 // Internet Explorer likes to give us 16 fucking values 307 if(matrix.length===16){ 308 index+=ieOffset; 309 } 310 return parseInt(matrix[index], 10); 311 } 312 return 0; 313 } 314 } 315 }, 316 317 /** 318 * Called when the element has finished transitioning 319 */ 320 easeCallback: function(fn){ 321 settings.element.style[cache.vendor+'Transition'] = ''; 322 cache.translation = action.translate.get.matrix(4); 323 cache.easing = false;
324 325 if(cache.easingTo===0){ 326 utils.klass.remove(doc.body, 'snapjs-right'); 327 utils.klass.remove(doc.body, 'snapjs-left'); 328 } 329 330 if( cache.once ){ 331 cache.once.call(self, self.state()); 332 delete cache.once; 333 } 334 335 utils.dispatchEvent('animated'); 336 utils.events.removeEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback); 337 338 }, 339 340 /** 341 * Animates the pane by the specified amount of pixels 342 * @param {Number} n The amount of pixels to move the pane 343 */ 344 easeTo: function(n, cb) { 345 346 if( !cache.canTransform ){ 347 cache.translation = n; 348 action.translate.x(n); 349 } else { 350 cache.easing = true; 351 cache.easingTo = n; 352 353 settings.element.style[cache.vendor+'Transition'] = 'all ' + settings.transitionSpeed + 's ' + settings.easing; 354 355 cache.once = cb; 356 357 utils.events.addEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback); 358 action.translate.x(n); 359 } 360 if(n===0){ 361 settings.element.style[cache.vendor+'Transform'] = ''; 362 } 363 }, 364 365 /** 366 * Immediately translates the element on its X axis 367 * @param {Number} n Amount of pixels to translate 368 */ 369 x: function(n) { 370 if( (settings.disable==='left' && n>0) || 371 (settings.disable==='right' && n<0) 372 ){ return; } 373 374 if( !settings.hyperextensible ){ 375 if( n===settings.maxPosition || n>settings.maxPosition ){ 376 n=settings.maxPosition; 377 } else if( n===settings.minPosition || n<settings.minPosition ){ 378 n=settings.minPosition; 379 } 380 } 381 382 n = parseInt(n, 10); 383 if(isNaN(n)){ 384 n = 0; 385 } 386 387 if( cache.canTransform ){ 388 var theTranslate = 'translate3d(' + n + 'px, 0,0)'; 389 settings.element.style[cache.vendor+'Transform'] = theTranslate; 390 } else { 391 settings.element.style.width = (win.innerWidth || doc.documentElement.clientWidth)+'px'; 392 393 settings.element.style.left = n+'px'; 394 settings.element.style.right = ''; 395 } 396 } 397 }, 398 399 /** 400 * Handles all the events that interface with dragging 401 * @type {Object} 402 */ 403 drag: { 404 405 /** 406 * Begins listening for drag events on our element 407 */ 408 listen: function() { 409 cache.translation = 0; 410 cache.easing = false; 411 utils.events.addEvent(self.settings.element, utils.eventType('down'), action.drag.startDrag); 412 utils.events.addEvent(self.settings.element, utils.eventType('move'), action.drag.dragging); 413 utils.events.addEvent(self.settings.element, utils.eventType('up'), action.drag.endDrag); 414 }, 415 416 /** 417 * Stops listening for drag events on our element 418 */ 419 stopListening: function() { 420 utils.events.removeEvent(settings.element, utils.eventType('down'), action.drag.startDrag); 421 utils.events.removeEvent(settings.element, utils.eventType('move'), action.drag.dragging); 422 utils.events.removeEvent(settings.element, utils.eventType('up'), action.drag.endDrag); 423 }, 424 425 /** 426 * Fired immediately when the user begins to drag the content pane 427 * @param {Object} e Event object 428 */ 429 startDrag: function(e) { 430 // No drag on ignored elements 431 var target = e.target ? e.target : e.srcElement, 432 ignoreParent = utils.parentUntil(target, 'data-snap-ignore'); 433 434 if (ignoreParent) { 435 utils.dispatchEvent('ignore'); 436 return; 437 } 438 439 440 if(settings.dragger){ 441 var dragParent = utils.parentUntil(target, settings.dragger); 442 443 // Only use dragger if we're in a closed state 444 if( !dragParent && 445 (cache.translation !== settings.minPosition && 446 cache.translation !== settings.maxPosition 447 )){ 448 return; 449 } 450 } 451 452 utils.dispatchEvent('start'); 453 settings.element.style[cache.vendor+'Transition'] = ''; 454 cache.isDragging = true; 455 456 cache.intentChecked = false;
457 cache.startDragX = utils.page('X', e); 458 cache.startDragY = utils.page('Y', e); 459 cache.dragWatchers = { 460 current: 0, 461 last: 0, 462 hold: 0, 463 state: '' 464 }; 465 cache.simpleStates = { 466 opening: null, 467 towards: null, 468 hyperExtending: null, 469 halfway: null, 470 flick: null, 471 translation: { 472 absolute: 0, 473 relative: 0, 474 sinceDirectionChange: 0, 475 percentage: 0 476 } 477 }; 478 }, 479 480 /** 481 * Fired while the user is moving the content pane 482 * @param {Object} e Event object 483 */ 484 dragging: function(e) { 485 486 if (cache.isDragging && settings.touchToDrag) { 487 488 var thePageX = utils.page('X', e), 489 thePageY = utils.page('Y', e), 490 translated = cache.translation, 491 absoluteTranslation = action.translate.get.matrix(4), 492 whileDragX = thePageX - cache.startDragX, 493 openingLeft = absoluteTranslation > 0, 494 translateTo = whileDragX, 495 diff; 496 497 // Shown no intent already 498 if((cache.intentChecked && !cache.hasIntent)){ 499 return; 500 } 501 502 if(settings.addBodyClasses){ 503 if((absoluteTranslation)>0){ 504 utils.klass.add(doc.body, 'snapjs-left'); 505 utils.klass.remove(doc.body, 'snapjs-right'); 506 } else if((absoluteTranslation)<0){ 507 utils.klass.add(doc.body, 'snapjs-right'); 508 utils.klass.remove(doc.body, 'snapjs-left'); 509 } 510 } 511 512 if (cache.hasIntent === false || cache.hasIntent === null) { 513 514 var deg = utils.angleOfDrag(thePageX, thePageY), 515 inRightRange = (deg >= 0 && deg <= settings.slideIntent) || (deg <= 360 && deg > (360 - settings.slideIntent)), 516 inLeftRange = (deg >= 180 && deg <= (180 + settings.slideIntent)) || (deg <= 180 && deg >= (180 - settings.slideIntent)); 517 if (!inLeftRange && !inRightRange) { 518 cache.hasIntent = false; 519 } else { 520 cache.hasIntent = true; 521 } 522 cache.intentChecked = true; 523 } 524 525 if ( 526 (settings.minDragDistance>=Math.abs(thePageX-cache.startDragX)) || // Has user met minimum drag distance? 527 (cache.hasIntent === false) 528 ) { 529 return; 530 } 531 532 utils.events.prevent(e); 533 utils.dispatchEvent('drag'); 534 535 cache.dragWatchers.current = thePageX; 536 537 // Determine which direction we are going 538 if (cache.dragWatchers.last > thePageX) { 539 if (cache.dragWatchers.state !== 'left') { 540 cache.dragWatchers.state = 'left'; 541 cache.dragWatchers.hold = thePageX; 542 } 543 cache.dragWatchers.last = thePageX; 544 } else if (cache.dragWatchers.last < thePageX) { 545 if (cache.dragWatchers.state !== 'right') { 546 cache.dragWatchers.state = 'right'; 547 cache.dragWatchers.hold = thePageX; 548 } 549 cache.dragWatchers.last = thePageX; 550 } 551 if (openingLeft) { 552 // Pulling too far to the right 553 if (settings.maxPosition < absoluteTranslation) { 554 diff = (absoluteTranslation - settings.maxPosition) *
554settings.resistance; 555 translateTo = whileDragX - diff; 556 } 557 cache.simpleStates = { 558 opening: 'left', 559 towards: cache.dragWatchers.state, 560 hyperExtending: settings.maxPosition < absoluteTranslation, 561 halfway: absoluteTranslation > (settings.maxPosition / 2), 562 flick: Math.abs(cache.dragWatchers.current - cache.dragWatchers.hold) > settings.flickThreshold, 563 translation: { 564 absolute: absoluteTranslation, 565 relative: whileDragX, 566 sinceDirectionChange: (cache.dragWatchers.current - cache.dragWatchers.hold), 567 percentage: (absoluteTranslation/settings.maxPosition)*100 568 } 569 }; 570 } else { 571 // Pulling too far to the left 572 if (settings.minPosition > absoluteTranslation) { 573 diff = (absoluteTranslation - settings.minPosition) * settings.resistance; 574 translateTo = whileDragX - diff; 575 } 576 cache.simpleStates = { 577 opening: 'right', 578 towards: cache.dragWatchers.state, 579 hyperExtending: settings.minPosition > absoluteTranslation, 580 halfway: absoluteTranslation < (settings.minPosition / 2), 581 flick: Math.abs(cache.dragWatchers.current - cache.dragWatchers.hold) > settings.flickThreshold, 582 translation: { 583 absolute: absoluteTranslation, 584 relative: whileDragX, 585 sinceDirectionChange: (cache.dragWatchers.current - cache.dragWatchers.hold), 586 percentage: (absoluteTranslation/settings.minPosition)*100 587 } 588 }; 589 } 590 action.translate.x(translateTo + translated); 591 } 592 }, 593 594 /** 595 * Fired when the user releases the content pane 596 * @param {Object} e Event object 597 */ 598 endDrag: function(e) { 599 if (cache.isDragging) { 600 utils.dispatchEvent('end'); 601 var translated = action.translate.get.matrix(4); 602 603 // Tap Close 604 if (cache.dragWatchers.current === 0 && translated !== 0 && settings.tapToClose) { 605 utils.dispatchEvent('close'); 606 utils.events.prevent(e); 607 action.translate.easeTo(0); 608 cache.isDragging = false; 609 cache.startDragX = 0; 610 return; 611 } 612 613 // Revealing Left 614 if (cache.simpleStates.opening === 'left') { 615 // Halfway, Flicking, or Too Far Out 616 if ((cache.simpleStates.halfway || cache.simpleStates.hyperExtending || cache.simpleStates.flick)) { 617 if (cache.simpleStates.flick && cache.simpleStates.towards === 'left') { // Flicking Closed 618 action.translate.easeTo(0); 619 } else if ( 620 (cache.simpleStates.flick && cache.simpleStates.towards === 'right') || // Flicking Open OR 621 (cache.simpleStates.halfway || cache.simpleStates.hyperExtending) // At least halfway open OR hyperextending 622 ) { 623 action.translate.easeTo(settings.maxPosition); // Open Left 624 } 625 } else { 626 action.translate.easeTo(0); // Close Left 627 } 628 // Revealing Right 629 } else if (cache.simpleStates.opening === 'right') { 630 // Halfway, Flicking, or Too Far Out 631 if ((cache.simpleStates.halfway || cache.simpleStates.hyperExtending || cache.simpleStates.flick)) { 632 if (cache.simpleStates.flick && cache.simpleStates.towards === 'right') { // Flicking Closed 633 action.translate.easeTo(0); 634 } else if ( 635 (cache.simpleStates.flick && cache.simpleStates.towards === 'left') || // Flicking Open OR 636 (cache.simpleStates.halfway || cache.simpleStates.hyperExtending) // At least halfway open OR hyperextending 637 ) { 638 action.translate.easeTo(settings.minPosition); // Open Right 639 } 640 } else { 641 action.translate.easeTo(0); // Close Right 642 } 643 } 644 cache.isDragging = false;
645 cache.startDragX = utils.page('X', e); 646 } 647 } 648 } 649 }; 650 651 652 // Initialize 653 if (opts.element) { 654 utils.extend(settings, opts); 655 cache.vendor = utils.vendor(); 656 cache.canTransform = utils.canTransform(); 657 action.drag.listen(); 658 } 659 }; 660 661 662 utils.extend(Core.prototype, { 663 664 /** 665 * Opens the specified side menu 666 * @param {String} side Must be "left" or "right" 667 */ 668 open: function(side, cb) { 669 utils.dispatchEvent('open'); 670 utils.klass.remove(doc.body, 'snapjs-expand-left'); 671 utils.klass.remove(doc.body, 'snapjs-expand-right'); 672 673 if (side === 'left') { 674 this.cache.simpleStates.opening = 'left'; 675 this.cache.simpleStates.towards = 'right'; 676 utils.klass.add(doc.body, 'snapjs-left'); 677 utils.klass.remove(doc.body, 'snapjs-right'); 678 this.action.translate.easeTo(this.settings.maxPosition, cb); 679 } else if (side === 'right') { 680 this.cache.simpleStates.opening = 'right'; 681 this.cache.simpleStates.towards = 'left'; 682 utils.klass.remove(doc.body, 'snapjs-left'); 683 utils.klass.add(doc.body, 'snapjs-right'); 684 this.action.translate.easeTo(this.settings.minPosition, cb); 685 } 686 }, 687 688 /** 689 * Closes the pane 690 */ 691 close: function(cb) { 692 utils.dispatchEvent('close'); 693 this.action.translate.easeTo(0, cb); 694 }, 695 696 /** 697 * Hides the content pane completely allowing for full menu visibility 698 * @param {String} side Must be "left" or "right" 699 */ 700 expand: function(side){ 701 var to = win.innerWidth || doc.documentElement.clientWidth; 702 703 if(side==='left'){ 704 utils.dispatchEvent('expandLeft'); 705 utils.klass.add(doc.body, 'snapjs-expand-left'); 706 utils.klass.remove(doc.body, 'snapjs-expand-right'); 707 } else { 708 utils.dispatchEvent('expandRight'); 709 utils.klass.add(doc.body, 'snapjs-expand-right'); 710 utils.klass.remove(doc.body, 'snapjs-expand-left'); 711 to *= -1; 712 } 713 this.action.translate.easeTo(to); 714 }, 715 716 /** 717 * Listen in to custom Snap events 718 * @param {String} evt The snap event name 719 * @param {Function} fn Callback function 720 * @return {Object} Snap instance 721 */ 722 on: function(evt, fn) { 723 this.eventList[evt] = fn; 724 return this; 725 }, 726 727 /** 728 * Stops listening to custom Snap events 729 * @param {String} evt The snap event name 730 */ 731 off: function(evt) { 732 if (this.eventList[evt]) { 733 this.eventList[evt] = false; 734 } 735 }, 736 737 /** 738 * Enables Snap.js events 739 */ 740 enable: function() { 741 utils.dispatchEvent('enable'); 742 this.action.drag.listen(); 743 }, 744 745 /** 746 * Disables Snap.js events 747 */ 748 disable: function() { 749 utils.dispatchEvent('disable'); 750 this.action.drag.stopListening(); 751 }, 752 753 /** 754 * Updates the instances settings 755 * @param {Object} opts The Snap options to set 756 */ 757 settings: function(opts){ 758 utils.extend(this.settings, opts); 759 }, 760 761 /** 762 * Returns information about the state of the content pane 763 * @return {Object} Information regarding the state of the pane 764 */ 765 state: function() { 766 var state, 767 fromLeft = this.action.translate.get.matrix(4); 768 if (fromLeft === this.settings.maxPosition) { 769 state = 'left'; 770 } else if (fromLeft === this.settings.minPosition) { 771 state = 'right'; 772 } else { 773 state = 'closed'; 774 } 775 return { 776 state: state, 777 info: this.cache.simpleStates 778 }; 779 } 780 }); 781 782 // Assign to the global namespace 783 this[Namespace] = Core; 784 785}).call(this, window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.