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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,379 @@
/* global ValuePicker */
'use strict';
/**
* SpinDatePicker is a html/js "widget" which enables users
* pick a specific date. It display the date in the way based
* on the language setting.
*
* The SpinDatePicker itself contains no UI for the controls.
*
* Example usage:
*
* // All necessary UI elements are contained in the root element.
* var picker = new SpinDatePicker(root);
* picker.value = new Date();
* // after users pick a date
* var newDate = picker.value;
*/
/* exported SpinDatePicker */
var SpinDatePicker = (function() {
var GLOBAL_MIN_YEAR = 1900;
var GLOBAL_MAX_YEAR = 2099;
var DateRange = (function DateRange(min, max) {
var _minYear = min.getFullYear();
var _maxYear = max.getFullYear();
var _minMonth = min.getMonth();
var _maxMonth = max.getMonth();
var _minDate = min.getDate();
var _maxDate = max.getDate();
return {
get min() {
return min;
},
get max() {
return max;
},
get minYear() {
return _minYear;
},
get maxYear() {
return _maxYear;
},
get minMonth() {
return _minMonth;
},
get maxMonth() {
return _maxMonth;
},
get minDate() {
return _minDate;
},
get maxDate() {
return _maxDate;
}
};
});
var _dateRange = new DateRange(new Date(GLOBAL_MIN_YEAR, 0, 1),
new Date(GLOBAL_MAX_YEAR, 11, 31));
function getYearText() {
var yearText = [];
var dateTimeFormat = new Intl.DateTimeFormat(navigator.languages, {
year: 'numeric'
});
for (var i = GLOBAL_MIN_YEAR; i <= GLOBAL_MAX_YEAR; i++) {
var date = new Date(i, 0, 1);
yearText.push({ raw: dateTimeFormat.format(date) });
}
return yearText;
}
function getMonthText() {
var monthText = [];
var date = new Date(1970, 0, 1);
var dateTimeFormat = new Intl.DateTimeFormat(navigator.languages, {
month: 'long'
});
for (var i = 0; i < 12; i++) {
date.setMonth(i);
monthText.push({ raw: dateTimeFormat.format(date) });
}
return monthText;
}
function getDateText() {
var dateText = [];
var date = new Date(1970, 0, 1);
var dateTimeFormat = new Intl.DateTimeFormat(navigator.languages, {
day: 'numeric'
});
for (var i = 1; i <= 31; i++) {
date.setDate(i);
dateText.push({ raw: dateTimeFormat.format(date) });
}
return dateText;
}
function getDaysInMonth(year, month) {
var date = new Date(year, month + 1, 0);
return date.getDate();
}
/**
* Get the order of date components.
*
* @param {Promise<String>} date format.
*/
function getDateComponentOrder() {
return navigator.mozL10n.formatValue('datePickerOrder');
}
/**
* Initialize a date picker widget.
*
* @param {HTMLELement} element target of widget creation.
*/
function SpinDatePicker(element, minDate, maxDate) {
this.element = element;
this.yearPicker = null;
this.monthPicker = null;
this.datePicker = null;
//XXX: When the document is localized again
// we must also re-render the month because
// the week days may have changed?
// This will only happen when we change timezones
// unless we add this information to the locales.
var pickerContainer =
element.querySelector('.picker-container');
var yearPickerContainer =
element.querySelector('.value-picker-year');
var monthPickerContainer =
element.querySelector('.value-picker-month');
var datePickerContainer =
element.querySelector('.value-picker-date');
var updateCurrentValue = (function spd_updateCurrentValue() {
var selectedYear = this.yearPicker.getSelectedIndex() + GLOBAL_MIN_YEAR;
var selectedMonth = this.monthPicker.getSelectedIndex();
var selectedDate = this.datePicker.getSelectedIndex() + 1;
var newDate = new Date();
newDate.setYear(selectedYear);
newDate.setMonth(selectedMonth);
newDate.setDate(selectedDate);
this._value = newDate;
}).bind(this);
var updatePickersRange =
(function spd_updatePickersRange() {
var selectedYear = this.yearPicker.getSelectedIndex() + GLOBAL_MIN_YEAR;
var selectedMonth = this.monthPicker.getSelectedIndex();
var minMonth = 0;
var maxMonth = 11;
var minDate = 1;
var maxDate = 31;
if (selectedYear === _dateRange.minYear) {
minMonth = _dateRange.minMonth;
if (selectedMonth === _dateRange.minMonth) {
minDate = _dateRange.minDate;
}
}
if (selectedYear === _dateRange.maxYear) {
maxMonth = _dateRange.maxMonth;
if (selectedMonth === _dateRange.maxMonth) {
maxDate = _dateRange.maxDate;
}
}
var days = getDaysInMonth(this.yearPicker.getSelectedIndex() +
GLOBAL_MIN_YEAR, this.monthPicker.getSelectedIndex());
minDate = Math.min(minDate, days);
maxDate = Math.min(maxDate, days);
this.monthPicker.setRange(minMonth, maxMonth);
this.datePicker.setRange(minDate - 1, maxDate - 1);
}).bind(this);
var onvaluechangeInternal =
(function spd_onvaluechangeInternal(newDateValue) {
this.yearPicker.setSelectedIndex(
newDateValue.getFullYear() - GLOBAL_MIN_YEAR);
this.monthPicker.setSelectedIndex(newDateValue.getMonth());
this.datePicker.setSelectedIndex(newDateValue.getDate() - 1);
updatePickersRange.apply(this);
updateCurrentValue.apply(this);
}).bind(this);
var onSelectedYearChanged =
function spd_onSelectedYearChanged(selectedYear) {
updatePickersRange.apply(this);
updateCurrentValue.apply(this);
};
var onSelectedMonthChanged =
function spd_onSelectedMonthChanged(selectedMonth) {
updatePickersRange.apply(this);
updateCurrentValue.apply(this);
};
var onSelectedDateChanged =
function spd_onSelectedDateChanged(selectedDate) {
updateCurrentValue.apply(this);
};
var unitClassName = 'picker-unit';
// year value picker
var yearUnitStyle = {
optionsL10n: getYearText(),
className: unitClassName
};
if (this.yearPicker) {
this.yearPicker.uninit();
}
this.yearPicker = new ValuePicker(yearPickerContainer, yearUnitStyle);
this.yearPicker.onselectedindexchange = onSelectedYearChanged.bind(this);
// month value picker
var monthUnitStyle = {
optionsL10n: getMonthText(),
className: unitClassName
};
if (this.monthPicker) {
this.monthPicker.uninit();
}
this.monthPicker =
new ValuePicker(monthPickerContainer, monthUnitStyle);
this.monthPicker.onselectedindexchange = onSelectedMonthChanged.bind(this);
// date value picker
var dateUnitStyle = {
optionsL10n: getDateText(),
className: unitClassName
};
if (this.datePicker) {
this.datePicker.uninit();
}
this.datePicker = new ValuePicker(datePickerContainer, dateUnitStyle);
this.datePicker.onselectedindexchange =
onSelectedDateChanged.bind(this);
// set component order
getDateComponentOrder().then(dcOrder => {
var pickerClassList = pickerContainer.classList;
pickerClassList.remove('YMD');
pickerClassList.remove('DMY');
pickerClassList.remove('MDY');
pickerClassList.add(dcOrder);
});
// Prevent focus being taken away by us for time picker.
// The event listener on outer box will not be triggered cause
// there is a evt.stopPropagation() in value_picker.js
this.pickerElements = [monthPickerContainer, yearPickerContainer,
datePickerContainer];
this.pickerElements.forEach((function pickerElements_forEach(picker) {
picker.addEventListener('mousedown', this);
}).bind(this));
this.onvaluechangeInternal = onvaluechangeInternal.bind(this);
}
SpinDatePicker.prototype = {
/**
* Internal value not exposed so we can fire events
* when the getter/setter's are used.
*
* @type Date
*/
_value: null,
/**
* Gets current value
*
* @return {Null|Date} date or null.
*/
get value() {
return this._value;
},
/**
* Sets the current value of the date picker.
* When value differs from the currently set the
* `onvaluechange` event will be fired with the new/old value.
*/
set value(value) {
var old = this._value;
if (old !== value) {
this._value = value;
this.onvaluechangeInternal(value);
}
},
/**
* Getter is used for date normalization.
*/
get year() {
return this._value.getFullYear();
},
/**
* Getter is used for date normalization.
*/
get month() {
return this._value.getMonth();
},
get date() {
return this._value.getDate();
},
handleEvent: function vs_handleEvent(evt) {
switch (evt.type) {
case 'mousedown':
// Prevent focus being taken away by us.
evt.preventDefault();
break;
}
},
setRange: function vs_setRange(minDate, maxDate) {
if (!minDate) {
minDate = new Date(GLOBAL_MIN_YEAR, 0, 1);
}
if (!maxDate) {
maxDate = new Date(GLOBAL_MAX_YEAR, 11, 31);
}
_dateRange = new DateRange(minDate, maxDate);
// set date picker
this.yearPicker.setRange(minDate.getFullYear() - GLOBAL_MIN_YEAR,
maxDate.getFullYear() - GLOBAL_MIN_YEAR);
},
uninit: function() {
if (this.yearPicker) {
this.yearPicker.uninit();
}
if (this.monthPicker) {
this.monthPicker.uninit();
}
if (this.datePicker) {
this.datePicker.uninit();
}
this.pickerElements.forEach((function pickerElements_forEach(picker) {
picker.removeEventListener('mousedown', this);
}).bind(this));
},
/**
* Called when the selected date changes.
*/
onvaluechangeInternal: function(date) {}
};
return SpinDatePicker;
}());

View file

@ -0,0 +1,294 @@
/* global KeyEvent */
'use strict';
/**
* base widget used in ValueSelector and SpinDatePicker widget
*/
/* exported ValuePicker */
var ValuePicker = (function() {
//
// Constructor
//
function VP(e, unitStyle) {
this.element = e;
this.container = e.parentNode;
this._optionsL10n = unitStyle.optionsL10n;
this._unitClassName = unitStyle.className;
this._top = 0;
this._lower = 0;
this._upper = unitStyle.optionsL10n.length - 1;
this._range = unitStyle.optionsL10n.length;
this._currentIndex = 0;
this._unitHeight = 0;
this._unitStyle = unitStyle;
this.init();
}
//
// Public methods
//
VP.prototype.getSelectedIndex = function() {
var selectedIndex = this._currentIndex;
return selectedIndex;
};
VP.prototype.getSelectedOptionL10n = function() {
return this._optionsL10n[this._currentIndex];
};
VP.prototype.setSelectedIndex = function(tunedIndex, ignorePicker) {
if ((tunedIndex % 1) > 0.5) {
tunedIndex = Math.floor(tunedIndex) + 1;
} else {
tunedIndex = Math.floor(tunedIndex);
}
if (tunedIndex < this._lower) {
tunedIndex = this._lower;
}
if (tunedIndex > this._upper) {
tunedIndex = this._upper;
}
var beforeIndex = this._currentIndex;
if (this._currentIndex != tunedIndex) {
this._currentIndex = tunedIndex;
this.onselectedindexchange(this._currentIndex);
}
this.updateUI(tunedIndex, ignorePicker);
var elementChildren = this.element.children;
elementChildren[beforeIndex].classList.remove('selected');
elementChildren[this._currentIndex].classList.add('selected');
return tunedIndex;
};
VP.prototype.setRange = function(lower, upper) {
if (lower !== null) {
this._lower = lower;
} else {
this._lower = 0;
}
if (upper !== null) {
this._upper = upper;
} else {
this._upper = this._unitStyle.optionsL10n.length - 1;
}
var unitElement = this.element.firstElementChild;
var index = 0;
while (unitElement) {
unitElement.dataset.disabled =
(index < this._lower || index > this._upper);
unitElement = unitElement.nextElementSibling;
index++;
}
this.container.setAttribute('aria-valuemin', this._lower);
this.container.setAttribute('aria-valuemax', this._upper);
this._range = this._upper - this._lower + 1;
this.setSelectedIndex(this._currentIndex);
};
//
// Internal methods
//
VP.prototype.init = function() {
this.initUI();
this.setSelectedIndex(0); // Default Index is zero
this.keypressHandler = this.onkeypress.bind(this);
this.touchstartHandler = this.ontouchstart.bind(this);
this.touchmoveHandler = this.ontouchmove.bind(this);
this.touchendHandler = this.ontouchend.bind(this);
this.addEventListeners();
};
VP.prototype.initUI = function() {
this.container.setAttribute('role', 'spinbutton');
this.container.setAttribute('aria-valuemin', this._lower);
this.container.setAttribute('aria-valuemax', this._upper);
var unitCount = this._optionsL10n.length;
for (var i = 0; i < unitCount; ++i) {
this.addPickerUnit(i);
}
};
VP.prototype.addPickerUnit = function(index) {
var l10n = this._optionsL10n[index];
var unit = document.createElement('div');
unit.className = this._unitClassName;
if (typeof l10n === 'string') {
unit.setAttribute('data-l10n-id', l10n);
} else if (l10n.hasOwnProperty('raw')) {
unit.textContent = l10n.raw;
} else {
navigator.mozL10n.setAttributes(unit, l10n.id, l10n.args);
}
this.element.appendChild(unit);
};
VP.prototype.updateUI = function(index, ignorePicker) {
if (true !== ignorePicker) {
// Bug 1075200 get unit height when needed and
// retry next time when dom is not rendered
if (this._unitHeight === 0) {
this._unitHeight = this.element.firstChild.clientHeight *
this.element.children.length / this._range;
}
this._top = -index * this._unitHeight;
this.element.style.transform = 'translateY(' + this._top + 'px)';
this.container.setAttribute('aria-valuenow', index);
var valPromise;
if (typeof this._optionsL10n[index] === 'string') {
valPromise = navigator.mozL10n.formatValue(this._optionsL10n[index]);
} else if (this._optionsL10n[index].hasOwnProperty('id')) {
valPromise = navigator.mozL10n.formatValue(
this._optionsL10n[index].id, this._optionsL10n[index].args);
} else {
valPromise = Promise.resolve(this._optionsL10n[index].raw);
}
valPromise.then(val => {
this.container.setAttribute('aria-valuetext', val);
});
}
};
VP.prototype.addEventListeners = function() {
this.container.addEventListener('keypress', this.keypressHandler, false);
this.element.addEventListener('touchstart', this.touchstartHandler, false);
};
VP.prototype.removeEventListeners = function() {
this.element.removeEventListener('touchend', this.touchendHandler, false);
this.element.removeEventListener('touchmove', this.touchmoveHandler, false);
};
VP.prototype.uninit = function() {
this._top = 0;
this.element.removeEventListener(
'touchstart', this.touchstartHandler, false);
this.element.removeEventListener('touchend', this.touchendHandler, false);
this.element.removeEventListener('touchmove', this.touchmoveHandler, false);
this.element.style.transform = 'translateY(0px)';
this.container.removeEventListener('keypress', this.keypressHandler, false);
this.container.removeAttribute('role');
this.container.removeAttribute('aria-valuemin');
this.container.removeAttribute('aria-valuemax');
this.container.removeAttribute('aria-valuenow');
this.container.removeAttribute('aria-valuetext');
this.onselectedindexchange = null;
empty(this.element);
};
VP.prototype.onselectedindexchange = function(index) {};
VP.prototype.ontouchmove = function vp_touchmove(event) {
event.stopPropagation();
event.target.setCapture(true);
currentEvent = cloneEvent(event);
calcSpeed();
// move selected index
this._top = this._top + getMovingSpace();
this.element.style.transform = 'translateY(' + this._top + 'px)';
tunedIndex = calcTargetIndex(this._unitHeight);
var roundedIndex = Math.round(tunedIndex * 10) / 10;
if (roundedIndex != this._currentIndex) {
this.setSelectedIndex(toFixed(roundedIndex), true);
}
startEvent = currentEvent;
};
VP.prototype.ontouchend = function vp_touchend(event) {
event.stopPropagation();
this.removeEventListeners();
this.element.classList.add('animation-on');
// Add momentum if speed is higher than a given threshold.
if (Math.abs(currentSpeed) > SPEED_THRESHOLD) {
var direction = currentSpeed > 0 ? 1 : -1;
tunedIndex += Math.min(Math.abs(currentSpeed) * 5, 5) * direction;
}
tunedIndex = this.setSelectedIndex(toFixed(tunedIndex));
currentSpeed = 0;
};
VP.prototype.ontouchstart = function vp_touchstart(event) {
event.stopPropagation();
this.element.classList.remove('animation-on');
startEvent = currentEvent = cloneEvent(event);
tunedIndex = this._currentIndex;
this.removeEventListeners();
this.element.addEventListener('touchmove', this.touchmoveHandler, false);
this.element.addEventListener('touchend', this.touchendHandler, false);
};
VP.prototype.onkeypress = function vp_keypress(event) {
if (event.keyCode == KeyEvent.DOM_VK_DOWN) {
this.setSelectedIndex(this._currentIndex - 1);
} else {
this.setSelectedIndex(this._currentIndex + 1);
}
};
function cloneEvent(evt) {
return {
x: evt.touches[0].pageX,
y: evt.touches[0].pageY,
timestamp: evt.timeStamp
};
}
function empty(element) {
while (element.hasChildNodes()) {
element.removeChild(element.lastChild);
}
element.innerHTML = '';
}
//
// Tuneable parameters
//
var SPEED_THRESHOLD = 0.1;
var currentEvent, startEvent, currentSpeed;
var tunedIndex = 0;
function toFixed(value) {
return parseFloat(value.toFixed(1));
}
function calcSpeed() {
var movingSpace = startEvent.y - currentEvent.y;
var deltaTime = currentEvent.timestamp - startEvent.timestamp;
var speed = movingSpace / deltaTime;
currentSpeed = parseFloat(speed.toFixed(2));
}
function calcTargetIndex(space) {
return tunedIndex - getMovingSpace() / space;
}
// If the user swap really slow, narrow down the moving space
// So the user can fine tune value.
function getMovingSpace() {
var movingSpace = currentEvent.y - startEvent.y;
var reValue = Math.abs(currentSpeed) > SPEED_THRESHOLD ?
movingSpace : movingSpace / 4;
return reValue;
}
return VP;
}());

View file

@ -0,0 +1,728 @@
/* global BaseUI, InputParser, ValueSelector, SpinDatePicker,
ValuePicker, Sanitizer, LazyLoader */
'use strict';
(function(exports) {
var _id = 0;
function toCamelCase(str) {
return str.replace(/\-(.)/g, function replacer(str, p1) {
return p1.toUpperCase();
});
}
exports.ValueSelector = function(app) {
this.app = app;
this.containerElement = app.element;
this.instanceID = _id++;
this._currentPickerType = null;
this._currentInputType = null;
this._currentDatetimeValue = '';
this._injected = false;
this._datePicker = null;
this._timePicker = null;
this.isShown = false;
app.element.addEventListener('_opening', this);
app.element.addEventListener('_closing', this);
app.element.addEventListener('_closed', this);
app.element.addEventListener('_inputfocus', this);
app.element.addEventListener('_inputblur', this);
app.element.addEventListener('_localized', this);
LazyLoader.load(['shared/js/input_parser.js']);
window.addEventListener('timeformatchange', this);
};
ValueSelector.prototype = Object.create(BaseUI.prototype);
ValueSelector.prototype.CLASS_NAME = 'ValueSelector';
ValueSelector.prototype.ELEMENT_PREFIX = 'value-selector-';
ValueSelector.prototype.EVENT_PREFIX = 'value-selector-';
ValueSelector.prototype.customID = function vs_customID() {
if (this.app) {
return '[' + this.app.origin + ']';
} else {
return '';
}
};
ValueSelector.prototype._im = navigator.mozInputMethod;
ValueSelector.prototype.destroy = function() {
window.removeEventListener('timeformatchange', this);
};
ValueSelector.prototype.handleEvent = function vs_handleEvent(evt) {
this.app.debug('handling ' + evt.type);
var target = evt.target;
switch (evt.type) {
case 'submit':
case 'mousedown':
evt.preventDefault();
break;
case 'click':
if (target.dataset.type === 'cancel') {
this.cancel();
} else if (target.dataset.type === 'ok') {
this.confirm();
} else {
this.handleSelect(target);
}
break;
case '_opening':
case '_closing':
case '_closed':
if (this._injected) {
this.hide();
}
break;
case '_localized':
// invalidate the current date and time picker when language setting
// changes
if (this._datePicker) {
this._datePicker.uninit();
this._datePicker = null;
}
if (this._timePicker) {
this._timePicker.uninit();
this._timePicker = null;
}
break;
case 'timeformatchange':
// invalidate the current time picker when time format changes
if (this._timePicker) {
this._timePicker.uninit();
this._timePicker = null;
}
break;
case '_inputfocus':
this._currentDatetimeValue = evt.detail.value;
this._currentInputType = evt.detail.inputType;
this.show(evt.detail);
break;
case '_inputblur':
this._currentDatetimeValue = undefined;
this._currentInputType = undefined;
this.hide();
break;
case 'transitionend':
this.element.classList.remove('transitioning');
break;
}
};
ValueSelector.prototype.render = function vs_render(callback) {
this.publish('willrender');
this.containerElement.insertAdjacentHTML('beforeend',
Sanitizer.unwrapSafeHTML(this.view()));
this._fetchElements();
this._registerEvents();
this._injected = true;
this.publish('rendered');
callback();
};
ValueSelector.prototype._fetchElements = function vs__fetchElements() {
this.element = document.getElementById(this.CLASS_NAME + this.instanceID);
this.elements = {};
this.elementClasses = ['select-option-popup', 'container',
'options-container', 'options-title', 'select-options-buttons',
'time-picker-popup', 'time-picker', 'time-picker-container',
'time-picker-buttons', 'spin-date-picker-popup', 'spin-date-picker',
'spin-date-picker-buttons'];
// Loop and add element with camel style name to Value Selector attribute.
this.elementClasses.forEach(function createElementRef(name) {
this.elements[toCamelCase(name)] =
this.element.querySelector('.' + this.ELEMENT_PREFIX + name);
}, this);
};
ValueSelector.prototype.view = function vs_view() {
/* jshint maxlen: false */
var id = this.CLASS_NAME + this.instanceID;
return Sanitizer.createSafeHTML `
<div data-z-index-level="value-selector" class="value-selector" id="${id}" hidden>
<form class="value-selector-select-option-popup" role="dialog" data-type="value-selector" hidden>
<section class="value-selector-container">
<h1 class="value-selector-options-title" data-l10n-id="choose-option"></h1>
<ol class="value-selector-options-container" role="listbox"></ol>
</section>
<menu class="value-selector-select-options-buttons value-selector-buttons">
<button class="value-option-confirm full" data-type="ok" data-l10n-id="ok"></button>
</menu>
</form>
<div class="value-selector-time-picker-popup" role="dialog" data-type="time-selector" hidden>
<h1 data-l10n-id="select-time"></h1>
<div class="value-selector-time-picker">
<div class="value-selector-time-picker-container picker-container">
<div class="picker-bar-background"></div>
<div class="value-picker-hours-wrapper">
<div class="value-picker-hours animation-on"></div>
</div>
<div class="value-picker-minutes-wrapper">
<div class="value-picker-minutes animation-on"></div>
</div>
<div class="value-picker-hour24-wrapper">
<div class="value-picker-hour24-state animation-on"></div>
</div>
<div class="value-indicator">
<div aria-hidden="true" class="value-indicator-colon hours-minutes-separator">:</div>
</div>
</div>
</div>
<menu class="value-selector-time-picker-buttons value-selector-buttons" data-items="2">
<button class="value-selector-cancel" data-type="cancel" data-l10n-id="cancel"></button>
<button class="value-selector-confirm affirmative" data-type="ok" data-l10n-id="ok"></button>
</menu>
</div>
<div role="dialog" data-type="date-selector" class="value-selector-spin-date-picker-popup" hidden>
<h1 data-l10n-id="select-day"></h1>
<div class="value-selector-spin-date-picker">
<div class="picker-container">
<div class="picker-bar-background"></div>
<div class="value-picker-date-wrapper">
<div class="value-picker-date animation-on"></div>
<div class="value-picker-date animation-on"></div>
<div class="value-picker-date animation-on"></div>
<div class="value-picker-date animation-on"></div>
</div>
<div class="value-picker-month-wrapper">
<div class="value-picker-month animation-on"></div>
</div>
<div class="value-picker-year-wrapper">
<div class="value-picker-year animation-on"></div>
</div>
<div class="value-indicator"></div>
</div>
</div>
<menu class="value-selector-spin-date-picker-buttons value-selector-buttons" data-items="2">
<button class="value-selector-cancel" data-type="cancel" data-l10n-id="cancel"></button>
<button class="value-option-confirm affirmative" data-type="ok" data-l10n-id="ok"></button>
</menu>
</div>
</div>`;
};
ValueSelector.prototype.optionView = function(
{index, checked, labelFor,text}) {
return Sanitizer.createSafeHTML `
<li role="option" data-option-index="${index}"
aria-selected="${checked}" dir="auto">
<label role="presentation" for="${labelFor}">
<span>${text}</span>
</label>
</li>`;
};
ValueSelector.prototype.groupView = function({text}) {
return Sanitizer.createSafeHTML `
<li role="subheader" dir="auto">
<label role="presentation">
<span>${text}</span>
</label>
</li>`;
};
ValueSelector.prototype._registerEvents = function vs__registerEvents() {
this.elements.container.addEventListener('click', this);
// Prevent the form from submit.
this.elements.selectOptionPopup.addEventListener('submit', this);
this.element.addEventListener('mousedown', this);
this.element.addEventListener('transitionend', this);
['selectOptionsButtons', 'timePickerButtons',
'spinDatePickerButtons'].forEach(function(elementId) {
this.elements[elementId].addEventListener('click', this);
}, this);
};
ValueSelector.prototype.show = function vs_show(detail) {
this.isShown = true;
if (this._injected) {
this._showSync(detail);
} else {
this.render(function afterRender() {
// Nesting two requestionAnimationFrames stops the style changes
// from this.show coalescing with the creation of the elements,
// without forcing a synchronous style flush.
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
// If isShown flag is reset by hide() we should not show ourselves.
if (!this.isShown) {
return;
}
// XXX: It's still possible the detail we use here ended up
// overwritten the currently-showning selector, but that can only
// happen if another show() take place before this place.
this._showSync(detail);
});
});
}.bind(this));
}
};
ValueSelector.prototype._showSync = function vs__showSync(detail) {
var currentInputType = this._currentInputType;
var currentValue = this._currentDatetimeValue;
this.publish('shown');
var min = detail.min;
var max = detail.max;
this.app._setVisibleForScreenReader(false);
if (this.element.hidden) {
this.element.classList.add('transitioning');
this.element.hidden = false;
}
switch (currentInputType) {
case 'select-one':
case 'select-multiple':
this.app.debug('select triggered' + JSON.stringify(detail));
this._currentPickerType = currentInputType;
this.showOptions(detail);
break;
case 'date':
this.showDatePicker(currentValue, min, max);
break;
case 'time':
this.showTimePicker(currentValue);
break;
case 'datetime':
case 'datetime-local':
this.showDatePicker(currentValue, min, max);
break;
}
};
ValueSelector.prototype.showPanel = function vs_showPanel(type) {
['selectOptionPopup', 'timePickerPopup', 'spinDatePickerPopup'].forEach(
function(elementId) {
this.elements[elementId].hidden = (type !== elementId);
}, this);
},
ValueSelector.prototype.hide = function vs_hide() {
this.isShown = false;
if (!this.element || this.element.hidden) {
return;
}
this.app._setVisibleForScreenReader(true);
this.element.blur();
this.element.classList.add('transitioning');
this.element.hidden = true;
if (this.app.getBottomMostWindow().isActive() && this.app.isActive()) {
this.app.focus();
}
this.publish('hidden');
};
ValueSelector.prototype.handleSelect = function vs_handleSelect(target) {
if (target.dataset === undefined ||
(target.dataset.optionIndex === undefined &&
target.dataset.optionValue === undefined)) {
return;
}
var selectee;
if (this._currentPickerType === 'select-one') {
selectee = this.elements.container.querySelectorAll(
'[aria-selected="true"]');
for (var i = 0; i < selectee.length; i++) {
selectee[i].removeAttribute('aria-selected');
}
target.setAttribute('aria-selected', 'true');
} else if (target.getAttribute('aria-selected') === 'true') {
target.removeAttribute('aria-selected');
} else {
target.setAttribute('aria-selected', 'true');
}
// setValue here to trigger change event
var singleOptionIndex;
var optionIndices = [];
selectee = this.elements.container.querySelectorAll(
'[aria-selected="true"]');
if (this._currentPickerType === 'select-one') {
if (selectee.length > 0) {
singleOptionIndex = selectee[0].dataset.optionIndex;
}
this._im.setSelectedOption(singleOptionIndex);
} else if (this._currentPickerType === 'select-multiple') {
// Multiple select case
for (var i = 0; i < selectee.length; i++) { // jshint ignore:line
var index = parseInt(selectee[i].dataset.optionIndex);
optionIndices.push(index);
}
this._im.setSelectedOptions(optionIndices);
}
};
ValueSelector.prototype.cancel = function vs_cancel() {
this.app.debug('cancel invoked');
this._im.removeFocus();
this.hide();
};
ValueSelector.prototype.confirm = function vs_confirm() {
var currentInputType = this._currentInputType;
// Time and Date value has to conform to RFC3339 spec
switch (currentInputType) {
case 'time':
var timeValue = this._timePicker.getTimeValue();
this.app.debug('output value: ' + timeValue);
this._im.setValue(timeValue);
break;
case 'date':
// The format should be 2012-09-19
var dateValue = InputParser.exportDate(this._datePicker._value);
this.app.debug('output value: ' + dateValue);
this._im.setValue(dateValue);
break;
case 'datetime':
case 'datetime-local':
var currentDatetimeValue = this._currentDatetimeValue;
if (this._currentPickerType === 'date') {
this.showTimePicker(currentDatetimeValue);
return;
} else if (this._currentPickerType === 'time') {
var selectedDate = this._datePicker.value;
var hour = this._timePicker.getHour();
var minute = this._timePicker.minute.getSelectedIndex();
var second = '';
var millisecond = '';
var date = null;
// The second and millisecond values can't be selected by picker.
// So set these values as same as
// the current value of datetime/datetime-local input field
// when currentDatetimeValue is not equal to ''(space),
// or set the values as same as current time.
if (currentDatetimeValue !== '') {
date = new Date(this._currentDatetimeValue);
} else {
date = new Date();
}
second = date.getSeconds();
millisecond = date.getMilliseconds();
selectedDate.setHours(hour);
selectedDate.setMinutes(minute);
selectedDate.setSeconds(second);
selectedDate.setMilliseconds(millisecond);
var datetimeValue = selectedDate.getTime();
this.app.debug('output value: ' + datetimeValue);
this._im.setValue(datetimeValue);
}
break;
}
this._im.removeFocus();
this.hide();
};
ValueSelector.prototype.showOptions = function vs_showOptions(detail) {
var options = null;
if (detail.choices && detail.choices.choices) {
options = detail.choices.choices;
}
if (options) {
this.buildOptions(options);
}
this.showPanel('selectOptionPopup');
};
ValueSelector.prototype.buildOptions = function(options) {
if (this.elements.optionsContainer) {
this.elements.optionsContainer.innerHTML = '';
} else {
return;
}
// Add ARIA property to notify if this is a multi-select or not.
this.elements.optionsContainer.setAttribute('aria-multiselectable',
this._currentPickerType !== 'select-one');
options.forEach(function(option) {
if (option.group) {
this.elements.optionsContainer.insertAdjacentHTML('beforeend',
Sanitizer.unwrapSafeHTML(this.groupView({
text: option.text
})));
} else {
this.elements.optionsContainer.insertAdjacentHTML('beforeend',
Sanitizer.unwrapSafeHTML(this.optionView({
index: option.optionIndex.toString(10),
checked: option.selected.toString(),
labelFor: 'gaia-option-' + option.optionIndex,
text: option.text
})));
}
}, this);
// Apply different style when the options are more than 1 page
this.elements.container.classList.toggle('scrollable', options.length > 5);
// Change the title for multiple select
var titleL10nId = this._currentPickerType === 'select-one' ?
'choose-option' : 'choose-options';
if (this.elements.optionsTitle) {
this.elements.optionsTitle.dataset.l10nId = titleL10nId;
}
};
ValueSelector.prototype.showTimePicker =
function vs_showTimePicker(currentValue) {
this._currentPickerType = 'time';
this.showPanel('timePickerPopup');
if (!this._timePicker) {
this._timePicker = new TimePicker(this.elements.timePickerContainer);
}
var time;
if (!currentValue) {
var now = new Date();
time = {
hours: now.getHours(),
minutes: now.getMinutes()
};
} else {
time = InputParser.importTime(currentValue);
}
// Set the value of time picker according to the current value
if (this._timePicker.is12hFormat) {
var hour = (time.hours % 12);
hour = (hour === 0) ? 12 : hour;
// 24-hour state value selector: AM = 0, PM = 1
var hour24State = (time.hours >= 12) ? 1 : 0;
this._timePicker.hour.setSelectedIndex(hour - 1);
this._timePicker.hour24State.setSelectedIndex(hour24State);
} else {
this._timePicker.hour.setSelectedIndex(time.hours);
}
this._timePicker.minute.setSelectedIndex(time.minutes);
};
ValueSelector.prototype.showDatePicker =
function vs_showDatePicker(currentValue, min, max) {
this._currentPickerType = 'date';
this.showPanel('spinDatePickerPopup');
var minDate = min ? InputParser.formatInputDate(min) : null;
var maxDate = max ? InputParser.formatInputDate(max) : null;
if (!this._datePicker) {
this._datePicker = new SpinDatePicker(this.elements.spinDatePicker);
}
this._datePicker.setRange(minDate, maxDate);
// Show current date as default value
var date = new Date();
if (currentValue) {
date = InputParser.formatInputDate(currentValue);
}
this._datePicker.value = date;
};
function TimePicker(element) {
this.element = element;
this._fetchElements();
var formatter = Intl.DateTimeFormat(navigator.languages, {
hour12: navigator.mozHour12,
hour: 'numeric'
});
var is12hFormat = formatter.resolvedOptions().hour12;
var startHour = is12hFormat ? 1 : 0;
var endHour = is12hFormat ? (startHour + 12) : (startHour + 12 * 2);
var unitClassName = 'picker-unit';
this.is12hFormat = is12hFormat;
var hourFormatter = Intl.NumberFormat(navigator.languages, {
style: 'decimal',
});
this.hour = new ValuePicker(this.elements.valuePickerHours, {
optionsL10n:
this._setDisplayedText(startHour, endHour, function(value) {
return { raw: hourFormatter.format(value) };
}
),
className: unitClassName
});
var minuteFormatter = Intl.NumberFormat(navigator.languages, {
style: 'decimal',
minimumIntegerDigits: 2
});
this.minute = new ValuePicker(this.elements.valuePickerMinutes, {
optionsL10n: this._setDisplayedText(0, 60, function(value) {
return { raw: minuteFormatter.format(value) };
}),
className: unitClassName
});
if (is12hFormat) {
this.hour24State = new ValuePicker(this.elements.valuePickerHour24State, {
optionsL10n: [
'time_am',
'time_pm'
],
className: unitClassName
});
}
this.elements.hoursMinutesSeparator.setAttribute('data-l10n-id',
'hourMinutesSeparator');
this.setTimePickerStyle();
this._registerEvents();
}
TimePicker.prototype = {
_fetchElements: function tp__fetchElements() {
this.elements = {};
this.elementClasses = ['value-picker-hours', 'value-picker-minutes',
'value-picker-hour24-state', 'hours-minutes-separator'];
// Loop and add element with camel style name to Time Picker attribute.
this.elementClasses.forEach(function createElementRef(name) {
this.elements[toCamelCase(name)] =
this.element.querySelector('.' + name);
}, this);
},
_registerEvents: function tp__registerEvents() {
// Prevent focus being taken away by us for time picker.
// The event listener on outer box will not be triggered cause there is a
// evt.stopPropagation() in value_picker.js
['valuePickerHours', 'valuePickerMinutes',
'valuePickerHour24State'].forEach(function(elementId) {
this.elements[elementId].addEventListener('mousedown', this);
}, this);
},
handleEvent: function tp_handleEvent(evt) {
evt.preventDefault();
},
_unregisterEvents: function tp__unregisterEvents() {
['valuePickerHours', 'valuePickerMinutes',
'valuePickerHour24State'].forEach(function(elementId) {
this.elements[elementId].removeEventListener('mousedown', this);
}, this);
},
_setDisplayedText: function tp__setDisplayedText(min, max, format) {
var list = [];
for (var i = min; i < max; ++i) {
list.push(format ? format(i) : i);
}
return list;
},
uninit: function tp_uninit() {
this._unregisterEvents();
this.minute.uninit();
this.hour.uninit();
if (this.hour24State) {
this.hour24State.uninit();
}
},
setTimePickerStyle: function tp_setTimePickerStyle() {
navigator.mozL10n.formatValue('timePickerOrder').then(tpOrder => {
var style = 'format24h';
if (this.is12hFormat) {
// handle revert appearance
var reversedPeriod =
(tpOrder.indexOf('p') < tpOrder.indexOf('M'));
style = (reversedPeriod) ? 'format12hrev' : 'format12h';
if ('format12h' === style) {
this.element.classList.remove('format12hrev');
this.element.classList.remove('format24h');
if (!this.element.classList.contains(style)) {
this.element.classList.add(style);
}
} else {
this.element.classList.remove('format12h');
this.element.classList.remove('format24h');
if (!this.element.classList.contains(style)) {
this.element.classList.add(style);
}
}
}
if ('format24h' === style) {
this.element.classList.remove('format12h');
this.element.classList.remove('format12hrev');
if (!this.element.classList.contains(style)) {
this.element.classList.add(style);
}
}
});
},
getHour: function tp_getHours() {
var hour = this.hour.getSelectedIndex();
if (this.is12hFormat) { // hour + 12 if is PM
hour = (hour == 11) ? 0 : hour + 1;
if (this.hour24State.getSelectedIndex()) {
hour += 12;
}
}
return hour;
},
getTimeValue: function tp_getTimeValue() {
var hour = this.getHour();
var minute = this.minute.getSelectedIndex();
var date = new Date(0);
date.setHours(hour);
date.setMinutes(minute);
// For <input type=time> we have to build a time string
// matching RFC3339 spec: HH:mm(:ss)
return date.toLocaleString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit'
});
}
};
})(window);