From 4444e9973e539cbef1a2b7aa13a5e3ac078bb75c Mon Sep 17 00:00:00 2001 From: IjzerenHein Date: Mon, 19 Jan 2015 15:34:35 +0100 Subject: [PATCH] Added new DateWheel widget --- Gruntfile.js | 1 + README.md | 8 +- docs/widgets/DateWheel.md | 94 ++++++++++ src/widgets/DateComponents.js | 287 ++++++++++++++++++++++++++++ src/widgets/DateWheel.js | 340 ++++++++++++++++++++++++++++++++++ 5 files changed, 728 insertions(+), 2 deletions(-) create mode 100644 docs/widgets/DateWheel.md create mode 100644 src/widgets/DateComponents.js create mode 100644 src/widgets/DateWheel.js diff --git a/Gruntfile.js b/Gruntfile.js index 354e6ca..39bf89d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -26,6 +26,7 @@ module.exports = function(grunt) { { src: 'src/LayoutController.js', dest: 'docs/LayoutController.md' }, { src: 'src/ScrollController.js', dest: 'docs/ScrollController.md' }, { src: 'src/FlexScrollView.js', dest: 'docs/FlexScrollView.md' }, + { src: 'src/widgets/DateWheel.js', dest: 'docs/widgets/DateWheel.md' }, { src: 'src/LayoutUtility.js', dest: 'docs/LayoutUtility.md' }, { src: 'src/VirtualViewSequence.js', dest: 'docs/VirtualViewSequence.md' }, // helpers diff --git a/README.md b/README.md index 6b0b8c2..2d7c205 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,11 @@ of renderables using a `GridLayout`, and change that into a `ListLayout`. When u - [Layout literals](#layout-literals) - [Layout helpers](#layout-helpers) -### [FlexScrollView](#flex-scrollview) -- [Tutorial](tutorials/FlexScrollView.md) +### Views / widgets +- [LayoutController](#layoutcontroller) ([API Reference](docs/LayoutController.md)) +- ScrollController ([API Reference](docs/ScrollController.md)) +- [FlexScrollView](#flex-scrollview) ([API Reference](docs/ScrollController.md) | [Tutorial](tutorials/FlexScrollView.md)) +- DateWheel ([API Reference](docs/DateWheel.md)) ### [Layouts](#standard-layouts) - [GridLayout](docs/layouts/GridLayout.md) @@ -267,6 +270,7 @@ custom layouts. Key features: |[LayoutController](docs/LayoutController.md)|Lays out renderables and optionally animates between layout states.| |[ScrollController](docs/ScrollController.md)|Scrollable LayoutController (base class for FlexScrollView).| |[FlexScrollView](docs/FlexScrollView.md)|Flexible scroll-view with pull-to-refresh, margins & spacing and more good stuff.| +|[DateWheel](docs/DateWheel.md)|Date(picker) wheel.| |[LayoutContext](docs/LayoutContext.md)|Context used for writing layout-functions.| |[LayoutUtility](docs/LayoutUtility.md)|Utility class containing helper functions.| |[VirtualViewSequence](docs/VirtualViewSequence.md)|Infinite view-sequence which uses a factory delegate to create renderables.| diff --git a/docs/widgets/DateWheel.md b/docs/widgets/DateWheel.md new file mode 100644 index 0000000..e73c72d --- /dev/null +++ b/docs/widgets/DateWheel.md @@ -0,0 +1,94 @@ + +#DateWheel +Date/time wheel (slot-machine layout) for famo.us. + +This component can be used as a date/time picker, a clock or +any other application which requires a date/time wheel. + +Example: + +```javascript +var DateWheel = require('famous-flex/widgets/DateWheel'); + +var dateWheel = new DateWheel({ + date: new Date(), // initial date + wheelOptions: { + itemSize: 100, // height of an item on the date/wheel + diameter: 300, // diameter of the wheel (undefined = 3 x itemSize) + radialOpacity: 0 // opacity at the top and bottom diameter edge + }, + components: [ + new DateWheel.Component.FullDay(), // full-day component (year + month + day) + new DateWheel.Component.Hour(), // hour component (0..23) + new DateWheel.Component.Minute() // minute compoent (0..59) + ] +}); +this.add(dateWheel); // add to the render-tree + +dateWheel.on('datechange', function(event) { + console.log('new date selected: ' + event.date.toLocaleString()); +}); +``` + +CSS: + +```css +.famous-flex-datewheel .item > div { + position: relative; + top: 50%; + transform: translateY(-50%); + text-align: center; + font-size: 40px; +} +``` + + +##class: DateWheel ⏏ +**Extends**: `View` +**Members** + +* [class: DateWheel ⏏](#exp_module_DateWheel) + * [new DateWheel(options)](#exp_new_module_DateWheel) + * [dateWheel.setOptions(options)](#module_DateWheel#setOptions) + * [dateWheel.setDate(date)](#module_DateWheel#setDate) + * [dateWheel.getDate()](#module_DateWheel#getDate) + + +###new DateWheel(options) +**Params** + +- options `Object` - Configurable options. + - \[perspective\] `Number` - Perspective to use when rendering the wheel. + - \[components\] `Array` - Date/time components that are displayed. + - \[wheelOptions\] `Object` - Layout-options that are passed to the WheelLayout. + - \[scrollSpring\] `Object` - Spring-options that are passed to the underlying ScrollControllers. + - \[container\] `Object` - Container-options that are passed to the underlying ContainerSurface. + +**Extends**: `View` + +###dateWheel.setOptions(options) +Patches the DateWheel instance's options with the passed-in ones. + +**Params** + +- options `Object` - Configurable options (see ScrollController for all inherited options). + - \[perspective\] `Number` - Perspective to use when rendering the wheel. + - \[components\] `Array` - Date/time components that are displayed. + - \[wheelOptions\] `Object` - Layout-options that are passed to the WheelLayout. + - \[scrollSpring\] `Object` - Spring-options that are passed to the underlying ScrollControllers. + +**Returns**: `DateWheel` - this + +###dateWheel.setDate(date) +Set the selected date. + +**Params** + +- date `Date` - Selected date/time. + +**Returns**: `DateWheel` - this + +###dateWheel.getDate() +Get the selected date. + +**Returns**: `Date` - selected date diff --git a/src/widgets/DateComponents.js b/src/widgets/DateComponents.js new file mode 100644 index 0000000..7baece2 --- /dev/null +++ b/src/widgets/DateComponents.js @@ -0,0 +1,287 @@ +/** + * This Source Code is licensed under the MIT license. If a copy of the + * MIT-license was not distributed with this file, You can obtain one at: + * http://opensource.org/licenses/mit-license.html. + * + * @author: Hein Rutjes (IjzerenHein) + * @license MIT + * @copyright Gloey Apps, 2015 + */ + +/*global define, console*/ +/*eslint no-use-before-define:0, no-console:0 */ + +/** + * Date/time components helper class (minute, seconds, full-day, month, etc...). + * + * @module + */ +define(function(require, exports, module) { + + // import dependencies + var Surface = require('famous/core/Surface'); + + /** + * Helper functions for formatting values with X decimal places. + */ + function decimal1(date) { + return ('' + date[this.get]()); + } + function decimal2(date) { + return ('0' + date[this.get]()).slice(-2); + } + function decimal3(date) { + return ('00' + date[this.get]()).slice(-3); + } + function decimal4(date) { + return ('000' + date[this.get]()).slice(-4); + } + + /** + * Base component class + */ + function Base(options) { + if (options) { + for (var key in options) { + this[key] = options[key]; + } + } + } + Base.prototype.step = 1; + Base.prototype.classes = ['item']; + Base.prototype.getComponent = function(date) { + return date[this.get](); + }; + Base.prototype.setComponent = function(date, value) { + return date[this.set](value); + }; + Base.prototype.format = function(date) { + return 'overide to implement'; + }; + Base.prototype.createNext = function(renderable) { + return this.create(this.getNext(renderable.date)); + }; + Base.prototype.getNext = function(date) { + date = new Date(date.getTime()); + var newVal = this.getComponent(date) + this.step; + if ((this.upperBound !== undefined) && (newVal >= this.upperBound)) { + if (!this.loop) { + return undefined; + } + newVal = Math.max(newVal % this.upperBound, this.lowerBound || 0); + } + this.setComponent(date, newVal); + return date; + }; + Base.prototype.createPrevious = function(renderable) { + return this.create(this.getPrevious(renderable.date)); + }; + Base.prototype.getPrevious = function(date) { + date = new Date(date.getTime()); + var newVal = this.getComponent(date) - this.step; + if ((this.lowerBound !== undefined) && (newVal < newVal)) { + if (!this.loop) { + return undefined; + } + newVal = newVal % this.upperBound; + } + this.setComponent(date, newVal); + return date; + }; + Base.prototype.create = function(date) { + date = date || new Date(); + var surface = new Surface({ + classes: this.classes, + content: '
' + this.format(date) + '
' + }); + surface.date = date; + return surface; + }; + Base.prototype.destroy = function(renderable) { + // perform any cleanup here if neccesary + }; + + /** + * Year component + */ + function Year() { + Base.apply(this, arguments); + } + Year.prototype = Object.create(Base.prototype); + Year.prototype.constructor = Year; + Year.prototype.classes = ['item', 'year']; + Year.prototype.format = decimal4; + Year.prototype.sizeRatio = 1; + Year.prototype.step = 1; + Year.prototype.loop = false; + Year.prototype.set = 'setFullYear'; + Year.prototype.get = 'getFullYear'; + + /** + * Month component + */ + function Month() { + Base.apply(this, arguments); + } + Month.prototype = Object.create(Base.prototype); + Month.prototype.constructor = Month; + Month.prototype.classes = ['item', 'month']; + Month.prototype.sizeRatio = 2; + Month.prototype.lowerBound = 0; + Month.prototype.upperBound = 12; + Month.prototype.step = 1; + Month.prototype.loop = true; + Month.prototype.set = 'setMonth'; + Month.prototype.get = 'getMonth'; + Month.prototype.strings = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ]; + Month.prototype.format = function(date) { + return this.strings[date.getMonth()]; + }; + + /** + * Full-day component + */ + function FullDay() { + Base.apply(this, arguments); + } + FullDay.prototype = Object.create(Base.prototype); + FullDay.prototype.constructor = FullDay; + FullDay.prototype.classes = ['item', 'fullday']; + FullDay.prototype.sizeRatio = 2; + FullDay.prototype.step = 1; + FullDay.prototype.set = 'setDate'; + FullDay.prototype.get = 'getDate'; + FullDay.prototype.format = function(date) { + return date.toLocaleDateString(); + }; + + /** + * Week-day component + */ + function WeekDay() { + Base.apply(this, arguments); + } + WeekDay.prototype = Object.create(Base.prototype); + WeekDay.prototype.constructor = WeekDay; + WeekDay.prototype.classes = ['item', 'weekday']; + WeekDay.prototype.sizeRatio = 2; + WeekDay.prototype.lowerBound = 0; + WeekDay.prototype.upperBound = 7; + WeekDay.prototype.step = 1; + WeekDay.prototype.loop = true; + WeekDay.prototype.set = 'setDate'; + WeekDay.prototype.get = 'getDate'; + WeekDay.prototype.strings = [ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', + 'Thursday', 'Friday', 'Saturday' + ]; + WeekDay.prototype.format = function(date) { + return this.strings[date.getDay()]; + }; + + /** + * Day component + */ + function Day() { + Base.apply(this, arguments); + } + Day.prototype = Object.create(Base.prototype); + Day.prototype.constructor = Day; + Day.prototype.classes = ['item', 'day']; + Day.prototype.format = decimal1; + Day.prototype.sizeRatio = 1; + Day.prototype.lowerBound = 1; + Day.prototype.upperBound = 32; + Day.prototype.step = 1; + Day.prototype.loop = true; + Day.prototype.set = 'setDate'; + Day.prototype.get = 'getDate'; + + /** + * Hour component + */ + function Hour() { + Base.apply(this, arguments); + } + Hour.prototype = Object.create(Base.prototype); + Hour.prototype.constructor = Hour; + Hour.prototype.classes = ['item', 'hour']; + Hour.prototype.format = decimal2; + Hour.prototype.sizeRatio = 1; + Hour.prototype.lowerBound = 0; + Hour.prototype.upperBound = 24; + Hour.prototype.step = 1; + Hour.prototype.loop = true; + Hour.prototype.set = 'setHours'; + Hour.prototype.get = 'getHours'; + + /** + * Minute component + */ + function Minute() { + Base.apply(this, arguments); + } + Minute.prototype = Object.create(Base.prototype); + Minute.prototype.constructor = Minute; + Minute.prototype.classes = ['item', 'minute']; + Minute.prototype.format = decimal2; + Minute.prototype.sizeRatio = 1; + Minute.prototype.lowerBound = 0; + Minute.prototype.upperBound = 60; + Minute.prototype.step = 1; + Minute.prototype.loop = true; + Minute.prototype.set = 'setMinutes'; + Minute.prototype.get = 'getMinutes'; + + /** + * Second component + */ + function Second() { + Base.apply(this, arguments); + } + Second.prototype = Object.create(Base.prototype); + Second.prototype.constructor = Second; + Second.prototype.classes = ['item', 'second']; + Second.prototype.format = decimal2; + Second.prototype.sizeRatio = 1; + Second.prototype.lowerBound = 0; + Second.prototype.upperBound = 60; + Second.prototype.step = 1; + Second.prototype.loop = true; + Second.prototype.set = 'setSeconds'; + Second.prototype.get = 'getSeconds'; + + /** + * Millisecond component + */ + function Millisecond() { + Base.apply(this, arguments); + } + Millisecond.prototype = Object.create(Base.prototype); + Millisecond.prototype.constructor = Millisecond; + Millisecond.prototype.classes = ['item', 'millisecond']; + Millisecond.prototype.format = decimal3; + Millisecond.prototype.sizeRatio = 1; + Millisecond.prototype.lowerBound = 0; + Millisecond.prototype.upperBound = 1000; + Millisecond.prototype.step = 1; + Millisecond.prototype.loop = true; + Millisecond.prototype.set = 'setMilliseconds'; + Millisecond.prototype.get = 'getMilliseconds'; + + module.exports = { + Base: Base, + Year: Year, + Month: Month, + FullDay: FullDay, + WeekDay: WeekDay, + Day: Day, + Hour: Hour, + Minute: Minute, + Second: Second, + Millisecond: Millisecond + }; +}); diff --git a/src/widgets/DateWheel.js b/src/widgets/DateWheel.js new file mode 100644 index 0000000..c89b275 --- /dev/null +++ b/src/widgets/DateWheel.js @@ -0,0 +1,340 @@ +/** + * This Source Code is licensed under the MIT license. If a copy of the + * MIT-license was not distributed with this file, You can obtain one at: + * http://opensource.org/licenses/mit-license.html. + * + * @author: Hein Rutjes (IjzerenHein) + * @license MIT + * @copyright Gloey Apps, 2015 + */ + +/*global define, console*/ +/*eslint no-use-before-define:0, no-console:0 */ + +/** + * Date/time wheel (slot-machine layout) for famo.us. + * + * This component can be used as a date/time picker, a clock or + * any other application which requires a date/time wheel. + * + * Example: + * + * ```javascript + * var DateWheel = require('famous-flex/widgets/DateWheel'); + * + * var dateWheel = new DateWheel({ + * date: new Date(), // initial date + * wheelOptions: { + * itemSize: 100, // height of an item on the date/wheel + * diameter: 300, // diameter of the wheel (undefined = 3 x itemSize) + * radialOpacity: 0 // opacity at the top and bottom diameter edge + * }, + * components: [ + * new DateWheel.Component.FullDay(), // full-day component (year + month + day) + * new DateWheel.Component.Hour(), // hour component (0..23) + * new DateWheel.Component.Minute() // minute compoent (0..59) + * ] + * }); + * this.add(dateWheel); // add to the render-tree + * + * dateWheel.on('datechange', function(event) { + * console.log('new date selected: ' + event.date.toLocaleString()); + * }); + * ``` + * + * CSS: + * + * ```css + * .famous-flex-datewheel .item > div { + * position: relative; + * top: 50%; + * transform: translateY(-50%); + * text-align: center; + * font-size: 40px; + * } + * ``` + * @module + */ +define(function(require, exports, module) { + + // import dependencies + var View = require('famous/core/View'); + var Utility = require('famous/utilities/Utility'); + var ContainerSurface = require('famous/surfaces/ContainerSurface'); + var LayoutController = require('../LayoutController'); + var ScrollController = require('../ScrollController'); + var WheelLayout = require('../layouts/WheelLayout'); + var ProportionalLayout = require('../layouts/ProportionalLayout'); + var VirtualViewSequence = require('../VirtualViewSequence'); + var DateComponents = require('./DateComponents'); + + /** + * @class + * @extends View + * @param {Object} options Configurable options. + * @param {Number} [options.perspective] Perspective to use when rendering the wheel. + * @param {Array} [options.components] Date/time components that are displayed. + * @param {Object} [options.wheelOptions] Layout-options that are passed to the WheelLayout. + * @param {Object} [options.scrollSpring] Spring-options that are passed to the underlying ScrollControllers. + * @param {Object} [options.container] Container-options that are passed to the underlying ContainerSurface. + * @alias module:DateWheel + */ + function DateWheel(options) { + View.apply(this, arguments); + + this._date = new Date((options && options.date) ? options.date.getTime() : undefined); + _createLayout.call(this); + _createComponents.call(this); + + this.setOptions(this.options); + } + DateWheel.prototype = Object.create(View.prototype); + DateWheel.prototype.constructor = DateWheel; + DateWheel.Component = DateComponents; + + DateWheel.DEFAULT_OPTIONS = { + perspective: 1000, + wheelOptions: { + itemSize: 100, + diameter: 500 + }, + scrollSpring: { + dampingRatio: 1.0, + period: 800 + }, + container: { + classes: ['famous-flex-datewheel'] + }, + components: [ + new DateWheel.Component.FullDay(), + new DateWheel.Component.Hour(), + new DateWheel.Component.Minute() + ] + }; + + /** + * Patches the DateWheel instance's options with the passed-in ones. + * + * @param {Object} options Configurable options (see ScrollController for all inherited options). + * @param {Number} [options.perspective] Perspective to use when rendering the wheel. + * @param {Array} [options.components] Date/time components that are displayed. + * @param {Object} [options.wheelOptions] Layout-options that are passed to the WheelLayout. + * @param {Object} [options.scrollSpring] Spring-options that are passed to the underlying ScrollControllers. + * @return {DateWheel} this + */ + DateWheel.prototype.setOptions = function(options) { + View.prototype.setOptions.call(this, options); + if (!this.layout) { + return this; + } + if (options.perspective !== undefined) { + this.container.context.setPerspective(options.perspective); + } + if (options.components) { + _createComponents.call(this); + } + if (options.wheelOptions !== undefined) { + for (var i = 0; i < this.components.length; i++) { + this.components[i].scrollView.setLayoutOptions(options.wheelOptions); + } + } + if (options.scrollSpring !== undefined) { + for (var j = 0; j < this.components.length; j++) { + this.components[j].scrollView.setOptions({ + scrollSpring: options.scrollSpring + }); + } + } + return this; + }; + + /** + * Set the selected date. + * + * @param {Date} date Selected date/time. + * @return {DateWheel} this + */ + DateWheel.prototype.setDate = function(date) { + this._date.setTime(date.getTime()); + _setDateToScrollWheels.call(this, this._date); + return this; + }; + + /** + * Get the selected date. + * + * @return {Date} selected date + */ + DateWheel.prototype.getDate = function() { + return this._date; + }; + + /** + * Selects the given date into the scrollwheels (causes scrolling) + */ + function _setDateToScrollWheels(date) { + for (var i = 0; i < this.scrollWheels.length; i++) { + var scrollWheel = this.scrollWheels[i]; + var component = scrollWheel.component; + var item = scrollWheel.scrollView.getFirstVisibleItem(); + if (item && item.viewSequence) { + var viewSequence = item.viewSequence; + var renderNode = item.viewSequence.get(); + var currentValue = component.getComponent(renderNode.date); + var destValue = component.getComponent(date); + + // Determine the direction to scroll to + var steps = 0; + if (currentValue !== destValue) { + steps = destValue - currentValue; + // when loop is enables, check whether there is a faster path + if (component.loop) { + var revSteps = (steps < 0) ? (steps + component.upperBound) : (steps - component.upperBound); + if (Math.abs(revSteps) < Math.abs(steps)) { + steps = revSteps; + } + } + } + + // Scroll to the item + if (!steps) { + scrollWheel.scrollView.goToRenderNode(renderNode); + } + else { + while (currentValue !== destValue) { + viewSequence = (steps > 0) ? viewSequence.getNext() : viewSequence.getPrevious(); + renderNode = viewSequence ? viewSequence.get() : undefined; + if (!renderNode) { + break; + } + currentValue = component.getComponent(renderNode.date); + if (steps > 0) { + scrollWheel.scrollView.goToNextPage(); + } + else { + scrollWheel.scrollView.goToPreviousPage(); + } + } + } + } + } + } + + /** + * Gets the selected date from all the scroll-wheels. + */ + function _getDateFromScrollWheels() { + var date = new Date(this._date); + for (var i = 0; i < this.scrollWheels.length; i++) { + var scrollWheel = this.scrollWheels[i]; + var component = scrollWheel.component; + var item = scrollWheel.scrollView.getFirstVisibleItem(); + if (item && item.renderNode) { + component.setComponent(date, component.getComponent(item.renderNode.date)); + } + } + return date; + } + + /** + * Sets up the overal layout + */ + function _createLayout() { + this.container = new ContainerSurface( + this.options.container + ); + this.layout = new LayoutController({ + layout: ProportionalLayout, + layoutOptions: { + ratios: [] + }, + direction: Utility.Direction.X + }); + this.container.add(this.layout); + this.add(this.container); + this.components = []; + } + + /** + * Emit scrollstart event when a wheel starts scrolling + */ + function _scrollWheelScrollStart() { + this._scrollingCount++; + if (this._scrollingCount === 1) { + this._eventOutput.emit('scrollstart', { + target: this + }); + } + } + + /** + * Emit scrollend event whenever all scrolling has come to a halt + */ + function _scrollWheelScrollEnd() { + this._scrollingCount--; + if (this._scrollingCount === 0) { + this._eventOutput.emit('scrollend', { + target: this, + date: this._date + }); + } + } + + /** + * Emit scrollend event whenever all scrolling has come to a halt + */ + function _scrollWheelPageChange() { + this._date = _getDateFromScrollWheels.call(this); + this._eventOutput.emit('datechange', { + target: this, + date: this._date + }); + } + + /** + * Updates the date/time components + */ + function _createComponents() { + this.scrollWheels = []; + this._scrollingCount = 0; + var dataSource = []; + var sizeRatios = []; + + for (var i = 0; i < this.options.components.length; i++) { + var component = this.options.components[i]; + var viewSequence = new VirtualViewSequence({ + factory: component, + value: component.create(this._date) + }); + var scrollView = new ScrollController({ + layout: WheelLayout, + layoutOptions: this.options.wheelOptions, + flow: false, + scrollSpring: this.options.scrollSpring, + direction: Utility.Direction.Y, + dataSource: viewSequence, + mouseMove: true, + autoPipeEvents: true, + paginated: true, + debug: true + }); + scrollView.on('scrollstart', _scrollWheelScrollStart.bind(this)); + scrollView.on('scrollend', _scrollWheelScrollEnd.bind(this)); + scrollView.on('pagechange', _scrollWheelPageChange.bind(this)); + this.scrollWheels.push({ + component: component, + scrollView: scrollView, + viewSequence: viewSequence + }); + dataSource.push(scrollView); + sizeRatios.push(component.sizeRatio); + } + + this.layout.setDataSource(dataSource); + this.layout.setLayoutOptions({ + ratios: sizeRatios + }); + } + + module.exports = DateWheel; +});