/* '(C) Ventis Media, Licensed under the Ventis Limited Reciprocal License - see: license.txt for details' */ "use strict"; /** @module UI */ requirejs('controls/control'); /** UI Slider element @class Slider @constructor @extends Control */ inheritClass('Slider', Control, { initialize: function (parentel, params) { Slider.$super.initialize.apply(this, arguments); this.initialized = false; this._orientation = 'horizontal', this._invert = false; this._fromZero = false; this._min = 0; this._max = 0; this._value = 0; this._step = 0; this._wheelStep = undefined; this._tickPlacement = 'none'; this._tickInterval = 1; this._seekBarSize = 0; this._seekBarThumbSizes = { offsetWidth: 0, offsetHeight: 0 }; this._seekBarSizes = { offsetWidth: 0, offsetHeight: 0 }; var _mouseOffset = 0; var _seeking = false; // ** to prevent having too many concurrent wheel events this._lastEventTime = 0; this._minTimeBetweenTicks = 30; this._seekBarOuter = document.createElement('div'); this.container.appendChild(this._seekBarOuter); this._ticksOuter = document.createElement('div'); this._seekBarOuter.appendChild(this._ticksOuter); this._seekBar = document.createElement('div'); this._seekBarOuter.appendChild(this._seekBar); this._seekBarBefore = document.createElement('div'); this._seekBarOuter.appendChild(this._seekBarBefore); this._seekBarThumb = document.createElement('div'); this._seekBarOuter.appendChild(this._seekBarThumb); var _isHorz = function () { return (this._orientation === 'horizontal'); }.bind(this); var _validSize = function () { return (this._max > this._min); }.bind(this); var _enumWidth = function () { if (!this._seekBarSize) { var wProp = ''; if (_isHorz()) { wProp = 'offsetWidth'; } else { wProp = 'offsetHeight'; } if (this._seekBarSizes[wProp] > 0) this._seekBarSize = this._seekBarSizes[wProp]; _mouseOffset = Math.round(this._seekBarThumbSizes[wProp] / 2); } }.bind(this); var _liveEvent = function () { var liveevt = createNewCustomEvent('livechange', { detail: { value: this.value }, bubbles: true, cancelable: true }); this.container.dispatchEvent(liveevt); }.bind(this); var _changeEvent = function () { var evt = createNewEvent('change'); this.container.dispatchEvent(evt); }.bind(this); this._roundToStep = function (val, st) { st = st || this.step; if (st !== 0) return Math.round(val / st) * st; else return val; }.bind(this); var _getSize = function () { _enumWidth(); return this._seekBarSize; }.bind(this); var _getOffset = function (e) { var horz = _isHorz(); var val; if (e.elX !== undefined) { if (horz) { val = e.elX - _mouseOffset; } else { val = e.elY - _mouseOffset; } } else { if (e.currentTarget !== e.target) { if (!this.sbOuterPos) { this.sbOuterPos = findScreenPos(this._seekBarOuter); } if (horz) { val = e.screenX - this.sbOuterPos.left - _mouseOffset; } else { val = e.screenY - this.sbOuterPos.top - _mouseOffset; } } else { if (horz) { val = e.offsetX - _mouseOffset; } else { val = e.offsetY - _mouseOffset; } } } return Math.min(Math.max(val, 0), _getSize()); }.bind(this); this._updateInvert = function () { var horz = _isHorz(); if (this._invert) { var twHalf = Math.round(this._seekBarThumbSizes[horz ? 'offsetWidth' : 'offsetHeight'] / 2); this._seekBarBefore.style[horz ? 'left' : 'top'] = 'auto'; this._seekBarBefore.style[horz ? 'right' : 'bottom'] = twHalf + 'px'; } else { this._seekBarBefore.style[horz ? 'left' : 'top'] = ''; this._seekBarBefore.style[horz ? 'right' : 'bottom'] = ''; } }; var _updateBarBefore = function (pos) { var horz = _isHorz(); var twHalf = Math.round(this._seekBarThumbSizes[horz ? 'offsetWidth' : 'offsetHeight'] / 2); if (this._invert) { var w = _getSize(); pos = w - pos /* + tw*/ ; } else { //pos += tw; } if (this._fromZero) { var zP = Math.round(this.zeroPos + twHalf); if (this.zeroPos < pos) { this._seekBarBefore.style[horz ? 'left' : 'top'] = zP + 'px'; this._seekBarBefore.style[horz ? 'width' : 'height'] = (pos - this.zeroPos) + 'px'; } else { this._seekBarBefore.style[horz ? 'left' : 'top'] = (pos + twHalf) + 'px'; this._seekBarBefore.style[horz ? 'width' : 'height'] = (this.zeroPos - pos) + 'px'; } } else { if (this._invert) { this._seekBarBefore.style[horz ? 'right' : 'bottom'] = twHalf + 'px'; } else { this._seekBarBefore.style[horz ? 'left' : 'top'] = twHalf + 'px'; } this._seekBarBefore.style[horz ? 'width' : 'height'] = pos + 'px'; } }.bind(this); var _setPos = function (pos) { pos = Math.round(pos); this._seekBarThumb.style[_isHorz() ? 'left' : 'top'] = pos + 'px'; _updateBarBefore(pos); }.bind(this); this._assignClasses = function () { if (!this.initialized) return; if (this._orientation == 'horizontal') { this._seekBarOuter.className = 'seekBarOuter'; this._seekBar.className = 'seekBar'; this._seekBarBefore.className = 'seekBarBefore'; this._seekBarThumb.className = 'seekBarThumb'; this._ticksOuter.className = 'ticksOuter'; } else { this._seekBarOuter.className = 'seekBarOuterVert'; this._seekBar.className = 'seekBarVert'; this._seekBarBefore.className = 'seekBarBeforeVert'; this._seekBarThumb.className = 'seekBarThumbVert'; this._ticksOuter.className = 'ticksOuterVert'; } this._seekBarSize = 0; }.bind(this); var _updateVal = function (e) { var w = _getSize(); if (w > 0) { var one = (this._max - this._min) / w; var val = _getOffset(e); var pos = one * val; if (this._invert) { this._value = this._max - pos; } else this._value = this._min + pos; } }.bind(this); this._updatePos = function () { if (this._seekBarThumb && !_seeking) { var w = _getSize(); if (w > 0) { var one = (this._max - this._min) / w; if (one > 0) { var pos = (this.value - this._min) / one; if (this._fromZero) { this.zeroPos = (0 - this._min) / one; } else if (this._invert) { pos = w - pos; } _setPos(pos); } } } }.bind(this); var _pointerDownEvent = function (e) { if (!_validSize() || this._disabled || (e.button !== 0) /* take only left button */ ) return; _seeking = true; var _moveaction = function (e) { if (_seeking) { _setPos(_getOffset(e)); _updateVal(e); _liveEvent(); } }.bind(this); var _endaction = function (e) { if (_seeking) { // apply _setPos(_getOffset(e)); _updateVal(e); } _seeking = false; this._updatePos(); _liveEvent(); _changeEvent(); }.bind(this); _liveEvent(); // call this, so listeners will know, that slider was catched handleCapture(this._seekBarOuter, _moveaction, _endaction); }.bind(this); var _keyDownHandler = function (evt) { if (!_validSize()) return; if (evt.ctrlKey || evt.altKey || evt.shiftKey) return; var horz = _isHorz(); var keydown = horz ? 37 : 40; var keyup = horz ? 39 : 38; var step = this.step || (this.max - this.min) / 100; if ((!this.invert && !horz) || (horz && this.invert)) { step = -step; } switch (evt.keyCode) { case keydown: { this.value = this.value - step; _liveEvent(); _changeEvent(); evt.stopPropagation(); break; } case keyup: { this.value = this.value + step; _liveEvent(); _changeEvent(); evt.stopPropagation(); break; } } }.bind(this); this.mouseWheelHandler = function (evt) { if (!_validSize()) return; var step = 0; if (evt.shiftKey) step = this.step; else step = this.wheelStep; step = step || (this.max - this.min) / 100; // ** only process event if it's after the minimum time var dt = Date.now() - this._lastEventTime; if (dt > this._minTimeBetweenTicks) { this._lastEventTime = Date.now(); var delta; // if trackpad if (Math.abs(evt.wheelDelta) < 60) { var maxStep, minStep; // if step is less than 1, it could be very small, so make maxStep in relation to minStep if (step < 1) { minStep = step / 2; // If it's already a decimal, we can lower the min for greater precision maxStep = step * 10; } // if step is greater than 1, give maxStep a bit more oomph else { minStep = 1; maxStep = (step > 2) ? step*2 : 5; } var rawDelta = evt.wheelDelta * (maxStep - minStep) / 40; // ** Add or subtract minStep from rawDelta depending on pos. or neg. delta = (rawDelta > 0) ? rawDelta + minStep : rawDelta - minStep; this.value = this._roundToStep(this.value + delta, minStep); } else { delta = step * (evt.wheelDelta / 120); this.value = this._roundToStep(this.value + delta, step); } evt.stopPropagation(); _liveEvent(); _changeEvent(); } else { evt.stopPropagation(); } }.bind(this); this.setTabbable = function (val) { if (val === this._tabbable) return; this._tabbable = val; if (this._tabbable) { this._seekBarOuter.tabIndex = 0; // makes it tabbable and able to catch keys app.listen(this._seekBarOuter, 'keydown', _keyDownHandler); app.listen(this._seekBarOuter, 'wheel', this.mouseWheelHandler); // ** mousewheel event deprecated } else { this._seekBarOuter.tabIndex = -1; app.unlisten(this._seekBarOuter, 'keydown', _keyDownHandler); app.unlisten(this._seekBarOuter, 'wheel', this.mouseWheelHandler); // ** mousewheel event deprecated } }.bind(this); this._generateTicks = function () { if (!this.initialized) return; cleanElement(this._ticksOuter); if ((this._tickPlacement !== 'none') && _validSize()) { if (this._tickInterval) { this._ticks = new Array(); for (var i = this.min; i <= this.max; i += this.tickInterval) { this._ticks.push(i); } } if (this._ticks) { var horz = _isHorz(); var both = (this._tickPlacement === 'both'); var cls = both ? 'topLeft' : this._tickPlacement; for (var b = 0; b < (both ? 2 : 1); b++) { for (var i = 0; i < this._ticks.length; i++) { var t = document.createElement('div'); t.className = 'tick ' + cls; var pval = 100.0 * ((this._ticks[i] - this.min) / (this.max - this.min)); if (this._invert) pval = 100 - pval; t.style[horz ? 'left' : 'top'] = pval + '%'; this._ticksOuter.appendChild(t); } cls = 'bottomRight'; } }; } }.bind(this); this.activateImmediateTooltipHandling = function (formatValueFunc) { this.container.tooltipImmediate = true; if (formatValueFunc) this.formatTooltipValue = formatValueFunc; else this.formatTooltipValue = undefined; app.listen(this._seekBarOuter, 'mousemove', function (e) { var w = _getSize(); if (w > 0) { var one = (this._max - this._min) / w; var val = _getOffset(e); var pos = one * val; if (this._invert) { this._hoverValue = this._max - pos; } else this._hoverValue = this._min + pos; } else this._hoverValue = undefined; }.bind(this), true); this.container.tooltipValueCallback = function (tipdiv, vis) { if (!vis || (this._hoverValue === undefined) || this.disabled) { tipdiv.innerText = ''; return; } else { if (this.formatTooltipValue) tipdiv.innerText = this.formatTooltipValue(this._hoverValue); else tipdiv.innerText = this._hoverValue; } }.bind(this); }.bind(this); this.tabbable = true; // default if (params) { // set passed attributes for (var key in params) { this[key] = params[key]; } }; this.initialized = true; this._assignClasses(); this._updateInvert(); this._generateTicks(); app.listen(this._seekBarOuter, 'mousedown', _pointerDownEvent); app.listen(this._seekBarOuter, 'touchstart', _pointerDownEvent); this.cleanUp = function () { app.unlisten(this._seekBarOuter); Slider.$super.cleanUp.apply(this, arguments); }; this.registerEventHandler('layoutchange'); this.handle_layoutchange(); // make initial setting }, handle_layoutchange: function (evt) { this.sbOuterPos = undefined; if (this._seekBarThumb) { this._seekBarThumbSizes.offsetWidth = this._seekBarThumb.offsetWidth; this._seekBarThumbSizes.offsetHeight = this._seekBarThumb.offsetHeight; } if (this._seekBar) { this._seekBarSizes.offsetWidth = this._seekBar.offsetWidth; this._seekBarSizes.offsetHeight = this._seekBar.offsetHeight; } if (this._max > this._min) { this._seekBarSize = 0; this._updatePos(); } if (evt) Slider.$super.handle_layoutchange.call(this, evt); }, ignoreHotkey: function (hotkey) { var ar = ['Right', 'Left']; return inArray(hotkey, ar, true /* ignore case */ ); } }, { /** Slider orientation. Possible values: 'vertical', 'horizontal' @property orientation @type string @default 'horizontal' */ orientation: { get: function () { return this._orientation; }, set: function (val) { if (this._orientation !== val) { this._orientation = val; this._assignClasses(); this._updateInvert(); this._generateTicks(); } } }, /** If true, reverse direction of the slider. @property invert @type boolean @default false */ invert: { get: function () { return this._invert; }, set: function (val) { if (this._invert !== val) { this._invert = val; this._updateInvert(); this._updatePos(); this._generateTicks(); } } }, /** If true, show slider value with beginning in zero @property fromZero @type boolean @default false */ fromZero: { get: function () { return this._fromZero; }, set: function (val) { if (this._fromZero !== val) { this._fromZero = val; if (this._fromZero) this._invert = false; this._updateInvert(); this._updatePos(); this._generateTicks(); } } }, /** Get/set minimal value of the slider. @property min @type number @default 0 */ min: { get: function () { return this._min; }, set: function (val) { if (this._min !== val) { this._min = val; if (this.value < this._min) this.value = this._min; this._updatePos(); this._generateTicks(); } } }, /** Get/set maximal value of the slider. @property max @type number @default 0 */ max: { get: function () { return this._max; }, set: function (val) { if (this._max !== val) { this._max = val; if (this.value > this._max) this.value = this._max; this._updatePos(); this._generateTicks(); } } }, /** Get/set step of the slider. 0 means no discrete step. @property step @type number @default 0 */ step: { get: function () { return this._step; }, set: function (val) { if (this._step !== val) { this._step = val; this._updatePos(); } } }, /** Get/set wheel step of the slider (step used by mouse wheel). Default is undefined = wheel uses "step" property. @property wheelStep @type number @default undefined */ wheelStep: { get: function () { if (this._wheelStep !== undefined) return this._wheelStep; else return this._step; }, set: function (val) { if (this._wheelStep !== val) { this._wheelStep = val; } } }, /** Get/set actual value of the slider. @property value @type number @default 0 */ value: { get: function () { return this._roundToStep(this._value); }, set: function (val) { val = this._roundToStep(val); if ((val >= this.min) && (val <= this.max) && (val !== this._value)) { this._value = val; this._updatePos(); } } }, /** If true, enables focusing control and controlling by arrow keys. @property tabbable @type boolean @default true */ tabbable: { get: function () { return this._tabbable; }, set: function (val) { this.setTabbable(val); } }, /** Slider ticks placement. Possible values: 'none', 'both', 'bottomRight', 'topLeft', 'center' @property tickPlacement @type string @default 'none' */ tickPlacement: { get: function () { return this._tickPlacement; }, set: function (val) { if (this._tickPlacement !== val) { this._tickPlacement = val; this._generateTicks(); } } }, /** Slider ticks interval. @property tickInterval @type number @default 1 */ tickInterval: { get: function () { return this._tickInterval; }, set: function (val) { if (this._tickInterval !== val) { this._tickInterval = val; this._generateTicks(); } } }, /** Array of slider ticks values. For regular ticks user should rather use tickInterval. @property tickInterval @type array @default [] */ ticks: { get: function () { return this._ticks; }, set: function (val) { this._ticks = val; this._tickInterval = 0; this._generateTicks(); } } });