Swipe to Archive for Inbox

Swipe to Archive for Google's Inbox

  1. // ==UserScript==
  2. // @name Swipe to Archive for Inbox
  3. // @namespace maurizi.org
  4. // @description Swipe to Archive for Google's Inbox
  5. // @include https://inbox.google.com/u/0/
  6. // @include https://inbox.google.com/u/1/
  7. // @include https://inbox.google.com/u/2/
  8. // @include https://inbox.google.com/u/3/
  9. // @include https://inbox.google.com/u/4/
  10. // @version 1
  11. // @grant none
  12. // ==/UserScript==
  13. /*! @license
  14. Slip.js 1.2.0
  15.  
  16. © 2014 Kornel Lesiński <kornel@geekhood.net>. All rights reserved.
  17.  
  18. Redistribution and use in source and binary forms, with or without modification,
  19. are permitted provided that the following conditions are met:
  20.  
  21. 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  22.  
  23. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
  24. the following disclaimer in the documentation and/or other materials provided with the distribution.
  25.  
  26. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  27. INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  28. DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  29. SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  30. SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  31. WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
  32. USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  33. */
  34.  
  35. window['Slip'] = (function(){
  36. 'use strict';
  37.  
  38. var damnYouChrome = /Chrome\/[34]/.test(navigator.userAgent); // For bugs that can't be programmatically detected :( Intended to catch all versions of Chrome 30-40
  39. var needsBodyHandlerHack = damnYouChrome; // Otherwise I _sometimes_ don't get any touchstart events and only clicks instead.
  40.  
  41. /* When dragging elements down in Chrome (tested 34-37) dragged element may appear below stationary elements.
  42. Looks like WebKit bug #61824, but iOS Safari doesn't have that problem. */
  43. var compositorDoesNotOrderLayers = damnYouChrome;
  44.  
  45. // -webkit-mess
  46. var testElement = document.createElement('div');
  47.  
  48. var transitionPrefix = "webkitTransition" in testElement.style ? "webkitTransition" : "transition";
  49. var transformPrefix = "webkitTransform" in testElement.style ? "webkitTransform" : "transform";
  50. var transformProperty = transformPrefix === "webkitTransform" ? "-webkit-transform" : "transform";
  51. var userSelectPrefix = "webkitUserSelect" in testElement.style ? "webkitUserSelect" : "userSelect";
  52.  
  53. testElement.style[transformPrefix] = 'translateZ(0)';
  54. var hwLayerMagic = testElement.style[transformPrefix] ? 'translateZ(0) ' : '';
  55. var hwTopLayerMagic = testElement.style[transformPrefix] ? 'translateZ(1px) ' : '';
  56. testElement = null;
  57.  
  58. var globalInstances = 0;
  59. var attachedBodyHandlerHack = false;
  60. var nullHandler = function(){};
  61.  
  62. function Slip(container, options) {
  63. if ('string' === typeof container) container = document.querySelector(container);
  64. if (!container || !container.addEventListener) throw new Error("Please specify DOM node to attach to");
  65.  
  66. if (!this || this === window) return new Slip(container, options);
  67.  
  68. this.options = options = options || {};
  69. this.options.keepSwipingPercent = options.keepSwipingPercent || 0;
  70. this.options.minimumSwipeVelocity = options.minimumSwipeVelocity || 1;
  71. this.options.minimumSwipeTime = options.minimumSwipeTime || 110;
  72.  
  73. // Functions used for as event handlers need usable `this` and must not change to be removable
  74. this.cancel = this.setState.bind(this, this.states.idle);
  75. this.onTouchStart = this.onTouchStart.bind(this);
  76. this.onTouchMove = this.onTouchMove.bind(this);
  77. this.onTouchEnd = this.onTouchEnd.bind(this);
  78. this.onMouseDown = this.onMouseDown.bind(this);
  79. this.onMouseMove = this.onMouseMove.bind(this);
  80. this.onMouseUp = this.onMouseUp.bind(this);
  81. this.onMouseLeave = this.onMouseLeave.bind(this);
  82. this.onSelection = this.onSelection.bind(this);
  83.  
  84. this.setState(this.states.idle);
  85. this.attach(container);
  86. }
  87.  
  88. function getTransform(node) {
  89. var transform = node.style[transformPrefix];
  90. if (transform) {
  91. return {
  92. value:transform,
  93. original:transform,
  94. };
  95. }
  96.  
  97. if (window.getComputedStyle) {
  98. var style = window.getComputedStyle(node).getPropertyValue(transformProperty);
  99. if (style && style !== 'none') return {value:style, original:''};
  100. }
  101. return {value:'', original:''};
  102. }
  103.  
  104. function findIndex(target, nodes) {
  105. var originalIndex = 0;
  106. var listCount = 0;
  107.  
  108. for (var i=0; i < nodes.length; i++) {
  109. if (nodes[i].nodeType === 1) {
  110. listCount++;
  111. if (nodes[i] === target.node) {
  112. originalIndex = listCount-1;
  113. }
  114. }
  115. }
  116.  
  117. return originalIndex;
  118. }
  119.  
  120. // All functions in states are going to be executed in context of Slip object
  121. Slip.prototype = {
  122.  
  123. container: null,
  124. options: {},
  125. state: null,
  126.  
  127. target: null, // the tapped/swiped/reordered node with height and backed up styles
  128.  
  129. usingTouch: false, // there's no good way to detect touchscreen preference other than receiving a touch event (really, trust me).
  130. mouseHandlersAttached: false,
  131.  
  132. startPosition: null, // x,y,time where first touch began
  133. latestPosition: null, // x,y,time where the finger is currently
  134. previousPosition: null, // x,y,time where the finger was ~100ms ago (for velocity calculation)
  135.  
  136. canPreventScrolling: false,
  137.  
  138. states: {
  139. idle: function idleStateInit() {
  140. this.target = null;
  141. this.usingTouch = false;
  142. this.removeMouseHandlers();
  143.  
  144. return {
  145. allowTextSelection: true,
  146. };
  147. },
  148.  
  149. undecided: function undecidedStateInit() {
  150. this.target.height = this.target.node.offsetHeight;
  151. this.target.node.style[transitionPrefix] = '';
  152.  
  153. if (!this.dispatch(this.target.originalTarget, 'beforewait')) {
  154. if (this.dispatch(this.target.originalTarget, 'beforereorder')) {
  155. this.setState(this.states.reorder);
  156. }
  157. } else {
  158. var holdTimer = setTimeout(function(){
  159. var move = this.getAbsoluteMovement();
  160. if (this.canPreventScrolling && move.x < 15 && move.y < 25) {
  161. if (this.dispatch(this.target.originalTarget, 'beforereorder')) {
  162. this.setState(this.states.reorder);
  163. }
  164. }
  165. }.bind(this), 300);
  166. }
  167.  
  168. return {
  169. leaveState: function() {
  170. clearTimeout(holdTimer);
  171. },
  172.  
  173. onMove: function() {
  174. var move = this.getAbsoluteMovement();
  175.  
  176. if (move.x > 20 && move.y < Math.max(100, this.target.height)) {
  177. if (this.dispatch(this.target.originalTarget, 'beforeswipe', {directionX: move.directionX, directionY: move.directionY})) {
  178. this.setState(this.states.swipe);
  179. return false;
  180. } else {
  181. this.setState(this.states.idle);
  182. }
  183. }
  184. if (move.y > 20) {
  185. this.setState(this.states.idle);
  186. }
  187.  
  188. // Chrome likes sideways scrolling :(
  189. if (move.x > move.y*1.2) return false;
  190. },
  191.  
  192. onLeave: function() {
  193. this.setState(this.states.idle);
  194. },
  195.  
  196. onEnd: function() {
  197. var allowDefault = this.dispatch(this.target.originalTarget, 'tap');
  198. this.setState(this.states.idle);
  199. return allowDefault;
  200. },
  201. };
  202. },
  203.  
  204. swipe: function swipeStateInit() {
  205. var swipeSuccess = false;
  206. var container = this.container;
  207.  
  208. var originalIndex = findIndex(this.target, this.container.childNodes);
  209.  
  210. container.className += ' slip-swiping-container';
  211. function removeClass() {
  212. container.className = container.className.replace(/(?:^| )slip-swiping-container/,'');
  213. }
  214.  
  215. this.target.height = this.target.node.offsetHeight;
  216.  
  217. return {
  218. leaveState: function() {
  219. if (swipeSuccess) {
  220. this.animateSwipe(function(target){
  221. target.node.style[transformPrefix] = target.baseTransform.original;
  222. target.node.style[transitionPrefix] = '';
  223. if (this.dispatch(target.node, 'afterswipe')) {
  224. removeClass();
  225. return true;
  226. } else {
  227. this.animateToZero(undefined, target);
  228. }
  229. }.bind(this));
  230. } else {
  231. this.animateToZero(removeClass);
  232. this.dispatch(this.target.node, 'cancelswipe');
  233. }
  234. },
  235.  
  236. onMove: function() {
  237. var move = this.getTotalMovement();
  238.  
  239. if (Math.abs(move.y) < this.target.height+20) {
  240. this.target.node.style[transformPrefix] = 'translate(' + move.x + 'px,0) ' + hwLayerMagic + this.target.baseTransform.value;
  241. return false;
  242. } else {
  243. this.setState(this.states.idle);
  244. }
  245. },
  246.  
  247. onLeave: function() {
  248. this.state.onEnd.call(this);
  249. },
  250.  
  251. onEnd: function() {
  252. var move = this.getAbsoluteMovement();
  253. var velocity = move.x / move.time;
  254.  
  255. // How far out has the item been swiped?
  256. var swipedPercent = Math.abs((this.startPosition.x - this.previousPosition.x) / this.container.clientWidth) * 100;
  257.  
  258. var swiped = (velocity > this.options.minimumSwipeVelocity && move.time > this.options.minimumSwipeTime) || (this.options.keepSwipingPercent && swipedPercent > this.options.keepSwipingPercent);
  259.  
  260. if (swiped) {
  261. if (this.dispatch(this.target.node, 'swipe', {direction: move.directionX, originalIndex: originalIndex})) {
  262. swipeSuccess = true; // can't animate here, leaveState overrides anim
  263. }
  264. }
  265. this.setState(this.states.idle);
  266. return !swiped;
  267. },
  268. };
  269. },
  270.  
  271. reorder: function reorderStateInit() {
  272. this.target.height = this.target.node.offsetHeight;
  273.  
  274. var nodes = this.container.childNodes;
  275. var originalIndex = findIndex(this.target, nodes);
  276. var mouseOutsideTimer;
  277. var zero = this.target.node.offsetTop + this.target.height/2;
  278. var otherNodes = [];
  279. for(var i=0; i < nodes.length; i++) {
  280. if (nodes[i].nodeType != 1 || nodes[i] === this.target.node) continue;
  281. var t = nodes[i].offsetTop;
  282. nodes[i].style[transitionPrefix] = transformProperty + ' 0.2s ease-in-out';
  283. otherNodes.push({
  284. node: nodes[i],
  285. baseTransform: getTransform(nodes[i]),
  286. pos: t + (t < zero ? nodes[i].offsetHeight : 0) - zero,
  287. });
  288. }
  289.  
  290. this.target.node.className += ' slip-reordering';
  291. this.target.node.style.zIndex = '99999';
  292. this.target.node.style[userSelectPrefix] = 'none';
  293. if (compositorDoesNotOrderLayers) {
  294. // Chrome's compositor doesn't sort 2D layers
  295. this.container.style.webkitTransformStyle = 'preserve-3d';
  296. }
  297.  
  298. function setPosition() {
  299. /*jshint validthis:true */
  300.  
  301. if (mouseOutsideTimer) {
  302. // don't care where the mouse is as long as it moves
  303. clearTimeout(mouseOutsideTimer); mouseOutsideTimer = null;
  304. }
  305.  
  306. var move = this.getTotalMovement();
  307. this.target.node.style[transformPrefix] = 'translate(0,' + move.y + 'px) ' + hwTopLayerMagic + this.target.baseTransform.value;
  308.  
  309. var height = this.target.height;
  310. otherNodes.forEach(function(o){
  311. var off = 0;
  312. if (o.pos < 0 && move.y < 0 && o.pos > move.y) {
  313. off = height;
  314. }
  315. else if (o.pos > 0 && move.y > 0 && o.pos < move.y) {
  316. off = -height;
  317. }
  318. // FIXME: should change accelerated/non-accelerated state lazily
  319. o.node.style[transformPrefix] = off ? 'translate(0,'+off+'px) ' + hwLayerMagic + o.baseTransform.value : o.baseTransform.original;
  320. });
  321. return false;
  322. }
  323.  
  324. setPosition.call(this);
  325.  
  326. return {
  327. leaveState: function() {
  328. if (mouseOutsideTimer) clearTimeout(mouseOutsideTimer);
  329.  
  330. if (compositorDoesNotOrderLayers) {
  331. this.container.style.webkitTransformStyle = '';
  332. }
  333.  
  334. this.target.node.className = this.target.node.className.replace(/(?:^| )slip-reordering/,'');
  335. this.target.node.style[userSelectPrefix] = '';
  336.  
  337. this.animateToZero(function(target){
  338. target.node.style.zIndex = '';
  339. });
  340. otherNodes.forEach(function(o){
  341. o.node.style[transformPrefix] = o.baseTransform.original;
  342. o.node.style[transitionPrefix] = ''; // FIXME: animate to new position
  343. });
  344. },
  345.  
  346. onMove: setPosition,
  347.  
  348. onLeave: function() {
  349. // don't let element get stuck if mouse left the window
  350. // but don't cancel immediately as it'd be annoying near window edges
  351. if (mouseOutsideTimer) clearTimeout(mouseOutsideTimer);
  352. mouseOutsideTimer = setTimeout(function(){
  353. mouseOutsideTimer = null;
  354. this.cancel();
  355. }.bind(this), 700);
  356. },
  357.  
  358. onEnd: function() {
  359. var move = this.getTotalMovement();
  360. if (move.y < 0) {
  361. for(var i=0; i < otherNodes.length; i++) {
  362. if (otherNodes[i].pos > move.y) {
  363. this.dispatch(this.target.node, 'reorder', {spliceIndex:i, insertBefore:otherNodes[i].node, originalIndex: originalIndex});
  364. break;
  365. }
  366. }
  367. } else {
  368. for(var i=otherNodes.length-1; i >= 0; i--) {
  369. if (otherNodes[i].pos < move.y) {
  370. this.dispatch(this.target.node, 'reorder', {spliceIndex:i+1, insertBefore:otherNodes[i+1] ? otherNodes[i+1].node : null, originalIndex: originalIndex});
  371. break;
  372. }
  373. }
  374. }
  375. this.setState(this.states.idle);
  376. return false;
  377. },
  378. };
  379. },
  380. },
  381.  
  382. attach: function(container) {
  383. globalInstances++;
  384. if (this.container) this.detach();
  385.  
  386. // In some cases taps on list elements send *only* click events and no touch events. Spotted only in Chrome 32+
  387. // Having event listener on body seems to solve the issue (although AFAIK may disable smooth scrolling as a side-effect)
  388. if (!attachedBodyHandlerHack && needsBodyHandlerHack) {
  389. attachedBodyHandlerHack = true;
  390. document.body.addEventListener('touchstart', nullHandler, false);
  391. }
  392.  
  393. this.container = container;
  394. this.otherNodes = [];
  395.  
  396. // selection on iOS interferes with reordering
  397. document.addEventListener("selectionchange", this.onSelection, false);
  398.  
  399. // cancel is called e.g. when iOS detects multitasking gesture
  400. this.container.addEventListener('touchcancel', this.cancel, false);
  401. this.container.addEventListener('touchstart', this.onTouchStart, false);
  402. this.container.addEventListener('touchmove', this.onTouchMove, false);
  403. this.container.addEventListener('touchend', this.onTouchEnd, false);
  404. this.container.addEventListener('mousedown', this.onMouseDown, false);
  405. // mousemove and mouseup are attached dynamically
  406. },
  407.  
  408. detach: function() {
  409. this.cancel();
  410.  
  411. this.container.removeEventListener('mousedown', this.onMouseDown, false);
  412. this.container.removeEventListener('touchend', this.onTouchEnd, false);
  413. this.container.removeEventListener('touchmove', this.onTouchMove, false);
  414. this.container.removeEventListener('touchstart', this.onTouchStart, false);
  415. this.container.removeEventListener('touchcancel', this.cancel, false);
  416.  
  417. document.removeEventListener("selectionchange", this.onSelection, false);
  418.  
  419. globalInstances--;
  420. if (!globalInstances && attachedBodyHandlerHack) {
  421. attachedBodyHandlerHack = false;
  422. document.body.removeEventListener('touchstart', nullHandler, false);
  423. }
  424. },
  425.  
  426. setState: function(newStateCtor){
  427. if (this.state) {
  428. if (this.state.ctor === newStateCtor) return;
  429. if (this.state.leaveState) this.state.leaveState.call(this);
  430. }
  431.  
  432. // Must be re-entrant in case ctor changes state
  433. var prevState = this.state;
  434. var nextState = newStateCtor.call(this);
  435. if (this.state === prevState) {
  436. nextState.ctor = newStateCtor;
  437. this.state = nextState;
  438. }
  439. },
  440.  
  441. findTargetNode: function(targetNode) {
  442. while(targetNode && targetNode.parentNode !== this.container) {
  443. targetNode = targetNode.parentNode;
  444. }
  445. return targetNode;
  446. },
  447.  
  448. onSelection: function(e) {
  449. var isRelated = e.target === document || this.findTargetNode(e);
  450. if (!isRelated) return;
  451.  
  452. if (e.cancelable || e.defaultPrevented) {
  453. if (!this.state.allowTextSelection) {
  454. e.preventDefault();
  455. }
  456. } else {
  457. // iOS doesn't allow selection to be prevented
  458. this.setState(this.states.idle);
  459. }
  460. },
  461.  
  462. addMouseHandlers: function() {
  463. // unlike touch events, mousemove/up is not conveniently fired on the same element,
  464. // but I don't need to listen to unrelated events all the time
  465. if (!this.mouseHandlersAttached) {
  466. this.mouseHandlersAttached = true;
  467. document.documentElement.addEventListener('mouseleave', this.onMouseLeave, false);
  468. window.addEventListener('mousemove', this.onMouseMove, true);
  469. window.addEventListener('mouseup', this.onMouseUp, true);
  470. window.addEventListener('blur', this.cancel, false);
  471. }
  472. },
  473.  
  474. removeMouseHandlers: function() {
  475. if (this.mouseHandlersAttached) {
  476. this.mouseHandlersAttached = false;
  477. document.documentElement.removeEventListener('mouseleave', this.onMouseLeave, false);
  478. window.removeEventListener('mousemove', this.onMouseMove, true);
  479. window.removeEventListener('mouseup', this.onMouseUp, true);
  480. window.removeEventListener('blur', this.cancel, false);
  481. }
  482. },
  483.  
  484. onMouseLeave: function(e) {
  485. if (this.usingTouch) return;
  486.  
  487. if (e.target === document.documentElement || e.relatedTarget === document.documentElement) {
  488. if (this.state.onLeave) {
  489. this.state.onLeave.call(this);
  490. }
  491. }
  492. },
  493.  
  494. onMouseDown: function(e) {
  495. if (this.usingTouch || e.button != 0 || !this.setTarget(e)) return;
  496.  
  497. this.addMouseHandlers(); // mouseup, etc.
  498.  
  499. this.canPreventScrolling = true; // or rather it doesn't apply to mouse
  500.  
  501. this.startAtPosition({
  502. x: e.clientX,
  503. y: e.clientY,
  504. time: e.timeStamp,
  505. });
  506. },
  507.  
  508. onTouchStart: function(e) {
  509. this.usingTouch = true;
  510. this.canPreventScrolling = true;
  511.  
  512. // This implementation cares only about single touch
  513. if (e.touches.length > 1) {
  514. this.setState(this.states.idle);
  515. return;
  516. }
  517.  
  518. if (!this.setTarget(e)) return;
  519.  
  520. this.startAtPosition({
  521. x: e.touches[0].clientX,
  522. y: e.touches[0].clientY - window.scrollY,
  523. time: e.timeStamp,
  524. });
  525. },
  526.  
  527. setTarget: function(e) {
  528. var targetNode = this.findTargetNode(e.target);
  529. if (!targetNode) {
  530. this.setState(this.states.idle);
  531. return false;
  532. }
  533.  
  534. //check for a scrollable parent
  535. var scrollContainer = targetNode.parentNode;
  536. while (scrollContainer){
  537. if (scrollContainer.scrollHeight > scrollContainer.clientHeight && window.getComputedStyle(scrollContainer)['overflow-y'] != 'visible') break;
  538. else scrollContainer = scrollContainer.parentNode;
  539. }
  540.  
  541. this.target = {
  542. originalTarget: e.target,
  543. node: targetNode,
  544. scrollContainer: scrollContainer,
  545. baseTransform: getTransform(targetNode),
  546. };
  547. return true;
  548. },
  549.  
  550. startAtPosition: function(pos) {
  551. this.startPosition = this.previousPosition = this.latestPosition = pos;
  552. this.setState(this.states.undecided);
  553. },
  554.  
  555. updatePosition: function(e, pos) {
  556. if(this.target == null)
  557. return;
  558. this.latestPosition = pos;
  559.  
  560. var triggerOffset = 40,
  561. offset = 0;
  562.  
  563. var scrollable = this.target.scrollContainer || document.body,
  564. containerRect = scrollable.getBoundingClientRect(),
  565. targetRect = this.target.node.getBoundingClientRect(),
  566. bottomOffset = Math.min(containerRect.bottom, window.innerHeight) - targetRect.bottom,
  567. topOffset = targetRect.top - Math.max(containerRect.top, 0);
  568.  
  569. if (bottomOffset < triggerOffset){
  570. offset = triggerOffset - bottomOffset;
  571. }
  572. else if (topOffset < triggerOffset){
  573. offset = topOffset - triggerOffset;
  574. }
  575.  
  576. var prevScrollTop = scrollable.scrollTop;
  577. scrollable.scrollTop += offset;
  578. if (prevScrollTop != scrollable.scrollTop) this.startPosition.y += prevScrollTop-scrollable.scrollTop;
  579.  
  580. if (this.state.onMove) {
  581. if (this.state.onMove.call(this) === false) {
  582. e.preventDefault();
  583. }
  584. }
  585.  
  586. // sample latestPosition 100ms for velocity
  587. if (this.latestPosition.time - this.previousPosition.time > 100) {
  588. this.previousPosition = this.latestPosition;
  589. }
  590. },
  591.  
  592. onMouseMove: function(e) {
  593. this.updatePosition(e, {
  594. x: e.clientX,
  595. y: e.clientY,
  596. time: e.timeStamp,
  597. });
  598. },
  599.  
  600. onTouchMove: function(e) {
  601. this.updatePosition(e, {
  602. x: e.touches[0].clientX,
  603. y: e.touches[0].clientY - window.scrollY,
  604. time: e.timeStamp,
  605. });
  606.  
  607. // In Apple's touch model only the first move event after touchstart can prevent scrolling (and event.cancelable is broken)
  608. this.canPreventScrolling = false;
  609. },
  610.  
  611. onMouseUp: function(e) {
  612. if (this.usingTouch || e.button !== 0) return;
  613.  
  614. if (this.state.onEnd && false === this.state.onEnd.call(this)) {
  615. e.preventDefault();
  616. }
  617. },
  618.  
  619. onTouchEnd: function(e) {
  620. if (e.touches.length > 1) {
  621. this.cancel();
  622. } else if (this.state.onEnd && false === this.state.onEnd.call(this)) {
  623. e.preventDefault();
  624. }
  625. },
  626.  
  627. getTotalMovement: function() {
  628. return {
  629. x:this.latestPosition.x - this.startPosition.x,
  630. y:this.latestPosition.y - this.startPosition.y,
  631. };
  632. },
  633.  
  634. getAbsoluteMovement: function() {
  635. return {
  636. x: Math.abs(this.latestPosition.x - this.startPosition.x),
  637. y: Math.abs(this.latestPosition.y - this.startPosition.y),
  638. time:this.latestPosition.time - this.startPosition.time,
  639. directionX:this.latestPosition.x - this.startPosition.x < 0 ? 'left' : 'right',
  640. directionY:this.latestPosition.y - this.startPosition.y < 0 ? 'up' : 'down',
  641. };
  642. },
  643.  
  644. dispatch: function(targetNode, eventName, detail) {
  645. var event = document.createEvent('CustomEvent');
  646. if (event && event.initCustomEvent) {
  647. event.initCustomEvent('slip:' + eventName, true, true, detail);
  648. } else {
  649. event = document.createEvent('Event');
  650. event.initEvent('slip:' + eventName, true, true);
  651. event.detail = detail;
  652. }
  653. return targetNode.dispatchEvent(event);
  654. },
  655.  
  656. getSiblings: function(target) {
  657. var siblings = [];
  658. var tmp = target.node.nextSibling;
  659. while(tmp) {
  660. if (tmp.nodeType == 1) siblings.push({
  661. node: tmp,
  662. baseTransform: getTransform(tmp),
  663. });
  664. tmp = tmp.nextSibling;
  665. }
  666. return siblings;
  667. },
  668.  
  669. animateToZero: function(callback, target) {
  670. // save, because this.target/container could change during animation
  671. target = target || this.target;
  672.  
  673. target.node.style[transitionPrefix] = transformProperty + ' 0.1s ease-out';
  674. target.node.style[transformPrefix] = 'translate(0,0) ' + hwLayerMagic + target.baseTransform.value;
  675. setTimeout(function(){
  676. target.node.style[transitionPrefix] = '';
  677. target.node.style[transformPrefix] = target.baseTransform.original;
  678. if (callback) callback.call(this, target);
  679. }.bind(this), 101);
  680. },
  681.  
  682. animateSwipe: function(callback) {
  683. var target = this.target;
  684. var siblings = this.getSiblings(target);
  685. var emptySpaceTransform = 'translate(0,' + this.target.height + 'px) ' + hwLayerMagic + ' ';
  686.  
  687. // FIXME: animate with real velocity
  688. target.node.style[transitionPrefix] = 'all 0.1s linear';
  689. target.node.style[transformPrefix] = ' translate(' + (this.getTotalMovement().x > 0 ? '' : '-') + '100%,0) ' + hwLayerMagic + target.baseTransform.value;
  690.  
  691. setTimeout(function(){
  692. if (callback.call(this, target)) {
  693. siblings.forEach(function(o){
  694. o.node.style[transitionPrefix] = '';
  695. o.node.style[transformPrefix] = emptySpaceTransform + o.baseTransform.value;
  696. });
  697. setTimeout(function(){
  698. siblings.forEach(function(o){
  699. o.node.style[transitionPrefix] = transformProperty + ' 0.1s ease-in-out';
  700. o.node.style[transformPrefix] = 'translate(0,0) ' + hwLayerMagic + o.baseTransform.value;
  701. });
  702. setTimeout(function(){
  703. siblings.forEach(function(o){
  704. o.node.style[transitionPrefix] = '';
  705. o.node.style[transformPrefix] = o.baseTransform.original;
  706. });
  707. },101);
  708. }, 1);
  709. }
  710. }.bind(this), 101);
  711. },
  712. };
  713.  
  714. // AMD
  715. if ('function' === typeof define && define.amd) {
  716. define(function(){
  717. return Slip;
  718. });
  719. }
  720. return Slip;
  721. })();
  722.  
  723. /*
  724. Slip - swiping and reordering in lists of elements on touch screens, no fuss.
  725.  
  726. Fires these events on list elements:
  727.  
  728. • slip:swipe
  729. When swipe has been done and user has lifted finger off the screen.
  730. If you execute event.preventDefault() the element will be animated back to original position.
  731. Otherwise it will be animated off the list and set to display:none.
  732.  
  733. • slip:beforeswipe
  734. Fired before first swipe movement starts.
  735. If you execute event.preventDefault() then element will not move at all.
  736.  
  737. • slip:reorder
  738. Element has been dropped in new location. event.detail contains the location:
  739. • insertBefore: DOM node before which element has been dropped (null is the end of the list). Use with node.insertBefore().
  740. • spliceIndex: Index of element before which current element has been dropped, not counting the element iself.
  741. For use with Array.splice() if the list is reflecting objects in some array.
  742.  
  743. • slip:beforereorder
  744. When reordering movement starts.
  745. Element being reordered gets class `slip-reordering`.
  746. If you execute event.preventDefault() then element will not move at all.
  747.  
  748. • slip:beforewait
  749. If you execute event.preventDefault() then reordering will begin immediately, blocking ability to scroll the page.
  750.  
  751. • slip:tap
  752. When element was tapped without being swiped/reordered.
  753.  
  754. • slip:cancelswipe
  755. Fired when the user stops dragging and the element returns to its original position.
  756.  
  757.  
  758. Usage:
  759.  
  760. CSS:
  761. You should set `user-select:none` (and WebKit prefixes, sigh) on list elements,
  762. otherwise unstoppable and glitchy text selection in iOS will get in the way.
  763.  
  764. You should set `overflow-x: hidden` on the container or body to prevent horizontal scrollbar
  765. appearing when elements are swiped off the list.
  766.  
  767.  
  768. var list = document.querySelector('ul#slippylist');
  769. new Slip(list);
  770.  
  771. list.addEventListener('slip:beforeswipe', function(e) {
  772. if (shouldNotSwipe(e.target)) e.preventDefault();
  773. });
  774.  
  775. list.addEventListener('slip:swipe', function(e) {
  776. // e.target swiped
  777. if (thatWasSwipeToRemove) {
  778. e.target.parentNode.removeChild(e.target);
  779. } else {
  780. e.preventDefault(); // will animate back to original position
  781. }
  782. });
  783.  
  784. list.addEventListener('slip:beforereorder', function(e) {
  785. if (shouldNotReorder(e.target)) e.preventDefault();
  786. });
  787.  
  788. list.addEventListener('slip:reorder', function(e) {
  789. // e.target reordered.
  790. if (reorderedOK) {
  791. e.target.parentNode.insertBefore(e.target, e.detail.insertBefore);
  792. } else {
  793. e.preventDefault();
  794. }
  795. });
  796.  
  797. Requires:
  798. • Touch events
  799. • CSS transforms
  800. • Function.bind()
  801.  
  802. Caveats:
  803. • Elements must not change size while reordering or swiping takes place (otherwise it will be visually out of sync)
  804. */
  805. /*! @license
  806. Slip.js 1.2.0
  807.  
  808. © 2014 Kornel Lesiński <kornel@geekhood.net>. All rights reserved.
  809.  
  810. Redistribution and use in source and binary forms, with or without modification,
  811. are permitted provided that the following conditions are met:
  812.  
  813. 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  814.  
  815. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
  816. the following disclaimer in the documentation and/or other materials provided with the distribution.
  817.  
  818. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
  819. INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  820. DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  821. SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  822. SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
  823. WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
  824. USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  825. */
  826.  
  827. window['Slip'] = (function(){
  828. 'use strict';
  829.  
  830. var damnYouChrome = /Chrome\/[34]/.test(navigator.userAgent); // For bugs that can't be programmatically detected :( Intended to catch all versions of Chrome 30-40
  831. var needsBodyHandlerHack = damnYouChrome; // Otherwise I _sometimes_ don't get any touchstart events and only clicks instead.
  832.  
  833. /* When dragging elements down in Chrome (tested 34-37) dragged element may appear below stationary elements.
  834. Looks like WebKit bug #61824, but iOS Safari doesn't have that problem. */
  835. var compositorDoesNotOrderLayers = damnYouChrome;
  836.  
  837. // -webkit-mess
  838. var testElement = document.createElement('div');
  839.  
  840. var transitionPrefix = "webkitTransition" in testElement.style ? "webkitTransition" : "transition";
  841. var transformPrefix = "webkitTransform" in testElement.style ? "webkitTransform" : "transform";
  842. var transformProperty = transformPrefix === "webkitTransform" ? "-webkit-transform" : "transform";
  843. var userSelectPrefix = "webkitUserSelect" in testElement.style ? "webkitUserSelect" : "userSelect";
  844.  
  845. testElement.style[transformPrefix] = 'translateZ(0)';
  846. var hwLayerMagic = testElement.style[transformPrefix] ? 'translateZ(0) ' : '';
  847. var hwTopLayerMagic = testElement.style[transformPrefix] ? 'translateZ(1px) ' : '';
  848. testElement = null;
  849.  
  850. var globalInstances = 0;
  851. var attachedBodyHandlerHack = false;
  852. var nullHandler = function(){};
  853.  
  854. function Slip(container, options) {
  855. if ('string' === typeof container) container = document.querySelector(container);
  856. if (!container || !container.addEventListener) throw new Error("Please specify DOM node to attach to");
  857.  
  858. if (!this || this === window) return new Slip(container, options);
  859.  
  860. this.options = options = options || {};
  861. this.options.keepSwipingPercent = options.keepSwipingPercent || 0;
  862. this.options.minimumSwipeVelocity = options.minimumSwipeVelocity || 1;
  863. this.options.minimumSwipeTime = options.minimumSwipeTime || 110;
  864.  
  865. // Functions used for as event handlers need usable `this` and must not change to be removable
  866. this.cancel = this.setState.bind(this, this.states.idle);
  867. this.onTouchStart = this.onTouchStart.bind(this);
  868. this.onTouchMove = this.onTouchMove.bind(this);
  869. this.onTouchEnd = this.onTouchEnd.bind(this);
  870. this.onMouseDown = this.onMouseDown.bind(this);
  871. this.onMouseMove = this.onMouseMove.bind(this);
  872. this.onMouseUp = this.onMouseUp.bind(this);
  873. this.onMouseLeave = this.onMouseLeave.bind(this);
  874. this.onSelection = this.onSelection.bind(this);
  875.  
  876. this.setState(this.states.idle);
  877. this.attach(container);
  878. }
  879.  
  880. function getTransform(node) {
  881. var transform = node.style[transformPrefix];
  882. if (transform) {
  883. return {
  884. value:transform,
  885. original:transform,
  886. };
  887. }
  888.  
  889. if (window.getComputedStyle) {
  890. var style = window.getComputedStyle(node).getPropertyValue(transformProperty);
  891. if (style && style !== 'none') return {value:style, original:''};
  892. }
  893. return {value:'', original:''};
  894. }
  895.  
  896. function findIndex(target, nodes) {
  897. var originalIndex = 0;
  898. var listCount = 0;
  899.  
  900. for (var i=0; i < nodes.length; i++) {
  901. if (nodes[i].nodeType === 1) {
  902. listCount++;
  903. if (nodes[i] === target.node) {
  904. originalIndex = listCount-1;
  905. }
  906. }
  907. }
  908.  
  909. return originalIndex;
  910. }
  911.  
  912. // All functions in states are going to be executed in context of Slip object
  913. Slip.prototype = {
  914.  
  915. container: null,
  916. options: {},
  917. state: null,
  918.  
  919. target: null, // the tapped/swiped/reordered node with height and backed up styles
  920.  
  921. usingTouch: false, // there's no good way to detect touchscreen preference other than receiving a touch event (really, trust me).
  922. mouseHandlersAttached: false,
  923.  
  924. startPosition: null, // x,y,time where first touch began
  925. latestPosition: null, // x,y,time where the finger is currently
  926. previousPosition: null, // x,y,time where the finger was ~100ms ago (for velocity calculation)
  927.  
  928. canPreventScrolling: false,
  929.  
  930. states: {
  931. idle: function idleStateInit() {
  932. this.target = null;
  933. this.usingTouch = false;
  934. this.removeMouseHandlers();
  935.  
  936. return {
  937. allowTextSelection: true,
  938. };
  939. },
  940.  
  941. undecided: function undecidedStateInit() {
  942. this.target.height = this.target.node.offsetHeight;
  943. this.target.node.style[transitionPrefix] = '';
  944.  
  945. if (!this.dispatch(this.target.originalTarget, 'beforewait')) {
  946. if (this.dispatch(this.target.originalTarget, 'beforereorder')) {
  947. this.setState(this.states.reorder);
  948. }
  949. } else {
  950. var holdTimer = setTimeout(function(){
  951. var move = this.getAbsoluteMovement();
  952. if (this.canPreventScrolling && move.x < 15 && move.y < 25) {
  953. if (this.dispatch(this.target.originalTarget, 'beforereorder')) {
  954. this.setState(this.states.reorder);
  955. }
  956. }
  957. }.bind(this), 300);
  958. }
  959.  
  960. return {
  961. leaveState: function() {
  962. clearTimeout(holdTimer);
  963. },
  964.  
  965. onMove: function() {
  966. var move = this.getAbsoluteMovement();
  967.  
  968. if (move.x > 20 && move.y < Math.max(100, this.target.height)) {
  969. if (this.dispatch(this.target.originalTarget, 'beforeswipe', {directionX: move.directionX, directionY: move.directionY})) {
  970. this.setState(this.states.swipe);
  971. return false;
  972. } else {
  973. this.setState(this.states.idle);
  974. }
  975. }
  976. if (move.y > 20) {
  977. this.setState(this.states.idle);
  978. }
  979.  
  980. // Chrome likes sideways scrolling :(
  981. if (move.x > move.y*1.2) return false;
  982. },
  983.  
  984. onLeave: function() {
  985. this.setState(this.states.idle);
  986. },
  987.  
  988. onEnd: function() {
  989. var allowDefault = this.dispatch(this.target.originalTarget, 'tap');
  990. this.setState(this.states.idle);
  991. return allowDefault;
  992. },
  993. };
  994. },
  995.  
  996. swipe: function swipeStateInit() {
  997. var swipeSuccess = false;
  998. var container = this.container;
  999.  
  1000. var originalIndex = findIndex(this.target, this.container.childNodes);
  1001.  
  1002. container.className += ' slip-swiping-container';
  1003. function removeClass() {
  1004. container.className = container.className.replace(/(?:^| )slip-swiping-container/,'');
  1005. }
  1006.  
  1007. this.target.height = this.target.node.offsetHeight;
  1008.  
  1009. return {
  1010. leaveState: function() {
  1011. if (swipeSuccess) {
  1012. this.animateSwipe(function(target){
  1013. target.node.style[transformPrefix] = target.baseTransform.original;
  1014. target.node.style[transitionPrefix] = '';
  1015. if (this.dispatch(target.node, 'afterswipe')) {
  1016. removeClass();
  1017. return true;
  1018. } else {
  1019. this.animateToZero(undefined, target);
  1020. }
  1021. }.bind(this));
  1022. } else {
  1023. this.animateToZero(removeClass);
  1024. this.dispatch(this.target.node, 'cancelswipe');
  1025. }
  1026. },
  1027.  
  1028. onMove: function() {
  1029. var move = this.getTotalMovement();
  1030.  
  1031. if (Math.abs(move.y) < this.target.height+20) {
  1032. this.target.node.style[transformPrefix] = 'translate(' + move.x + 'px,0) ' + hwLayerMagic + this.target.baseTransform.value;
  1033. return false;
  1034. } else {
  1035. this.setState(this.states.idle);
  1036. }
  1037. },
  1038.  
  1039. onLeave: function() {
  1040. this.state.onEnd.call(this);
  1041. },
  1042.  
  1043. onEnd: function() {
  1044. var move = this.getAbsoluteMovement();
  1045. var velocity = move.x / move.time;
  1046.  
  1047. // How far out has the item been swiped?
  1048. var swipedPercent = Math.abs((this.startPosition.x - this.previousPosition.x) / this.container.clientWidth) * 100;
  1049.  
  1050. var swiped = (velocity > this.options.minimumSwipeVelocity && move.time > this.options.minimumSwipeTime) || (this.options.keepSwipingPercent && swipedPercent > this.options.keepSwipingPercent);
  1051.  
  1052. if (swiped) {
  1053. if (this.dispatch(this.target.node, 'swipe', {direction: move.directionX, originalIndex: originalIndex})) {
  1054. swipeSuccess = true; // can't animate here, leaveState overrides anim
  1055. }
  1056. }
  1057. this.setState(this.states.idle);
  1058. return !swiped;
  1059. },
  1060. };
  1061. },
  1062.  
  1063. reorder: function reorderStateInit() {
  1064. this.target.height = this.target.node.offsetHeight;
  1065.  
  1066. var nodes = this.container.childNodes;
  1067. var originalIndex = findIndex(this.target, nodes);
  1068. var mouseOutsideTimer;
  1069. var zero = this.target.node.offsetTop + this.target.height/2;
  1070. var otherNodes = [];
  1071. for(var i=0; i < nodes.length; i++) {
  1072. if (nodes[i].nodeType != 1 || nodes[i] === this.target.node) continue;
  1073. var t = nodes[i].offsetTop;
  1074. nodes[i].style[transitionPrefix] = transformProperty + ' 0.2s ease-in-out';
  1075. otherNodes.push({
  1076. node: nodes[i],
  1077. baseTransform: getTransform(nodes[i]),
  1078. pos: t + (t < zero ? nodes[i].offsetHeight : 0) - zero,
  1079. });
  1080. }
  1081.  
  1082. this.target.node.className += ' slip-reordering';
  1083. this.target.node.style.zIndex = '99999';
  1084. this.target.node.style[userSelectPrefix] = 'none';
  1085. if (compositorDoesNotOrderLayers) {
  1086. // Chrome's compositor doesn't sort 2D layers
  1087. this.container.style.webkitTransformStyle = 'preserve-3d';
  1088. }
  1089.  
  1090. function setPosition() {
  1091. /*jshint validthis:true */
  1092.  
  1093. if (mouseOutsideTimer) {
  1094. // don't care where the mouse is as long as it moves
  1095. clearTimeout(mouseOutsideTimer); mouseOutsideTimer = null;
  1096. }
  1097.  
  1098. var move = this.getTotalMovement();
  1099. this.target.node.style[transformPrefix] = 'translate(0,' + move.y + 'px) ' + hwTopLayerMagic + this.target.baseTransform.value;
  1100.  
  1101. var height = this.target.height;
  1102. otherNodes.forEach(function(o){
  1103. var off = 0;
  1104. if (o.pos < 0 && move.y < 0 && o.pos > move.y) {
  1105. off = height;
  1106. }
  1107. else if (o.pos > 0 && move.y > 0 && o.pos < move.y) {
  1108. off = -height;
  1109. }
  1110. // FIXME: should change accelerated/non-accelerated state lazily
  1111. o.node.style[transformPrefix] = off ? 'translate(0,'+off+'px) ' + hwLayerMagic + o.baseTransform.value : o.baseTransform.original;
  1112. });
  1113. return false;
  1114. }
  1115.  
  1116. setPosition.call(this);
  1117.  
  1118. return {
  1119. leaveState: function() {
  1120. if (mouseOutsideTimer) clearTimeout(mouseOutsideTimer);
  1121.  
  1122. if (compositorDoesNotOrderLayers) {
  1123. this.container.style.webkitTransformStyle = '';
  1124. }
  1125.  
  1126. this.target.node.className = this.target.node.className.replace(/(?:^| )slip-reordering/,'');
  1127. this.target.node.style[userSelectPrefix] = '';
  1128.  
  1129. this.animateToZero(function(target){
  1130. target.node.style.zIndex = '';
  1131. });
  1132. otherNodes.forEach(function(o){
  1133. o.node.style[transformPrefix] = o.baseTransform.original;
  1134. o.node.style[transitionPrefix] = ''; // FIXME: animate to new position
  1135. });
  1136. },
  1137.  
  1138. onMove: setPosition,
  1139.  
  1140. onLeave: function() {
  1141. // don't let element get stuck if mouse left the window
  1142. // but don't cancel immediately as it'd be annoying near window edges
  1143. if (mouseOutsideTimer) clearTimeout(mouseOutsideTimer);
  1144. mouseOutsideTimer = setTimeout(function(){
  1145. mouseOutsideTimer = null;
  1146. this.cancel();
  1147. }.bind(this), 700);
  1148. },
  1149.  
  1150. onEnd: function() {
  1151. var move = this.getTotalMovement();
  1152. if (move.y < 0) {
  1153. for(var i=0; i < otherNodes.length; i++) {
  1154. if (otherNodes[i].pos > move.y) {
  1155. this.dispatch(this.target.node, 'reorder', {spliceIndex:i, insertBefore:otherNodes[i].node, originalIndex: originalIndex});
  1156. break;
  1157. }
  1158. }
  1159. } else {
  1160. for(var i=otherNodes.length-1; i >= 0; i--) {
  1161. if (otherNodes[i].pos < move.y) {
  1162. this.dispatch(this.target.node, 'reorder', {spliceIndex:i+1, insertBefore:otherNodes[i+1] ? otherNodes[i+1].node : null, originalIndex: originalIndex});
  1163. break;
  1164. }
  1165. }
  1166. }
  1167. this.setState(this.states.idle);
  1168. return false;
  1169. },
  1170. };
  1171. },
  1172. },
  1173.  
  1174. attach: function(container) {
  1175. globalInstances++;
  1176. if (this.container) this.detach();
  1177.  
  1178. // In some cases taps on list elements send *only* click events and no touch events. Spotted only in Chrome 32+
  1179. // Having event listener on body seems to solve the issue (although AFAIK may disable smooth scrolling as a side-effect)
  1180. if (!attachedBodyHandlerHack && needsBodyHandlerHack) {
  1181. attachedBodyHandlerHack = true;
  1182. document.body.addEventListener('touchstart', nullHandler, false);
  1183. }
  1184.  
  1185. this.container = container;
  1186. this.otherNodes = [];
  1187.  
  1188. // selection on iOS interferes with reordering
  1189. document.addEventListener("selectionchange", this.onSelection, false);
  1190.  
  1191. // cancel is called e.g. when iOS detects multitasking gesture
  1192. this.container.addEventListener('touchcancel', this.cancel, false);
  1193. this.container.addEventListener('touchstart', this.onTouchStart, false);
  1194. this.container.addEventListener('touchmove', this.onTouchMove, false);
  1195. this.container.addEventListener('touchend', this.onTouchEnd, false);
  1196. this.container.addEventListener('mousedown', this.onMouseDown, false);
  1197. // mousemove and mouseup are attached dynamically
  1198. },
  1199.  
  1200. detach: function() {
  1201. this.cancel();
  1202.  
  1203. this.container.removeEventListener('mousedown', this.onMouseDown, false);
  1204. this.container.removeEventListener('touchend', this.onTouchEnd, false);
  1205. this.container.removeEventListener('touchmove', this.onTouchMove, false);
  1206. this.container.removeEventListener('touchstart', this.onTouchStart, false);
  1207. this.container.removeEventListener('touchcancel', this.cancel, false);
  1208.  
  1209. document.removeEventListener("selectionchange", this.onSelection, false);
  1210.  
  1211. globalInstances--;
  1212. if (!globalInstances && attachedBodyHandlerHack) {
  1213. attachedBodyHandlerHack = false;
  1214. document.body.removeEventListener('touchstart', nullHandler, false);
  1215. }
  1216. },
  1217.  
  1218. setState: function(newStateCtor){
  1219. if (this.state) {
  1220. if (this.state.ctor === newStateCtor) return;
  1221. if (this.state.leaveState) this.state.leaveState.call(this);
  1222. }
  1223.  
  1224. // Must be re-entrant in case ctor changes state
  1225. var prevState = this.state;
  1226. var nextState = newStateCtor.call(this);
  1227. if (this.state === prevState) {
  1228. nextState.ctor = newStateCtor;
  1229. this.state = nextState;
  1230. }
  1231. },
  1232.  
  1233. findTargetNode: function(targetNode) {
  1234. while(targetNode && targetNode.parentNode !== this.container) {
  1235. targetNode = targetNode.parentNode;
  1236. }
  1237. return targetNode;
  1238. },
  1239.  
  1240. onSelection: function(e) {
  1241. var isRelated = e.target === document || this.findTargetNode(e);
  1242. if (!isRelated) return;
  1243.  
  1244. if (e.cancelable || e.defaultPrevented) {
  1245. if (!this.state.allowTextSelection) {
  1246. e.preventDefault();
  1247. }
  1248. } else {
  1249. // iOS doesn't allow selection to be prevented
  1250. this.setState(this.states.idle);
  1251. }
  1252. },
  1253.  
  1254. addMouseHandlers: function() {
  1255. // unlike touch events, mousemove/up is not conveniently fired on the same element,
  1256. // but I don't need to listen to unrelated events all the time
  1257. if (!this.mouseHandlersAttached) {
  1258. this.mouseHandlersAttached = true;
  1259. document.documentElement.addEventListener('mouseleave', this.onMouseLeave, false);
  1260. window.addEventListener('mousemove', this.onMouseMove, true);
  1261. window.addEventListener('mouseup', this.onMouseUp, true);
  1262. window.addEventListener('blur', this.cancel, false);
  1263. }
  1264. },
  1265.  
  1266. removeMouseHandlers: function() {
  1267. if (this.mouseHandlersAttached) {
  1268. this.mouseHandlersAttached = false;
  1269. document.documentElement.removeEventListener('mouseleave', this.onMouseLeave, false);
  1270. window.removeEventListener('mousemove', this.onMouseMove, true);
  1271. window.removeEventListener('mouseup', this.onMouseUp, true);
  1272. window.removeEventListener('blur', this.cancel, false);
  1273. }
  1274. },
  1275.  
  1276. onMouseLeave: function(e) {
  1277. if (this.usingTouch) return;
  1278.  
  1279. if (e.target === document.documentElement || e.relatedTarget === document.documentElement) {
  1280. if (this.state.onLeave) {
  1281. this.state.onLeave.call(this);
  1282. }
  1283. }
  1284. },
  1285.  
  1286. onMouseDown: function(e) {
  1287. if (this.usingTouch || e.button != 0 || !this.setTarget(e)) return;
  1288.  
  1289. this.addMouseHandlers(); // mouseup, etc.
  1290.  
  1291. this.canPreventScrolling = true; // or rather it doesn't apply to mouse
  1292.  
  1293. this.startAtPosition({
  1294. x: e.clientX,
  1295. y: e.clientY,
  1296. time: e.timeStamp,
  1297. });
  1298. },
  1299.  
  1300. onTouchStart: function(e) {
  1301. this.usingTouch = true;
  1302. this.canPreventScrolling = true;
  1303.  
  1304. // This implementation cares only about single touch
  1305. if (e.touches.length > 1) {
  1306. this.setState(this.states.idle);
  1307. return;
  1308. }
  1309.  
  1310. if (!this.setTarget(e)) return;
  1311.  
  1312. this.startAtPosition({
  1313. x: e.touches[0].clientX,
  1314. y: e.touches[0].clientY - window.scrollY,
  1315. time: e.timeStamp,
  1316. });
  1317. },
  1318.  
  1319. setTarget: function(e) {
  1320. var targetNode = this.findTargetNode(e.target);
  1321. if (!targetNode) {
  1322. this.setState(this.states.idle);
  1323. return false;
  1324. }
  1325.  
  1326. //check for a scrollable parent
  1327. var scrollContainer = targetNode.parentNode;
  1328. while (scrollContainer){
  1329. if (scrollContainer.scrollHeight > scrollContainer.clientHeight && window.getComputedStyle(scrollContainer)['overflow-y'] != 'visible') break;
  1330. else scrollContainer = scrollContainer.parentNode;
  1331. }
  1332.  
  1333. this.target = {
  1334. originalTarget: e.target,
  1335. node: targetNode,
  1336. scrollContainer: scrollContainer,
  1337. baseTransform: getTransform(targetNode),
  1338. };
  1339. return true;
  1340. },
  1341.  
  1342. startAtPosition: function(pos) {
  1343. this.startPosition = this.previousPosition = this.latestPosition = pos;
  1344. this.setState(this.states.undecided);
  1345. },
  1346.  
  1347. updatePosition: function(e, pos) {
  1348. if(this.target == null)
  1349. return;
  1350. this.latestPosition = pos;
  1351.  
  1352. var triggerOffset = 40,
  1353. offset = 0;
  1354.  
  1355. var scrollable = this.target.scrollContainer || document.body,
  1356. containerRect = scrollable.getBoundingClientRect(),
  1357. targetRect = this.target.node.getBoundingClientRect(),
  1358. bottomOffset = Math.min(containerRect.bottom, window.innerHeight) - targetRect.bottom,
  1359. topOffset = targetRect.top - Math.max(containerRect.top, 0);
  1360.  
  1361. if (bottomOffset < triggerOffset){
  1362. offset = triggerOffset - bottomOffset;
  1363. }
  1364. else if (topOffset < triggerOffset){
  1365. offset = topOffset - triggerOffset;
  1366. }
  1367.  
  1368. var prevScrollTop = scrollable.scrollTop;
  1369. scrollable.scrollTop += offset;
  1370. if (prevScrollTop != scrollable.scrollTop) this.startPosition.y += prevScrollTop-scrollable.scrollTop;
  1371.  
  1372. if (this.state.onMove) {
  1373. if (this.state.onMove.call(this) === false) {
  1374. e.preventDefault();
  1375. }
  1376. }
  1377.  
  1378. // sample latestPosition 100ms for velocity
  1379. if (this.latestPosition.time - this.previousPosition.time > 100) {
  1380. this.previousPosition = this.latestPosition;
  1381. }
  1382. },
  1383.  
  1384. onMouseMove: function(e) {
  1385. this.updatePosition(e, {
  1386. x: e.clientX,
  1387. y: e.clientY,
  1388. time: e.timeStamp,
  1389. });
  1390. },
  1391.  
  1392. onTouchMove: function(e) {
  1393. this.updatePosition(e, {
  1394. x: e.touches[0].clientX,
  1395. y: e.touches[0].clientY - window.scrollY,
  1396. time: e.timeStamp,
  1397. });
  1398.  
  1399. // In Apple's touch model only the first move event after touchstart can prevent scrolling (and event.cancelable is broken)
  1400. this.canPreventScrolling = false;
  1401. },
  1402.  
  1403. onMouseUp: function(e) {
  1404. if (this.usingTouch || e.button !== 0) return;
  1405.  
  1406. if (this.state.onEnd && false === this.state.onEnd.call(this)) {
  1407. e.preventDefault();
  1408. }
  1409. },
  1410.  
  1411. onTouchEnd: function(e) {
  1412. if (e.touches.length > 1) {
  1413. this.cancel();
  1414. } else if (this.state.onEnd && false === this.state.onEnd.call(this)) {
  1415. e.preventDefault();
  1416. }
  1417. },
  1418.  
  1419. getTotalMovement: function() {
  1420. return {
  1421. x:this.latestPosition.x - this.startPosition.x,
  1422. y:this.latestPosition.y - this.startPosition.y,
  1423. };
  1424. },
  1425.  
  1426. getAbsoluteMovement: function() {
  1427. return {
  1428. x: Math.abs(this.latestPosition.x - this.startPosition.x),
  1429. y: Math.abs(this.latestPosition.y - this.startPosition.y),
  1430. time:this.latestPosition.time - this.startPosition.time,
  1431. directionX:this.latestPosition.x - this.startPosition.x < 0 ? 'left' : 'right',
  1432. directionY:this.latestPosition.y - this.startPosition.y < 0 ? 'up' : 'down',
  1433. };
  1434. },
  1435.  
  1436. dispatch: function(targetNode, eventName, detail) {
  1437. var event = document.createEvent('CustomEvent');
  1438. if (event && event.initCustomEvent) {
  1439. event.initCustomEvent('slip:' + eventName, true, true, detail);
  1440. } else {
  1441. event = document.createEvent('Event');
  1442. event.initEvent('slip:' + eventName, true, true);
  1443. event.detail = detail;
  1444. }
  1445. return targetNode.dispatchEvent(event);
  1446. },
  1447.  
  1448. getSiblings: function(target) {
  1449. var siblings = [];
  1450. var tmp = target.node.nextSibling;
  1451. while(tmp) {
  1452. if (tmp.nodeType == 1) siblings.push({
  1453. node: tmp,
  1454. baseTransform: getTransform(tmp),
  1455. });
  1456. tmp = tmp.nextSibling;
  1457. }
  1458. return siblings;
  1459. },
  1460.  
  1461. animateToZero: function(callback, target) {
  1462. // save, because this.target/container could change during animation
  1463. target = target || this.target;
  1464.  
  1465. target.node.style[transitionPrefix] = transformProperty + ' 0.1s ease-out';
  1466. target.node.style[transformPrefix] = 'translate(0,0) ' + hwLayerMagic + target.baseTransform.value;
  1467. setTimeout(function(){
  1468. target.node.style[transitionPrefix] = '';
  1469. target.node.style[transformPrefix] = target.baseTransform.original;
  1470. if (callback) callback.call(this, target);
  1471. }.bind(this), 101);
  1472. },
  1473.  
  1474. animateSwipe: function(callback) {
  1475. var target = this.target;
  1476. var siblings = this.getSiblings(target);
  1477. var emptySpaceTransform = 'translate(0,' + this.target.height + 'px) ' + hwLayerMagic + ' ';
  1478.  
  1479. // FIXME: animate with real velocity
  1480. target.node.style[transitionPrefix] = 'all 0.1s linear';
  1481. target.node.style[transformPrefix] = ' translate(' + (this.getTotalMovement().x > 0 ? '' : '-') + '100%,0) ' + hwLayerMagic + target.baseTransform.value;
  1482.  
  1483. setTimeout(function(){
  1484. if (callback.call(this, target)) {
  1485. siblings.forEach(function(o){
  1486. o.node.style[transitionPrefix] = '';
  1487. o.node.style[transformPrefix] = emptySpaceTransform + o.baseTransform.value;
  1488. });
  1489. setTimeout(function(){
  1490. siblings.forEach(function(o){
  1491. o.node.style[transitionPrefix] = transformProperty + ' 0.1s ease-in-out';
  1492. o.node.style[transformPrefix] = 'translate(0,0) ' + hwLayerMagic + o.baseTransform.value;
  1493. });
  1494. setTimeout(function(){
  1495. siblings.forEach(function(o){
  1496. o.node.style[transitionPrefix] = '';
  1497. o.node.style[transformPrefix] = o.baseTransform.original;
  1498. });
  1499. },101);
  1500. }, 1);
  1501. }
  1502. }.bind(this), 101);
  1503. },
  1504. };
  1505.  
  1506. // AMD
  1507. if ('function' === typeof define && define.amd) {
  1508. define(function(){
  1509. return Slip;
  1510. });
  1511. }
  1512. return Slip;
  1513. })();
  1514.  
  1515. function closest(elem, selector) {
  1516. var matchesSelector = elem.matches || elem.msMatchesSelector;
  1517. while (elem) {
  1518. if (matchesSelector.bind(elem)(selector)) {
  1519. return elem;
  1520. } else {
  1521. elem = elem.parentNode;
  1522. }
  1523. }
  1524. return false;
  1525. }
  1526.  
  1527. // Add CSS rules to prevent text selection of subject lines, which messes with swiping
  1528. var sheet = window.document.styleSheets[0]
  1529. sheet.insertRule('.scroll-list-item.top-level-item:not(.scroll-list-item-open) * { user-select: none; -moz-user-select: none; -webkit- user-select: none; -ms-user-select: none; }', sheet.cssRules.length);
  1530. // Add rules for showing the background green or yellow
  1531. sheet.insertRule(".scroll-list-section-body.swipe-right { background-color: green; }", sheet.cssRules.length);
  1532. sheet.insertRule(".scroll-list-section-body.swipe-left { background-color: orange; }", sheet.cssRules.length);
  1533.  
  1534. // We record which elements we've already wired up events for in this list
  1535. var handledScrollSections = [];
  1536.  
  1537. function setupEvents() {
  1538. // There are two main selector here - the first is for bundles and unbundled email, the second is for emails inside bundled email groups
  1539. var scrollSections = document.querySelectorAll('.scroll-list-section-body,.full-cluster-section-body');
  1540. for(var i = 0; i < scrollSections.length; i++) {
  1541. var elem = scrollSections[i];
  1542. if (handledScrollSections.indexOf(elem) === -1) {
  1543. setupEventsForSection(elem);
  1544. handledScrollSections.push(elem);
  1545. }
  1546. }
  1547. }
  1548.  
  1549. function setupEventsForSection(elem) {
  1550. new Slip(elem, {keepSwipingPercent: 50});
  1551. elem.addEventListener('slip:beforeswipe', function(e) {
  1552. var container = closest(e.target, '.scroll-list-section-body,.full-cluster-section-body');
  1553. var swipedItem = closest(e.target, '.scroll-list-item.top-level-item');
  1554. var matchesSelector = swipedItem.matches || swipedItem.msMatchesSelector;
  1555. // TODO: Enable snoozing after I figure out how to trigger it :(
  1556. if (e.detail.directionX === "left") {
  1557. e.preventDefault();
  1558. return;
  1559. }
  1560. // No snoozing bundled emails and no archiving open bundles
  1561. if (matchesSelector.bind(swipedItem)('.scroll-list-item-cluster')) {
  1562. if (e.detail.directionX === "left" || matchesSelector.bind(swipedItem)('.scroll-list-item-open')) {
  1563. e.preventDefault();
  1564. return;
  1565. }
  1566. }
  1567. container.classList.remove('swipe-right');
  1568. container.classList.remove('swipe-left');
  1569. if (e.detail.directionX === "left") {
  1570. container.classList.add('swipe-left');
  1571. } else {
  1572. container.classList.add('swipe-right');
  1573. }
  1574. });
  1575. // When swiping is done remove backgrounds from containers
  1576. elem.addEventListener('slip:afterswipe', function(e) {
  1577. var container = closest(e.target, '.scroll-list-section-body,.full-cluster-section-body');
  1578. container.classList.remove('swipe-right');
  1579. container.classList.remove('swipe-left');
  1580. });
  1581. elem.addEventListener('slip:cancelswipe', function(e) {
  1582. var container = closest(e.target, '.scroll-list-section-body,.full-cluster-section-body');
  1583. container.classList.remove('swipe-right');
  1584. container.classList.remove('swipe-left');
  1585. });
  1586. elem.addEventListener('slip:swipe', function(e) {
  1587. e.preventDefault();
  1588. var event = new MouseEvent('click', {
  1589. 'view': window,
  1590. 'bubbles': true,
  1591. 'cancelable': true
  1592. });
  1593. if (e.detail.direction === "right") {
  1594. var button = e.target.querySelector('[jsaction="click:global.archive"],[jsaction="global.archive"]');
  1595. } else {
  1596. return;
  1597. // TODO: This *does* get the button, but triggering a click on it does nothing...
  1598. var button = e.target.querySelector('[jsaction^="click:"][jsaction*="toggle_snooze_menu"]');
  1599. }
  1600. button.dispatchEvent(event);
  1601. });
  1602. }
  1603.  
  1604. // Call setupEvents once when the page loads
  1605. setupEvents();
  1606.  
  1607. // This is the container of all of the scroll sections, we'll listen for DOM changes to wire up events for new emails
  1608. var mainContainer = document.querySelector('[role="list"][aria-multiselectable="false"]');
  1609.  
  1610. // Setup MutationObserver to wire events when new emails are added
  1611. var observer = new MutationObserver(setupEvents);
  1612. observer.observe(mainContainer, {subtree: true, childList: true});

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址