View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0017098 | MMW 5 | Other | public | 2020-11-15 04:23 | 2020-11-21 20:29 |
| Reporter | drakinite | Assigned To | |||
| Priority | immediate | Severity | tweak | Reproducibility | always |
| Status | closed | Resolution | fixed | ||
| Product Version | 5.0 | ||||
| Target Version | 5.0 | Fixed in Version | 5.0 | ||
| Summary | 0017098: Improvement to sliders on laptop trackpads | ||||
| Description | Sliders (e.g. volume) could not be controlled easily on laptop trackpads, as described here: https://www.mediamonkey.com/forum/viewtopic.php?f=30&t=97712 I made a significant improvement to sliders' behavior on trackpads. Makes it easy to move sliders quickly and precisely. Tested on volume, equalizer, and tools>options>player>popupdisplay sliders. Additionally, the mousewheel event is deprecated (see: https://developer.mozilla.org/en-US/docs/Web/API/Element/mousewheel_event and https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event) so the event listener in slider.js is changed accordingly. | ||||
| Additional Information | (modified slider.js is attached) | ||||
| Tags | No tags attached. | ||||
| Attached Files | slider.js (23,976 bytes)
/* '(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();
}
}
});
| ||||
| Fixed in build | 2275 | ||||