+
{{> UI.contentBlock}}
diff --git a/components/ionView/ionView.js b/components/ionView/ionView.js
index 0466a7a..e127747 100644
--- a/components/ionView/ionView.js
+++ b/components/ionView/ionView.js
@@ -1,28 +1,97 @@
-Template.ionView.rendered = function () {
- // Reset our transition preference
- IonNavigation.skipTransitions = false;
-
- // Reset our scroll position
- var routePath = Router.current().route.path(Router.current().params);
- if(IonScrollPositions[routePath]) {
- $('.overflow-scroll').not('.nav-view-leaving .overflow-scroll').scrollTop(IonScrollPositions[routePath]);
- delete IonScrollPositions[routePath];
- }
-};
+Template.ionView.onCreated(function() {
+ this.entering = false;
+ this.leaving = false;
-Template.ionView.helpers({
- classes: function () {
- var classes = ['view'];
+ this.activate_view_timeout_id = null;
+ this.deactivate_view_timeout_id = null;
+});
- if (this.class) {
- classes.push(this.class);
- }
+Template.ionView.onRendered(function () {
+ // Reset our transition preference
+ IonNavigation.skipTransitions = false;
- return classes.join(' ');
- },
- title: function () {
- if ( Template.instance().data && Template.instance().data.title ) {
- return Template.instance().data.title;
+ // Reset our scroll position
+ var routePath = Router.current().route.path(Router.current().params);
+ if(IonScrollPositions[routePath]) {
+ $('.overflow-scroll').not('.nav-view-leaving .overflow-scroll').scrollTop(IonScrollPositions[routePath]);
+ delete IonScrollPositions[routePath];
}
- }
+
+ // Get this now, so that when Meteor.setTimeout is called, Template.instance() retrieves the correct element.
+ let $view = this.$('.view');
+
+ let activate_view = () => {
+ this.entering = false;
+
+ let activate_timer_active = !!this.activate_view_timeout_id;
+ if (activate_timer_active) {
+ Meteor.clearTimeout(this.activate_view_timeout_id);
+ this.activate_view_timeout_id = null;
+ }
+
+ $view.attr('nav-view', 'active');
+ $('[data-nav-container]').attr('nav-view-direction', 'forward');
+ };
+
+ $view.attr('nav-view', 'stage');
+ Meteor.setTimeout(() => {
+ this.entering = true;
+ $view.attr('nav-view', 'entering');
+ $view.one(METEORIC.UTILITY.transitionend_events.join(' '), activate_view);
+ }, 0);
+
+ // Worst case scenario, transitionend did not occur. Just place view in.
+ this.activate_view_timeout_id = Meteor.setTimeout(activate_view, METEORIC.maximum_transition_duration);
+});
+
+Template.ionView.onDestroyed(function () {
+ // Get this now, so that when Meteor.setTimeout is called, Template.instance() retrieves the correct element.
+ let $view = this.$('.view');
+
+ let deactivate_view = () => {
+ this.leaving = false;
+
+ // If the user have trigger fingers, in which he/she can click back buttons
+ // really fast, activate view timer might still be going. Kill it.
+ let activate_timer_active = !!this.activate_view_timeout_id;
+ if (activate_timer_active) {
+ Meteor.clearTimeout(this.activate_view_timeout_id);
+ this.activate_view_timeout_id = null;
+ }
+
+ let deactivate_timer_active = !!this.deactivate_view_timeout_id;
+ if (deactivate_timer_active) {
+ Meteor.clearTimeout(this.deactivate_view_timeout_id);
+ this.deactivate_view_timeout_id = null;
+ }
+
+ this.deactivate_view_timeout_id = null;
+ $view.remove();
+ };
+
+ Meteor.setTimeout(() => {
+ this.leaving = true;
+ $view.attr('nav-view', 'leaving');
+ $view.one(METEORIC.UTILITY.transitionend_events.join(' '), deactivate_view);
+ }, 0);
+
+ // Worst case scenario, transitionend did not occur. Just remove the view.
+ this.deactivate_view_timeout_id = Meteor.setTimeout(deactivate_view, METEORIC.maximum_transition_duration);
});
+
+Template.ionView.helpers({
+ classes: function () {
+ var classes = [];
+
+ if (this.class) {
+ classes.push(this.class);
+ }
+
+ return classes.join(' ');
+ },
+ title: function () {
+ if ( Template.instance().data && Template.instance().data.title ) {
+ return Template.instance().data.title;
+ }
+ }
+});
\ No newline at end of file
diff --git a/lib/platform.js b/lib/platform.js
new file mode 100644
index 0000000..21d946f
--- /dev/null
+++ b/lib/platform.js
@@ -0,0 +1,431 @@
+(function(window, document, METEORIC) {
+ var IOS = 'ios';
+ var ANDROID = 'android';
+ var WINDOWS_PHONE = 'windowsphone';
+ var EDGE = 'edge';
+ var CROSSWALK = 'crosswalk';
+ var requestAnimationFrame = window.requestAnimFrame;
+
+ /**
+ * @ngdoc utility
+ * @name METEORIC.PLATFORM
+ * @module METEORIC
+ * @description
+ * A set of utility methods that can be used to retrieve the device ready state and
+ * various other information such as what kind of platform the app is currently installed on.
+ */
+ let self = METEORIC.PLATFORM = {
+
+ // Put navigator on platform so it can be mocked and set
+ // the browser does not allow window.navigator to be set
+ navigator: window.navigator,
+
+ /**
+ * @ngdoc property
+ * @name METEORIC.PLATFORM#isReady
+ * @returns {boolean} Whether the device is ready.
+ */
+ isReady: false,
+ /**
+ * @ngdoc property
+ * @name METEORIC.PLATFORM#isFullScreen
+ * @returns {boolean} Whether the device is fullscreen.
+ */
+ isFullScreen: false,
+ /**
+ * @ngdoc property
+ * @name METEORIC.PLATFORM#platforms
+ * @returns {Array(string)} An array of all platforms found.
+ */
+ platforms: null,
+ /**
+ * @ngdoc property
+ * @name METEORIC.PLATFORM#grade
+ * @returns {string} What grade the current platform is.
+ */
+ grade: null,
+ ua: navigator.userAgent,
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#ready
+ * @description
+ * Trigger a callback once the device is ready, or immediately
+ * if the device is already ready. This method can be run from
+ * anywhere and does not need to be wrapped by any additonal methods.
+ * When the app is within a WebView (Cordova), it'll fire
+ * the callback once the device is ready. If the app is within
+ * a web browser, it'll fire the callback after `window.load`.
+ * Please remember that Cordova features (Camera, FileSystem, etc) still
+ * will not work in a web browser.
+ * @param {function} callback The function to call.
+ */
+ ready: function(cb) {
+ // run through tasks to complete now that the device is ready
+ if (self.isReady) {
+ cb();
+ } else {
+ // the platform isn't ready yet, add it to this array
+ // which will be called once the platform is ready
+ readyCallbacks.push(cb);
+ }
+ },
+
+ /**
+ * @private
+ */
+ detect: function() {
+ self._checkPlatforms();
+
+ requestAnimationFrame(function() {
+ // only add to the body class if we got platform info
+ for (var i = 0; i < self.platforms.length; i++) {
+ document.body.classList.add('platform-' + self.platforms[i]);
+ }
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#setGrade
+ * @description Set the grade of the device: 'a', 'b', or 'c'. 'a' is the best
+ * (most css features enabled), 'c' is the worst. By default, sets the grade
+ * depending on the current device.
+ * @param {string} grade The new grade to set.
+ */
+ setGrade: function(grade) {
+ var oldGrade = self.grade;
+ self.grade = grade;
+ requestAnimationFrame(function() {
+ if (oldGrade) {
+ document.body.classList.remove('grade-' + oldGrade);
+ }
+ document.body.classList.add('grade-' + grade);
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#device
+ * @description Return the current device (given by cordova).
+ * @returns {object} The device object.
+ */
+ device: function() {
+ return window.device || {};
+ },
+
+ _checkPlatforms: function() {
+ self.platforms = [];
+ var grade = 'a';
+
+ if (self.isWebView()) {
+ self.platforms.push('webview');
+ if (!(!window.cordova && !window.PhoneGap && !window.phonegap)) {
+ self.platforms.push('cordova');
+ } else if (window.forge) {
+ self.platforms.push('trigger');
+ }
+ } else {
+ self.platforms.push('browser');
+ }
+ if (self.isIPad()) self.platforms.push('ipad');
+
+ var platform = self.platform();
+ if (platform) {
+ self.platforms.push(platform);
+
+ var version = self.version();
+ if (version) {
+ var v = version.toString();
+ if (v.indexOf('.') > 0) {
+ v = v.replace('.', '_');
+ } else {
+ v += '_0';
+ }
+ self.platforms.push(platform + v.split('_')[0]);
+ self.platforms.push(platform + v);
+
+ if (self.isAndroid() && version < 4.4) {
+ grade = (version < 4 ? 'c' : 'b');
+ } else if (self.isWindowsPhone()) {
+ grade = 'b';
+ }
+ }
+ }
+
+ self.setGrade(grade);
+ },
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#isWebView
+ * @returns {boolean} Check if we are running within a WebView (such as Cordova).
+ */
+ isWebView: function() {
+ return !(!window.cordova && !window.PhoneGap && !window.phonegap && !window.forge);
+ },
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#isIPad
+ * @returns {boolean} Whether we are running on iPad.
+ */
+ isIPad: function() {
+ if (/iPad/i.test(self.navigator.platform)) {
+ return true;
+ }
+ return /iPad/i.test(self.ua);
+ },
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#isIOS
+ * @returns {boolean} Whether we are running on iOS.
+ */
+ isIOS: function() {
+ return self.is(IOS);
+ },
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#isAndroid
+ * @returns {boolean} Whether we are running on Android.
+ */
+ isAndroid: function() {
+ return self.is(ANDROID);
+ },
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#isWindowsPhone
+ * @returns {boolean} Whether we are running on Windows Phone.
+ */
+ isWindowsPhone: function() {
+ return self.is(WINDOWS_PHONE);
+ },
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#isEdge
+ * @returns {boolean} Whether we are running on MS Edge/Windows 10 (inc. Phone)
+ */
+ isEdge: function() {
+ return self.is(EDGE);
+ },
+
+ isCrosswalk: function() {
+ return self.is(CROSSWALK);
+ },
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#platform
+ * @returns {string} The name of the current platform.
+ */
+ platform: function() {
+ // singleton to get the platform name
+ if (platformName === null) self.setPlatform(self.device().platform);
+ return platformName;
+ },
+
+ /**
+ * @private
+ */
+ setPlatform: function(n) {
+ if (typeof n != 'undefined' && n !== null && n.length) {
+ platformName = n.toLowerCase();
+ } else if (self.ua.indexOf('Edge') > -1) {
+ platformName = EDGE;
+ } else if (self.ua.indexOf('Windows Phone') > -1) {
+ platformName = WINDOWS_PHONE;
+ } else if (self.ua.indexOf('Android') > 0) {
+ platformName = ANDROID;
+ } else if (/iPhone|iPad|iPod/.test(self.ua)) {
+ platformName = IOS;
+ } else {
+ platformName = self.navigator.platform && navigator.platform.toLowerCase().split(' ')[0] || '';
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#version
+ * @returns {number} The version of the current device platform.
+ */
+ version: function() {
+ // singleton to get the platform version
+ if (platformVersion === null) self.setVersion(self.device().version);
+ return platformVersion;
+ },
+
+ /**
+ * @private
+ */
+ setVersion: function(v) {
+ if (typeof v != 'undefined' && v !== null) {
+ v = v.split('.');
+ v = parseFloat(v[0] + '.' + (v.length > 1 ? v[1] : 0));
+ if (!isNaN(v)) {
+ platformVersion = v;
+ return;
+ }
+ }
+
+ platformVersion = 0;
+
+ // fallback to user-agent checking
+ var pName = self.platform();
+ var versionMatch = {
+ 'android': /Android (\d+).(\d+)?/,
+ 'ios': /OS (\d+)_(\d+)?/,
+ 'windowsphone': /Windows Phone (\d+).(\d+)?/
+ };
+ if (versionMatch[pName]) {
+ v = self.ua.match(versionMatch[pName]);
+ if (v && v.length > 2) {
+ platformVersion = parseFloat(v[1] + '.' + v[2]);
+ }
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#is
+ * @param {string} Platform name.
+ * @returns {boolean} Whether the platform name provided is detected.
+ */
+ is: function(type) {
+ type = type.toLowerCase();
+ // check if it has an array of platforms
+ if (self.platforms) {
+ for (var x = 0; x < self.platforms.length; x++) {
+ if (self.platforms[x] === type) return true;
+ }
+ }
+ // exact match
+ var pName = self.platform();
+ if (pName) {
+ return pName === type.toLowerCase();
+ }
+
+ // A quick hack for to check userAgent
+ return self.ua.toLowerCase().indexOf(type) >= 0;
+ },
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#exitApp
+ * @description Exit the app.
+ */
+ exitApp: function() {
+ self.ready(function() {
+ navigator.app && navigator.app.exitApp && navigator.app.exitApp();
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#showStatusBar
+ * @description Shows or hides the device status bar (in Cordova). Requires `cordova plugin add org.apache.cordova.statusbar`
+ * @param {boolean} shouldShow Whether or not to show the status bar.
+ */
+ showStatusBar: function(val) {
+ // Only useful when run within cordova
+ self._showStatusBar = val;
+ self.ready(function() {
+ // run this only when or if the platform (cordova) is ready
+ requestAnimationFrame(function() {
+ if (self._showStatusBar) {
+ // they do not want it to be full screen
+ window.StatusBar && window.StatusBar.show();
+ document.body.classList.remove('status-bar-hide');
+ } else {
+ // it should be full screen
+ window.StatusBar && window.StatusBar.hide();
+ document.body.classList.add('status-bar-hide');
+ }
+ });
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name METEORIC.PLATFORM#fullScreen
+ * @description
+ * Sets whether the app is fullscreen or not (in Cordova).
+ * @param {boolean=} showFullScreen Whether or not to set the app to fullscreen. Defaults to true. Requires `cordova plugin add org.apache.cordova.statusbar`
+ * @param {boolean=} showStatusBar Whether or not to show the device's status bar. Defaults to false.
+ */
+ fullScreen: function(showFullScreen, showStatusBar) {
+ // showFullScreen: default is true if no param provided
+ self.isFullScreen = (showFullScreen !== false);
+
+ // add/remove the fullscreen classname to the body
+ METEORIC.DomUtil.ready(function() {
+ // run this only when or if the DOM is ready
+ requestAnimationFrame(function() {
+ if (self.isFullScreen) {
+ document.body.classList.add('fullscreen');
+ } else {
+ document.body.classList.remove('fullscreen');
+ }
+ });
+ // showStatusBar: default is false if no param provided
+ self.showStatusBar((showStatusBar === true));
+ });
+ }
+
+ };
+
+ var platformName = null, // just the name, like iOS or Android
+ platformVersion = null, // a float of the major and minor, like 7.1
+ readyCallbacks = [],
+ windowLoadListenderAttached,
+ platformReadyTimer = 2000; // How long to wait for platform ready before emitting a warning
+
+ verifyPlatformReady();
+
+// Warn the user if deviceready did not fire in a reasonable amount of time, and how to fix it.
+ function verifyPlatformReady() {
+ setTimeout(function() {
+ if(!self.isReady && self.isWebView()) {
+ console.warn('Possible issue: deviceready did not fire in a reasonable amount of time. ' +
+ 'This can be caused by plugins in an inconsistent state. One possible solution: uninstall/remove all ' +
+ 'plugins and reinstall them. Additionally, one or more plugins might be faulty or out of date.');
+ }
+ }, platformReadyTimer);
+ }
+
+ // setup listeners to know when the device is ready to go
+ function onWindowLoad() {
+ if (self.isWebView()) {
+ // the window and scripts are fully loaded, and a cordova/phonegap
+ // object exists then let's listen for the deviceready
+ document.addEventListener("deviceready", onPlatformReady, false);
+ } else {
+ // the window and scripts are fully loaded, but the window object doesn't have the
+ // cordova/phonegap object, so its just a browser, not a webview wrapped w/ cordova
+ onPlatformReady();
+ }
+ if (windowLoadListenderAttached) {
+ window.removeEventListener("load", onWindowLoad, false);
+ }
+ }
+ if (document.readyState === 'complete') {
+ onWindowLoad();
+ } else {
+ windowLoadListenderAttached = true;
+ window.addEventListener("load", onWindowLoad, false);
+ }
+
+ function onPlatformReady() {
+ // the device is all set to go, init our own stuff then fire off our event
+ self.isReady = true;
+ self.detect();
+ for (var x = 0; x < readyCallbacks.length; x++) {
+ // fire off all the callbacks that were added before the platform was ready
+ readyCallbacks[x]();
+ }
+ readyCallbacks = [];
+ $(window).trigger('platformready', { target: document });
+
+ requestAnimationFrame(function() {
+ document.body.classList.add('platform-ready');
+ });
+ }
+})(window, document, METEORIC);
\ No newline at end of file
diff --git a/lib/polyfill.js b/lib/polyfill.js
new file mode 100644
index 0000000..4bdf04d
--- /dev/null
+++ b/lib/polyfill.js
@@ -0,0 +1,14 @@
+/**
+ * Older browsers, specifically, Android 4.0 - 4.3 needs this.
+ * @see http://caniuse.com/#search=requestAnimationFrame
+ */
+window.requestAnimFrame = (function(){
+ return window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame ||
+ function(/* function */ callback, /* DOMElement */ element){
+ window.setTimeout(callback, 1000 / 60);
+ };
+})();
\ No newline at end of file
diff --git a/lib/utility.js b/lib/utility.js
new file mode 100644
index 0000000..c79639b
--- /dev/null
+++ b/lib/utility.js
@@ -0,0 +1,63 @@
+METEORIC = {
+ hasHeader: new ReactiveVar(false),
+ hasFooter: new ReactiveVar(false),
+
+ /**
+ * In a worst case scenario, such that "transitionend" event is not called,
+ * for any reason. This will be the maximum alloted duration. This will
+ * prevent memory leaks during transition out events, in which some views
+ * are still in the DOM tree, even though they should've been removed.
+ */
+ maximum_transition_duration: 1100
+};
+
+METEORIC.UTILITY = {
+ transitionend_events: [
+ 'transitionend',
+ 'webkitTransitionEnd',
+ 'oTransitionEnd' // oTransitionEnd in very old Opera
+ ],
+ animationend_events: [
+ 'webkitanimationend' ,
+ 'mozanimationend',
+ 'oanimationend',
+ 'MSanimationEnd',
+ 'animationend'
+ ],
+ /**
+ * Throttle the given fun, only allowing it to be
+ * called at most every `wait` ms.
+ */
+ throttle(func, wait, options) {
+ var context, args, result;
+ var timeout = null;
+ var previous = 0;
+ options || (options = {});
+ var later = function() {
+ previous = options.leading === false ? 0 : Date.now();
+ timeout = null;
+ result = func.apply(context, args);
+ };
+ return function() {
+ var now = Date.now();
+ if (!previous && options.leading === false) previous = now;
+ var remaining = wait - (now - previous);
+ context = this;
+ args = arguments;
+ if (remaining <= 0) {
+ clearTimeout(timeout);
+ timeout = null;
+ previous = now;
+ result = func.apply(context, args);
+ } else if (!timeout && options.trailing !== false) {
+ timeout = setTimeout(later, remaining);
+ }
+ return result;
+ };
+ }
+};
+
+Router.onBeforeAction(function() {
+ $(window).trigger('statechange');
+ this.next();
+});
\ No newline at end of file
diff --git a/meteoric-logo.png b/meteoric-logo.png
new file mode 100644
index 0000000..ddc6462
Binary files /dev/null and b/meteoric-logo.png differ
diff --git a/package.js b/package.js
index 866fdc4..f820059 100644
--- a/package.js
+++ b/package.js
@@ -1,35 +1,56 @@
Package.describe({
- name: "meteoric:ionic",
+ name: "jandres:ionic",
summary: "Ionic components for Meteor. No Angular!",
- version: "0.1.19",
- git: "https://github.com/meteoric/meteor-ionic.git"
+ version: "1.0.0",
+ git: "https://github.com/JoeyAndres/meteor-ionic.git"
});
+
+
Cordova.depends({
'ionic-plugin-keyboard': '1.0.8'
});
Package.onUse(function(api) {
api.versionsFrom("1.0");
+
api.use([
+ "jandres:template-extension@4.0.4",
+ "ecmascript@0.1.6",
"templating",
"underscore",
+ "reactive-var",
"fastclick",
"iron:router@1.0.0",
"tracker",
"session",
- "jquery"
+ "jquery",
+ "jandres:snapjs@2.0.9",
+ "fourseven:scss@3.3.3",
+
+ "jandres:meteoric-sass@1.2.4"
], "client");
api.addFiles([
- "vendor/snap.js",
- "vendor/snap.css",
"vendor/slick.js",
"vendor/slick.css",
- "vendor/slip.js"
+ "vendor/slip.js",
+
+ "vendor/Animate.js",
+ "vendor/EasyScroller.js",
+ "vendor/Scroller.js"
+ ], "client");
+
+ api.addFiles([
+ "styles/_transitions.scss",
+ "styles/main.scss"
], "client");
api.addFiles([
+ "lib/utility.js",
+ "lib/polyfill.js",
+ "lib/platform.js",
+
"components/ionActionSheet/ionActionSheet.html",
"components/ionActionSheet/ionActionSheet.js",
@@ -42,18 +63,27 @@ Package.onUse(function(api) {
"components/ionContent/ionContent.html",
"components/ionContent/ionContent.js",
+ "components/ionDeleteButton/ionDeleteButton.html",
+ "components/ionDeleteButton/ionDeleteButton.js",
+
"components/ionFooterBar/ionFooterBar.html",
"components/ionFooterBar/ionFooterBar.js",
"components/ionHeaderBar/ionHeaderBar.html",
"components/ionHeaderBar/ionHeaderBar.js",
- "components/ionIcon/ionIcon.html",
- "components/ionIcon/ionIcon.js",
+ "components/ionInfiniteScroll/ionInfiniteScroll.html",
+ "components/ionInfiniteScroll/ionInfiniteScroll.js",
"components/ionItem/ionItem.html",
"components/ionItem/ionItem.js",
+ "components/ionItemOptions/ionItemOptions.html",
+ "components/ionItemOptions/ionItemOptions.js",
+
+ "components/ionItemContent/ionItemContent.html",
+ "components/ionItemContent/ionItemContent.js",
+
"components/ionKeyboard/ionKeyboard.js",
"components/ionKeyboard/ionInputFocus.js",
@@ -72,6 +102,9 @@ Package.onUse(function(api) {
"components/ionNavBar/ionNavBar.html",
"components/ionNavBar/ionNavBar.js",
+ "components/ionOptionButton/ionOptionButton.html",
+ "components/ionOptionButton/ionOptionButton.js",
+
"components/ionNavBackButton/ionNavBackButton.html",
"components/ionNavBackButton/ionNavBackButton.js",
@@ -90,6 +123,12 @@ Package.onUse(function(api) {
"components/ionRadio/ionRadio.html",
"components/ionRadio/ionRadio.js",
+ "components/ionReorderButton/ionReorderButton.html",
+ "components/ionReorderButton/ionReorderButton.js",
+
+ "components/ionScroll/ionScroll.html",
+ "components/ionScroll/ionScroll.js",
+
"components/ionSideMenu/ionSideMenu.html",
"components/ionSideMenu/ionSideMenu.js",
diff --git a/styles/_transitions.scss b/styles/_transitions.scss
new file mode 100644
index 0000000..6c681f1
--- /dev/null
+++ b/styles/_transitions.scss
@@ -0,0 +1,88 @@
+@import '{jandres:meteoric-sass}/scss/_mixins.scss';
+@import '{jandres:meteoric-sass}/scss/_variables.scss';
+
+[nav-view-transition] {
+ /**
+ * Transition states:
+ * 1. stage: Set the initial position of the not yet existent view.
+ * 2. entering: Triggers animation toward translate3d(0, 0, 0), centering the view.
+ * 3. active: Keeps the translate3d(0, 0, 0) for the rest of the states, until leaving.
+ * 4. leaving: Transitions out.
+ */
+
+ [nav-view="stage"] { opacity: 0; }
+ &[nav-view-direction="back"] [nav-view="stage"] {
+ @include translate3d(-100%, 0px, 0px);
+ }
+ &[nav-view-direction="forward"] [nav-view="stage"] {
+ @include translate3d(100%, 0px, 0px);
+ }
+
+ [nav-view="entering"] { opacity: 1; }
+ &[nav-view-direction="back"] [nav-view="entering"],
+ &[nav-view-direction="forward"] [nav-view="entering"]{
+ @include translate3d(0px, 0px, 0px);
+ }
+
+ [nav-view="leaving"] { opacity: 0; }
+ &[nav-view-direction="back"] [nav-view="leaving"] {
+ @include translate3d(100%, 0px, 0px);
+ }
+ &[nav-view-direction="forward"] [nav-view="leaving"] {
+ @include translate3d(-100%, 0px, 0px);
+ }
+
+ &[nav-view-direction="back"],
+ &[nav-view-direction="forward"] {
+ [nav-view="active"] {
+ @include translate3d(0px, 0px, 0px);
+ }
+ }
+}
+
+[nav-bar-transition] {
+ [nav-bar="leaving"] {
+ z-index: 11;
+ .bar.bar-header { background: transparent; }
+ }
+
+ [nav-bar="leaving"], [nav-bar="stage"] {
+ .buttons, .title {
+ opacity: 0.0;
+ }
+ }
+ [nav-bar="entering"], [nav-bar="active"] {
+ .buttons, .title {
+ opacity: 1.0;
+ }
+ }
+}
+
+[nav-bar-transition="ios"] {
+ &[nav-bar-direction="back"] {
+ [nav-bar="stage"] {
+ .back-text, .title { @include translate3d(-50%, 0px, 0px); }
+ }
+ [nav-bar="entering"],
+ [nav-bar="active"] {
+ .back-text, .title { @include translate3d(0px, 0px, 0px); }
+ }
+ [nav-bar="leaving"] {
+ .back-text, .title { @include translate3d(50%, 0px, 0px); }
+ }
+ }
+ &[nav-bar-direction="forward"] {
+ [nav-bar="stage"] {
+ .back-text, .title { @include translate3d(50%, 0px, 0px); }
+ }
+ [nav-bar="entering"],
+ [nav-bar="active"] {
+ .back-text, .title {
+ @include translate3d(0px, 0px, 0px);
+ }
+ }
+ [nav-bar="leaving"] {
+ .back-text, .title { @include translate3d(-50%, 0px, 0px); }
+ }
+ }
+}
\ No newline at end of file
diff --git a/styles/main.scss b/styles/main.scss
new file mode 100644
index 0000000..4e6de10
--- /dev/null
+++ b/styles/main.scss
@@ -0,0 +1,49 @@
+@import "_transitions.scss";
+
+.snap-drawers {
+ .snap-drawer-left {}
+ .item-options.snap-drawer.snap-drawer-right {
+ width: initial;
+ overflow: initial;
+ }
+}
+
+* {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+html {
+ -ms-touch-action: none;
+}
+
+body,ul,li {
+ padding: 0;
+ margin: 0;
+ border: 0;
+}
+
+body {
+ font-size: 12px;
+ font-family: ubuntu, helvetica, arial;
+ overflow: hidden; /* this is important to prevent the whole page to bounce */
+}
+
+.meteoric-scroller-container {
+ overflow: hidden;
+ position: relative;
+ max-height: 100%;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
+ -ms-touch-action: none;
+
+ width: 100%;
+ height: 100%;
+}
+
+// Copied from ft-scroller to make zynga scroller.
+.meteoric-scroller-x, .meteoric-scroller-y { min-width: 100%; min-height: 100%; overflow: hidden; top: 0; bottom: 0; left: 0; right: 0; }
+.overflow-scroll {
+ .meteoric-scroller-x, .meteoric-scroller-y { overflow: auto; }
+}
+.meteoric-scroller-x { display: inline-block }
\ No newline at end of file
diff --git a/vendor/Animate.js b/vendor/Animate.js
new file mode 100644
index 0000000..b316a72
--- /dev/null
+++ b/vendor/Animate.js
@@ -0,0 +1,239 @@
+/*
+ * Scroller
+ * http://github.com/zynga/scroller
+ *
+ * Copyright 2011, Zynga Inc.
+ * Licensed under the MIT License.
+ * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
+ *
+ * Based on the work of: Unify Project (unify-project.org)
+ * http://unify-project.org
+ * Copyright 2011, Deutsche Telekom AG
+ * License: MIT + Apache (V2)
+ */
+
+/**
+ * Generic animation class with support for dropped frames both optional easing and duration.
+ *
+ * Optional duration is useful when the lifetime is defined by another condition than time
+ * e.g. speed of an animating object, etc.
+ *
+ * Dropped frame logic allows to keep using the same updater logic independent from the actual
+ * rendering. This eases a lot of cases where it might be pretty complex to break down a state
+ * based on the pure time difference.
+ */
+(function(global) {
+ var time = Date.now || function() {
+ return +new Date();
+ };
+ var desiredFrames = 60;
+ var millisecondsPerSecond = 1000;
+ var running = {};
+ var counter = 1;
+
+ // Create namespaces
+ if (!global.core) {
+ global.core = { effect : {} };
+
+ } else if (!core.effect) {
+ core.effect = {};
+ }
+
+ core.effect.Animate = {
+
+ /**
+ * A requestAnimationFrame wrapper / polyfill.
+ *
+ * @param callback {Function} The callback to be invoked before the next repaint.
+ * @param root {HTMLElement} The root element for the repaint
+ */
+ requestAnimationFrame: (function() {
+
+ // Check for request animation Frame support
+ var requestFrame = global.requestAnimationFrame || global.webkitRequestAnimationFrame || global.mozRequestAnimationFrame || global.oRequestAnimationFrame;
+ var isNative = !!requestFrame;
+
+ if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
+ isNative = false;
+ }
+
+ if (isNative) {
+ return function(callback, root) {
+ requestFrame(callback, root)
+ };
+ }
+
+ var TARGET_FPS = 60;
+ var requests = {};
+ var requestCount = 0;
+ var rafHandle = 1;
+ var intervalHandle = null;
+ var lastActive = +new Date();
+
+ return function(callback, root) {
+ var callbackHandle = rafHandle++;
+
+ // Store callback
+ requests[callbackHandle] = callback;
+ requestCount++;
+
+ // Create timeout at first request
+ if (intervalHandle === null) {
+
+ intervalHandle = setInterval(function() {
+
+ var time = +new Date();
+ var currentRequests = requests;
+
+ // Reset data structure before executing callbacks
+ requests = {};
+ requestCount = 0;
+
+ for(var key in currentRequests) {
+ if (currentRequests.hasOwnProperty(key)) {
+ currentRequests[key](time);
+ lastActive = time;
+ }
+ }
+
+ // Disable the timeout when nothing happens for a certain
+ // period of time
+ if (time - lastActive > 2500) {
+ clearInterval(intervalHandle);
+ intervalHandle = null;
+ }
+
+ }, 1000 / TARGET_FPS);
+ }
+
+ return callbackHandle;
+ };
+
+ })(),
+
+
+ /**
+ * Stops the given animation.
+ *
+ * @param id {Integer} Unique animation ID
+ * @return {Boolean} Whether the animation was stopped (aka, was running before)
+ */
+ stop: function(id) {
+ var cleared = running[id] != null;
+ if (cleared) {
+ running[id] = null;
+ }
+
+ return cleared;
+ },
+
+
+ /**
+ * Whether the given animation is still running.
+ *
+ * @param id {Integer} Unique animation ID
+ * @return {Boolean} Whether the animation is still running
+ */
+ isRunning: function(id) {
+ return running[id] != null;
+ },
+
+
+ /**
+ * Start the animation.
+ *
+ * @param stepCallback {Function} Pointer to function which is executed on every step.
+ * Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
+ * @param verifyCallback {Function} Executed before every animation step.
+ * Signature of the method should be `function() { return continueWithAnimation; }`
+ * @param completedCallback {Function}
+ * Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
+ * @param duration {Integer} Milliseconds to run the animation
+ * @param easingMethod {Function} Pointer to easing function
+ * Signature of the method should be `function(percent) { return modifiedValue; }`
+ * @param root {Element ? document.body} Render root, when available. Used for internal
+ * usage of requestAnimationFrame.
+ * @return {Integer} Identifier of animation. Can be used to stop it any time.
+ */
+ start: function(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
+
+ var start = time();
+ var lastFrame = start;
+ var percent = 0;
+ var dropCounter = 0;
+ var id = counter++;
+
+ if (!root) {
+ root = document.body;
+ }
+
+ // Compacting running db automatically every few new animations
+ if (id % 20 === 0) {
+ var newRunning = {};
+ for (var usedId in running) {
+ newRunning[usedId] = true;
+ }
+ running = newRunning;
+ }
+
+ // This is the internal step method which is called every few milliseconds
+ var step = function(virtual) {
+
+ // Normalize virtual value
+ var render = virtual !== true;
+
+ // Get current time
+ var now = time();
+
+ // Verification is executed before next animation step
+ if (!running[id] || (verifyCallback && !verifyCallback(id))) {
+
+ running[id] = null;
+ completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, false);
+ return;
+
+ }
+
+ // For the current rendering to apply let's update omitted steps in memory.
+ // This is important to bring internal state variables up-to-date with progress in time.
+ if (render) {
+
+ var droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1;
+ for (var j = 0; j < Math.min(droppedFrames, 4); j++) {
+ step(true);
+ dropCounter++;
+ }
+
+ }
+
+ // Compute percent value
+ if (duration) {
+ percent = (now - start) / duration;
+ if (percent > 1) {
+ percent = 1;
+ }
+ }
+
+ // Execute step callback, then...
+ var value = easingMethod ? easingMethod(percent) : percent;
+ if ((stepCallback(value, now, render) === false || percent === 1) && render) {
+ running[id] = null;
+ completedCallback && completedCallback(desiredFrames - (dropCounter / ((now - start) / millisecondsPerSecond)), id, percent === 1 || duration == null);
+ } else if (render) {
+ lastFrame = now;
+ core.effect.Animate.requestAnimationFrame(step, root);
+ }
+ };
+
+ // Mark as running
+ running[id] = true;
+
+ // Init first step
+ core.effect.Animate.requestAnimationFrame(step, root);
+
+ // Return unique animation ID
+ return id;
+ }
+ };
+})(this);
+
diff --git a/vendor/EasyScroller.js b/vendor/EasyScroller.js
new file mode 100644
index 0000000..209228a
--- /dev/null
+++ b/vendor/EasyScroller.js
@@ -0,0 +1,257 @@
+var EasyScroller = function(content, options) {
+
+ this.content = content;
+ this.container = content.parentNode;
+
+ options = options || {};
+ options.content = content;
+ this.options = options;
+ this.options.stopPropagation = typeof this.options.stopPropagation !== "undefined" ? this.options.stopPropagation : true;
+
+ // create Scroller instance
+ var that = this;
+ this.scroller = new Scroller(function(left, top, zoom) {
+ that.render(left, top, zoom);
+ }, options);
+
+ // bind events
+ this.bindEvents();
+
+ // the content element needs a correct transform origin for zooming
+ this.content.style[EasyScroller.vendorPrefix + 'TransformOrigin'] = "left top";
+
+ let MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window[EasyScroller.vendorPrefix + 'MutationObserver'];
+ this._mutationObserver = new MutationObserver(this._domChanged.bind(this));
+
+ if (this._mutationObserver) {
+ this._mutationObserver.observe(this.container, {
+ childList: true,
+ characterData: true,
+ subtree: true
+ });
+ } else {
+ this.container.addEventListener('DOMSubtreeModified', function (e) {
+ // Ignore changes to nested FT Scrollers - even updating a transform style
+ // can trigger a DOMSubtreeModified in IE, causing nested scrollers to always
+ // favour the deepest scroller as parent scrollers 'resize'/end scrolling.
+ if (e && (e.srcElement === that.container)) {
+ return;
+ }
+
+ that._domChanged(e);
+ }, true);
+ }
+
+ // reflow for the first time
+ this.reflow();
+};
+
+EasyScroller.prototype._domChanged = function(e) {
+ var self = this;
+ // If the timer is active, clear it
+ if (this._domChangeDebouncer) {
+ window.clearTimeout(this._domChangeDebouncer);
+ }
+
+ // React to resizes at once
+ if (e && e.type === 'resize') {
+ this.reflow();
+
+ // For other changes, which may occur in groups, set up the DOM changed timer
+ } else {
+ this._domChangeDebouncer = setTimeout(self.reflow.bind(self), 100);
+ }
+};
+
+EasyScroller.prototype.render = (function() {
+
+ var docStyle = document.documentElement.style;
+
+ var engine;
+ if (window.opera && Object.prototype.toString.call(opera) === '[object Opera]') {
+ engine = 'presto';
+ } else if ('MozAppearance' in docStyle) {
+ engine = 'gecko';
+ } else if ('WebkitAppearance' in docStyle) {
+ engine = 'webkit';
+ } else if (typeof navigator.cpuClass === 'string') {
+ engine = 'trident';
+ }
+
+ var vendorPrefix = EasyScroller.vendorPrefix = {
+ trident: 'ms',
+ gecko: 'Moz',
+ webkit: 'Webkit',
+ presto: 'O'
+ }[engine];
+
+ var helperElem = document.createElement("div");
+ var undef;
+
+ var perspectiveProperty = vendorPrefix + "Perspective";
+ var transformProperty = vendorPrefix + "Transform";
+
+ if (helperElem.style[perspectiveProperty] !== undef) {
+
+ return function(left, top, zoom) {
+ this.content.style[transformProperty] = 'translate3d(' + (-left) + 'px,' + (-top) + 'px,0) scale(' + zoom + ')';
+ };
+
+ } else if (helperElem.style[transformProperty] !== undef) {
+
+ return function(left, top, zoom) {
+ this.content.style[transformProperty] = 'translate(' + (-left) + 'px,' + (-top) + 'px) scale(' + zoom + ')';
+ };
+
+ } else {
+
+ return function(left, top, zoom) {
+ this.content.style.marginLeft = left ? (-left/zoom) + 'px' : '';
+ this.content.style.marginTop = top ? (-top/zoom) + 'px' : '';
+ this.content.style.zoom = zoom || '';
+ };
+
+ }
+})();
+
+EasyScroller.prototype.reflow = function() {
+
+ // set the right scroller dimensions
+ this.scroller.setDimensions();
+};
+
+EasyScroller.prototype.bindEvents = function() {
+
+ var that = this;
+
+ // reflow handling
+ window.addEventListener("resize", that.reflow.bind(that), false);
+
+ // touch devices bind touch events
+ if ('ontouchstart' in window) {
+
+ this.container.addEventListener("touchstart", function(e) {
+
+ // Don't react if initial down happens on a form element
+ if (e.touches[0] && e.touches[0].target && e.touches[0].target.tagName.match(/input|textarea|select/i)) {
+ return;
+ }
+
+ that.scroller.doTouchStart(e.touches, e.timeStamp);
+ that.options.stopPropagation && e.stopPropagation();
+ }, false);
+
+ document.addEventListener("touchmove", function(e) {
+ that.scroller.doTouchMove(e.touches, e.timeStamp, e.scale);
+ that.options.stopPropagation && e.stopPropagation();
+ }, false);
+
+ document.addEventListener("touchend", function(e) {
+ that.scroller.doTouchEnd(e.timeStamp);
+ that.options.stopPropagation && e.stopPropagation();
+ }, false);
+
+ document.addEventListener("touchcancel", function(e) {
+ that.scroller.doTouchEnd(e.timeStamp);
+ }, false);
+
+ // non-touch bind mouse events
+ } else {
+
+ var mousedown = false;
+
+ this.container.addEventListener("mousedown", function(e) {
+
+ if (e.target.tagName.match(/input|textarea|select/i)) {
+ return;
+ }
+
+ that.options.stopPropagation && e.stopPropagation();
+
+ that.scroller.doTouchStart([{
+ pageX: e.pageX,
+ pageY: e.pageY
+ }], e.timeStamp);
+
+ mousedown = true;
+ }, false);
+
+ document.addEventListener("mousemove", function(e) {
+
+ if (!mousedown) {
+ return;
+ }
+
+ that.options.stopPropagation && e.stopPropagation();
+
+ that.scroller.doTouchMove([{
+ pageX: e.pageX,
+ pageY: e.pageY
+ }], e.timeStamp);
+
+ mousedown = true;
+
+ }, false);
+
+ document.addEventListener("mouseup", function(e) {
+
+ if (!mousedown) {
+ return;
+ }
+
+ that.options.stopPropagation && e.stopPropagation();
+
+ that.scroller.doTouchEnd(e.timeStamp);
+
+ mousedown = false;
+
+ }, false);
+
+ this.container.addEventListener("mousewheel", function(e) {
+ if(that.scroller.options.zooming) {
+ that.scroller.doMouseZoom(e.wheelDelta, e.timeStamp, e.pageX, e.pageY);
+ }
+ }, false);
+
+ }
+
+ // Stolen from ft-scroller.
+ // By removing preventDefault() above and only allowing click if not dragging and not decelerating,
+ // we can allow click when not scrolling or decelerating.
+ document.addEventListener("click", function(e) {
+ var preventClick = that.scroller.__isDragging || that.scroller.__isDecelerating;
+
+ if (!preventClick) { return true; }
+
+ e.preventDefault();
+ e.stopPropagation();
+ return false;
+ });
+};
+
+// automatically attach an EasyScroller to elements found with the right data attributes
+document.addEventListener("DOMContentLoaded", function() {
+
+ var elements = document.querySelectorAll('[data-scrollable],[data-zoomable]'), element;
+ for (var i = 0; i < elements.length; i++) {
+
+ element = elements[i];
+ var scrollable = element.dataset.scrollable;
+ var zoomable = element.dataset.zoomable || '';
+ var zoomOptions = zoomable.split('-');
+ var minZoom = zoomOptions.length > 1 && parseFloat(zoomOptions[0]);
+ var maxZoom = zoomOptions.length > 1 && parseFloat(zoomOptions[1]);
+
+ new EasyScroller(element, {
+ scrollingX: scrollable === 'true' || scrollable === 'x',
+ scrollingY: scrollable === 'true' || scrollable === 'y',
+ zooming: zoomable === 'true' || zoomOptions.length > 1,
+ minZoom: minZoom,
+ maxZoom: maxZoom
+ });
+
+ };
+
+}, false);
+
+window.EasyScroller = EasyScroller;
\ No newline at end of file
diff --git a/vendor/Scroller.js b/vendor/Scroller.js
new file mode 100644
index 0000000..1515e12
--- /dev/null
+++ b/vendor/Scroller.js
@@ -0,0 +1,1326 @@
+/*
+ * Scroller
+ * http://github.com/zynga/scroller
+ *
+ * Copyright 2011, Zynga Inc.
+ * Licensed under the MIT License.
+ * https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
+ *
+ * Based on the work of: Unify Project (unify-project.org)
+ * http://unify-project.org
+ * Copyright 2011, Deutsche Telekom AG
+ * License: MIT + Apache (V2)
+ */
+
+var Scroller;
+
+(function() {
+ var NOOP = function(){};
+
+ /**
+ * A pure logic 'component' for 'virtual' scrolling/zooming.
+ */
+ Scroller = function(callback, options) {
+
+ this.__callback = callback;
+
+ this.options = {
+
+ /** Enable scrolling on x-axis */
+ scrollingX: true,
+
+ /** Enable scrolling on y-axis */
+ scrollingY: true,
+
+ /** Enable animations for deceleration, snap back, zooming and scrolling */
+ animating: true,
+
+ /** duration for animations triggered by scrollTo/zoomTo */
+ animationDuration: 250,
+
+ /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */
+ bouncing: true,
+
+ /** Enable locking to the main axis if user moves only slightly on one of them at start */
+ locking: true,
+
+ /** Enable pagination mode (switching between full page content panes) */
+ paging: false,
+
+ /** Enable snapping of content to a configured pixel grid */
+ snapping: false,
+
+ /** Enable zooming of content via API, fingers and mouse wheel */
+ zooming: false,
+
+ /** Minimum zoom level */
+ minZoom: 0.5,
+
+ /** Maximum zoom level */
+ maxZoom: 3,
+
+ /** Multiply or decrease scrolling speed **/
+ speedMultiplier: 1,
+
+ /**
+ * Callback that is fired while scrolling.
+ */
+ scrolling: NOOP,
+
+ /** Callback that is fired on the later of touch end or deceleration end,
+ provided that another scrolling action has not begun. Used to know
+ when to fade out a scrollbar. */
+ scrollingComplete: NOOP,
+
+ /** This configures the amount of change applied to deceleration when reaching boundaries **/
+ penetrationDeceleration : 0.03,
+
+ /** This configures the amount of change applied to acceleration when reaching boundaries **/
+ penetrationAcceleration : 0.08,
+
+
+ content: null
+ };
+
+ for (var key in options) {
+ this.options[key] = options[key];
+ }
+
+ this.__container = this.options.content.parentNode;
+ };
+
+
+ // Easing Equations (c) 2003 Robert Penner, all rights reserved.
+ // Open source under the BSD License.
+
+ /**
+ * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
+ **/
+ var easeOutCubic = function(pos) {
+ return (Math.pow((pos - 1), 3) + 1);
+ };
+
+ /**
+ * @param pos {Number} position between 0 (start of effect) and 1 (end of effect)
+ **/
+ var easeInOutCubic = function(pos) {
+ if ((pos /= 0.5) < 1) {
+ return 0.5 * Math.pow(pos, 3);
+ }
+
+ return 0.5 * (Math.pow((pos - 2), 3) + 2);
+ };
+
+
+ var members = {
+
+ /*
+ ---------------------------------------------------------------------------
+ INTERNAL FIELDS :: STATUS
+ ---------------------------------------------------------------------------
+ */
+
+ /** {Boolean} Whether only a single finger is used in touch handling */
+ __isSingleTouch: false,
+
+ /** {Boolean} Whether a touch event sequence is in progress */
+ __isTracking: false,
+
+ /** {Boolean} Whether a deceleration animation went to completion. */
+ __didDecelerationComplete: false,
+
+ /**
+ * {Boolean} Whether a gesture zoom/rotate event is in progress. Activates when
+ * a gesturestart event happens. This has higher priority than dragging.
+ */
+ __isGesturing: false,
+
+ /**
+ * {Boolean} Whether the user has moved by such a distance that we have enabled
+ * dragging mode. Hint: It's only enabled after some pixels of movement to
+ * not interrupt with clicks etc.
+ */
+ __isDragging: false,
+
+ /**
+ * {Boolean} Not touching and dragging anymore, and smoothly animating the
+ * touch sequence using deceleration.
+ */
+ __isDecelerating: false,
+
+ /**
+ * {Boolean} Smoothly animating the currently configured change
+ */
+ __isAnimating: false,
+
+
+
+ /*
+ ---------------------------------------------------------------------------
+ INTERNAL FIELDS :: DIMENSIONS
+ ---------------------------------------------------------------------------
+ */
+
+ /** {Integer} Available outer left position (from document perspective) */
+ __clientLeft: function() {
+ var rect = this.__container.getBoundingClientRect();
+ return rect.clientLeft + rect.left;
+ },
+
+ /** {Integer} Available outer top position (from document perspective) */
+ __clientTop: function() {
+ var rect = this.__container.getBoundingClientRect();
+ return rect.clientTop + rect.top;
+ },
+
+ /** {Integer} Available outer width */
+ __clientWidth: function() { return this.__container.clientWidth; },
+
+ /** {Integer} Available outer height */
+ __clientHeight: function() { return this.__container.clientHeight; },
+
+ /** {Integer} Outer width of content */
+ __contentWidth: function() { return this.options.content.offsetWidth; },
+
+ /** {Integer} Outer height of content */
+ __contentHeight: function() { return this.options.content.offsetHeight; },
+
+ /** {Integer} Snapping width for content */
+ __snapWidth: 100,
+
+ /** {Integer} Snapping height for content */
+ __snapHeight: 100,
+
+ /** {Integer} Height to assign to refresh area */
+ __refreshHeight: null,
+
+ /** {Boolean} Whether the refresh process is enabled when the event is released now */
+ __refreshActive: false,
+
+ /** {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release */
+ __refreshActivate: null,
+
+ /** {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled */
+ __refreshDeactivate: null,
+
+ /** {Function} Callback to execute to start the actual refresh. Call {@link #refreshFinish} when done */
+ __refreshStart: null,
+
+ /** {Number} Zoom level */
+ __zoomLevel: 1,
+
+ /** {Number} Scroll position on x-axis */
+ __scrollLeft: 0,
+
+ /** {Number} Scroll position on y-axis */
+ __scrollTop: 0,
+
+ /** {Integer} Maximum allowed scroll position on x-axis */
+ __maxScrollLeft: function() { return Math.max((this.__contentWidth() * this.__zoomLevel) - this.__clientWidth(), 0); },
+
+ /** {Integer} Maximum allowed scroll position on y-axis */
+ __maxScrollTop: function() { return Math.max((this.__contentHeight() * this.__zoomLevel) - this.__clientHeight(), 0); },
+
+ /* {Number} Scheduled left position (final position when animating) */
+ __scheduledLeft: 0,
+
+ /* {Number} Scheduled top position (final position when animating) */
+ __scheduledTop: 0,
+
+ /* {Number} Scheduled zoom level (final scale when animating) */
+ __scheduledZoom: 0,
+
+
+
+ /*
+ ---------------------------------------------------------------------------
+ INTERNAL FIELDS :: LAST POSITIONS
+ ---------------------------------------------------------------------------
+ */
+
+ /** {Number} Left position of finger at start */
+ __lastTouchLeft: null,
+
+ /** {Number} Top position of finger at start */
+ __lastTouchTop: null,
+
+ /** {Date} Timestamp of last move of finger. Used to limit tracking range for deceleration speed. */
+ __lastTouchMove: null,
+
+ /** {Array} List of positions, uses three indexes for each state: left, top, timestamp */
+ __positions: null,
+
+
+
+ /*
+ ---------------------------------------------------------------------------
+ INTERNAL FIELDS :: DECELERATION SUPPORT
+ ---------------------------------------------------------------------------
+ */
+
+ /** {Integer} Minimum left scroll position during deceleration */
+ __minDecelerationScrollLeft: null,
+
+ /** {Integer} Minimum top scroll position during deceleration */
+ __minDecelerationScrollTop: null,
+
+ /** {Integer} Maximum left scroll position during deceleration */
+ __maxDecelerationScrollLeft: null,
+
+ /** {Integer} Maximum top scroll position during deceleration */
+ __maxDecelerationScrollTop: null,
+
+ /** {Number} Current factor to modify horizontal scroll position with on every step */
+ __decelerationVelocityX: null,
+
+ /** {Number} Current factor to modify vertical scroll position with on every step */
+ __decelerationVelocityY: null,
+
+
+
+ /*
+ ---------------------------------------------------------------------------
+ PUBLIC API
+ ---------------------------------------------------------------------------
+ */
+
+ /**
+ * Configures the dimensions of the client (outer) and content (inner) elements.
+ * Requires the available space for the outer element and the outer size of the inner element.
+ * All values which are falsy (null or zero etc.) are ignored and the old value is kept.
+ *
+ * @param clientWidth {Integer ? null} Inner width of outer element
+ * @param clientHeight {Integer ? null} Inner height of outer element
+ * @param contentWidth {Integer ? null} Outer width of inner element
+ * @param contentHeight {Integer ? null} Outer height of inner element
+ */
+ setDimensions: function() {
+ var self = this;
+
+ // Refresh scroll position
+ self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
+
+ },
+
+
+ /**
+ * Configures the snapping (when snapping is active)
+ *
+ * @param width {Integer} Snapping width
+ * @param height {Integer} Snapping height
+ */
+ setSnapSize: function(width, height) {
+
+ var self = this;
+
+ self.__snapWidth = width;
+ self.__snapHeight = height;
+
+ },
+
+
+ /**
+ * Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
+ * the user event is released during visibility of this zone. This was introduced by some apps on iOS like
+ * the official Twitter client.
+ *
+ * @param height {Integer} Height of pull-to-refresh zone on top of rendered list
+ * @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
+ * @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
+ * @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
+ */
+ activatePullToRefresh: function(height, activateCallback, deactivateCallback, startCallback) {
+
+ var self = this;
+
+ self.__refreshHeight = height;
+ self.__refreshActivate = activateCallback;
+ self.__refreshDeactivate = deactivateCallback;
+ self.__refreshStart = startCallback;
+
+ },
+
+
+ /**
+ * Starts pull-to-refresh manually.
+ */
+ triggerPullToRefresh: function() {
+ // Use publish instead of scrollTo to allow scrolling to out of boundary position
+ // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
+ this.__publish(this.__scrollLeft, -this.__refreshHeight, this.__zoomLevel, true);
+
+ if (this.__refreshStart) {
+ this.__refreshStart();
+ }
+ },
+
+
+ /**
+ * Signalizes that pull-to-refresh is finished.
+ */
+ finishPullToRefresh: function() {
+
+ var self = this;
+
+ self.__refreshActive = false;
+ if (self.__refreshDeactivate) {
+ self.__refreshDeactivate();
+ }
+
+ self.scrollTo(self.__scrollLeft, self.__scrollTop, true);
+
+ },
+
+
+ /**
+ * Returns the scroll position and zooming values
+ *
+ * @return {Map} `left` and `top` scroll position and `zoom` level
+ */
+ getValues: function() {
+
+ var self = this;
+
+ return {
+ left: self.__scrollLeft,
+ top: self.__scrollTop,
+ zoom: self.__zoomLevel
+ };
+
+ },
+
+
+ /**
+ * Returns the maximum scroll values
+ *
+ * @return {Map} `left` and `top` maximum scroll values
+ */
+ getScrollMax: function() {
+
+ var self = this;
+
+ return {
+ left: self.__maxScrollLeft(),
+ top: self.__maxScrollTop()
+ };
+
+ },
+
+
+ /**
+ * Zooms to the given level. Supports optional animation. Zooms
+ * the center when no coordinates are given.
+ *
+ * @param level {Number} Level to zoom to
+ * @param animate {Boolean ? false} Whether to use animation
+ * @param originLeft {Number ? null} Zoom in at given left coordinate
+ * @param originTop {Number ? null} Zoom in at given top coordinate
+ * @param callback {Function ? null} A callback that gets fired when the zoom is complete.
+ */
+ zoomTo: function(level, animate, originLeft, originTop, callback) {
+
+ var self = this;
+ if (!self.options.zooming) {
+ throw new Error("Zooming is not enabled!");
+ }
+
+ // Add callback if exists
+ if(callback) {
+ self.__zoomComplete = callback;
+ }
+
+ // Stop deceleration
+ if (self.__isDecelerating) {
+ core.effect.Animate.stop(self.__isDecelerating);
+ self.__isDecelerating = false;
+ }
+
+ var oldLevel = self.__zoomLevel;
+
+ // Normalize input origin to center of viewport if not defined
+ if (originLeft == null) {
+ originLeft = self.__clientWidth() / 2;
+ }
+
+ if (originTop == null) {
+ originTop = self.__clientHeight() / 2;
+ }
+
+ // Limit level according to configuration
+ level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
+
+ // Recompute maximum values while temporary tweaking maximum scroll ranges
+ self.__computeScrollMax(level);
+
+ // Recompute left and top coordinates based on new zoom level
+ var left = ((originLeft + self.__scrollLeft) * level / oldLevel) - originLeft;
+ var top = ((originTop + self.__scrollTop) * level / oldLevel) - originTop;
+
+ // Limit x-axis
+ if (left > self.__maxScrollLeft()) {
+ left = self.__maxScrollLeft();
+ } else if (left < 0) {
+ left = 0;
+ }
+
+ // Limit y-axis
+ if (top > self.__maxScrollTop()) {
+ top = self.__maxScrollTop();
+ } else if (top < 0) {
+ top = 0;
+ }
+
+ // Push values out
+ self.__publish(left, top, level, animate);
+
+ },
+
+
+ /**
+ * Zooms the content by the given factor.
+ *
+ * @param factor {Number} Zoom by given factor
+ * @param animate {Boolean ? false} Whether to use animation
+ * @param originLeft {Number ? 0} Zoom in at given left coordinate
+ * @param originTop {Number ? 0} Zoom in at given top coordinate
+ * @param callback {Function ? null} A callback that gets fired when the zoom is complete.
+ */
+ zoomBy: function(factor, animate, originLeft, originTop, callback) {
+
+ var self = this;
+
+ self.zoomTo(self.__zoomLevel * factor, animate, originLeft, originTop, callback);
+
+ },
+
+
+ /**
+ * Scrolls to the given position. Respect limitations and snapping automatically.
+ *
+ * @param left {Number?null} Horizontal scroll position, keeps current if value is
null
+ * @param top {Number?null} Vertical scroll position, keeps current if value is
null
+ * @param animate {Boolean?false} Whether the scrolling should happen using an animation
+ * @param zoom {Number?null} Zoom level to go to
+ */
+ scrollTo: function(left, top, animate, zoom) {
+
+ var self = this;
+
+ // Stop deceleration
+ if (self.__isDecelerating) {
+ core.effect.Animate.stop(self.__isDecelerating);
+ self.__isDecelerating = false;
+ }
+
+ // Correct coordinates based on new zoom level
+ if (zoom != null && zoom !== self.__zoomLevel) {
+
+ if (!self.options.zooming) {
+ throw new Error("Zooming is not enabled!");
+ }
+
+ left *= zoom;
+ top *= zoom;
+
+ // Recompute maximum values while temporary tweaking maximum scroll ranges
+ self.__computeScrollMax(zoom);
+
+ } else {
+
+ // Keep zoom when not defined
+ zoom = self.__zoomLevel;
+
+ }
+
+ if (!self.options.scrollingX) {
+
+ left = self.__scrollLeft;
+
+ } else {
+
+ if (self.options.paging) {
+ left = Math.round(left / self.__clientWidth()) * self.__clientWidth();
+ } else if (self.options.snapping) {
+ left = Math.round(left / self.__snapWidth) * self.__snapWidth;
+ }
+
+ }
+
+ if (!self.options.scrollingY) {
+
+ top = self.__scrollTop;
+
+ } else {
+
+ if (self.options.paging) {
+ top = Math.round(top / self.__clientHeight()) * self.__clientHeight();
+ } else if (self.options.snapping) {
+ top = Math.round(top / self.__snapHeight) * self.__snapHeight;
+ }
+
+ }
+
+ // Limit for allowed ranges
+ left = Math.max(Math.min(self.__maxScrollLeft(), left), 0);
+ top = Math.max(Math.min(self.__maxScrollTop(), top), 0);
+
+ // Don't animate when no change detected, still call publish to make sure
+ // that rendered position is really in-sync with internal data
+ if (left === self.__scrollLeft && top === self.__scrollTop) {
+ animate = false;
+ }
+
+ // Publish new values
+ self.__publish(left, top, zoom, animate);
+
+ },
+
+
+ /**
+ * Scroll by the given offset
+ *
+ * @param left {Number ? 0} Scroll x-axis by given offset
+ * @param top {Number ? 0} Scroll x-axis by given offset
+ * @param animate {Boolean ? false} Whether to animate the given change
+ */
+ scrollBy: function(left, top, animate) {
+
+ var self = this;
+
+ var startLeft = self.__isAnimating ? self.__scheduledLeft : self.__scrollLeft;
+ var startTop = self.__isAnimating ? self.__scheduledTop : self.__scrollTop;
+
+ self.scrollTo(startLeft + (left || 0), startTop + (top || 0), animate);
+
+ },
+
+
+
+ /*
+ ---------------------------------------------------------------------------
+ EVENT CALLBACKS
+ ---------------------------------------------------------------------------
+ */
+
+ /**
+ * Mouse wheel handler for zooming support
+ */
+ doMouseZoom: function(wheelDelta, timeStamp, pageX, pageY) {
+
+ var self = this;
+ var change = wheelDelta > 0 ? 0.97 : 1.03;
+ return self.zoomTo(self.__zoomLevel * change, false, pageX - self.__clientLeft(), pageY - self.__clientTop());
+
+ },
+
+
+ /**
+ * Touch start handler for scrolling support
+ */
+ doTouchStart: function(touches, timeStamp) {
+
+ // Array-like check is enough here
+ if (touches.length == null) {
+ throw new Error("Invalid touch list: " + touches);
+ }
+
+ if (timeStamp instanceof Date) {
+ timeStamp = timeStamp.valueOf();
+ }
+ if (typeof timeStamp !== "number") {
+ throw new Error("Invalid timestamp value: " + timeStamp);
+ }
+
+ var self = this;
+
+ // Reset interruptedAnimation flag
+ self.__interruptedAnimation = true;
+
+ // Stop deceleration
+ if (self.__isDecelerating) {
+ core.effect.Animate.stop(self.__isDecelerating);
+ self.__isDecelerating = false;
+ self.__interruptedAnimation = true;
+ }
+
+ // Stop animation
+ if (self.__isAnimating) {
+ core.effect.Animate.stop(self.__isAnimating);
+ self.__isAnimating = false;
+ self.__interruptedAnimation = true;
+ }
+
+ // Use center point when dealing with two fingers
+ var currentTouchLeft, currentTouchTop;
+ var isSingleTouch = touches.length === 1;
+ if (isSingleTouch) {
+ currentTouchLeft = touches[0].pageX;
+ currentTouchTop = touches[0].pageY;
+ } else {
+ currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
+ currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
+ }
+
+ // Store initial positions
+ self.__initialTouchLeft = currentTouchLeft;
+ self.__initialTouchTop = currentTouchTop;
+
+ // Store current zoom level
+ self.__zoomLevelStart = self.__zoomLevel;
+
+ // Store initial touch positions
+ self.__lastTouchLeft = currentTouchLeft;
+ self.__lastTouchTop = currentTouchTop;
+
+ // Store initial move time stamp
+ self.__lastTouchMove = timeStamp;
+
+ // Reset initial scale
+ self.__lastScale = 1;
+
+ // Reset locking flags
+ self.__enableScrollX = !isSingleTouch && self.options.scrollingX;
+ self.__enableScrollY = !isSingleTouch && self.options.scrollingY;
+
+ // Reset tracking flag
+ self.__isTracking = true;
+
+ // Reset deceleration complete flag
+ self.__didDecelerationComplete = false;
+
+ // Dragging starts directly with two fingers, otherwise lazy with an offset
+ self.__isDragging = !isSingleTouch;
+
+ // Some features are disabled in multi touch scenarios
+ self.__isSingleTouch = isSingleTouch;
+
+ // Clearing data structure
+ self.__positions = [];
+
+ },
+
+
+ /**
+ * Touch move handler for scrolling support
+ */
+ doTouchMove: function(touches, timeStamp, scale) {
+
+ // Array-like check is enough here
+ if (touches.length == null) {
+ throw new Error("Invalid touch list: " + touches);
+ }
+
+ if (timeStamp instanceof Date) {
+ timeStamp = timeStamp.valueOf();
+ }
+ if (typeof timeStamp !== "number") {
+ throw new Error("Invalid timestamp value: " + timeStamp);
+ }
+
+ var self = this;
+
+ // Ignore event when tracking is not enabled (event might be outside of element)
+ if (!self.__isTracking) {
+ return;
+ }
+
+
+ var currentTouchLeft, currentTouchTop;
+
+ // Compute move based around of center of fingers
+ if (touches.length === 2) {
+ currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2;
+ currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2;
+ } else {
+ currentTouchLeft = touches[0].pageX;
+ currentTouchTop = touches[0].pageY;
+ }
+
+ var positions = self.__positions;
+
+ // Are we already is dragging mode?
+ if (self.__isDragging) {
+
+ // Compute move distance
+ var moveX = currentTouchLeft - self.__lastTouchLeft;
+ var moveY = currentTouchTop - self.__lastTouchTop;
+
+ // Read previous scroll position and zooming
+ var scrollLeft = self.__scrollLeft;
+ var scrollTop = self.__scrollTop;
+ var level = self.__zoomLevel;
+
+ // Work with scaling
+ if (scale != null && self.options.zooming) {
+
+ var oldLevel = level;
+
+ // Recompute level based on previous scale and new scale
+ level = level / self.__lastScale * scale;
+
+ // Limit level according to configuration
+ level = Math.max(Math.min(level, self.options.maxZoom), self.options.minZoom);
+
+ // Only do further compution when change happened
+ if (oldLevel !== level) {
+
+ // Compute relative event position to container
+ var currentTouchLeftRel = currentTouchLeft - self.__clientLeft();
+ var currentTouchTopRel = currentTouchTop - self.__clientTop();
+
+ // Recompute left and top coordinates based on new zoom level
+ scrollLeft = ((currentTouchLeftRel + scrollLeft) * level / oldLevel) - currentTouchLeftRel;
+ scrollTop = ((currentTouchTopRel + scrollTop) * level / oldLevel) - currentTouchTopRel;
+
+ // Recompute max scroll values
+ self.__computeScrollMax(level);
+
+ }
+ }
+
+ if (self.__enableScrollX) {
+ scrollLeft -= moveX * this.options.speedMultiplier;
+ var maxScrollLeft = self.__maxScrollLeft();
+
+ if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
+
+ // Slow down on the edges
+ if (self.options.bouncing) {
+
+ scrollLeft += (moveX / 2 * this.options.speedMultiplier);
+
+ } else if (scrollLeft > maxScrollLeft) {
+
+ scrollLeft = maxScrollLeft;
+
+ } else {
+
+ scrollLeft = 0;
+
+ }
+ }
+
+ self.options.scrolling();
+ }
+
+ // Compute new vertical scroll position
+ if (self.__enableScrollY) {
+
+ scrollTop -= moveY * this.options.speedMultiplier;
+ var maxScrollTop = self.__maxScrollTop();
+
+ if (scrollTop > maxScrollTop || scrollTop < 0) {
+
+ // Slow down on the edges
+ if (self.options.bouncing) {
+
+ scrollTop += (moveY / 2 * this.options.speedMultiplier);
+
+ // Support pull-to-refresh (only when only y is scrollable)
+ if (!self.__enableScrollX && self.__refreshHeight != null) {
+
+ if (!self.__refreshActive && scrollTop <= -self.__refreshHeight) {
+
+ self.__refreshActive = true;
+ if (self.__refreshActivate) {
+ self.__refreshActivate();
+ }
+
+ } else if (self.__refreshActive && scrollTop > -self.__refreshHeight) {
+
+ self.__refreshActive = false;
+ if (self.__refreshDeactivate) {
+ self.__refreshDeactivate();
+ }
+
+ }
+ }
+
+ } else if (scrollTop > maxScrollTop) {
+
+ scrollTop = maxScrollTop;
+
+ } else {
+
+ scrollTop = 0;
+
+ }
+ }
+
+ self.options.scrolling();
+ }
+
+ // Keep list from growing infinitely (holding min 10, max 20 measure points)
+ if (positions.length > 60) {
+ positions.splice(0, 30);
+ }
+
+ // Track scroll movement for decleration
+ positions.push(scrollLeft, scrollTop, timeStamp);
+
+ // Sync scroll position
+ self.__publish(scrollLeft, scrollTop, level);
+
+ // Otherwise figure out whether we are switching into dragging mode now.
+ } else {
+
+ var minimumTrackingForScroll = self.options.locking ? 3 : 0;
+ var minimumTrackingForDrag = 5;
+
+ var distanceX = Math.abs(currentTouchLeft - self.__initialTouchLeft);
+ var distanceY = Math.abs(currentTouchTop - self.__initialTouchTop);
+
+ self.__enableScrollX = self.options.scrollingX && distanceX >= minimumTrackingForScroll;
+ self.__enableScrollY = self.options.scrollingY && distanceY >= minimumTrackingForScroll;
+
+ positions.push(self.__scrollLeft, self.__scrollTop, timeStamp);
+
+ self.__isDragging = (self.__enableScrollX || self.__enableScrollY) && (distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag);
+ if (self.__isDragging) {
+ self.__interruptedAnimation = false;
+ }
+
+ }
+
+ // Update last touch positions and time stamp for next event
+ self.__lastTouchLeft = currentTouchLeft;
+ self.__lastTouchTop = currentTouchTop;
+ self.__lastTouchMove = timeStamp;
+ self.__lastScale = scale;
+
+ },
+
+
+ /**
+ * Touch end handler for scrolling support
+ */
+ doTouchEnd: function(timeStamp) {
+
+ if (timeStamp instanceof Date) {
+ timeStamp = timeStamp.valueOf();
+ }
+ if (typeof timeStamp !== "number") {
+ throw new Error("Invalid timestamp value: " + timeStamp);
+ }
+
+ var self = this;
+
+ // Ignore event when tracking is not enabled (no touchstart event on element)
+ // This is required as this listener ('touchmove') sits on the document and not on the element itself.
+ if (!self.__isTracking) {
+ return;
+ }
+
+ // Not touching anymore (when two finger hit the screen there are two touch end events)
+ self.__isTracking = false;
+
+ // Be sure to reset the dragging flag now. Here we also detect whether
+ // the finger has moved fast enough to switch into a deceleration animation.
+ if (self.__isDragging) {
+
+ // Reset dragging flag
+ self.__isDragging = false;
+
+ // Start deceleration
+ // Verify that the last move detected was in some relevant time frame
+ if (self.__isSingleTouch && self.options.animating && (timeStamp - self.__lastTouchMove) <= 100) {
+
+ // Then figure out what the scroll position was about 100ms ago
+ var positions = self.__positions;
+ var endPos = positions.length - 1;
+ var startPos = endPos;
+
+ // Move pointer to position measured 100ms ago
+ for (var i = endPos; i > 0 && positions[i] > (self.__lastTouchMove - 100); i -= 3) {
+ startPos = i;
+ }
+
+ // If start and stop position is identical in a 100ms timeframe,
+ // we cannot compute any useful deceleration.
+ if (startPos !== endPos) {
+ // Compute relative movement between these two points
+ var timeOffset = positions[endPos] - positions[startPos];
+ var movedLeft = self.__scrollLeft - positions[startPos - 2];
+ var movedTop = self.__scrollTop - positions[startPos - 1];
+
+ // Based on 50ms compute the movement to apply for each render step
+ self.__decelerationVelocityX = movedLeft / timeOffset * (1000 / 60);
+ self.__decelerationVelocityY = movedTop / timeOffset * (1000 / 60);
+
+ // How much velocity is required to start the deceleration
+ var minVelocityToStartDeceleration = self.options.paging || self.options.snapping ? 4 : 1;
+
+ // Verify that we have enough velocity to start deceleration
+ if (Math.abs(self.__decelerationVelocityX) > minVelocityToStartDeceleration || Math.abs(self.__decelerationVelocityY) > minVelocityToStartDeceleration) {
+
+ // Deactivate pull-to-refresh when decelerating
+ if (!self.__refreshActive) {
+ self.__startDeceleration(timeStamp);
+ }
+ }
+ } else {
+ self.options.scrollingComplete();
+ }
+ } else if ((timeStamp - self.__lastTouchMove) > 100) {
+ self.options.scrollingComplete();
+ }
+ }
+
+ // If this was a slower move it is per default non decelerated, but this
+ // still means that we want snap back to the bounds which is done here.
+ // This is placed outside the condition above to improve edge case stability
+ // e.g. touchend fired without enabled dragging. This should normally do not
+ // have modified the scroll positions or even showed the scrollbars though.
+ if (!self.__isDecelerating) {
+
+ if (self.__refreshActive && self.__refreshStart) {
+
+ // Use publish instead of scrollTo to allow scrolling to out of boundary position
+ // We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
+ self.__publish(self.__scrollLeft, -self.__refreshHeight, self.__zoomLevel, true);
+
+ if (self.__refreshStart) {
+ self.__refreshStart();
+ }
+
+ } else {
+
+ if (self.__interruptedAnimation || self.__isDragging) {
+ self.options.scrollingComplete();
+ }
+ self.scrollTo(self.__scrollLeft, self.__scrollTop, true, self.__zoomLevel);
+
+ // Directly signalize deactivation (nothing todo on refresh?)
+ if (self.__refreshActive) {
+
+ self.__refreshActive = false;
+ if (self.__refreshDeactivate) {
+ self.__refreshDeactivate();
+ }
+
+ }
+ }
+ }
+
+ // Fully cleanup list
+ self.__positions.length = 0;
+
+ },
+
+
+
+ /*
+ ---------------------------------------------------------------------------
+ PRIVATE API
+ ---------------------------------------------------------------------------
+ */
+
+ /**
+ * Applies the scroll position to the content element
+ *
+ * @param left {Number} Left scroll position
+ * @param top {Number} Top scroll position
+ * @param animate {Boolean?false} Whether animation should be used to move to the new coordinates
+ */
+ __publish: function(left, top, zoom, animate) {
+
+ var self = this;
+
+ // Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
+ var wasAnimating = self.__isAnimating;
+ if (wasAnimating) {
+ core.effect.Animate.stop(wasAnimating);
+ self.__isAnimating = false;
+ }
+
+ if (animate && self.options.animating) {
+
+ // Keep scheduled positions for scrollBy/zoomBy functionality
+ self.__scheduledLeft = left;
+ self.__scheduledTop = top;
+ self.__scheduledZoom = zoom;
+
+ var oldLeft = self.__scrollLeft;
+ var oldTop = self.__scrollTop;
+ var oldZoom = self.__zoomLevel;
+
+ var diffLeft = left - oldLeft;
+ var diffTop = top - oldTop;
+ var diffZoom = zoom - oldZoom;
+
+ var step = function(percent, now, render) {
+
+ if (render) {
+
+ self.__scrollLeft = oldLeft + (diffLeft * percent);
+ self.__scrollTop = oldTop + (diffTop * percent);
+ self.__zoomLevel = oldZoom + (diffZoom * percent);
+
+ // Push values out
+ if (self.__callback) {
+ self.__callback(self.__scrollLeft, self.__scrollTop, self.__zoomLevel);
+ }
+
+ }
+ };
+
+ var verify = function(id) {
+ return self.__isAnimating === id;
+ };
+
+ var completed = function(renderedFramesPerSecond, animationId, wasFinished) {
+ if (animationId === self.__isAnimating) {
+ self.__isAnimating = false;
+ }
+ if (self.__didDecelerationComplete || wasFinished) {
+ self.options.scrollingComplete();
+ }
+
+ if (self.options.zooming) {
+ self.__computeScrollMax();
+ if(self.__zoomComplete) {
+ self.__zoomComplete();
+ self.__zoomComplete = null;
+ }
+ }
+ };
+
+ // When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
+ self.__isAnimating = core.effect.Animate.start(step, verify, completed, self.options.animationDuration, wasAnimating ? easeOutCubic : easeInOutCubic);
+
+ } else {
+
+ self.__scheduledLeft = self.__scrollLeft = left;
+ self.__scheduledTop = self.__scrollTop = top;
+ self.__scheduledZoom = self.__zoomLevel = zoom;
+
+ // Push values out
+ if (self.__callback) {
+ self.__callback(left, top, zoom);
+ }
+
+ // Fix max scroll ranges
+ if (self.options.zooming) {
+ self.__computeScrollMax();
+ if(self.__zoomComplete) {
+ self.__zoomComplete();
+ self.__zoomComplete = null;
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Recomputes scroll minimum values based on client dimensions and content dimensions.
+ */
+ __computeScrollMax: function(zoomLevel) {
+
+ var self = this;
+
+ if (zoomLevel == null) {
+ zoomLevel = self.__zoomLevel;
+ }
+ },
+
+
+
+ /*
+ ---------------------------------------------------------------------------
+ ANIMATION (DECELERATION) SUPPORT
+ ---------------------------------------------------------------------------
+ */
+
+ /**
+ * Called when a touch sequence end and the speed of the finger was high enough
+ * to switch into deceleration mode.
+ */
+ __startDeceleration: function(timeStamp) {
+
+ var self = this;
+
+ if (self.options.paging) {
+
+ var scrollLeft = Math.max(Math.min(self.__scrollLeft, self.__maxScrollLeft()), 0);
+ var scrollTop = Math.max(Math.min(self.__scrollTop, self.__maxScrollTop()), 0);
+ var clientWidth = self.__clientWidth();
+ var clientHeight = self.__clientHeight();
+
+ // We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
+ // Each page should have exactly the size of the client area.
+ self.__minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth;
+ self.__minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight;
+ self.__maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth;
+ self.__maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight;
+
+ } else {
+
+ self.__minDecelerationScrollLeft = 0;
+ self.__minDecelerationScrollTop = 0;
+ self.__maxDecelerationScrollLeft = self.__maxScrollLeft();
+ self.__maxDecelerationScrollTop = self.__maxScrollTop();
+
+ }
+
+ // Wrap class method
+ var step = function(percent, now, render) {
+ self.__stepThroughDeceleration(render);
+ };
+
+ // How much velocity is required to keep the deceleration running
+ var minVelocityToKeepDecelerating = self.options.snapping ? 4 : 0.1;
+
+ // Detect whether it's still worth to continue animating steps
+ // If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
+ var verify = function() {
+ var shouldContinue = Math.abs(self.__decelerationVelocityX) >= minVelocityToKeepDecelerating || Math.abs(self.__decelerationVelocityY) >= minVelocityToKeepDecelerating;
+ if (!shouldContinue) {
+ self.__didDecelerationComplete = true;
+ }
+ return shouldContinue;
+ };
+
+ var completed = function(renderedFramesPerSecond, animationId, wasFinished) {
+ self.__isDecelerating = false;
+ if (self.__didDecelerationComplete) {
+ self.options.scrollingComplete();
+ }
+
+ // Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
+ self.scrollTo(self.__scrollLeft, self.__scrollTop, self.options.snapping);
+ };
+
+ // Start animation and switch on flag
+ self.__isDecelerating = core.effect.Animate.start(step, verify, completed);
+
+ },
+
+
+ /**
+ * Called on every step of the animation
+ *
+ * @param inMemory {Boolean?false} Whether to not render the current step, but keep it in memory only. Used internally only!
+ */
+ __stepThroughDeceleration: function(render) {
+
+ var self = this;
+
+
+ //
+ // COMPUTE NEXT SCROLL POSITION
+ //
+
+ // Add deceleration to scroll position
+ var scrollLeft = self.__scrollLeft + self.__decelerationVelocityX;
+ var scrollTop = self.__scrollTop + self.__decelerationVelocityY;
+
+
+ //
+ // HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
+ //
+
+ if (!self.options.bouncing) {
+
+ var scrollLeftFixed = Math.max(Math.min(self.__maxDecelerationScrollLeft, scrollLeft), self.__minDecelerationScrollLeft);
+ if (scrollLeftFixed !== scrollLeft) {
+ scrollLeft = scrollLeftFixed;
+ self.__decelerationVelocityX = 0;
+ }
+
+ var scrollTopFixed = Math.max(Math.min(self.__maxDecelerationScrollTop, scrollTop), self.__minDecelerationScrollTop);
+ if (scrollTopFixed !== scrollTop) {
+ scrollTop = scrollTopFixed;
+ self.__decelerationVelocityY = 0;
+ }
+
+ }
+
+
+ //
+ // UPDATE SCROLL POSITION
+ //
+
+ if (render) {
+
+ self.__publish(scrollLeft, scrollTop, self.__zoomLevel);
+
+ } else {
+
+ self.__scrollLeft = scrollLeft;
+ self.__scrollTop = scrollTop;
+
+ }
+
+
+ //
+ // SLOW DOWN
+ //
+
+ // Slow down velocity on every iteration
+ if (!self.options.paging) {
+
+ // This is the factor applied to every iteration of the animation
+ // to slow down the process. This should emulate natural behavior where
+ // objects slow down when the initiator of the movement is removed
+ var frictionFactor = 0.95;
+
+ self.__decelerationVelocityX *= frictionFactor;
+ self.__decelerationVelocityY *= frictionFactor;
+
+ }
+
+
+ //
+ // BOUNCING SUPPORT
+ //
+
+ if (self.options.bouncing) {
+
+ var scrollOutsideX = 0;
+ var scrollOutsideY = 0;
+
+ // This configures the amount of change applied to deceleration/acceleration when reaching boundaries
+ var penetrationDeceleration = self.options.penetrationDeceleration;
+ var penetrationAcceleration = self.options.penetrationAcceleration;
+
+ // Check limits
+ if (scrollLeft < self.__minDecelerationScrollLeft) {
+ scrollOutsideX = self.__minDecelerationScrollLeft - scrollLeft;
+ } else if (scrollLeft > self.__maxDecelerationScrollLeft) {
+ scrollOutsideX = self.__maxDecelerationScrollLeft - scrollLeft;
+ }
+
+ if (scrollTop < self.__minDecelerationScrollTop) {
+ scrollOutsideY = self.__minDecelerationScrollTop - scrollTop;
+ } else if (scrollTop > self.__maxDecelerationScrollTop) {
+ scrollOutsideY = self.__maxDecelerationScrollTop - scrollTop;
+ }
+
+ // Slow down until slow enough, then flip back to snap position
+ if (scrollOutsideX !== 0) {
+ if (scrollOutsideX * self.__decelerationVelocityX <= 0) {
+ self.__decelerationVelocityX += scrollOutsideX * penetrationDeceleration;
+ } else {
+ self.__decelerationVelocityX = scrollOutsideX * penetrationAcceleration;
+ }
+ }
+
+ if (scrollOutsideY !== 0) {
+ if (scrollOutsideY * self.__decelerationVelocityY <= 0) {
+ self.__decelerationVelocityY += scrollOutsideY * penetrationDeceleration;
+ } else {
+ self.__decelerationVelocityY = scrollOutsideY * penetrationAcceleration;
+ }
+ }
+ }
+ }
+ };
+
+ // Copy over members to prototype
+ for (var key in members) {
+ Scroller.prototype[key] = members[key];
+ }
+
+})();
+
+window.Scroller = Scroller;
diff --git a/vendor/slip.js b/vendor/slip.js
index 30ab0a8..34b7ea3 100644
--- a/vendor/slip.js
+++ b/vendor/slip.js
@@ -1,115 +1,129 @@
/*
- Slip - swiping and reordering in lists of elements on touch screens, no fuss.
+ Slip - swiping and reordering in lists of elements on touch screens, no fuss.
- Fires these events on list elements:
+ Fires these events on list elements:
- • slip:swipe
- When swipe has been done and user has lifted finger off the screen.
- If you execute event.preventDefault() the element will be animated back to original position.
- Otherwise it will be animated off the list and set to display:none.
+ • slip:swipe
+ When swipe has been done and user has lifted finger off the screen.
+ If you execute event.preventDefault() the element will be animated back to original position.
+ Otherwise it will be animated off the list and set to display:none.
- • slip:beforeswipe
- Fired before first swipe movement starts.
- If you execute event.preventDefault() then element will not move at all.
+ • slip:beforeswipe
+ Fired before first swipe movement starts.
+ If you execute event.preventDefault() then element will not move at all.
- • slip:reorder
- Element has been dropped in new location. event.detail contains the location:
- • insertBefore: DOM node before which element has been dropped (null is the end of the list). Use with node.insertBefore().
- • spliceIndex: Index of element before which current element has been dropped, not counting the element iself.
- For use with Array.splice() if the list is reflecting objects in some array.
+ • slip:reorder
+ Element has been dropped in new location. event.detail contains the location:
+ • insertBefore: DOM node before which element has been dropped (null is the end of the list). Use with node.insertBefore().
+ • spliceIndex: Index of element before which current element has been dropped, not counting the element iself.
+ For use with Array.splice() if the list is reflecting objects in some array.
- • slip:beforereorder
- When reordering movement starts.
- Element being reordered gets class `slip-reordering`.
- If you execute event.preventDefault() then element will not move at all.
+ • slip:beforereorder
+ When reordering movement starts.
+ Element being reordered gets class `slip-reordering`.
+ If you execute event.preventDefault() then element will not move at all.
- • slip:beforewait
- If you execute event.preventDefault() then reordering will begin immediately, blocking ability to scroll the page.
+ • slip:beforewait
+ If you execute event.preventDefault() then reordering will begin immediately, blocking ability to scroll the page.
- • slip:tap
- When element was tapped without being swiped/reordered.
+ • slip:tap
+ When element was tapped without being swiped/reordered.
- • slip:cancelswipe
- Fired when the user stops dragging and the element returns to its original position.
+ • slip:cancelswipe
+ Fired when the user stops dragging and the element returns to its original position.
- Usage:
+ Usage:
- CSS:
- You should set `user-select:none` (and WebKit prefixes, sigh) on list elements,
- otherwise unstoppable and glitchy text selection in iOS will get in the way.
+ CSS:
+ You should set `user-select:none` (and WebKit prefixes, sigh) on list elements,
+ otherwise unstoppable and glitchy text selection in iOS will get in the way.
- You should set `overflow-x: hidden` on the container or body to prevent horizontal scrollbar
- appearing when elements are swiped off the list.
+ You should set `overflow-x: hidden` on the container or body to prevent horizontal scrollbar
+ appearing when elements are swiped off the list.
- var list = document.querySelector('ul#slippylist');
- new Slip(list);
+ var list = document.querySelector('ul#slippylist');
+ new Slip(list);
- list.addEventListener('slip:beforeswipe', function(e) {
- if (shouldNotSwipe(e.target)) e.preventDefault();
- });
+ list.addEventListener('slip:beforeswipe', function(e) {
+ if (shouldNotSwipe(e.target)) e.preventDefault();
+ });
- list.addEventListener('slip:swipe', function(e) {
- // e.target swiped
- if (thatWasSwipeToRemove) {
- e.target.parentNode.removeChild(e.target);
- } else {
- e.preventDefault(); // will animate back to original position
- }
- });
+ list.addEventListener('slip:swipe', function(e) {
+ // e.target swiped
+ if (thatWasSwipeToRemove) {
+ e.target.parentNode.removeChild(e.target);
+ } else {
+ e.preventDefault(); // will animate back to original position
+ }
+ });
- list.addEventListener('slip:beforereorder', function(e) {
- if (shouldNotReorder(e.target)) e.preventDefault();
- });
+ list.addEventListener('slip:beforereorder', function(e) {
+ if (shouldNotReorder(e.target)) e.preventDefault();
+ });
- list.addEventListener('slip:reorder', function(e) {
- // e.target reordered.
- if (reorderedOK) {
- e.target.parentNode.insertBefore(e.target, e.detail.insertBefore);
- } else {
- e.preventDefault();
- }
- });
+ list.addEventListener('slip:reorder', function(e) {
+ // e.target reordered.
+ if (reorderedOK) {
+ e.target.parentNode.insertBefore(e.target, e.detail.insertBefore);
+ } else {
+ e.preventDefault();
+ }
+ });
- Requires:
- • Touch events
- • CSS transforms
- • Function.bind()
+ Requires:
+ • Touch events
+ • CSS transforms
+ • Function.bind()
- Caveats:
- • Elements must not change size while reordering or swiping takes place (otherwise it will be visually out of sync)
-*/
+ Caveats:
+ • Elements must not change size while reordering or swiping takes place (otherwise it will be visually out of sync)
+ */
/*! @license
- Slip.js 1.2.0
+ Slip.js 1.2.0
- © 2014 Kornel Lesiński
. All rights reserved.
+ © 2014 Kornel Lesiński . All rights reserved.
- Redistribution and use in source and binary forms, with or without modification,
- are permitted provided that the following conditions are met:
+ Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
- 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
- the following disclaimer in the documentation and/or other materials provided with the distribution.
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
+ the following disclaimer in the documentation and/or other materials provided with the distribution.
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
- WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
- USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-*/
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
+ USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
-window['Slip'] = (function(){
+window['Slip'] = (function() {
'use strict';
+ var accessibility = {
+ // Set values to false if you don't want Slip to manage them
+ container: {
+ ariaRole: "listbox",
+ tabIndex: 0,
+ focus: true, // focuses after drop
+ },
+ items: {
+ ariaRole: "option", // If "option" flattens items, try "group": https://www.marcozehe.de/2013/03/08/sometimes-you-have-to-use-illegal-wai-aria-to-make-stuff-work/
+ tabIndex: -1, // 0 will make every item tabbable, which isn't always useful
+ focus: true, // focuses when dragging
+ },
+ };
+
var damnYouChrome = /Chrome\/[34]/.test(navigator.userAgent); // For bugs that can't be programmatically detected :( Intended to catch all versions of Chrome 30-40
var needsBodyHandlerHack = damnYouChrome; // Otherwise I _sometimes_ don't get any touchstart events and only clicks instead.
/* When dragging elements down in Chrome (tested 34-37) dragged element may appear below stationary elements.
- Looks like WebKit bug #61824, but iOS Safari doesn't have that problem. */
+ Looks like WebKit bug #61824, but iOS Safari doesn't have that problem. */
var compositorDoesNotOrderLayers = damnYouChrome;
// -webkit-mess
@@ -127,7 +141,7 @@ window['Slip'] = (function(){
var globalInstances = 0;
var attachedBodyHandlerHack = false;
- var nullHandler = function(){};
+ var nullHandler = function() {};
function Slip(container, options) {
if ('string' === typeof container) container = document.querySelector(container);
@@ -135,7 +149,10 @@ window['Slip'] = (function(){
if (!this || this === window) return new Slip(container, options);
- this.options = options;
+ this.options = options = options || {};
+ this.options.keepSwipingPercent = options.keepSwipingPercent || 0;
+ this.options.minimumSwipeVelocity = options.minimumSwipeVelocity || 1;
+ this.options.minimumSwipeTime = options.minimumSwipeTime || 110;
// Functions used for as event handlers need usable `this` and must not change to be removable
this.cancel = this.setState.bind(this, this.states.idle);
@@ -147,6 +164,7 @@ window['Slip'] = (function(){
this.onMouseUp = this.onMouseUp.bind(this);
this.onMouseLeave = this.onMouseLeave.bind(this);
this.onSelection = this.onSelection.bind(this);
+ this.onContainerFocus = this.onContainerFocus.bind(this);
this.setState(this.states.idle);
this.attach(container);
@@ -156,32 +174,38 @@ window['Slip'] = (function(){
var transform = node.style[transformPrefix];
if (transform) {
return {
- value:transform,
- original:transform,
+ value: transform,
+ original: transform,
};
}
if (window.getComputedStyle) {
var style = window.getComputedStyle(node).getPropertyValue(transformProperty);
- if (style && style !== 'none') return {value:style, original:''};
+ if (style && style !== 'none') return {
+ value: style,
+ original: ''
+ };
}
- return {value:'', original:''};
+ return {
+ value: '',
+ original: ''
+ };
}
function findIndex(target, nodes) {
- var originalIndex = 0;
- var listCount = 0;
-
- for (var i=0; i < nodes.length; i++) {
- if (nodes[i].nodeType === 1) {
- listCount++;
- if (nodes[i] === target.node) {
- originalIndex = listCount-1;
- }
+ var originalIndex = 0;
+ var listCount = 0;
+
+ for (var i = 0; i < nodes.length; i++) {
+ if (nodes[i].nodeType === 1) {
+ listCount++;
+ if (nodes[i] === target.node) {
+ originalIndex = listCount - 1;
+ }
+ }
}
- }
- return originalIndex;
+ return originalIndex;
}
// All functions in states are going to be executed in context of Slip object
@@ -204,9 +228,12 @@ window['Slip'] = (function(){
states: {
idle: function idleStateInit() {
- this.target = null;
- this.usingTouch = false;
this.removeMouseHandlers();
+ if (this.target) {
+ this.target.node.style.willChange = '';
+ this.target = null;
+ }
+ this.usingTouch = false;
return {
allowTextSelection: true,
@@ -215,14 +242,15 @@ window['Slip'] = (function(){
undecided: function undecidedStateInit() {
this.target.height = this.target.node.offsetHeight;
+ this.target.node.style.willChange = transformProperty;
this.target.node.style[transitionPrefix] = '';
if (!this.dispatch(this.target.originalTarget, 'beforewait')) {
- if (this.dispatch(this.target.originalTarget, 'beforereorder')) {
- this.setState(this.states.reorder);
- }
+ if (this.dispatch(this.target.originalTarget, 'beforereorder')) {
+ this.setState(this.states.reorder);
+ }
} else {
- var holdTimer = setTimeout(function(){
+ var holdTimer = setTimeout(function() {
var move = this.getAbsoluteMovement();
if (this.canPreventScrolling && move.x < 15 && move.y < 25) {
if (this.dispatch(this.target.originalTarget, 'beforereorder')) {
@@ -241,7 +269,10 @@ window['Slip'] = (function(){
var move = this.getAbsoluteMovement();
if (move.x > 20 && move.y < Math.max(100, this.target.height)) {
- if (this.dispatch(this.target.originalTarget, 'beforeswipe')) {
+ if (this.dispatch(this.target.originalTarget, 'beforeswipe', {
+ directionX: move.directionX,
+ directionY: move.directionY
+ })) {
this.setState(this.states.swipe);
return false;
} else {
@@ -253,7 +284,7 @@ window['Slip'] = (function(){
}
// Chrome likes sideways scrolling :(
- if (move.x > move.y*1.2) return false;
+ if (move.x > move.y * 1.2) return false;
},
onLeave: function() {
@@ -275,8 +306,9 @@ window['Slip'] = (function(){
var originalIndex = findIndex(this.target, this.container.childNodes);
container.className += ' slip-swiping-container';
+
function removeClass() {
- container.className = container.className.replace(/(?:^| )slip-swiping-container/,'');
+ container.className = container.className.replace(/(?:^| )slip-swiping-container/, '');
}
this.target.height = this.target.node.offsetHeight;
@@ -284,7 +316,7 @@ window['Slip'] = (function(){
return {
leaveState: function() {
if (swipeSuccess) {
- this.animateSwipe(function(target){
+ this.animateSwipe(function(target) {
target.node.style[transformPrefix] = target.baseTransform.original;
target.node.style[transitionPrefix] = '';
if (this.dispatch(target.node, 'afterswipe')) {
@@ -303,7 +335,7 @@ window['Slip'] = (function(){
onMove: function() {
var move = this.getTotalMovement();
- if (Math.abs(move.y) < this.target.height+20) {
+ if (Math.abs(move.y) < this.target.height + 20) {
this.target.node.style[transformPrefix] = 'translate(' + move.x + 'px,0) ' + hwLayerMagic + this.target.baseTransform.value;
return false;
} else {
@@ -316,22 +348,19 @@ window['Slip'] = (function(){
},
onEnd: function() {
- var dx = this.latestPosition.x - this.previousPosition.x;
- var dy = this.latestPosition.y - this.previousPosition.y;
- var velocity = Math.sqrt(dx*dx + dy*dy) / (this.latestPosition.time - this.previousPosition.time + 1);
-
var move = this.getAbsoluteMovement();
- var swiped = velocity > 0.6 && move.time > 110;
+ var velocity = move.x / move.time;
- var direction;
- if (dx > 0) {
- direction = "right";
- } else {
- direction = "left";
- }
+ // How far out has the item been swiped?
+ var swipedPercent = Math.abs((this.startPosition.x - this.previousPosition.x) / this.container.clientWidth) * 100;
+
+ var swiped = (velocity > this.options.minimumSwipeVelocity && move.time > this.options.minimumSwipeTime) || (this.options.keepSwipingPercent && swipedPercent > this.options.keepSwipingPercent);
if (swiped) {
- if (this.dispatch(this.target.node, 'swipe', {direction: direction, originalIndex: originalIndex})) {
+ if (this.dispatch(this.target.node, 'swipe', {
+ direction: move.directionX,
+ originalIndex: originalIndex
+ })) {
swipeSuccess = true; // can't animate here, leaveState overrides anim
}
}
@@ -342,14 +371,18 @@ window['Slip'] = (function(){
},
reorder: function reorderStateInit() {
+ if (this.target.node.focus && accessibility.items.focus) {
+ this.target.node.focus();
+ }
+
this.target.height = this.target.node.offsetHeight;
var nodes = this.container.childNodes;
var originalIndex = findIndex(this.target, nodes);
var mouseOutsideTimer;
- var zero = this.target.node.offsetTop + this.target.height/2;
+ var zero = this.target.node.offsetTop + this.target.height / 2;
var otherNodes = [];
- for(var i=0; i < nodes.length; i++) {
+ for (var i = 0; i < nodes.length; i++) {
if (nodes[i].nodeType != 1 || nodes[i] === this.target.node) continue;
var t = nodes[i].offsetTop;
nodes[i].style[transitionPrefix] = transformProperty + ' 0.2s ease-in-out';
@@ -373,23 +406,23 @@ window['Slip'] = (function(){
if (mouseOutsideTimer) {
// don't care where the mouse is as long as it moves
- clearTimeout(mouseOutsideTimer); mouseOutsideTimer = null;
+ clearTimeout(mouseOutsideTimer);
+ mouseOutsideTimer = null;
}
var move = this.getTotalMovement();
this.target.node.style[transformPrefix] = 'translate(0,' + move.y + 'px) ' + hwTopLayerMagic + this.target.baseTransform.value;
var height = this.target.height;
- otherNodes.forEach(function(o){
+ otherNodes.forEach(function(o) {
var off = 0;
if (o.pos < 0 && move.y < 0 && o.pos > move.y) {
off = height;
- }
- else if (o.pos > 0 && move.y > 0 && o.pos < move.y) {
+ } else if (o.pos > 0 && move.y > 0 && o.pos < move.y) {
off = -height;
}
// FIXME: should change accelerated/non-accelerated state lazily
- o.node.style[transformPrefix] = off ? 'translate(0,'+off+'px) ' + hwLayerMagic + o.baseTransform.value : o.baseTransform.original;
+ o.node.style[transformPrefix] = off ? 'translate(0,' + off + 'px) ' + hwLayerMagic + o.baseTransform.value : o.baseTransform.original;
});
return false;
}
@@ -404,25 +437,40 @@ window['Slip'] = (function(){
this.container.style.webkitTransformStyle = '';
}
- this.target.node.className = this.target.node.className.replace(/(?:^| )slip-reordering/,'');
+ if (this.container.focus && accessibility.container.focus) {
+ this.container.focus();
+ }
+
+ this.target.node.className = this.target.node.className.replace(/(?:^| )slip-reordering/, '');
this.target.node.style[userSelectPrefix] = '';
- this.animateToZero(function(target){
+ this.animateToZero(function(target) {
target.node.style.zIndex = '';
});
- otherNodes.forEach(function(o){
+ otherNodes.forEach(function(o) {
o.node.style[transformPrefix] = o.baseTransform.original;
o.node.style[transitionPrefix] = ''; // FIXME: animate to new position
});
},
+ onStart: function() {
+ var scroller = this.options.scroller;
+ if (!!scroller) {
+ this.baseScrollerPos = {x: scroller.__scrollLeft, y: scroller.__scrollTop };
+ scroller.scrollTo(0, 0);
+
+ var scrollable = scroller.__container;
+ scrollable.scrollTop = this.baseScrollerPos.y;
+ }
+ },
+
onMove: setPosition,
onLeave: function() {
// don't let element get stuck if mouse left the window
// but don't cancel immediately as it'd be annoying near window edges
if (mouseOutsideTimer) clearTimeout(mouseOutsideTimer);
- mouseOutsideTimer = setTimeout(function(){
+ mouseOutsideTimer = setTimeout(function() {
mouseOutsideTimer = null;
this.cancel();
}.bind(this), 700);
@@ -431,20 +479,37 @@ window['Slip'] = (function(){
onEnd: function() {
var move = this.getTotalMovement();
if (move.y < 0) {
- for(var i=0; i < otherNodes.length; i++) {
+ for (var i = 0; i < otherNodes.length; i++) {
if (otherNodes[i].pos > move.y) {
- this.dispatch(this.target.node, 'reorder', {spliceIndex:i, insertBefore:otherNodes[i].node, originalIndex: originalIndex});
+ this.dispatch(this.target.node, 'reorder', {
+ spliceIndex: i,
+ insertBefore: otherNodes[i].node,
+ originalIndex: originalIndex
+ });
break;
}
}
} else {
- for(var i=otherNodes.length-1; i >= 0; i--) {
+ for (var i = otherNodes.length - 1; i >= 0; i--) {
if (otherNodes[i].pos < move.y) {
- this.dispatch(this.target.node, 'reorder', {spliceIndex:i+1, insertBefore:otherNodes[i+1] ? otherNodes[i+1].node : null, originalIndex: originalIndex});
+ this.dispatch(this.target.node, 'reorder', {
+ spliceIndex: i + 1,
+ insertBefore: otherNodes[i + 1] ? otherNodes[i + 1].node : null,
+ originalIndex: originalIndex
+ });
break;
}
}
}
+
+ var scroller = this.options.scroller;
+ if (!!scroller) {
+ var scrollable = scroller.__container;
+ var newScrollTop = scrollable.scrollTop;
+ scrollable.scrollTop = 0;
+ scroller.scrollTo(0, newScrollTop);
+ }
+
this.setState(this.states.idle);
return false;
},
@@ -464,6 +529,17 @@ window['Slip'] = (function(){
}
this.container = container;
+
+ // Accessibility
+ if (false !== accessibility.container.tabIndex) {
+ this.container.tabIndex = accessibility.container.tabIndex;
+ }
+ if (accessibility.container.ariaRole) {
+ this.container.setAttribute('aria-role', accessibility.container.ariaRole);
+ }
+ this.setChildNodesAriaRoles();
+ this.container.addEventListener('focus', this.onContainerFocus, false);
+
this.otherNodes = [];
// selection on iOS interferes with reordering
@@ -496,7 +572,7 @@ window['Slip'] = (function(){
}
},
- setState: function(newStateCtor){
+ setState: function(newStateCtor) {
if (this.state) {
if (this.state.ctor === newStateCtor) return;
if (this.state.leaveState) this.state.leaveState.call(this);
@@ -512,12 +588,29 @@ window['Slip'] = (function(){
},
findTargetNode: function(targetNode) {
- while(targetNode && targetNode.parentNode !== this.container) {
+ while (targetNode && targetNode.parentNode !== this.container) {
targetNode = targetNode.parentNode;
}
return targetNode;
},
+ onContainerFocus: function(e) {
+ this.setChildNodesAriaRoles();
+ },
+
+ setChildNodesAriaRoles: function() {
+ var nodes = this.container.childNodes;
+ for (var i = 0; i < nodes.length; i++) {
+ if (nodes[i].nodeType != 1) continue;
+ if (accessibility.items.ariaRole) {
+ nodes[i].setAttribute('aria-role', accessibility.items.ariaRole);
+ }
+ if (false !== accessibility.items.tabIndex) {
+ nodes[i].tabIndex = accessibility.items.tabIndex;
+ }
+ }
+ },
+
onSelection: function(e) {
var isRelated = e.target === document || this.findTargetNode(e);
if (!isRelated) return;
@@ -605,10 +698,11 @@ window['Slip'] = (function(){
}
//check for a scrollable parent
- var scrollContainer = targetNode.parentNode;
- while (scrollContainer){
- if (scrollContainer.scrollHeight > scrollContainer.clientHeight && window.getComputedStyle(scrollContainer)['overflow-y'] != 'visible') break;
- else scrollContainer = scrollContainer.parentNode;
+ var scroller = this.options.scroller;
+ var scrollContainer = scroller ? scroller.__container : targetNode.parentNode;
+ while (scrollContainer && !scroller) {
+ if (scrollContainer.scrollHeight > scrollContainer.clientHeight && window.getComputedStyle(scrollContainer)['overflow-y'] != 'visible') break;
+ else scrollContainer = scrollContainer.parentNode;
}
this.target = {
@@ -617,17 +711,20 @@ window['Slip'] = (function(){
scrollContainer: scrollContainer,
baseTransform: getTransform(targetNode),
};
+
return true;
},
startAtPosition: function(pos) {
this.startPosition = this.previousPosition = this.latestPosition = pos;
this.setState(this.states.undecided);
+ this.state.onStart && this.state.onStart.call(this);
},
updatePosition: function(e, pos) {
- if(this.target == null)
+ if (this.target == null) {
return;
+ }
this.latestPosition = pos;
var triggerOffset = 40,
@@ -639,20 +736,20 @@ window['Slip'] = (function(){
bottomOffset = Math.min(containerRect.bottom, window.innerHeight) - targetRect.bottom,
topOffset = targetRect.top - Math.max(containerRect.top, 0);
- if (bottomOffset < triggerOffset){
- offset = triggerOffset - bottomOffset;
- }
- else if (topOffset < triggerOffset){
- offset = topOffset - triggerOffset;
+ if (bottomOffset < triggerOffset) {
+ offset = triggerOffset - bottomOffset;
+ } else if (topOffset < triggerOffset) {
+ offset = topOffset - triggerOffset;
}
var prevScrollTop = scrollable.scrollTop;
scrollable.scrollTop += offset;
- if (prevScrollTop != scrollable.scrollTop) this.startPosition.y += prevScrollTop-scrollable.scrollTop;
+ if (prevScrollTop != scrollable.scrollTop) this.startPosition.y += prevScrollTop - scrollable.scrollTop;
if (this.state.onMove) {
if (this.state.onMove.call(this) === false) {
e.preventDefault();
+ e.stopPropagation();
}
}
@@ -699,8 +796,8 @@ window['Slip'] = (function(){
getTotalMovement: function() {
return {
- x:this.latestPosition.x - this.startPosition.x,
- y:this.latestPosition.y - this.startPosition.y,
+ x: this.latestPosition.x - this.startPosition.x,
+ y: this.latestPosition.y - this.startPosition.y,
};
},
@@ -708,7 +805,9 @@ window['Slip'] = (function(){
return {
x: Math.abs(this.latestPosition.x - this.startPosition.x),
y: Math.abs(this.latestPosition.y - this.startPosition.y),
- time:this.latestPosition.time - this.startPosition.time,
+ time: this.latestPosition.time - this.startPosition.time,
+ directionX: this.latestPosition.x - this.startPosition.x < 0 ? 'left' : 'right',
+ directionY: this.latestPosition.y - this.startPosition.y < 0 ? 'up' : 'down',
};
},
@@ -727,7 +826,7 @@ window['Slip'] = (function(){
getSiblings: function(target) {
var siblings = [];
var tmp = target.node.nextSibling;
- while(tmp) {
+ while (tmp) {
if (tmp.nodeType == 1) siblings.push({
node: tmp,
baseTransform: getTransform(tmp),
@@ -741,9 +840,9 @@ window['Slip'] = (function(){
// save, because this.target/container could change during animation
target = target || this.target;
- // target.node.style[transitionPrefix] = transformProperty + ' 5s ease-out';
+ target.node.style[transitionPrefix] = transformProperty + ' 0.1s ease-out';
target.node.style[transformPrefix] = 'translate(0,0) ' + hwLayerMagic + target.baseTransform.value;
- setTimeout(function(){
+ setTimeout(function() {
target.node.style[transitionPrefix] = '';
target.node.style[transformPrefix] = target.baseTransform.original;
if (callback) callback.call(this, target);
@@ -759,23 +858,23 @@ window['Slip'] = (function(){
target.node.style[transitionPrefix] = 'all 0.1s linear';
target.node.style[transformPrefix] = ' translate(' + (this.getTotalMovement().x > 0 ? '' : '-') + '100%,0) ' + hwLayerMagic + target.baseTransform.value;
- setTimeout(function(){
+ setTimeout(function() {
if (callback.call(this, target)) {
- siblings.forEach(function(o){
+ siblings.forEach(function(o) {
o.node.style[transitionPrefix] = '';
o.node.style[transformPrefix] = emptySpaceTransform + o.baseTransform.value;
});
- setTimeout(function(){
- siblings.forEach(function(o){
+ setTimeout(function() {
+ siblings.forEach(function(o) {
o.node.style[transitionPrefix] = transformProperty + ' 0.1s ease-in-out';
o.node.style[transformPrefix] = 'translate(0,0) ' + hwLayerMagic + o.baseTransform.value;
});
- setTimeout(function(){
- siblings.forEach(function(o){
+ setTimeout(function() {
+ siblings.forEach(function(o) {
o.node.style[transitionPrefix] = '';
o.node.style[transformPrefix] = o.baseTransform.original;
});
- },101);
+ }, 101);
}, 1);
}
}.bind(this), 101);
@@ -784,9 +883,9 @@ window['Slip'] = (function(){
// AMD
if ('function' === typeof define && define.amd) {
- define(function(){
+ define(function() {
return Slip;
});
}
return Slip;
-})();
+})();
\ No newline at end of file
diff --git a/vendor/snap.css b/vendor/snap.css
deleted file mode 100644
index b09c3a6..0000000
--- a/vendor/snap.css
+++ /dev/null
@@ -1,64 +0,0 @@
-.snap-content {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- width: auto;
- height: auto;
- z-index: 2;
- overflow: auto;
- -webkit-overflow-scrolling: touch;
- -webkit-transform: translate3d(0, 0, 0);
- -moz-transform: translate3d(0, 0, 0);
- -ms-transform: translate3d(0, 0, 0);
- -o-transform: translate3d(0, 0, 0);
- transform: translate3d(0, 0, 0);
-}
-
-.snap-drawers {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- width: auto;
- height: auto;
-}
-
-.snap-drawer {
- position: absolute;
- top: 0;
- right: auto;
- bottom: 0;
- left: auto;
- width: 265px;
- height: auto;
- overflow: auto;
- -webkit-overflow-scrolling: touch;
- -webkit-transition: width 0.3s ease;
- -moz-transition: width 0.3s ease;
- -ms-transition: width 0.3s ease;
- -o-transition: width 0.3s ease;
- transition: width 0.3s ease;
-}
-
-.snap-drawer-left {
- left: 0;
- z-index: 1;
-}
-
-.snap-drawer-right {
- right: 0;
- z-index: 1;
-}
-
-.snapjs-left .snap-drawer-right,
-.snapjs-right .snap-drawer-left {
- display: none;
-}
-
-.snapjs-expand-left .snap-drawer-left,
-.snapjs-expand-right .snap-drawer-right {
- width: 100%;
-}
diff --git a/vendor/snap.js b/vendor/snap.js
deleted file mode 100644
index 07b5a7f..0000000
--- a/vendor/snap.js
+++ /dev/null
@@ -1,568 +0,0 @@
-/*
-* Snap.js
-*
-* Copyright 2013, Jacob Kelley - http://jakiestfu.com/
-* Released under the MIT Licence
-* http://opensource.org/licenses/MIT
-*
-* Github: http://github.com/jakiestfu/Snap.js/
-* Version: 1.9.3
-*/
-/*jslint browser: true*/
-/*global define, module, ender*/
-(function(win, doc) {
- 'use strict';
- var Snap = Snap || function(userOpts) {
- var settings = {
- element: null,
- dragger: null,
- disable: 'none',
- addBodyClasses: true,
- hyperextensible: true,
- resistance: 0.5,
- flickThreshold: 50,
- transitionSpeed: 0.3,
- easing: 'ease',
- maxPosition: 266,
- minPosition: -266,
- tapToClose: true,
- touchToDrag: true,
- slideIntent: 40, // degrees
- minDragDistance: 5
- },
- cache = {
- simpleStates: {
- opening: null,
- towards: null,
- hyperExtending: null,
- halfway: null,
- flick: null,
- translation: {
- absolute: 0,
- relative: 0,
- sinceDirectionChange: 0,
- percentage: 0
- }
- }
- },
- eventList = {},
- utils = {
- hasTouch: ('ontouchstart' in doc.documentElement || win.navigator.msPointerEnabled),
- eventType: function(action) {
- var eventTypes = {
- down: (utils.hasTouch ? 'touchstart' : 'mousedown'),
- move: (utils.hasTouch ? 'touchmove' : 'mousemove'),
- up: (utils.hasTouch ? 'touchend' : 'mouseup'),
- out: (utils.hasTouch ? 'touchcancel' : 'mouseout')
- };
- return eventTypes[action];
- },
- page: function(t, e){
- return (utils.hasTouch && e.touches.length && e.touches[0]) ? e.touches[0]['page'+t] : e['page'+t];
- },
- klass: {
- has: function(el, name){
- return (el.className).indexOf(name) !== -1;
- },
- add: function(el, name){
- if(!utils.klass.has(el, name) && settings.addBodyClasses){
- el.className += " "+name;
- }
- },
- remove: function(el, name){
- if(settings.addBodyClasses){
- el.className = (el.className).replace(name, "").replace(/^\s+|\s+$/g, '');
- }
- }
- },
- dispatchEvent: function(type) {
- if (typeof eventList[type] === 'function') {
- return eventList[type].call();
- }
- },
- vendor: function(){
- var tmp = doc.createElement("div"),
- prefixes = 'webkit Moz O ms'.split(' '),
- i;
- for (i in prefixes) {
- if (typeof tmp.style[prefixes[i] + 'Transition'] !== 'undefined') {
- return prefixes[i];
- }
- }
- },
- transitionCallback: function(){
- return (cache.vendor==='Moz' || cache.vendor==='ms') ? 'transitionend' : cache.vendor+'TransitionEnd';
- },
- canTransform: function(){
- return typeof settings.element.style[cache.vendor+'Transform'] !== 'undefined';
- },
- deepExtend: function(destination, source) {
- var property;
- for (property in source) {
- if (source[property] && source[property].constructor && source[property].constructor === Object) {
- destination[property] = destination[property] || {};
- utils.deepExtend(destination[property], source[property]);
- } else {
- destination[property] = source[property];
- }
- }
- return destination;
- },
- angleOfDrag: function(x, y) {
- var degrees, theta;
- // Calc Theta
- theta = Math.atan2(-(cache.startDragY - y), (cache.startDragX - x));
- if (theta < 0) {
- theta += 2 * Math.PI;
- }
- // Calc Degrees
- degrees = Math.floor(theta * (180 / Math.PI) - 180);
- if (degrees < 0 && degrees > -180) {
- degrees = 360 - Math.abs(degrees);
- }
- return Math.abs(degrees);
- },
- events: {
- addEvent: function addEvent(element, eventName, func) {
- if (element.addEventListener) {
- return element.addEventListener(eventName, func, false);
- } else if (element.attachEvent) {
- return element.attachEvent("on" + eventName, func);
- }
- },
- removeEvent: function addEvent(element, eventName, func) {
- if (element.addEventListener) {
- return element.removeEventListener(eventName, func, false);
- } else if (element.attachEvent) {
- return element.detachEvent("on" + eventName, func);
- }
- },
- prevent: function(e) {
- if (e.preventDefault) {
- e.preventDefault();
- } else {
- e.returnValue = false;
- }
- }
- },
- parentUntil: function(el, attr) {
- var isStr = typeof attr === 'string';
- while (el.parentNode) {
- if (isStr && el.getAttribute && el.getAttribute(attr)){
- return el;
- } else if(!isStr && el === attr){
- return el;
- }
- el = el.parentNode;
- }
- return null;
- }
- },
- action = {
- translate: {
- get: {
- matrix: function(index) {
-
- if( !utils.canTransform() ){
- return parseInt(settings.element.style.left, 10);
- } else {
- var matrix = win.getComputedStyle(settings.element)[cache.vendor+'Transform'].match(/\((.*)\)/),
- ieOffset = 8;
- if (matrix) {
- matrix = matrix[1].split(',');
- if(matrix.length===16){
- index+=ieOffset;
- }
- return parseInt(matrix[index], 10);
- }
- return 0;
- }
- }
- },
- easeCallback: function(){
- settings.element.style[cache.vendor+'Transition'] = '';
- cache.translation = action.translate.get.matrix(4);
- cache.easing = false;
- clearInterval(cache.animatingInterval);
-
- if(cache.easingTo===0){
- utils.klass.remove(doc.body, 'snapjs-right');
- utils.klass.remove(doc.body, 'snapjs-left');
- }
-
- utils.dispatchEvent('animated');
- utils.events.removeEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback);
- },
- easeTo: function(n) {
-
- if( !utils.canTransform() ){
- cache.translation = n;
- action.translate.x(n);
- } else {
- cache.easing = true;
- cache.easingTo = n;
-
- settings.element.style[cache.vendor+'Transition'] = 'all ' + settings.transitionSpeed + 's ' + settings.easing;
-
- cache.animatingInterval = setInterval(function() {
- utils.dispatchEvent('animating');
- }, 1);
-
- utils.events.addEvent(settings.element, utils.transitionCallback(), action.translate.easeCallback);
- action.translate.x(n);
- }
- if(n===0){
- settings.element.style[cache.vendor+'Transform'] = '';
- }
- },
- x: function(n) {
- if( (settings.disable==='left' && n>0) ||
- (settings.disable==='right' && n<0)
- ){ return; }
-
- if( !settings.hyperextensible ){
- if( n===settings.maxPosition || n>settings.maxPosition ){
- n=settings.maxPosition;
- } else if( n===settings.minPosition || n 0,
- translateTo = whileDragX,
- diff;
-
- // Shown no intent already
- if((cache.intentChecked && !cache.hasIntent)){
- return;
- }
-
- if(settings.addBodyClasses){
- if((absoluteTranslation)>0){
- utils.klass.add(doc.body, 'snapjs-left');
- utils.klass.remove(doc.body, 'snapjs-right');
- } else if((absoluteTranslation)<0){
- utils.klass.add(doc.body, 'snapjs-right');
- utils.klass.remove(doc.body, 'snapjs-left');
- }
- }
-
- if (cache.hasIntent === false || cache.hasIntent === null) {
- var deg = utils.angleOfDrag(thePageX, thePageY),
- inRightRange = (deg >= 0 && deg <= settings.slideIntent) || (deg <= 360 && deg > (360 - settings.slideIntent)),
- inLeftRange = (deg >= 180 && deg <= (180 + settings.slideIntent)) || (deg <= 180 && deg >= (180 - settings.slideIntent));
- if (!inLeftRange && !inRightRange) {
- cache.hasIntent = false;
- } else {
- cache.hasIntent = true;
- }
- cache.intentChecked = true;
- }
-
- if (
- (settings.minDragDistance>=Math.abs(thePageX-cache.startDragX)) || // Has user met minimum drag distance?
- (cache.hasIntent === false)
- ) {
- return;
- }
-
- utils.events.prevent(e);
- utils.dispatchEvent('drag');
-
- cache.dragWatchers.current = thePageX;
- // Determine which direction we are going
- if (cache.dragWatchers.last > thePageX) {
- if (cache.dragWatchers.state !== 'left') {
- cache.dragWatchers.state = 'left';
- cache.dragWatchers.hold = thePageX;
- }
- cache.dragWatchers.last = thePageX;
- } else if (cache.dragWatchers.last < thePageX) {
- if (cache.dragWatchers.state !== 'right') {
- cache.dragWatchers.state = 'right';
- cache.dragWatchers.hold = thePageX;
- }
- cache.dragWatchers.last = thePageX;
- }
- if (openingLeft) {
- // Pulling too far to the right
- if (settings.maxPosition < absoluteTranslation) {
- diff = (absoluteTranslation - settings.maxPosition) * settings.resistance;
- translateTo = whileDragX - diff;
- }
- cache.simpleStates = {
- opening: 'left',
- towards: cache.dragWatchers.state,
- hyperExtending: settings.maxPosition < absoluteTranslation,
- halfway: absoluteTranslation > (settings.maxPosition / 2),
- flick: Math.abs(cache.dragWatchers.current - cache.dragWatchers.hold) > settings.flickThreshold,
- translation: {
- absolute: absoluteTranslation,
- relative: whileDragX,
- sinceDirectionChange: (cache.dragWatchers.current - cache.dragWatchers.hold),
- percentage: (absoluteTranslation/settings.maxPosition)*100
- }
- };
- } else {
- // Pulling too far to the left
- if (settings.minPosition > absoluteTranslation) {
- diff = (absoluteTranslation - settings.minPosition) * settings.resistance;
- translateTo = whileDragX - diff;
- }
- cache.simpleStates = {
- opening: 'right',
- towards: cache.dragWatchers.state,
- hyperExtending: settings.minPosition > absoluteTranslation,
- halfway: absoluteTranslation < (settings.minPosition / 2),
- flick: Math.abs(cache.dragWatchers.current - cache.dragWatchers.hold) > settings.flickThreshold,
- translation: {
- absolute: absoluteTranslation,
- relative: whileDragX,
- sinceDirectionChange: (cache.dragWatchers.current - cache.dragWatchers.hold),
- percentage: (absoluteTranslation/settings.minPosition)*100
- }
- };
- }
- action.translate.x(translateTo + translated);
- }
- },
- endDrag: function(e) {
- if (cache.isDragging) {
- utils.dispatchEvent('end');
- var translated = action.translate.get.matrix(4);
-
- // Tap Close
- if (cache.dragWatchers.current === 0 && translated !== 0 && settings.tapToClose) {
- utils.dispatchEvent('close');
- utils.events.prevent(e);
- action.translate.easeTo(0);
- cache.isDragging = false;
- cache.startDragX = 0;
- return;
- }
-
- // Revealing Left
- if (cache.simpleStates.opening === 'left') {
- // Halfway, Flicking, or Too Far Out
- if ((cache.simpleStates.halfway || cache.simpleStates.hyperExtending || cache.simpleStates.flick)) {
- if (cache.simpleStates.flick && cache.simpleStates.towards === 'left') { // Flicking Closed
- action.translate.easeTo(0);
- } else if (
- (cache.simpleStates.flick && cache.simpleStates.towards === 'right') || // Flicking Open OR
- (cache.simpleStates.halfway || cache.simpleStates.hyperExtending) // At least halfway open OR hyperextending
- ) {
- action.translate.easeTo(settings.maxPosition); // Open Left
- }
- } else {
- action.translate.easeTo(0); // Close Left
- }
- // Revealing Right
- } else if (cache.simpleStates.opening === 'right') {
- // Halfway, Flicking, or Too Far Out
- if ((cache.simpleStates.halfway || cache.simpleStates.hyperExtending || cache.simpleStates.flick)) {
- if (cache.simpleStates.flick && cache.simpleStates.towards === 'right') { // Flicking Closed
- action.translate.easeTo(0);
- } else if (
- (cache.simpleStates.flick && cache.simpleStates.towards === 'left') || // Flicking Open OR
- (cache.simpleStates.halfway || cache.simpleStates.hyperExtending) // At least halfway open OR hyperextending
- ) {
- action.translate.easeTo(settings.minPosition); // Open Right
- }
- } else {
- action.translate.easeTo(0); // Close Right
- }
- }
- cache.isDragging = false;
- cache.startDragX = utils.page('X', e);
- }
- }
- }
- },
- init = function(opts) {
- if (opts.element) {
- utils.deepExtend(settings, opts);
- cache.vendor = utils.vendor();
- action.drag.listen();
- }
- };
- /*
- * Public
- */
- this.open = function(side) {
- utils.dispatchEvent('open');
- utils.klass.remove(doc.body, 'snapjs-expand-left');
- utils.klass.remove(doc.body, 'snapjs-expand-right');
-
- if (side === 'left') {
- cache.simpleStates.opening = 'left';
- cache.simpleStates.towards = 'right';
- utils.klass.add(doc.body, 'snapjs-left');
- utils.klass.remove(doc.body, 'snapjs-right');
- action.translate.easeTo(settings.maxPosition);
- } else if (side === 'right') {
- cache.simpleStates.opening = 'right';
- cache.simpleStates.towards = 'left';
- utils.klass.remove(doc.body, 'snapjs-left');
- utils.klass.add(doc.body, 'snapjs-right');
- action.translate.easeTo(settings.minPosition);
- }
- };
- this.close = function() {
- utils.dispatchEvent('close');
- action.translate.easeTo(0);
- };
- this.expand = function(side){
- var to = win.innerWidth || doc.documentElement.clientWidth;
-
- if(side==='left'){
- utils.dispatchEvent('expandLeft');
- utils.klass.add(doc.body, 'snapjs-expand-left');
- utils.klass.remove(doc.body, 'snapjs-expand-right');
- } else {
- utils.dispatchEvent('expandRight');
- utils.klass.add(doc.body, 'snapjs-expand-right');
- utils.klass.remove(doc.body, 'snapjs-expand-left');
- to *= -1;
- }
- action.translate.easeTo(to);
- };
-
- this.on = function(evt, fn) {
- eventList[evt] = fn;
- return this;
- };
- this.off = function(evt) {
- if (eventList[evt]) {
- eventList[evt] = false;
- }
- };
-
- this.enable = function() {
- utils.dispatchEvent('enable');
- action.drag.listen();
- };
- this.disable = function() {
- utils.dispatchEvent('disable');
- action.drag.stopListening();
- };
-
- this.settings = function(opts){
- utils.deepExtend(settings, opts);
- };
-
- this.state = function() {
- var state,
- fromLeft = action.translate.get.matrix(4);
- if (fromLeft === settings.maxPosition) {
- state = 'left';
- } else if (fromLeft === settings.minPosition) {
- state = 'right';
- } else {
- state = 'closed';
- }
- return {
- state: state,
- info: cache.simpleStates
- };
- };
- init(userOpts);
- };
- if ((typeof module !== 'undefined') && module.exports) {
- module.exports = Snap;
- }
- if (typeof ender === 'undefined') {
- this.Snap = Snap;
- }
- if ((typeof define === "function") && define.amd) {
- define("snap", [], function() {
- return Snap;
- });
- }
- }).call(this, window, document);