UNPKG

dojox

Version:

Dojo eXtensions, a rollup of many useful sub-projects and varying states of maturity – from very stable and robust, to alpha and experimental. See individual projects contain README files for details.

568 lines (513 loc) 17.2 kB
define([ "dojo/_base/kernel", "dojo/_base/array", "dojo/_base/declare", "dojo/_base/window", "dojo/dom-class", "dojo/dom-construct", "dojo/has", "dojo/has!dojo-bidi?dojox/mobile/bidi/SpinWheelSlot", "dojo/touch", "dojo/on", "dijit/_Contained", "dijit/_WidgetBase", "./scrollable", "./common" ], function(dojo, array, declare, win, domClass, domConstruct, has, BidiSpinWheelSlot, touch, on, Contained, WidgetBase, Scrollable){ // module: // dojox/mobile/SpinWheelSlot var SpinWheelSlot = declare(has("dojo-bidi") ? "dojox.mobile.NonBidiSpinWheelSlot" : "dojox.mobile.SpinWheelSlot", [WidgetBase, Contained, Scrollable], { // summary: // A slot of a SpinWheel. // description: // SpinWheelSlot is a slot that is placed in the SpinWheel widget. // items: Array // An array of array of key-label pairs // (e.g. [[0, "Jan"], [1, "Feb"], ...]). If key values for each label // are not necessary, labels can be used instead. items: [], // labels: Array // An array of labels to be displayed on the slot // (e.g. ["Jan", "Feb", ...]). This is a simplified version of the // items property. labels: [], // labelFrom: Number // The start value of display values of the slot. This parameter is // especially useful when the slot has serial values. labelFrom: 0, // labelTo: Number // The end value of display values of the slot. labelTo: 0, // zeroPad: Number // Length of zero padding numbers. // Ex. zeroPad=2 -> "00", "01", ... // Ex. zeroPad=3 -> "000", "001", ... zeroPad: 0, // value: String // The initial value of the slot. value: "", // step: Number // The steps between labelFrom and labelTo. step: 1, // pageStep: Number // The number of items in a page when using pageup/pagedown keys to navigate with the keyboard. pageSteps: 1, /* internal properties */ baseClass: "mblSpinWheelSlot", // maxSpeed: [private] Number // Maximum speed. maxSpeed: 500, // minItems: [private] int // Minimum number of items. minItems: 15, // centerPos: [private] Number // Inherited from parent. centerPos: 0, // scrollbar: [private] Boolean // False: no scrollbars must be shown. scrollBar: false, // constraint: [private] Boolean // False: no scroll constraint. constraint: false, // propagatable: [private] Boolean // False: stop touchstart event propagation. propagatable: false, // stop touchstart event propagation to make spin wheel work inside scrollable // androidWorkaroud: [private] Boolean // False. androidWorkaroud: false, // disable workaround in SpinWheel TODO:remove this line later buildRendering: function(){ this.inherited(arguments); this.initLabels(); var i, j; if(this.labels.length > 0){ this.items = []; for(i = 0; i < this.labels.length; i++){ this.items.push([i, this.labels[i]]); } } this.containerNode = domConstruct.create("div", {className:"mblSpinWheelSlotContainer"}); this.containerNode.style.height = (win.global.innerHeight||win.doc.documentElement.clientHeight) * 2 + "px"; // must bigger than the screen this.panelNodes = []; for(var k = 0; k < 3; k++){ this.panelNodes[k] = domConstruct.create("div", {className:"mblSpinWheelSlotPanel"}); this.panelNodes[k].setAttribute("aria-hidden", "true"); var len = this.items.length; if(len > 0){ // if the slot is not empty var n = Math.ceil(this.minItems / len); for(j = 0; j < n; j++){ for(i = 0; i < len; i++){ domConstruct.create("div", { className: "mblSpinWheelSlotLabel", name: this.items[i][0], "data-mobile-val": this.items[i][1], innerHTML: this._cv ? this._cv(this.items[i][1]) : this.items[i][1] }, this.panelNodes[k]); } } } this.containerNode.appendChild(this.panelNodes[k]); } this.domNode.appendChild(this.containerNode); this.touchNode = domConstruct.create("div", {className:"mblSpinWheelSlotTouch"}, this.domNode); this.setSelectable(this.domNode, false); this.touchNode.setAttribute("tabindex", 0); this.touchNode.setAttribute("role", "slider"); if(this.value === "" && this.items.length > 0){ this.value = this.items[0][1]; } this._initialValue = this.value; if(has("windows-theme")){ var self = this, containerNode = this.containerNode, threshold = 5; this.own(on(self.touchNode, touch.press, function(e){ var posY = e.pageY, slots = self.getParent().getChildren(); for(var i = 0, ln = slots.length; i < ln; i++){ var container = slots[i].containerNode; if(containerNode !== container){ domClass.remove(container, "mblSelectedSlot"); container.selected = false; }else{ domClass.add(containerNode, "mblSelectedSlot"); } } var moveHandler = on(self.touchNode, touch.move, function(e){ if(Math.abs(e.pageY - posY) < threshold){ return; } moveHandler.remove(); releaseHandler.remove(); containerNode.selected = true; var item = self.getCenterItem(); if(item){ domClass.remove(item, "mblSelectedSlotItem"); } }); var releaseHandler = on(self.touchNode, touch.release, function(){ releaseHandler.remove(); moveHandler.remove(); containerNode.selected ? domClass.remove(containerNode, "mblSelectedSlot") : domClass.add(containerNode, "mblSelectedSlot"); containerNode.selected = !containerNode.selected; }); })); this.on("flickAnimationEnd", function(){ var item = self.getCenterItem(); if(self.previousCenterItem) { domClass.remove(self.previousCenterItem, "mblSelectedSlotItem"); } domClass.add(item, "mblSelectedSlotItem"); self.previousCenterItem = item; }); } }, startup: function(){ if(this._started){ return; } this.inherited(arguments); this.noResize = true; if(this.items.length > 0){ // if the slot is not empty this.init(); this.centerPos = this.getParent().centerPos; var items = this.panelNodes[1].childNodes; this._itemHeight = items[0].offsetHeight; this.adjust(); this.connect(this.touchNode, "onkeydown", "_onKeyDown"); // for desktop browsers } if(has("windows-theme")){ this.previousCenterItem = this.getCenterItem(); if(this.previousCenterItem){ domClass.add(this.previousCenterItem, "mblSelectedSlotItem"); } } }, initLabels: function(){ // summary: // Initializes the slot labels according to the labelFrom/labelTo properties. // tags: // private if(this.labelFrom !== this.labelTo){ var a = this.labels = [], zeros = this.zeroPad && Array(this.zeroPad).join("0"); for(var i = this.labelFrom; i <= this.labelTo; i += this.step){ a.push(this.zeroPad ? (zeros + i).slice(-this.zeroPad) : i + ""); } } }, onTouchStart: function(e) { this.touchNode.focus(); // give focus to enable key navigation this.inherited(arguments); }, adjust: function(){ // summary: // Adjusts the position of slot panels. var items = this.panelNodes[1].childNodes; var adjustY; for(var i = 0, len = items.length; i < len; i++){ var item = items[i]; if(item.offsetTop <= this.centerPos && this.centerPos < item.offsetTop + item.offsetHeight){ adjustY = this.centerPos - (item.offsetTop + Math.round(item.offsetHeight/2)); break; } } var h = this.panelNodes[0].offsetHeight; this.panelNodes[0].style.top = -h + adjustY + "px"; this.panelNodes[1].style.top = adjustY + "px"; this.panelNodes[2].style.top = h + adjustY + "px"; }, setInitialValue: function(){ // summary: // Sets the initial value using this.value or the first item. this.set("value", this._initialValue); this.touchNode.setAttribute("aria-valuetext", this._initialValue); }, _onKeyDown: function(e){ if(!e || e.type !== "keydown" || e.altKey || e.ctrlKey || e.shiftKey){ return true; } switch(e.keyCode){ case 38: // up arrow key (fallthrough) case 39: // right arrow key this.spin(1); e.stopPropagation(); return false; case 40: // down arrow key (fallthrough) case 37: // left arrow key this.spin(-1); e.stopPropagation(); return false; case 33: // pageup this.spin(this.pageSteps); e.stopPropagation(); return false; case 34: // pagedown this.spin(-1 * this.pageSteps); e.stopPropagation(); return false; } return true; }, _getCenterPanel: function(){ // summary: // Gets a panel that contains the currently selected item. var pos = this.getPos(); for(var i = 0, len = this.panelNodes.length; i < len; i++){ var top = pos.y + this.panelNodes[i].offsetTop; if(top <= this.centerPos && this.centerPos < top + this.panelNodes[i].offsetHeight){ return this.panelNodes[i]; } } return null; }, setColor: function(/*String*/value, /*String?*/color){ // summary: // Sets the color of the specified item as blue. array.forEach(this.panelNodes, function(panel){ array.forEach(panel.childNodes, function(node, i){ domClass.toggle(node, color || "mblSpinWheelSlotLabelBlue", node.innerHTML === value); }, this); }, this); }, disableValues: function(/*Number*/n){ // summary: // Grays out the items with an index higher or equal to the specified number. array.forEach(this.panelNodes, function(panel){ for(var i = 0; i < panel.childNodes.length; i++){ domClass.toggle(panel.childNodes[i], "mblSpinWheelSlotLabelGray", i >= n); } }); }, getCenterItem: function(){ // summary: // Gets the currently selected item. var pos = this.getPos(); var centerPanel = this._getCenterPanel(); if(centerPanel){ var top = pos.y + centerPanel.offsetTop; var items = centerPanel.childNodes; for(var i = 0, len = items.length; i < len; i++){ if(top + items[i].offsetTop <= this.centerPos && this.centerPos < top + items[i].offsetTop + items[i].offsetHeight){ return items[i]; } } } return null; }, _getKeyAttr: function(){ // summary: // Gets the key for the currently selected value. if(!this._started){ if(this.items){ var v = this.value; for(var i = 0; i < this.items.length; i++){ if(this.items[i][1] == this.value){ return this.items[i][0]; } } } return null; } var item = this.getCenterItem(); return (item && item.getAttribute("name")); }, _getValueAttr: function(){ // summary: // Gets the currently selected value. if(!this._started){ return this.value; } if(this.items.length > 0){ // if the slot is not empty var item = this.getCenterItem(); return (item && item.getAttribute("data-mobile-val")); }else{ return this._initialValue; } }, _setValueAttr: function(value){ // summary: // Sets the value to this slot. if(this.items.length > 0){ // no-op for empty slots this._spinToValue(value, true); } }, _spinToValue: function(value, applyValue){ // summary: // Spins the slot to the specified value. // tags: // private var idx0, idx1; var curValue = this.get("value"); if(!curValue){ this._pendingValue = value; return; } if(curValue == value){ return; // no change; avoid notification } this._pendingValue = undefined; // to avoid unnecessary notifications, applyValue is false when // _spinToValue is called by _DatePickerMixin. if(applyValue){ this._set("value", value); } var n = this.items.length; for(var i = 0; i < n; i++){ if(this.items[i][1] === String(curValue)){ idx0 = i; } if(this.items[i][1] === String(value)){ idx1 = i; } if(idx0 !== undefined && idx1 !== undefined){ break; } } var d = idx1 - (idx0 || 0); var m; if(d > 0){ m = (d < n - d) ? -d : n - d; }else{ m = (-d < n + d) ? -d : -(n + d); } this.spin(m); }, onFlickAnimationStart: function(e){ // summary: // Overrides dojox/mobile/scrollable.onFlickAnimationStart(). this._onFlickAnimationStartCalled = true; this.inherited(arguments); }, onFlickAnimationEnd: function(e){ // summary: // Overrides dojox/mobile/scrollable.onFlickAnimationEnd(). this._duringSlideTo = false; this._onFlickAnimationStartCalled = false; this.inherited(arguments); this.touchNode.setAttribute("aria-valuetext", this.get("value")); }, spin: function(/*Number*/steps){ // summary: // Spins the slot as specified by steps. // do nothing before startup and during slide if(!this._started || this._duringSlideTo){ return; } var to = this.getPos(); to.y += steps * this._itemHeight; this.slideTo(to, 1); }, getSpeed: function(){ // summary: // Overrides dojox/mobile/scrollable.getSpeed(). var y = 0, n = this._time.length; var delta = (new Date()).getTime() - this.startTime - this._time[n - 1]; if(n >= 2 && delta < 200){ var dy = this._posY[n - 1] - this._posY[(n - 6) >= 0 ? n - 6 : 0]; var dt = this._time[n - 1] - this._time[(n - 6) >= 0 ? n - 6 : 0]; y = this.calcSpeed(dy, dt); } return {x:0, y:y}; }, calcSpeed: function(/*Number*/d, /*Number*/t){ // summary: // Overrides dojox/mobile/scrollable.calcSpeed(). var speed = this.inherited(arguments); if(!speed){ return 0; } var v = Math.abs(speed); var ret = speed; if(v > this.maxSpeed){ ret = this.maxSpeed*(speed/v); } return ret; }, adjustDestination: function(to, pos, dim){ // summary: // Overrides dojox/mobile/scrollable.adjustDestination(). var h = this._itemHeight; var j = to.y + Math.round(h/2); var r = j >= 0 ? j % h : j % h + h; to.y = j - r; return true; }, resize: function(e){ // Correct internal variables & adjust slot panels if(this.panelNodes && this.panelNodes.length > 0){ var items = this.panelNodes[1].childNodes; // TODO investigate - the position is calculated incorrectly for // windows theme, disable this logic for now. if(items.length > 0 && !has("windows-theme")){ // empty slot? var parent = this.getParent(); if(parent){ // #18012: null in same cases on IE8/9 this._itemHeight = items[0].offsetHeight; this.centerPos = parent.centerPos; if(!this.panelNodes[0].style.top){ // (#17339) to avoid messing up the layout of the panels, call adjust() // only if it didn't manage yet to set the style.top (this happens // typically because the slot was initially hidden). this.adjust(); } } } } if(this._pendingValue){ this.set("value", this._pendingValue); } }, slideTo: function(/*Object*/to, /*Number*/duration, /*String*/easing){ // summary: // Overrides dojox/mobile/scrollable.slideTo(). this._duringSlideTo = true; var pos = this.getPos(); var top = pos.y + this.panelNodes[1].offsetTop; var bottom = top + this.panelNodes[1].offsetHeight; var vh = this.domNode.parentNode.offsetHeight; var t; if(pos.y < to.y){ // going down if(bottom > vh){ // move up the bottom panel t = this.panelNodes[2]; t.style.top = this.panelNodes[0].offsetTop - this.panelNodes[0].offsetHeight + "px"; this.panelNodes[2] = this.panelNodes[1]; this.panelNodes[1] = this.panelNodes[0]; this.panelNodes[0] = t; } }else if(pos.y > to.y){ // going up if(top < 0){ // move down the top panel t = this.panelNodes[0]; t.style.top = this.panelNodes[2].offsetTop + this.panelNodes[2].offsetHeight + "px"; this.panelNodes[0] = this.panelNodes[1]; this.panelNodes[1] = this.panelNodes[2]; this.panelNodes[2] = t; } } if(this.getParent()._duringStartup){ duration = 0; // to reduce flickers at start-up especially on android // No scroll animation at startup. This avoids flickering especially on Android, // and avoids the issue in #17775. }else if(Math.abs(this._speed.y) < 40){ duration = 0.2; } if(duration && duration > 0){ this.inherited(arguments, [to, duration, easing]); // 2nd arg is to avoid excessive optimization by closure compiler if(!this._onFlickAnimationStartCalled){ // if slideTo() didn't call itself (synchronously) onFlickAnimationEnd(): this._duringSlideTo = false; // (otherwise, wait for onFlickAnimationEnd which deletes the flag) } }else{ // #17775: at startup, no scroll animation, because it is not needed ergonomically, // and because the animation would imply an asynchrouns notification of // onFlickAnimationEnd() which would forbid resetting the value right after startup. // this.onFlickAnimationStart(); // not called by scrollTo() this.onFlickAnimationStart(); // not called by scrollTo() this.scrollTo(to, true); this.onFlickAnimationEnd(); // not called by scrollTo() } } }); return has("dojo-bidi") ? declare("dojox.mobile.SpinWheelSlot", [SpinWheelSlot, BidiSpinWheelSlot]) : SpinWheelSlot; });