VulpesOS/gaia/apps/clock/js/picker/spinner.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

289 lines
7.1 KiB
JavaScript
Executable file

define(function(require) {
'use strict';
/* global IntlHelper */
var Template = require('template');
var GestureDetector = require('shared/js/gesture_detector');
var spinnerHtml = require('text!picker/spinner.html');
// units covered per millisecond threshold to kick off inertia
var SPEED_THRESHOLD = 0.01;
// max units covered by inertia
var INERTIA_MAXIMUM = 15;
// not the same as animation duration, this accounts for "slowing down",
// measured in miliseconds
var INERTIA_DURATION = 300;
// number of milliseconds after last motion without leting go
// we will select whatever is being "hovered" and cancel momentum
var DRAGGING_TIMEOUT = 200;
IntlHelper.define('digit-padding', 'number', {
style: 'decimal',
useGrouping: false,
minimumIntegerDigits: 2
});
function calculateSpeed(previous, current) {
/* jshint validthis:true */
var motion = (previous.y - current.y) / this.unitHeight;
var delta = current.time - previous.time;
var speed = motion / delta;
return parseFloat(speed.toFixed(4)) || 0;
}
function Touch(touch = {}) {
this.x = touch.x || 0;
this.y = touch.y || 0;
this.time = touch.time || 0;
}
/**
* Spinner
*
* Create a select list spinner.
*
* @param {Object} setup An object containing setup data.
* - element, a DOM element to create
* the spinner with.
* - values, an array of values to
* populate the spinner with.
*
*
* new Spinner({
* element: this.selector[picker],
* values: values
* });
*
*
*/
function Spinner(setup = {}) {
// define some non writable properties
Object.defineProperties(this, {
value: {
get: function() {
return this.values[this.index];
},
set: function(value) {
this.select(this.values.indexOf(parseInt(value)));
}
},
unitHeight: {
get: function() {
return this.element.children[0].clientHeight;
}
},
space: {
get: function() {
return this.unitHeight * this.length;
}
},
container: {
value: setup.element.parentNode
},
element: {
value: setup.element
},
values: {
value: setup.values
},
length: {
value: setup.values.length
},
textValues: {
value: setup.textValues || setup.values
},
isPadded: {
value: setup.isPadded || false
},
l10nId: {
value: setup.l10nId || null
}
});
this.template = new Template(spinnerHtml);
this.top = 0;
this.index = 0;
this.previous = new Touch();
this.current = new Touch();
this.timeout = null;
var html = '';
var formatter = IntlHelper.get(
this.isPadded ? 'digit-padding' : 'digit-nopadding');
for (var i = 0; i < this.length; i++) {
html += this.template.interpolate({
value: this.values[i].toString(),
text: formatter.format(this.values[i])
});
}
this.element.innerHTML = html;
this.container.addEventListener('touchstart', this, false);
this.container.addEventListener('pan', this, false);
this.container.addEventListener('swipe', this, false);
this.container.addEventListener('keypress', this, false);
this.container.setAttribute('aria-valuemax', this.upper);
this.container.setAttribute('aria-valuemin', this.lower);
this.container.setAttribute('aria-valuenow', this.index);
if (this.l10nId) {
document.l10n.setAttributes(
this.container,
this.l10nId,
{n: this.values[i]}
);
}
IntlHelper.observe(['digit-padding', 'digit-nopadding'],
this.resetValues.bind(this));
this.reset();
new GestureDetector(this.container).startDetecting();
}
Spinner.prototype.resetValues = function() {
var formatter = IntlHelper.get(
this.isPadded ? 'digit-padding' : 'digit-nopadding');
var units = [...this.container.querySelectorAll('.picker-unit')];
units.forEach(unit => {
var value = parseInt(unit.dataset.value);
unit.textContent = formatter.format(value);
});
};
Spinner.prototype.reset = function() {
this.index = 0;
this.top = 0;
this.update();
};
Spinner.prototype.update = function() {
this.container.setAttribute('aria-valuenow', this.index);
this.container.setAttribute('aria-valuetext', this.textValues[this.index]);
this.element.style.transform = 'translateY(' + this.top + 'px)';
};
Spinner.prototype.select = function(index) {
index = Math.round(index);
if (index < 0) {
index = 0;
}
if (index > this.length - 1) {
index = this.length - 1;
}
if (index !== this.index) {
this.index = index;
}
this.top = -this.index * this.unitHeight;
this.update();
return index;
};
Spinner.prototype.handleEvent = function(event) {
this['on' + event.type](event);
};
Spinner.prototype.stopInteraction = function() {
this.element.classList.add('animation-on');
clearTimeout(this.timeout);
this.select(this.index);
this.speed = 0;
};
/**
* ontouchstart - prevent default action (stops scrolling)
*/
Spinner.prototype.ontouchstart = function(event) {
event.preventDefault();
};
Spinner.prototype.onpan = function(event) {
event.stopPropagation();
var position = event.detail.position;
var diff;
// If this is the first pan event after a swipe...
if (this.element.classList.contains('animation-on')) {
this.element.classList.remove('animation-on');
this.select(this.index);
this.previous.y = position.clientY;
this.previous.time = position.timeStamp;
return;
}
this.current.y = position.clientY;
this.current.time = position.timeStamp;
this.speed = calculateSpeed.call(this, this.previous, this.current);
diff = this.current.y - this.previous.y;
this.top = this.top + diff;
if (this.top > 0) {
this.top = 0;
}
if (this.top < -this.space) {
this.top = -this.space;
}
this.index = Math.round(Math.abs(this.top) / this.unitHeight);
this.update();
clearTimeout(this.timeout);
var stopInteraction = this.stopInteraction.bind(this);
this.timeout = setTimeout(stopInteraction, DRAGGING_TIMEOUT);
this.previous.y = this.current.y;
this.previous.time = this.current.time;
};
Spinner.prototype.onswipe = function(event) {
event.stopPropagation();
// Add momentum if speed is higher than a given threshold.
var direction = this.speed > 0 ? 1 : -1;
var speed = this.speed / direction;
if (speed >= SPEED_THRESHOLD) {
this.index += Math.round(
Math.min(speed * INERTIA_DURATION, INERTIA_MAXIMUM) * direction
);
}
this.stopInteraction();
};
Spinner.prototype.onkeypress = function(event) {
this.element.classList.add('animation-on');
if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
this.select(this.index - 1);
} else {
this.select(this.index + 1);
}
};
return Spinner;
});