diff --git a/.gitignore b/.gitignore index 964bc06..2d06007 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +/.idea +.versions .build* .DS_Store diff --git a/.versions b/.versions deleted file mode 100644 index ba3a556..0000000 --- a/.versions +++ /dev/null @@ -1,49 +0,0 @@ -application-configuration@1.0.4 -base64@1.0.2 -binary-heap@1.0.2 -blaze@2.0.4 -blaze-tools@1.0.2 -boilerplate-generator@1.0.2 -callback-hook@1.0.2 -check@1.0.4 -ddp@1.0.14 -deps@1.0.6 -ejson@1.0.5 -fastclick@1.0.2 -follower-livedata@1.0.3 -geojson-utils@1.0.2 -html-tools@1.0.3 -htmljs@1.0.3 -id-map@1.0.2 -iron:controller@1.0.7 -iron:core@1.0.7 -iron:dynamic-template@1.0.7 -iron:layout@1.0.7 -iron:location@1.0.7 -iron:middleware-stack@1.0.7 -iron:router@1.0.7 -iron:url@1.0.7 -jquery@1.11.3 -json@1.0.2 -logging@1.0.6 -meteor@1.1.4 -meteoric:ionic@0.1.17 -minifiers@1.1.3 -minimongo@1.0.6 -mongo@1.0.11 -observe-sequence@1.0.4 -ordered-dict@1.0.2 -random@1.0.2 -reactive-dict@1.0.5 -reactive-var@1.0.4 -retry@1.0.2 -routepolicy@1.0.4 -session@1.0.5 -spacebars@1.0.5 -spacebars-compiler@1.0.4 -templating@1.0.11 -tracker@1.0.5 -ui@1.0.5 -underscore@1.0.2 -webapp@1.1.6 -webapp-hashing@1.0.2 diff --git a/README.md b/README.md index 8409241..d97ef13 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -![](http://f.cl.ly/items/391y4708420P0H001k1G/meteoric.png) +![Meteoric Logo](meteoric-logo.png) + +[![Gitter](https://badges.gitter.im/JoeyAndres/meteor-ionic.svg)](https://gitter.im/JoeyAndres/meteor-ionic?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) # meteor-ionic ### Build [Ionic](http://ionicframework.com/) apps in [Meteor](https://www.meteor.com/)! -## NOTE: This package is no longer being actively maintained. - -If you are interested in maintaining it, contact me through [my Github profile](https://github.com/nickw). Now that Meteor is officially supporting (and actively recommending) React and Angular, I suggest using [Ionic](https://github.com/driftyco/ionic), [Ionic 2](https://github.com/driftyco/ionic2) or [Reapp](https://github.com/reapp) as alternatives for building hybrid mobile apps with Meteor. - ## Overview This is an attempt at **real Ionic and Meteor integration**. This is not just Ionic's CSS framework wrapped in a Meteor package. It aims to be a complete port of [Ionic’s Angular directives](http://ionicframework.com/docs/api/) to [Meteor Blaze](https://www.meteor.com/blaze) templates. +Note: This is a forked of meteoric:ionic, which is now not maintained. In response, I decided to fork it, and maintain this package for my own. I have written acceptance test on [JoeyAndres/ionic-demo](https://github.com/JoeyAndres/ionic-demo), which is a fork of meteoric/demo. + ## Why? [Ionic](http://ionicframework.com/) is arguably the most comprehensive, polished, cross-platform mobile framework available. But unfortunately a large portion of its functionality comes from Angular directives. [I'm not a fan of trying to force-fit Angular into Meteor](https://medium.com/space-camp/your-meteor-app-probably-doesnt-need-angular-13986a0323f6), so I wanted to see if I could rewrite Ionic specifically for Meteor. @@ -22,14 +22,27 @@ Check out the [GUIDE.md](GUIDE.md) for a guide on how to get started. **Beta** See the TODO section below to see which Angular Directives have been ported to Blaze. +## Install + +```bash +meteor add jandres:ionic +``` + ## Dependencies -Rather than include compiled or CDN versions of Ionic's CSS Framework we’ve extraced it into two separate packages: -- [meteoric:ionicons-sass](http://github.com/meteoric/ionicons-sass) Ionic’s Ionicons set wrapped for Meteor. -- [meteoric:ionic-sass](http://github.com/meteoric/ionic-sass) The base Ionic CSS Framework wrapped for Meteor. +- [jandres:meteoric-sass](https://github.com/JoeyAndres/meteoric-sass/) Ionic's scss only package and currently up to date with ionic v1.2.4 ## Examples +### Demo of all components +The demo app of various ionic components + +[Demo](http://jandres-ionic.meteor.com/) | [Code](https://github.com/JoeyAndres/ionic-demo) + +## Pre-fork Examples + +The following are examples prior to this fork. + ### Contacts App A simple CRUD app to manage contacts. @@ -40,13 +53,6 @@ A [Product Hunt](http://producthunt.com) clone built in Meteor Ionic. (In Progre [Demo](http://meteorhunt.meteor.com/) | [Code](https://github.com/meteoric/meteorhunt) -### Demo of all components -The demo app of various meteoric components - -[Demo](http://meteor-ionic.meteor.com/) | [Code](https://github.com/meteoric/demo) - -You can also keep track of the various other repos from the [Meteoric team](https://github.com/meteoric) - ## TODO ### Angular Directives to convert to Blaze: @@ -67,15 +73,15 @@ You can also keep track of the various other repos from the [Meteoric team](http * [x] ion-footer-bar * [x] Keyboard (requires [cordova](http://cordova.apache.org/) integration) * [ ] Lists (needs edit/remove/sort functionality) - * [ ] ion-list - * [ ] ion-item - * [ ] ion-delete-button - * [ ] ion-reorder-button - * [ ] ion-option-button + * [x] ion-list + * [x] ion-item + * [x] ion-delete-button + * [x] ion-reorder-button + * [x] ion-option-button * [ ] collection-repeat * [x] Loading * [x] Modal -* [x] Navigation (requires [iron:router](https://github.com/EventedMind/iron-router) integration) +* [x] Navigation (requires [iron:router](https://github.com/EventedMind/iron-router) integration)a * [x] ion-nav-view * [x] ion-view * [x] ion-nav-bar @@ -87,9 +93,9 @@ You can also keep track of the various other repos from the [Meteoric team](http * [x] Platform * [x] Popover * [x] Popup -* [ ] Scroll - * [ ] ion-scroll - * [ ] ion-infinite-scroll +* [x] Scroll + * [x] ion-scroll + * [x] ion-infinite-scroll * [x] Side Menus * [x] ion-side-menus * [x] ion-side-menu-content @@ -101,6 +107,10 @@ You can also keep track of the various other repos from the [Meteoric team](http * [x] Tabs (requires [iron:router](https://github.com/EventedMind/iron-router) integration) * [x] ion-tabs * [x] ion-tab + +### Code Style Change: +These are code styles that I want to impose on this forked repo. +* [ ] Get rid of all session variables. ## License [MIT License](https://github.com/meteoric/meteor-ionic/blob/master/LICENSE) diff --git a/components/ionBody/ionBody.js b/components/ionBody/ionBody.js index cf78c2f..f3a4e70 100644 --- a/components/ionBody/ionBody.js +++ b/components/ionBody/ionBody.js @@ -18,6 +18,7 @@ Template.registerHelper('isAndroid', function () { return Platform.isAndroid(); }); +// todo: This is a duplicate of METEORIC.PLATFORM, what to do?? Do we put the Meteor.isCordova test there? Template.ionBody.helpers({ platformClasses: function () { var classes = ['grade-a']; diff --git a/components/ionContent/ionContent.html b/components/ionContent/ionContent.html index f848164..e410858 100644 --- a/components/ionContent/ionContent.html +++ b/components/ionContent/ionContent.html @@ -1,7 +1,30 @@ diff --git a/components/ionContent/ionContent.js b/components/ionContent/ionContent.js index 5ec59b4..9461a31 100644 --- a/components/ionContent/ionContent.js +++ b/components/ionContent/ionContent.js @@ -1,39 +1,104 @@ +let ionContentDefault = { + direction: 'y', + locking: true, + padding: true, + scroll: true, + overflowScroll: false, + scrollBarX: true, + scrollBarY: true, + startX: '0', + startY: '0', + onScroll: null, + onScrollComplete: null, + hasBouncing: true, + scrollEventInterval: 10 +}; + +Template.ionContent.onCreated(function() { + this.direction = new ReactiveVar(ionContentDefault.direction); + this.locking = new ReactiveVar(ionContentDefault.locking); + this.padding = new ReactiveVar(ionContentDefault.padding); // todo: make this platform dependent. + this.scroll = new ReactiveVar(ionContentDefault.scroll); + this.overflowScroll = new ReactiveVar(ionContentDefault.overflowScroll); // todo: Make a Meteoric config for defaults. + this.scrollBarX = new ReactiveVar(ionContentDefault.scrollBarX); + this.scrollBarY = new ReactiveVar(ionContentDefault.scrollBarY); + this.startX = new ReactiveVar(ionContentDefault.startX); + this.startY = new ReactiveVar(ionContentDefault.startY); + this.onScroll = new ReactiveVar(ionContentDefault.onScroll); + this.onScrollComplete = new ReactiveVar(ionContentDefault.onScrollComplete); + this.hasBouncing = new ReactiveVar(ionContentDefault.hasBouncing); // tdo: Make platform dependent. + this.scrollEventInterval = new ReactiveVar(ionContentDefault.scrollEventInterval); + + this.autorun(() => { + let td = Template.currentData(); + if (!td) return; + this.direction.set(td.direction || ionContentDefault.direction); + this.locking.set(!!td.locking ? td.locking : ionContentDefault.locking); + this.padding.set(!!td.padding ? td.padding : ionContentDefault.padding); + this.scroll.set(!!td.scroll ? td.scroll : ionContentDefault.scroll); + this.overflowScroll.set(!!td.overflowScroll ? td.overflowScroll : ionContentDefault.overflowScroll); + this.scrollBarX.set(!!td.scrollBarX ? td.scrollBarX : ionContentDefault.scrollBarX); + this.scrollBarY.set(!!td.scrollBarY ? td.scrollBarY : ionContentDefault.scrollBarY); + this.startX.set(!!td.startX ? td.startX : ionContentDefault.startX); + this.startY.set(!!td.startX ? td.startY : ionContentDefault.startY); + this.onScroll.set(td.onScroll); + this.onScrollComplete.set(td.onScrollComplete); + this.hasBouncing.set(!!td.hasBouncing ? td.hasBouncing : ionContentDefault.hasBouncing); + this.scrollEventInterval.set(!!this.scrollEventInterval ? this.scrollEventInterval : ionContentDefault.scrollEventInterval); + }); +}); + Template.ionContent.helpers({ - classes: function () { - var classes = ['content']; + hasHeader: function() { return METEORIC.hasHeader.get(); }, - if (this.class) { - classes.push(this.class); - } + hasFooter: function() { return METEORIC.hasFooter.get(); }, - if (this.scroll !== false) { - classes.push('overflow-scroll'); - } + classes: function () { + var classes = ['content']; - if (Session.get('hasHeader')) { - classes.push('has-header'); - } + if (this.class) { + classes.push(this.class); + } - if (Session.get('hasSubheader')) { - classes.push('has-subheader'); - } + if (this.scroll !== false) { + //classes.push('overflow-scroll'); + classes.push('scroll'); + } - if (Session.get('hasTabs')) { - classes.push('has-tabs'); - } + if (Session.get('hasSubheader')) { + classes.push('has-subheader'); + } - if (Session.get('hasTabsTop')) { - classes.push('has-tabs-top'); - } + if (Session.get('hasTabs')) { + classes.push('has-tabs'); + } - if (Session.get('hasFooter')) { - classes.push('has-footer'); - } + if (Session.get('hasTabsTop')) { + classes.push('has-tabs-top'); + } - if (Session.get('hasSubfooter')) { - classes.push('has-subfooter'); - } + if (Session.get('hasFooter')) { + classes.push('has-footer'); + } - return classes.join(' '); - } -}); + if (Session.get('hasSubfooter')) { + classes.push('has-subfooter'); + } + + return classes.join(' '); + }, + + direction: function() { return Template.instance().direction.get(); }, + locking: function() { return Template.instance().locking.get(); }, + padding: function() { return Template.instance().padding.get(); }, + scroll: function() { return Template.instance().scroll.get(); }, + overflowScroll: function() { return Template.instance().overflowScroll.get(); }, + scrollBarX: function() { return Template.instance().scrollBarX.get(); }, + scrollBarY: function() { return Template.instance().scrollBarY.get(); }, + startX: function() { return Template.instance().startX.get(); }, + startY: function() { return Template.instance().startY.get(); }, + onScroll: function() { return Template.instance().onScroll.get(); }, + onScrollComplete: function() { return Template.instance().onScrollComplete.get(); }, + hasBouncing: function() { return Template.instance().hasBouncing.get(); }, + scrollEventInterval: function() { return Template.instance().scrollEventInterval.get(); } +}); \ No newline at end of file diff --git a/components/ionDeleteButton/ionDeleteButton.html b/components/ionDeleteButton/ionDeleteButton.html new file mode 100644 index 0000000..8ccd42c --- /dev/null +++ b/components/ionDeleteButton/ionDeleteButton.html @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/components/ionDeleteButton/ionDeleteButton.js b/components/ionDeleteButton/ionDeleteButton.js new file mode 100644 index 0000000..190f75d --- /dev/null +++ b/components/ionDeleteButton/ionDeleteButton.js @@ -0,0 +1,13 @@ +Template.ionDeleteButton.onCreated(function() { + let parent = this.parent((t) => t.view.name === "Template.ionItem", true); + if (!parent) { throw "Template.ionDeleteButton must be a descendant of Template.ionItem."; } + _.extend(this, { + showDelete: parent.showDelete + }); +}); + +Template.ionDeleteButton.helpers({ + showDelete: function() { + return Template.instance().showDelete.get(); + } +}); \ No newline at end of file diff --git a/components/ionFooterBar/ionFooterBar.html b/components/ionFooterBar/ionFooterBar.html index 5e78ec3..c389e9f 100644 --- a/components/ionFooterBar/ionFooterBar.html +++ b/components/ionFooterBar/ionFooterBar.html @@ -1,5 +1,7 @@ + + \ No newline at end of file diff --git a/components/ionFooterBar/ionFooterBar.js b/components/ionFooterBar/ionFooterBar.js index 0a1c3b8..af9b280 100644 --- a/components/ionFooterBar/ionFooterBar.js +++ b/components/ionFooterBar/ionFooterBar.js @@ -1,11 +1,58 @@ -Template.ionFooterBar.rendered = function () { - Session.set('hasFooter', true); -}; +IonFooterBar = { + alignTitle: function () { + var align = this.alignTitle; + var $title = this.$('.title'); + + if (Platform.isAndroid() && !this.alignTitle) { + $title.addClass('title-left'); + return; + } + + if (align === 'center') { + $title.addClass('title-center'); + } else if (align === 'left') { + $title.addClass('title-left'); + } else if (align === 'right') { + $title.addClass('title-right'); + } + }, + + positionTitle: function () { + var $title = this.$('.title'); + var $leftButton = this.$('.buttons').eq(0); + var $rightButton = this.$('.buttons').eq(1); -Template.ionFooterBar.destroyed = function () { - Session.set('hasFooter', false); + // Find out which button is wider, + // use that to offset the title on both sides + let leftButtonWidth = $leftButton.outerWidth(); + let rightButtonWidth = $rightButton.outerWidth(); + + // If we're on Android, we only care about the left button + var margin; + if (Platform.isAndroid()) { + margin = leftButtonWidth; + } else { + margin = Math.max(leftButtonWidth, rightButtonWidth); + } + $title.css('left', margin); + $title.css('right', margin); + } }; +Template.ionFooterBar.onCreated(function() { + this.alignTitle = this.data? this.data.alignTitle : null; +}); + +Template.ionFooterBar.onRendered(function () { + METEORIC.hasFooter.set(true); + IonFooterBar.alignTitle.call(this); + IonFooterBar.positionTitle.call(this); +}); + +Template.ionFooterBar.onDestroyed(function () { + METEORIC.hasFooter.set(false); +}); + Template.ionFooterBar.helpers({ classes: function () { var classes = ['bar', 'bar-footer']; diff --git a/components/ionHeaderBar/ionHeaderBar.html b/components/ionHeaderBar/ionHeaderBar.html index 9e25640..558905d 100644 --- a/components/ionHeaderBar/ionHeaderBar.html +++ b/components/ionHeaderBar/ionHeaderBar.html @@ -1,5 +1,7 @@ diff --git a/components/ionHeaderBar/ionHeaderBar.js b/components/ionHeaderBar/ionHeaderBar.js index 96d830e..40e3702 100644 --- a/components/ionHeaderBar/ionHeaderBar.js +++ b/components/ionHeaderBar/ionHeaderBar.js @@ -1,78 +1,68 @@ IonHeaderBar = { - alignTitle: function () { - var align = this.data.alignTitle || 'center'; - var $title = this.$('.title'); + alignTitle: function () { + var align = this.alignTitle; + var $title = this.$('.title'); - if (Platform.isAndroid() && !this.alignTitle) { - $title.addClass('title-left'); - return; - } + if (Platform.isAndroid() && !this.alignTitle) { + $title.addClass('title-left'); + return; + } - if (align === 'center') { - $title.addClass('title-center'); - } else if (align === 'left') { - $title.addClass('title-left'); - } else if (align === 'right') { - $title.addClass('title-right'); - } - }, + if (align === 'center') { + $title.addClass('title-center'); + } else if (align === 'left') { + $title.addClass('title-left'); + } else if (align === 'right') { + $title.addClass('title-right'); + } + }, - positionTitle: function () { - var $title = this.$('.title'); - var $leftButton = $('.button.pull-left'); - var $rightButton = $('.button.pull-right'); + positionTitle: function () { + var $title = this.$('.title'); + var $leftButton = this.$('.buttons').eq(0); + var $rightButton = this.$('.buttons').eq(1); - // Find out which button is wider, - // use that to offset the title on both sides - var leftButtonWidth = 0; - var rightButtonWidth = 0; - if ($leftButton.length) { - $leftButton.each(function(index, element){ - leftButtonWidth += $(element).outerWidth(); - }); - } - if ($rightButton.length) { - $rightButton.each(function(index, element){ - rightButtonWidth += $(element).outerWidth(); - }); - } + // Find out which button is wider, + // use that to offset the title on both sides + let leftButtonWidth = $leftButton.outerWidth(); + let rightButtonWidth = $rightButton.outerWidth(); - // If we're on Android, we only care about the left button - var margin; - if (Platform.isAndroid()) { - margin = leftButtonWidth; - } else { - margin = Math.max(leftButtonWidth, rightButtonWidth); + // If we're on Android, we only care about the left button + var margin; + if (Platform.isAndroid()) { + margin = leftButtonWidth; + } else { + margin = Math.max(leftButtonWidth, rightButtonWidth); + } + $title.css('left', margin); + $title.css('right', margin); } - $title.css('left', margin); - $title.css('right', margin); - } }; -Template.ionHeaderBar.created = function () { - this.data = this.data || {}; -}; +Template.ionHeaderBar.onCreated(function() { + this.alignTitle = this.data? this.data.alignTitle : null; +}); -Template.ionHeaderBar.rendered = function () { - Session.set('hasHeader', true); - IonHeaderBar.alignTitle.call(this); - IonHeaderBar.positionTitle.call(this); -}; +Template.ionHeaderBar.onRendered(function () { + METEORIC.hasHeader.set(true); + IonHeaderBar.alignTitle.call(this); + IonHeaderBar.positionTitle.call(this); +}); -Template.ionHeaderBar.destroyed = function () { - Session.set('hasHeader', false); -}; +Template.ionHeaderBar.onDestroyed(function () { + METEORIC.hasHeader.set(false); +}); Template.ionHeaderBar.helpers({ - classes: function () { - var classes = ['bar', 'bar-header']; + classes: function () { + var classes = ['bar', 'bar-header']; - if (this.class) { - classes.push(this.class); - } else { - classes.push('bar-stable'); - } + if (this.class) { + classes.push(this.class); + } else { + classes.push('bar-stable'); + } - return classes.join(' '); - } + return classes.join(' '); + } }); diff --git a/components/ionIcon/ionIcon.html b/components/ionIcon/ionIcon.html deleted file mode 100644 index ed8693f..0000000 --- a/components/ionIcon/ionIcon.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/components/ionIcon/ionIcon.js b/components/ionIcon/ionIcon.js deleted file mode 100644 index 2c5e0b7..0000000 --- a/components/ionIcon/ionIcon.js +++ /dev/null @@ -1,12 +0,0 @@ -Template.ionIcon.helpers({ - classes: function () { - var classes = ['icon']; - classes.push('ion-' + this.icon); - - if (this.class) { - classes.push(this.class); - } - - return classes.join(' '); - } -}); diff --git a/components/ionInfiniteScroll/ionInfiniteScroll.html b/components/ionInfiniteScroll/ionInfiniteScroll.html new file mode 100644 index 0000000..5df80a0 --- /dev/null +++ b/components/ionInfiniteScroll/ionInfiniteScroll.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/components/ionInfiniteScroll/ionInfiniteScroll.js b/components/ionInfiniteScroll/ionInfiniteScroll.js new file mode 100644 index 0000000..e9d6f58 --- /dev/null +++ b/components/ionInfiniteScroll/ionInfiniteScroll.js @@ -0,0 +1,82 @@ +Template.ionInfiniteScroll.onCreated(function() { + this.onInfinite = null; + this.distance = "1%"; + this.enable = new ReactiveVar(true); + this.immediateCheck = true; + + this.isLoading = new ReactiveVar(false); + + this.finishInfiniteScroll = null; + + this.autorun(() => { + if (!Template.currentData()) return; // Don't do a thing if data context don't exist. + this.onInfinite = Template.currentData().onInfinite; + this.distance = !_.isUndefined(Template.currentData().distance) ? parseFloat(Template.currentData().distance) : "1%"; + this.enable.set(!_.isUndefined(Template.currentData().enable) ? Template.currentData().enable : true); + this.immediateCheck = !!Template.currentData().immediateCheck; + }); +}); + +Template.ionInfiniteScroll.onRendered(function() { + let parentTemplate = this.parent(1, true); + + let calculateMaxValue = function(maximum) { + let distance = (this.distance || '2.5%').trim(); + let isPercent = distance.indexOf('%') !== -1; + return isPercent ? maximum * (1 - parseFloat(distance) / 100) : maximum - parseFloat(distance); + }; + + this.autorun(() => { + let scroller = parentTemplate.scroller.get(); + if (!!scroller) { + let scrollHeight = () => parentTemplate.$('.meteoric-scroller-container').get(0).scrollHeight; + let maxScrollHeight = () => calculateMaxValue(scrollHeight()); + let clientHeight = () => parentTemplate.$('.meteoric-scroller-container').get(0).clientHeight; + let scrollTop = () => scroller.scroller.__scrollTop; + + // @see https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight + let reachedBottom = () => scrollTop() > maxScrollHeight() - clientHeight(); + let onInfinite = () => { + this.isLoading.set(true); + _.isFunction(this.onInfinite) ? this.onInfinite() : () => { + }; + }; + let checkInfiniteBounds = () => { + if (this.isLoading.get()) return; + if (reachedBottom()) { + onInfinite(); + } + }; + let checkBounds = METEORIC.UTILITY.throttle(checkInfiniteBounds, 300); + this.finishInfiniteScroll = () => { + Meteor.setTimeout(checkBounds, 30); + this.isLoading.set(false); + }; + + let onScrollHandler = () => { + if (!this.enable.get()) return; // Don't do a thing when disabled. + checkBounds(); // Ensure that this is throttled. + }; + + // ft-scroll scrolling. + scroller.scroller.options.scrolling = onScrollHandler; + + // todo: Native scrolling. + // $overflowScrollContainer.scroll(e => onScrollHandler()); + + if (this.immediateCheck) { Meteor.setTimeout(checkBounds); } + $(window).on('scroll.infiniteScrollComplete', this.finishInfiniteScroll); + } + }); +}); + +Template.ionInfiniteScroll.onDestroyed(function() { + this.enable.set(false); + + this.finishInfiniteScroll && $(window).off('scroll.infiniteScrollComplete', this.finishInfiniteScroll); +}); + +Template.ionInfiniteScroll.helpers({ + enable: function() { return Template.instance().enable.get(); }, + active: function() { return Template.instance().isLoading.get(); } +}); \ No newline at end of file diff --git a/components/ionItem/ionItem.html b/components/ionItem/ionItem.html index e1788ab..baea793 100644 --- a/components/ionItem/ionItem.html +++ b/components/ionItem/ionItem.html @@ -1,11 +1,12 @@ + \ No newline at end of file diff --git a/components/ionItem/ionItem.js b/components/ionItem/ionItem.js index 811345d..b68e245 100644 --- a/components/ionItem/ionItem.js +++ b/components/ionItem/ionItem.js @@ -1,48 +1,97 @@ -Template.ionItem.helpers({ - idAttribute: function () { - if (this.id) { - return this.id; - } - }, - itemClasses: function () { - var classes = ['item']; - - if (this.class) { - var customClasses = this.class.split(' '); - _(customClasses).each(function (customClass) { - classes.push(customClass); - }); +Template.ionItem.onCreated(function() { + this.snapper = null; + this.itemComplex = new ReactiveVar(false); + + let parent = this.parent((template) => template.view.name === "Template.ionList", true); + if (!parent) { throw "Template.ionItem must be a descendant of Template.ionList."; } + + _.extend(this, { + // Props. + showDelete: parent.showDelete, + showReorder: parent.showReorder, + canSwipe: parent.canSwipe, + + // Methods. + closeIonItemSiblings: () => { + let ionItemSiblings = this.getSiblings().filter(sibling => sibling.view.name === "Template.ionItem"); + _.each(ionItemSiblings, sibling => !!sibling.snapper && sibling.snapper.close()); + }, + dragSetTransitionNone: () => this.$('.item-content').css({ transition: "none" }), + dragEndSetTransitionToInitial: () => this.$('.item-content').css({ transition: "initial"}), + initDragTransitionHandler: () => { + this.snapper.on('drag', this.dragSetTransitionNone); + this.snapper.on('end', this.dragEndSetTransitionToInitial); + }, + destroyDragTransitionHandler: () => { + this.snapper.off('drag', this.dragSetTransitionNone); + this.snapper.off('end', this.dragEndSetTransitionToInitial); + }, + isitemComplex: () => { + let complex = !!_.find(this.children(1), + elem => + elem.view.name === 'Template.ionItemOptions' || + elem.view.name === 'Template.ionDeleteButton' || + elem.view.name === 'Template.ionReorderButton'); + return complex; } + }); +}); - if (this.avatar) { - classes.push('item-avatar' + (this.avatar === 'right' ? '-right' : '')); - } +Template.ionItem.onRendered(function() { + this.autorun(() => { + if (this.canSwipe.get() && !this.showDelete.get() && !this.showReorder.get()) { + let ionOptions = this.getChildren() + .filter(child => child.view && child.view.name === 'Template.ionItemOptions'); + let ionOptionsWidth = ionOptions.reduce((width, child) => width + child.width(), 0); + if (!this.snapper) { + this.snapper = new Snap({ + element: this.$('.item-content').get(0), + disable: 'left', + minPosition: -ionOptionsWidth + }); + } - if (this.iconLeft) { - classes.push('item-icon-left'); - } + this.snapper.settings({ + element: this.$('.item-content').get(0), // In case the child template ionItemContent got changed. + minPosition: -ionOptionsWidth + }); + this.snapper.enable(); - if (this.iconRight) { - classes.push('item-icon-right'); - } + this.snapper.on('start', () => { + this.closeIonItemSiblings(); + }); - if (this.buttonLeft) { - classes.push('item-button-left'); + this.initDragTransitionHandler(); + } else { + if (this.snapper) { + this.destroyDragTransitionHandler(); + this.snapper.disable(); + this.snapper.close(); + } } + }); - if(Session.get('ionSortable')){ - classes.push('item-complex', 'item-left-editable'); - } + this.autorun(() => { + this.itemComplex.set(this.isitemComplex()); + }); +}); - if (this.buttonRight) { - classes.push('item-button-right'); - } +Template.ionItem.onDestroyed(function() { + if (!!this.snapper) { + this.destroyDragTransitionHandler(); + this.snapper.disable(); + this.snapper.close(); + } +}); - if (this.textWrap) { - classes.push('item-text-wrap'); +Template.ionItem.helpers({ + idAttribute: function () { + if (this.id) { + return this.id; } - - return classes.join(' '); + }, + itemComplex: function() { + return Template.instance().itemComplex.get(); }, isAnchor: function () { @@ -85,4 +134,4 @@ Template.ionItem.helpers({ } } } -}); +}); \ No newline at end of file diff --git a/components/ionItemContent/ionItemContent.html b/components/ionItemContent/ionItemContent.html new file mode 100644 index 0000000..239aceb --- /dev/null +++ b/components/ionItemContent/ionItemContent.html @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/components/ionItemContent/ionItemContent.js b/components/ionItemContent/ionItemContent.js new file mode 100644 index 0000000..640bb15 --- /dev/null +++ b/components/ionItemContent/ionItemContent.js @@ -0,0 +1,13 @@ +Template.ionItemContent.onCreated(function() { + let parent = this.parent((t) => t.view.name === "Template.ionItem", true); + if (!parent) { throw "Template.ionItemContent must be a descendant of Template.ionItem."; } + _.extend(this, { + itemComplex: parent.itemComplex + }); +}); + +Template.ionItemContent.helpers({ + itemComplex: function() { + return Template.instance().itemComplex.get(); + } +}); \ No newline at end of file diff --git a/components/ionItemOptions/ionItemOptions.html b/components/ionItemOptions/ionItemOptions.html new file mode 100644 index 0000000..1415299 --- /dev/null +++ b/components/ionItemOptions/ionItemOptions.html @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/components/ionItemOptions/ionItemOptions.js b/components/ionItemOptions/ionItemOptions.js new file mode 100644 index 0000000..9bd3d00 --- /dev/null +++ b/components/ionItemOptions/ionItemOptions.js @@ -0,0 +1,7 @@ +Template.ionItemOptions.onCreated(function() { + // todo: create package that gets the width of current element, if first = last. + // otherwise, nearest parent (which is just the parent). + this.width = function() { + return this.$('.item-options').width(); + }; +}); \ No newline at end of file diff --git a/components/ionList/ionList.html b/components/ionList/ionList.html index 6331f63..dba2a27 100644 --- a/components/ionList/ionList.html +++ b/components/ionList/ionList.html @@ -1,5 +1,8 @@ \ No newline at end of file diff --git a/components/ionList/ionList.js b/components/ionList/ionList.js index 529972d..f317f89 100644 --- a/components/ionList/ionList.js +++ b/components/ionList/ionList.js @@ -1,94 +1,57 @@ -Template.ionList.helpers({ - classes: function () { - var classes = ['list']; - - if (this.class) { - var customClasses = this.class.split(' '); - _(customClasses).each(function (customClass) { - classes.push(customClass); - }); - } - - return classes.join(' '); - } +Template.ionList.onCreated(function() { + this.showDelete = new ReactiveVar(false); + this.showReorder = new ReactiveVar(false); + this.canSwipe = new ReactiveVar(false); }); - -Template.ionList.rendered = function() { - - if (this.data && this.data.ionSortable){ - Session.set("ionSortable", true ); - var list = this.$('.list')[0]; - new Slip(list); -} - -}; - - -Template.ionList.events({ - 'click .item-delete' : function(e, template){ - e.preventDefault(); - - var target = $(e.target).closest('.item').get(0); - var targetData = Blaze.getData(target.getElementsByClassName('item-content')[0])._id || undefined; - - template.data.ionSortable.find({}).forEach(function(item, i) { - if (item._id === targetData) { - template.data.ionSortable._collection.remove({ - _id: item._id - }, function(error, result) { }); - } - }); - }, - 'slip:swipe .list, slip:beforeswipe .list, slip:beforewait .list, slip:afterswipe .list': function(e, template) { - e.preventDefault(); - }, - 'slip:beforereorder .list': function(e, template) { - if (e.originalEvent.target.className.indexOf('instant') == -1) { - e.preventDefault(); - } - }, - 'slip:reorder .list': function(e, template) { - spliceIndex = e.originalEvent.detail.spliceIndex - originalIndex = e.originalEvent.detail.originalIndex - - if (spliceIndex != originalIndex) { - - template.data.ionSortable.find({}, { - sort: { - order: 1 - } - }).forEach(function(item, i) { - template.data.ionSortable._collection.pauseObservers() - if (item._id == Blaze.getData(e.target.getElementsByClassName('item-content')[0])._id) { - temp = template.data.ionSortable.update({ - _id: item._id - }, { - $set: { - order: spliceIndex +Template.ionList.onRendered(function() { + let parent = this.parent(t => t.view.name = 'Template.ionScroll', true); + this.autorun(() => { + if (this.showReorder.get()) { + IonSideMenu.disable(); + if (!this.slip) { + var list = this.$('.list')[0]; + this.slip = new Slip(list, { scroller: parent.scroller.get().scroller }); } - }) } else { - if (spliceIndex > originalIndex) { - newOrder = ((spliceIndex >= i) && (originalIndex < i)) ? (i - 1) : i - } else if (spliceIndex == '0') { - newOrder = (originalIndex > i) ? (i + 1) : i - } else { - newOrder = ((spliceIndex <= i) && (originalIndex > i)) ? (i + 1) : i - } + IonSideMenu.enable(); + } + }); - temp = template.data.ionSortable.update({ - _id: item._id - }, { - $set: { - order: newOrder - } - }) + this.autorun(() => { + if (!!Template.currentData()) { + this.canSwipe.set(!!Template.currentData().canSwipe); + this.showReorder.set(!!Template.currentData().showReorder); + this.showDelete.set(!!Template.currentData().showDelete); } - template.data.ionSortable._collection.resumeObservers() - }) + }); +}); +Template.ionList.events({ + 'click .item-delete' : function(e, template){ + e.preventDefault(); + }, + 'slip:swipe .list, slip:beforeswipe .list, slip:beforewait .list, slip:afterswipe .list': function(e, template) { + e.preventDefault(); + }, + 'slip:beforereorder .list': function(e, template) { + // Two case to consider: + // 1. instant class is in ionItem. In which case, we allow reorder, but we don't show the dragging animation. + // 2. This thing still shows the animation ven when reorder is disabled. Element goes back to orig spot, but could + // easily mislead the user. + if (e.originalEvent.target.className.indexOf('instant') !== -1 || + !template.showReorder.get()) { + e.preventDefault(); + } + }, + 'slip:reorder .list': function(e, template) { + let toIndex = e.originalEvent.detail.spliceIndex; + let fromIndex = e.originalEvent.detail.originalIndex; + + let index_change = toIndex !== fromIndex; + let sortable = index_change && template.showReorder.get() && !!template.data.onReorder; + if (sortable) { + template.data.onReorder(Template.instance().children()[fromIndex], fromIndex, toIndex); + } } - } - }); \ No newline at end of file diff --git a/components/ionLoading/ionLoading.html b/components/ionLoading/ionLoading.html index 316fbc1..97f3024 100644 --- a/components/ionLoading/ionLoading.html +++ b/components/ionLoading/ionLoading.html @@ -4,7 +4,7 @@ {{#if template}} {{{template}}} {{else}} - + {{> ionSpinner}} {{/if}} diff --git a/components/ionNavBackButton/ionNavBackButton.html b/components/ionNavBackButton/ionNavBackButton.html index 64a5a8e..2f762c8 100644 --- a/components/ionNavBackButton/ionNavBackButton.html +++ b/components/ionNavBackButton/ionNavBackButton.html @@ -1,6 +1,6 @@ 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);