diff --git a/playground/basic-example/main.html b/playground/basic-example/main.html index fc6a1528ac..e4456bd680 100644 --- a/playground/basic-example/main.html +++ b/playground/basic-example/main.html @@ -1,6 +1,17 @@ - - - + + + Hello ... +

...

+

...

+

...

+

...

+

...

+ Side menu! +
+
+ + + @@ -11,3 +22,11 @@

I'm a modal!

--> + + +
+ diff --git a/playground/basic-example/main.js b/playground/basic-example/main.js index d2ee150ae7..71095b590f 100644 --- a/playground/basic-example/main.js +++ b/playground/basic-example/main.js @@ -1,16 +1,16 @@ import {bootstrap} from 'angular2/core'; import {Component, Template} from 'angular2/angular2'; -import {Tabbar} from 'ionic/components/tabbar/tabbar'; import {Modal} from 'ionic/components/modal/modal'; import {SideMenu, SideMenuParent} from 'ionic/components/sidemenu/sidemenu'; import {Switch} from 'ionic/components/switch/switch'; +import {SideMenu, SideMenuParent} from 'ionic/components'; -import 'ionic/components/tabbar/mixins/android/android-tabbar'; +// import 'ionic/components/tabbar/mixins/android/android-tabbar'; @Component({ selector: '[playground-main]' }) @Template({ url: 'main.html', - directives: [Tabbar, Modal, SideMenu, SideMenuParent, Switch] + directives: [SideMenu, SideMenuParent] }) class PlaygroundMain { constructor() { diff --git a/src/components.js b/src/components.js new file mode 100644 index 0000000000..1c87558555 --- /dev/null +++ b/src/components.js @@ -0,0 +1,3 @@ +export * from './components/sidemenu/sidemenu'; + +import './components/sidemenu/behaviors/direction/direction'; diff --git a/src/components/ion.js b/src/components/ion.js index ad1714d93f..6ef2059c07 100644 --- a/src/components/ion.js +++ b/src/components/ion.js @@ -1,31 +1,12 @@ import * as Platform from '../platform'; +import * as util from '../util'; var ILLEGAL_ASSIGN_FIELDS = {}; export class Ion { - constructor() { - var platformName = Platform.getPlatform(); - var platformConfig = this.$config._platforms[platformName]; - if (platformConfig) { - for (var i = 0, ii = platformConfig._mixins.length; i < ii; i++) { - platformConfig._mixins[i](this); - } - } - } - - extend() { - for (var i = 0, ii = arguments.length; i < ii; i++) { - var obj = arguments[i]; - if (obj) { - var keys = Object.keys(obj); - for (var j = 0, jj = keys.length; j < jj; j++) { - var key = keys[j]; - if (!ILLEGAL_ASSIGN_FIELDS[key]) { - this[key] = obj[key]; - } - } - } - } + extend(...args) { + args.unshift(this); + return util.extend.apply(null, args); } } diff --git a/src/components/sidemenu/behaviors/direction/direction.js b/src/components/sidemenu/behaviors/direction/direction.js new file mode 100644 index 0000000000..fc7e8838cb --- /dev/null +++ b/src/components/sidemenu/behaviors/direction/direction.js @@ -0,0 +1,104 @@ +import {sideMenuConfig} from '../../sidemenu'; +import * as util from '../../../../util'; + +sideMenuConfig.when(instance => instance.side === 'bottom') + .mixin(function() { + + this.gesture.options({ + direction: Hammer.DIRECTION_VERTICAL + }); + this.domElement.classList.add('bottom'); + util.extend(this.dragMethods, { + getMenuStart: (drag, ev) => { + return this.isOpen ? -drag.height : 0; + }, + onDrag: (drag, ev) => { + drag.pos = util.clamp( + 0, -drag.menuStart + drag.pointerStart - ev.center.y, drag.height + ); + this.domElement.style.transform = 'translate3d(0,' + + (drag.height - drag.pos) + 'px,0)'; + }, + onEnd: (drag, ev) => { + this.setOpen(drag.pos > drag.height / 2); + this.domElement.style.transform = ''; + }, + getEventPos: ev => { + return ev.center.y; + } + }); + + }); + +sideMenuConfig.when(instance => instance.side === 'left') + .mixin(function() { + + this.domElement.classList.add('left'); + util.extend(this.dragMethods, { + canStart: (ev) => { + return this.isOpen || ev.center.x < this.dragThreshold; + }, + getMenuStart: (drag, ev) => { + return this.isOpen ? drag.width : 0; + }, + onDrag: (drag, ev) => { + drag.pos = util.clamp( + 0, drag.menuStart + ev.center.x - drag.pointerStart, drag.width + ); + this.domElement.style.transform = 'translate3d(' + (-drag.width + drag.pos) + 'px, 0, 0)'; + }, + onEnd: (drag, ev) => { + this.setOpen(drag.pos > drag.width / 2); + this.domElement.style.transform = ''; + } + }); + + }); + +sideMenuConfig.when(instance => instance.side === 'right') + .mixin(function() { + + this.domElement.classList.add('right'); + util.extend(this.dragMethods, { + getMenuStart: (drag, ev) => { + return this.isOpen ? -drag.width : 0; + }, + onDrag: (drag, ev) => { + drag.pos = util.clamp( + 0, -drag.menuStart + drag.pointerStart - ev.center.x, drag.height + ); + this.domElement.style.transform = 'translate3d(' + + (drag.width - drag.pos) + 'px,0,0)'; + }, + onEnd: (drag, ev) => { + this.setOpen(drag.pos > drag.width / 2); + this.domElement.style.transform = ''; + } + }); + + }); + +sideMenuConfig.when(instance => instance.side === 'top') + .mixin(function() { + + this.domElement.classList.add('top'); + util.extend(this.dragMethods, { + getMenuStart: (drag, ev) => { + return this.isOpen ? drag.height : 0; + }, + onDrag: (drag, ev) => { + drag.pos = util.clamp( + 0, drag.menuStart + ev.center.y - drag.pointerStart, drag.height + ); + this.domElement.style.transform = 'translate3d(0, ' + + (-drag.height + drag.pos) + 'px, 0)'; + }, + onEnd: (drag, ev) => { + this.setOpen(drag.pos > drag.height / 2); + this.domElement.style.transform = ''; + }, + getEventPos: (ev) => { + return ev.center.y; + } + }); + }); diff --git a/src/components/sidemenu/behaviors/direction/directions.scss b/src/components/sidemenu/behaviors/direction/directions.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/components/sidemenu/sidemenu.js b/src/components/sidemenu/sidemenu.js index b6452d8ceb..725ed977c5 100644 --- a/src/components/sidemenu/sidemenu.js +++ b/src/components/sidemenu/sidemenu.js @@ -1,53 +1,99 @@ import {Component, Template, Inject, Parent, NgElement} from 'angular2/angular2'; import {Ion} from '../ion'; -// import {EdgeDragGesture} from '../../core/gestures/edge-drag-gesture'; +import {IonConfig} from '../../config'; +import {DragGesture} from '../../core/gestures/drag-gesture'; +import * as util from '../../util'; + +export var sideMenuConfig = new IonConfig(); + +sideMenuConfig.defaults({ + side: 'left', + dragThreshold: '50' +}); @Component({ - selector: 'ion-side-menu', - bind: { - side: 'side' - } + selector: 'ion-side-menu' + // bind: { + // side: 'side', + // dragThreshold: 'dragThreshold' + // }, }) @Template({ inline: `` }) export class SideMenu extends Ion { constructor( - @Parent() sideMenuParent: SideMenuParent, + @Parent() sideMenuParent: SideMenuParent, @NgElement() element: NgElement ) { - this.el = element; - // this.gesture = new EdgeDragGesture(sideMenuParent.el.domElement, this); + debugger; + this.domElement = element.domElement; + this._drag = {}; - super(); + + this.gesture = new DragGesture(sideMenuParent.domElement, { + onDrag: this.onDrag.bind(this), + onDragStart: this.onDragStart.bind(this), + onDragEnd: this.onDragEnd.bind(this) + }); + this.dragMethods = { + getMenuStart() { return 0; }, + getEventPos(ev) { return ev.center.x; } + }; + this.gesture.listen(); + + this.domElement.addEventListener('transitionend', ev => { + this.setChanging(false); + }) + + sideMenuConfig(this); } onDragStart(ev) { - this._drag = { - width: this.el.domElement.offsetWidth - }; - this.el.domElement.classList.add('no-animate'); + if (!this.dragMethods.canStart(ev)) { + return false; + } + + this.setChanging(true); + this.domElement.classList.add('dragging'); + requestAnimationFrame(() => { + this._drag = { + containerWidth: window.innerWidth, + containerHeight: window.innerHeight, + width: this.domElement.offsetWidth, + height: this.domElement.offsetHeight, + pointerStart: this.dragMethods.getEventPos(ev) + }; + this._drag.menuStart = this.dragMethods.getMenuStart(this._drag, ev); + this._drag.started = true; + }); } onDrag(ev) { - var pos = this._drag.pos = Math.max(0, Math.min(ev.center.x, this._drag.width)); - this.el.domElement.style.transform = 'translate3d(0,' + pos + 'px,0)'; + console.log('ondrag'); + if (!this._drag) return; + this.dragMethods.onDrag(this._drag, ev); } onDragEnd(ev) { + if (!this._drag) return; var { pos, width } = this._drag; - this.el.domElement.style.transform = ''; - if (pos < width / 2) { - this.close(); - } else if (pos > width / 2) { - this.open(); - } - this.el.domElement.style.transform = ''; - this.el.domElement.classList.remove('no-animate'); + + this.domElement.classList.remove('dragging'); + this.dragMethods.onEnd(this._drag, ev); this._drag = null; } - open() { - this.el.domElement.classList.add('open'); + setChanging(isChanging) { + if (isChanging !== this.isChanging) { + this.isChanging = isChanging; + this.domElement.classList[isChanging ? 'add' : 'remove']('changing'); + } } - close() { - this.el.domElement.classList.remove('open'); + setOpen(isOpen) { + if (isOpen !== this.isOpen) { + this.isOpen = isOpen; + this.setChanging(true); + requestAnimationFrame(() => { + this.domElement.classList[isOpen ? 'add' : 'remove']('open'); + }) + } } } @@ -57,9 +103,9 @@ export class SideMenu extends Ion { @Template({ inline: '' }) -export class SideMenuParent extends Ion { +export class SideMenuParent { constructor(@NgElement() element: NgElement) { - this.el = element; + this.domElement = element.domElement; super(); } } diff --git a/src/components/sidemenu/sidemenu.scss b/src/components/sidemenu/sidemenu.scss index f1caecff70..353ba21d12 100644 --- a/src/components/sidemenu/sidemenu.scss +++ b/src/components/sidemenu/sidemenu.scss @@ -1,20 +1,74 @@ +$side-menu-width: 304px; +$side-menu-height: 304px; + ion-side-menu { + display: block; position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 304px; background: rgba(255,0,0,0.5); border-right: 1px solid black; transition: transform 0.3s; - - transform: translate3d(0, -304px, 0); - &.open { - transform: translate3d(0,0,0); + &:not(.open):not(.changing) { + display: none; } - &.no-animate { + &.dragging { transition-duration: 0s; } + + &.left { + width: $side-menu-width; + left: 0; + top: 0; + bottom: 0; + + transform: translate3d(-$side-menu-width, 0, 0); + &.open { + transform: translate3d(0,0,0); + } + } + &.right { + width: $side-menu-width; + right: 0; + top: 0; + bottom: 0; + + transform: translate3d($side-menu-width,0,0); + &.open { + transform: translate3d(0,0,0); + } + } + &.top { + height: $side-menu-width; + top: 0; + left: 0; + right: 0; + + transform: translate3d(0,-$side-menu-width,0); + &.open { + transform: translate3d(0,0,0); + } + } + &.bottom { + height: $side-menu-width; + bottom: 0; + left: 0; + right: 0; + + transform: translate3d(0,$side-menu-width,0); + &.open { + transform: translate3d(0,0,0); + } + } + +} + +ion-side-menu-parent { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: block; + overflow:hidden; } diff --git a/src/config.js b/src/config.js index 067ab246ff..be45bfd33a 100644 --- a/src/config.js +++ b/src/config.js @@ -1,29 +1,42 @@ import * as Platform from './platform'; +import * as util from './util'; -var id = 0; -export function IonConfigService() { +export function IonConfig() { - function Config() { - this._platforms = {}; - for (var key in Config._platforms) { - this._platforms[key] = Config._platforms[key]._clone(); + // TODO automatically add platform class + // TODO do bindings/defaults have to be written twice? + // TODO maybe add config to IonicComponent annotation + function Config(instance) { + util.defaults(instance, Config._defaults || {}); + var conditions = Config._conditions; + for (var i = 0, ii = conditions.length; i < ii; i++) { + if (conditions[i]._callback(instance)) { + for (var j = 0, jj = conditions[i]._mixins.length; j < jj; j++) { + conditions[i]._mixins[j].call(instance); + } + } } - this.id = id++; } - Config._platforms = {}; - Config.platform = platformFn.bind(Config); - Config.prototype.platform = platformFn; - function platformFn(name) { - return this._platforms[name] || (this._platforms[name] = new SubConfig(name)); - } + Config._conditions = []; + Config.defaults = function(defaults) { + Config._defaults = defaults; + }; + Config.when = function when(callback) { + var condition = new ConfigCondition(callback); + Config._conditions.push(condition); + return condition; + }; + Config.platform = function platform(name) { + return Config.when(() => Platform.getPlatform() === name); + }; return Config; } -class SubConfig { - constructor(name, mixins = [], template = '') { - this._name = name; +class ConfigCondition { + constructor(callback, mixins = [], template = '') { + this._callback = callback; this._mixins = mixins; this._template = template; } @@ -35,7 +48,4 @@ class SubConfig { this._template = url; return this; } - _clone() { - return new SubConfig(this._name, this._mixins.slice(), this._template); - } } diff --git a/src/core/gestures/drag-gesture.js b/src/core/gestures/drag-gesture.js index bec31868a8..6283da8a3a 100644 --- a/src/core/gestures/drag-gesture.js +++ b/src/core/gestures/drag-gesture.js @@ -1,31 +1,36 @@ import {Gesture} from './gesture'; -var noop = function() {}; +import * as util from '../../util'; -class DragGesture extends Gesture { - // constructor(element, opts = {}) { - // super(element, opts); - // this.onDrag = opts.onDrag; - // this.onDragStart = opts.onDragStart; - // this.onDragEnd = opts.onDragEnd; - // } - // listen() { - // super.listen(); - // this.hammertime.on('dragstart', this._onDragStart.bind(this)); - // this.hammertime.on('drag', this._onDrag.bind(this)); - // this.hammertime.on('dragend', this._onDragEnd.bind(this)); - // } - // unlisten() { - // super.unlisten(); - // this.hammertime.destroy(); - // } - - // _onDragStart(ev) { - // (this.onDragStart || noop)(ev); - // } - // _onDrag(ev) { - // (this.onDrag || noop)(ev); - // } - // _onDragEnd(ev) { - // (this.onDragEnd || noop)(ev); - // } +export class DragGesture extends Gesture { + constructor(element, opts = {}) { + util.extend(this, { + onDrag: opts.onDrag, + onDragEnd: opts.onDragEnd, + onDragStart: opts.onDragStart + }); + super(element, opts); + } + listen() { + super.listen(); + console.log('listening'); + this.hammertime.on('panstart', ev => { + console.log('panstart'); + if (this.onDragStart && this.onDragStart(ev) !== false) { + this.dragging = true; + } + }); + this.hammertime.on('panmove', ev => { + console.log('panmove'); + if (!this.dragging) return; + if (this.onDrag && this.onDrag(ev) === false) { + this.dragging = false; + } + }); + this.hammertime.on('panend', ev => { + console.log('panend'); + if (!this.dragging) return; + this.onDragEnd && this.onDragEnd(ev); + this.dragging = false; + }); + } } diff --git a/src/core/gestures/edge-drag-gesture.js b/src/core/gestures/edge-drag-gesture.js deleted file mode 100644 index 6d3efbf055..0000000000 --- a/src/core/gestures/edge-drag-gesture.js +++ /dev/null @@ -1,17 +0,0 @@ -import {DragGesture} from './drag-gesture'; - -class EdgeDragGesture extends DragGesture { - // constructor(element, opts = { edge = 'left', buffer = 25 } = {}) { - // super(element, opts); - // } - - // _onDragStart(ev) { - // var { buffer, edge } = this._options; - // var { gesture } = ev; - // if (edge === 'left' && gesture.center.x > buffer) return; - // if (edge === 'top' && gesture.center.y > buffer) return; - // if (edge === 'right' && gesture.center.y < window.innerWidth - buffer) return; - // if (edge === 'bottom' && gesture.center.y < window.innerHeight - buffer) return; - // super._onDragStart(ev); - // } -} diff --git a/src/core/gestures/gesture.js b/src/core/gestures/gesture.js index 6d26cfbcae..2aa6e6e4cc 100644 --- a/src/core/gestures/gesture.js +++ b/src/core/gestures/gesture.js @@ -1,37 +1,22 @@ +import * as util from '../../util'; export class Gesture { - // constructor(element, opts = {}) { - // this.element = element; - // this._options = opts; - // } - // options(opts = {}) { - // extend(this._options, opts); - // } + constructor(element, opts = {}) { + this.element = element; + this._options = opts; + } + options(opts = {}) { + util.extend(this._options, opts); + } - // listen() { - // this.hammertime = Hammer(element, this._options); - // } - // unlisten() { - // this.hammertime.destroy(); - // this.hammertime = null; - // } - // destroy() { - // this.hammertime.destroy(); - // } -} - -// TODO make a utils.js -function extend() { - for (var i = 0, ii = arguments.length; i < ii; i++) { - var obj = arguments[i]; - if (obj) { - var keys = Object.keys(obj); - for (var j = 0, jj = keys.length; j < jj; j++) { - var key = keys[j]; - if (!ILLEGAL_ASSIGN_FIELDS[key]) { - this[key] = obj[key]; - } - } - } + listen() { + this.hammertime = Hammer(this.element, this._options); + } + unlisten() { + this.hammertime.destroy(); + this.hammertime = null; + } + destroy() { + this.hammertime.destroy(); } } diff --git a/src/core/gestures/slide-gesture.js b/src/core/gestures/slide-gesture.js new file mode 100644 index 0000000000..e3e17427e7 --- /dev/null +++ b/src/core/gestures/slide-gesture.js @@ -0,0 +1,38 @@ +import {DragGesture} from './drag-gesture'; +import * as util from '../../util'; + +export class SlideGesture extends DragGesture { + + // These getters are overridden by the implementation + getSlideRange() { + return [0, this.element.offsetWidth]; + } + getEventPos(ev, slideRange) { + return -slideRange[1] + n; + } + getElementPos(ev, slideRange) { + return slideRange[0]; + } + + onDragStart(ev) { + var { direction } = this._options.direction; + var slideRange = this.getSlideRange(ev); + var dragStartPos = this.getEventPos(ev); + var elementStartpos = this.getElementPos(ev, distance); + this._state = { distance, dragStartPos, elementStartPos }; + + return this.onSlideStart && this.onSlideStart(this._state, ev); + } + onDrag(ev) { + var { distance, dragStartPos, elementStartPos } = this._state; + var pos = elementStartPos + this.getEventPos(ev) - dragStartPos; + this._state.position = util.clamp(slideRange[0], n, slideRange[1]); + + return this.onSlide && this.onSlide(this._state, ev); + } + onDragEnd() { + var ret = this.onSlideEnd && this.onSlideEnd(this._state, ev); + this._state = null; + return ret; + } +} diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000000..82ff937dea --- /dev/null +++ b/src/util.js @@ -0,0 +1,26 @@ +export function noop() {} + +export function extend(dest) { + for (var i = 1, ii = arguments.length; i < ii; i++) { + var source = arguments[i] || {}; + for (var key in source) { + if (source.hasOwnProperty(key)) { + dest[key] = source[key]; + } + } + } + return dest; +} + +export function clamp(min, n, max) { + return Math.max(min, Math.min(n, max)); +} + +export function defaults(obj, src) { + for (var key in src) { + if (src.hasOwnProperty(key) && !obj.hasOwnProperty(key)) { + obj[key] = src[key]; + } + } + return obj; +}