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,229 @@
define(function(require) {
'use strict';
var App = require('app');
var alarmDatabase = require('alarm_database');
var Timer = require('timer');
var Utils = require('utils');
var ChildWindowManager = require('./child_window_manager');
var PostMessageProxy = require('./post_message_proxy');
/**
* ActiveAlarm handles the system event that fires when an alarm
* goes off. This includes opening an attention window and updating
* an alarm's schedule when the user taps 'snooze'. The interaction
* is mediated through a PostMessageProxy (as `this.ringView`),
* which makes it trivial to interact with a JavaScript object
* hosted in another window.
*/
function ActiveAlarm() {
this.alertWindow = new ChildWindowManager(
window.location.origin + '/onring.html');
// Handle the system's alarm event.
navigator.mozSetMessageHandler('alarm', this.onMozAlarm.bind(this));
window.addEventListener('test-alarm', this.onMozAlarm.bind(this));
// Handle events transparently from the child window.
PostMessageProxy.receive('activeAlarm', this);
this.ringView = PostMessageProxy.create(null, 'ringView');
}
function logForAlarmDebugging() {
// The following logs are included for debugging reported
// problems with alarm firing times: <https://bugzil.la/1052245>
console.log('[Clock] =====================================\n');
console.log('[Clock] Alarm Debug:', JSON.stringify({
'now': new Date().toJSON(),
'tz': new Date().getTimezoneOffset()
}));
alarmDatabase.getAll().then((alarms) => {
console.log('[Clock] ===== Raw IndexedDB Alarm Data: =====\n');
// Logging in a loop to ensure we don't overrun the line buffer:
alarms.forEach(function(a) {
console.log('[Clock] ', JSON.stringify(a.toJSON()));
});
console.log('[Clock] -------------------------------------\n');
});
var request = navigator.mozAlarms.getAll();
request.onsuccess = function() {
console.log('[Clock] ======= Remaining mozAlarms: ========\n');
if (!request.result) {
console.log('[Clock] mozAlarm API invariant failure?');
} else {
request.result.forEach(function(alarm) {
console.log('[Clock] ', JSON.stringify(alarm));
});
}
console.log('[Clock] -------------------------------------\n');
};
request.onerror = function() {
console.error('[Clock] Failed to get list of mozAlarms:', this.error);
};
}
// Log at startup.
logForAlarmDebugging();
// Log periodically when the Clock app is running.
setInterval(logForAlarmDebugging, 10 * 60 * 1000);
ActiveAlarm.prototype = {
/**
* Fired when the system triggers an alarm. We acquire a wake lock
* here to ensure that the system doesn't fall asleep before we
* have a chance to present the attention alert window.
*/
onMozAlarm: function(message) {
// message.detail in only for marionette test.
// We pass it via AlarmActions.fire method.
var data = message.data || message.detail;
data.date = message.date || message.detail.date;
console.log('[Clock] ### ALARM FIRED! ### Details:',
JSON.stringify(message));
logForAlarmDebugging(message);
Utils.safeWakeLock({ timeoutMs: 30000 }, (done) => {
switch (data.type) {
case 'normal':
case 'snooze':
this.onAlarmFired(data, done);
break;
case 'timer':
this.onTimerFired(data, done);
break;
}
});
},
/**
* Add `alert` to the attention screen. The child alert window
* expects to receive any number of alert messages; if the child
* window has not been presented yet, this function opens the
* window before passing along the alert.
*
* An Alert object (which can represent a timer or an alarm)
* adheres to the following structure:
*
* @param {Alert} alert An alert to pass to the child window.
* @param {string} alert.type 'alarm' or 'timer'
* @param {string} [alert.label] Optional label
* @param {string} [alert.sound] Optional filename of a sound to play
* @param {boolean} alert.vibrate True if the alarm should vibrate
* @param {Date} alert.time The time the alert was supposed to fire
* @param {string} [alert.id] The ID of the alert, if type === 'alarm'
*/
popAlert: function(alert) {
this.alertWindow.whenReady(() => {
this.ringView.window = this.alertWindow.childWindow;
this.ringView.addAlert(alert);
});
},
/**
* Handle an alarm firing. Immediately reschedule the alarm for
* its next firing interval (if the alarm was a repeat alarm).
*
* @param {object} message The message as retrieved by mozAlarm
* @param {function} done Callback to release the wake lock.
*/
onAlarmFired: function(data, done) {
var id = data.id;
var date = data.date;
var type = data.type;
alarmDatabase.get(id).then((alarm) => {
this.popAlert({
type: 'alarm',
label: alarm.label,
sound: alarm.sound,
vibrate: alarm.vibrate,
time: date,
hour: alarm.hour,
minute: alarm.minute,
id: alarm.id
});
if (type === 'normal') {
// The alarm instance doesn't yet know that a mozAlarm has
// fired, so we call .cancel() to wipe this mozAlarm ID from
// alarm.registeredAlarms().
alarm.cancel();
if (alarm.isRepeating()) {
alarm.schedule('normal').then(done);
} else {
done();
}
} else if (type === 'snooze') {
// Inform the Alarm instance that a mozAlarm snooze has fired.
alarm.cancel('snooze');
done();
} else {
done();
}
});
},
/**
* Handle a timer firing.
*
* @param {object} message The message as retrieved by mozAlarm
* @param {function} done Callback to release the wake lock.
*/
onTimerFired: function(data, done) {
Timer.getFromStorage((timer) => {
this.popAlert({
type: 'timer',
label: timer.label,
sound: timer.sound,
vibrate: timer.vibrate,
time: new Date(timer.startTime + timer.duration)
});
done();
});
},
/**
* Snooze the given alarm.
*
* @param {string} alarmId The ID of the alarm.
*/
snoozeAlarm: function(alarmId) {
alarmDatabase.get(alarmId).then((alarm) => {
alarm.schedule('snooze');
});
},
/**
* Close the current alert window.
*
* @param {string} type 'alarm' or 'timer'
* @param {string} alarmId The ID of the alarm, if type === 'alarm'
*/
close: function(type, alarmId) {
this.alertWindow.close();
if (type === 'timer') {
App.navigate({ hash: '#timer-panel' });
Timer.singleton(function(err, timer) {
if (!err) {
timer.cancel();
timer.save();
}
});
} else if (type === 'alarm') {
App.navigate({ hash: '#alarm-panel' });
}
}
};
return ActiveAlarm;
});

View file

@ -0,0 +1,211 @@
define(function(require) {
'use strict';
/* global IntlHelper */
var Banner = require('banner/main');
var alarmDatabase = require('alarm_database');
var Utils = require('utils');
var App = require('app');
var alarmTemplate = require('tmpl!panels/alarm/list_item.html');
var AsyncQueue = require('async_queue');
/**
* AlarmListPanel displays the list of alarms on the Clock tab.
*/
function AlarmListPanel(element) {
this.alarms = element;
this.newAlarmButton.addEventListener(
'click', this.onClickNewAlarm.bind(this));
this.alarms.addEventListener('click', this.onClickAlarmItem.bind(this));
this.banner = new Banner('banner-countdown');
alarmDatabase.getAll().then((alarms) => {
for (var i = 0; alarms && i < alarms.length; i++) {
this.addOrUpdateAlarm(alarms[i]);
}
this.updateAlarmStatusBar();
App.alarmListLoaded();
});
// This will be fired on language and timeformat changes
IntlHelper.observe('time-html', this.refreshDisplay.bind(this));
window.addEventListener('alarm-changed', (evt) => {
var alarm = evt.detail.alarm;
this.addOrUpdateAlarm(alarm);
if (evt.detail.showBanner) {
this.banner.show(alarm.getNextAlarmFireTime());
}
this.updateAlarmStatusBar();
});
window.addEventListener('alarm-removed', (evt) => {
this.removeAlarm(evt.detail.alarm);
this.updateAlarmStatusBar();
});
}
AlarmListPanel.prototype = {
alarmIdMap: {},
refreshDisplay: function(evt) {
for (var key in this.alarmIdMap) {
var alarm = this.alarmIdMap[key];
this.addOrUpdateAlarm(alarm);
}
},
onClickNewAlarm: function(evt) {
evt.preventDefault();
App.navigate({ hash: '#alarm-edit-panel', data: null });
},
onClickAlarmItem: function(evt) {
var link = evt.target;
var alarm = this.alarmIdMap[link.dataset.id];
if (link.classList.contains('input-enable')) {
this.toggleAlarm(alarm, link.checked);
} else if (link.classList.contains('alarm-item')) {
App.navigate({ hash: '#alarm-edit-panel', data: alarm });
evt.preventDefault();
}
},
/**
* Render an alarm into a DOM node.
*
* @param alarm The alarm to render.
* @param {Element} [li] Existing element to re-use, if any.
*/
renderAlarm: function(alarm) {
var li = (this.alarms.querySelector('#alarm-' + alarm.id) ||
alarmTemplate.cloneNode(true));
var isActive = ('normal' in alarm.registeredAlarms ||
'snooze' in alarm.registeredAlarms);
var d = new Date();
d.setHours(alarm.hour);
d.setMinutes(alarm.minute);
li.id = 'alarm-' + alarm.id;
li.dataset.id = alarm.id;
var enableButton = li.querySelector('.input-enable');
enableButton.dataset.id = alarm.id;
enableButton.checked = isActive;
var link = li.querySelector('.alarm-item');
link.classList.toggle('with-repeat', alarm.isRepeating());
link.dataset.id = alarm.id;
li.querySelector('.time').innerHTML = Utils.getLocalizedTimeHtml(d);
if (alarm.label) {
li.querySelector('.label').removeAttribute('data-l10n-id');
li.querySelector('.label').textContent = alarm.label;
} else {
li.querySelector('.label').setAttribute('data-l10n-id', 'alarm');
}
if (alarm.isRepeating()) {
Utils.summarizeDaysOfWeek(alarm.repeat).then(l10nId => {
if (typeof l10nId === 'string') {
li.querySelector('.repeat').setAttribute('data-l10n-id', l10nId);
} else if (l10nId.raw) {
li.querySelector('.repeat').removeAttribute('data-l10n-id');
li.querySelector('.repeat').textContent = l10nId.raw;
}
});
} else {
li.querySelector('.repeat').removeAttribute('data-l10n-id');
li.querySelector('.repeat').textContent = '';
}
return li;
},
refreshClockView: function() {
window.dispatchEvent(new CustomEvent('alarm-list-changed'));
},
addOrUpdateAlarm: function(alarm) {
this.alarmIdMap[alarm.id] = alarm;
var li = this.renderAlarm(alarm);
var liId = parseInt(li.dataset.id, 10) || null;
// Go through the list of existing alarms, inserting this alarm
// before the first alarm that has a lower ID than this one.
var node = this.alarms.firstChild;
while (true) {
var nodeId = (node ? parseInt(node.dataset.id, 10) : -1);
if (nodeId < liId) {
this.alarms.insertBefore(li, node);
break;
}
node = node.nextSibling;
}
this.refreshClockView();
},
removeAlarm: function(alarm) {
delete this.alarmIdMap[alarm.id];
var li = this.alarms.querySelector('#alarm-' + alarm.id);
if (li) {
li.parentNode.removeChild(li);
}
this.refreshClockView();
},
toggleAlarmQueue: new AsyncQueue(),
/**
* Toggle an alarm's enabled state. To ensure that the database
* state remains consistent with the DOM, perform operations
* serially in a queue.
*
* @param {Alarm} alarm
* @param {boolean} enabled
* @param {function} callback Optional callback.
*/
toggleAlarm: function(alarm, enabled) {
this.toggleAlarmQueue.push((done) => {
if (enabled) {
alarm.schedule('normal').then(() => {
this.addOrUpdateAlarm(alarm);
this.updateAlarmStatusBar();
if (alarm.isEnabled()) {
this.banner.show(alarm.getNextAlarmFireTime());
}
}).then(done, done);
} else {
alarm.cancel().then(done);
}
});
},
updateAlarmStatusBar: function() {
if (navigator.mozSettings) {
var anyAlarmEnabled = false;
for (var id in this.alarmIdMap) {
if (this.alarmIdMap[id].isEnabled()) {
anyAlarmEnabled = true;
break;
}
}
navigator.mozSettings.createLock().set({
'alarm.enabled': anyAlarmEnabled
});
}
}
};
Utils.extendWithDomGetters(AlarmListPanel.prototype, {
title: '#alarms-title',
newAlarmButton: '#alarm-new'
});
return AlarmListPanel;
});

View file

@ -0,0 +1,103 @@
define(function(require) {
'use strict';
var Utils = require('utils');
const READY_EVENT_TYPE = 'childWindowReady';
/**
* ChildWindowManager maintains the lifecycle of a child attention
* window. When you instantiate a ChildWindowManager, no window
* exists yet; use .whenReady() or postMessage() to call up the
* child window and perform actions on it. Provided the child window
* calls ChildWindowManager.fireReady(), this class takes care of
* the bookkeeping to make sure that the child window is ready to
* receive events before sending over any messages. Similarly, this
* class acquires a CPU wake lock when opening the window to ensure
* that your messages are delivered reliably without the phone going
* to sleep while waiting for the child window to receive a message.
*/
function ChildWindowManager(url) {
this.url = url;
this.childWindow = null;
this.childWindowReady = false;
this.childOnReadyCallbacks = [];
this.releaseCpuLock = null;
window.addEventListener('message', this);
}
ChildWindowManager.prototype = {
/**
* Post a message to the child window. If the window is not yet
* open, acquire a CPU wake lock and open the window, then deliver
* the message. Subsequent calls to postMessage will similarly
* wait until the window is ready before delivery.
*/
postMessage: function(message) {
this.whenReady(() => {
this.childWindow.postMessage(message, window.location.origin);
});
},
/**
* Closes the window. You may reinstantiate a window again by
* sending another postMessage.
*/
close: function() {
if (this.childWindow && !this.childWindow.closed) {
this.childWindow.close();
}
this.childWindow = null;
},
/**
* Call a function when the window is ready and opened. This is
* used internally by postMessage.
*/
whenReady: function(callback) {
if (!this.childWindow || this.childWindow.closed) {
Utils.safeWakeLock({ timeoutMs: 30000 }, (releaseCpu) => {
this.childWindow = window.VulpesCompat ? VulpesCompat.openAttention(this.url) : window.open(this.url, '_blank', 'attention');
this.childWindowReady = false;
this.releaseCpuLock = releaseCpu;
});
}
if (this.childWindowReady) {
callback();
} else {
this.childOnReadyCallbacks.push(callback);
}
},
/** Private. Handle DOM events. */
handleEvent: function(evt) {
if (evt.data.type === READY_EVENT_TYPE) {
this.childWindowReady = true;
while (this.childOnReadyCallbacks.length) {
var callback = this.childOnReadyCallbacks.shift();
callback();
}
if (this.releaseCpuLock) {
this.releaseCpuLock();
}
}
}
};
/**
* Call this method from a child window when the child has loaded.
* This fires a message to the parent window, instructing it to pass
* along any queued events.
*/
ChildWindowManager.fireReady = function() {
if (!window.opener) {
throw new Error('fireReady must be called from the child window.');
}
window.opener.postMessage({
type: READY_EVENT_TYPE
}, window.location.origin);
};
return ChildWindowManager;
});

View file

@ -0,0 +1,336 @@
define(function(require) {
'use strict';
/* global IntlHelper */
var asyncStorage = require('shared/js/async_storage');
var Utils = require('utils');
var SETTINGS_CLOCKMODE = 'settings_clockoptions_mode';
var viewMode = null;
IntlHelper.define('date-long', 'mozdatetime', {
weekday: 'long',
month: 'long',
day: 'numeric'
});
// Retrieve stored view mode data as early as possible.
asyncStorage.getItem(SETTINGS_CLOCKMODE, function(value) {
// If no value has been stored, don't update
// the viewMode closure.
if (value === null) {
return;
}
// If the ClockView hasn't initialized yet,
// and the stored value is different from
// the arbitrarily chosen default view (analog)
// then update the viewMode closure.
if (!ClockView.isInitialized && viewMode !== value) {
viewMode = value;
}
});
var ClockView = {
get mode() {
// Closure value, stored in settings,
// or the default (analog)
return viewMode;
},
set mode(value) {
// If the `mode` is being updated to a new value:
//
// - Update the viewMode closure
// - Store the new value in persistent data storage
//
// Always return `value`
if (viewMode !== value) {
viewMode = value;
asyncStorage.setItem(
SETTINGS_CLOCKMODE, value
);
}
return viewMode;
},
timeouts: {
analog: null,
dayDate: null,
digital: null
},
get digital() {
delete this.digital;
return (this.digital = document.getElementById('digital-clock'));
},
get analog() {
delete this.analog;
return (this.analog = document.getElementById('analog-clock'));
},
get time() {
delete this.time;
return (this.time = document.getElementById('clock-time'));
},
get dayDate() {
delete this.dayDate;
return (this.dayDate = document.getElementById('clock-day-date'));
},
get container() {
delete this.container;
return (this.container =
document.getElementById('analog-clock-container'));
},
isInitialized: false,
init: function cv_init() {
var handler = this.handleEvent.bind(this);
document.addEventListener('visibilitychange', handler);
this.analog.addEventListener('click', handler, false);
this.digital.addEventListener('click', handler, false);
window.addEventListener('alarm-list-changed',
this.resizeAnalogClock.bind(this));
this.hands = {};
['second', 'minute', 'hour'].forEach(function(hand) {
this.hands[hand] = document.getElementById(hand + 'hand');
}, this);
// Kick off the day date display (upper left string)
this.updateDayDate();
// This will get fired on language change
IntlHelper.observe('date-long', this.updateDayDate.bind(this));
// Those two will get fired on language and timeformat changes
IntlHelper.observe('time-html', this.updateDigitalClock.bind(this));
IntlHelper.observe('time-text', this.updateAnalogClock.bind(this));
// If the attempt to request and set the viewMode
// closure early has failed to respond before the
// call to ClockView.init(), make an async request,
// passing the response value as an argument to this.show()
if (this.mode === null) {
asyncStorage.getItem(
SETTINGS_CLOCKMODE, this.show.bind(this)
);
} else {
// Display the clock face
this.show();
}
this.isInitialized = true;
},
updateDayDate: function cv_updateDayDate() {
var d = new Date();
var f = IntlHelper.get('date-long');
var remainMillisecond = (24 - d.getHours()) * 3600 * 1000 -
d.getMinutes() * 60 * 1000 -
d.getMilliseconds();
// If the date of the month is part of the locale format as a
// number, insert bold tags to accentuate the number itself.
var dateString = f.format(d, {
day: '<b>$&</b>'
});
this.dayDate.innerHTML = dateString;
this.timeouts.dayDate = setTimeout(
this.updateDayDate.bind(this), remainMillisecond
);
},
update: function cv_update(opts) {
opts = opts || {};
if (this.mode === 'digital') {
this.updateDigitalClock(opts);
} else {
this.updateAnalogClock(opts);
}
},
updateDigitalClock: function cv_updateDigitalClock(opts) {
opts = opts || {};
// this function may be called manually, reset the timer
clearTimeout(this.timeouts.digital);
var d = new Date();
this.time.innerHTML = Utils.getLocalizedTimeHtml(d);
this.timeouts.digital = setTimeout(
this.updateDigitalClock.bind(this), (60 - d.getSeconds()) * 1000
);
},
updateAnalogClock: function cv_updateAnalogClock(opts) {
opts = opts || {};
// this function may be called manually, reset the timer
clearTimeout(this.timeouts.analog);
if (opts.needsResize) {
this.resizeAnalogClock();
}
var now = new Date();
var sec, min, hour;
sec = now.getSeconds();
min = now.getMinutes();
// hours progress gradually
hour = (now.getHours() % 12) + min / 60;
this.setTransform('second', sec);
this.setTransform('minute', min);
this.setTransform('hour', hour);
// Update aria label for analog view.
this.container.setAttribute('aria-label', Utils.getLocalizedTimeText(now));
// update again in one second
this.timeouts.analog = setTimeout(
this.updateAnalogClock.bind(this), 1000 - now.getMilliseconds()
);
},
setTransform: function cv_setTransform(id, angle) {
var hand = this.hands[id];
// return correct angle for different hands
function conv(timeFrag) {
var mult;
// generate a conformable number to rotate about
// 30 degrees per hour 6 per second and minute
mult = id === 'hour' ? 30 : 6;
// we generate the angle from the fractional sec/min/hour
return (timeFrag * mult);
}
// Use transform rotate on the rect itself vs on a child element
// avoids unexpected behavior if either dur and fill are set to defaults
// Use translateZ to force it on its own layer, which will invoke the GPU
// and thus do the minimum amount of work required (reduces power usage)
hand.style.transform = 'rotate(' + conv(angle) + 'deg) translateZ(1px)';
},
handleEvent: function cv_handleEvent(event) {
var newMode, target;
switch (event.type) {
case 'visibilitychange':
if (document.hidden) {
if (this.timeouts.dayDate) {
clearTimeout(this.timeouts.dayDate);
}
if (this.timeouts.digital) {
clearTimeout(this.timeouts.digital);
}
if (this.timeouts.analog) {
clearTimeout(this.timeouts.analog);
}
return;
} else if (!document.hidden) {
// Refresh the view when app return to foreground.
this.updateDayDate();
this.update();
}
break;
case 'click':
target = event.target;
if (!target) {
return;
}
if (this.digital.contains(target) ||
target.id === 'digital-clock') {
newMode = 'analog';
}
if (this.analog.contains(target) ||
target.id === 'analog-clock') {
newMode = 'digital';
}
if (newMode) {
this.show(newMode);
}
break;
}
},
calAnalogClockType: function cv_calAnalogClockType(count) {
var type = 'small';
if (count < 2) {
type = 'large';
} else if (count === 2) {
type = 'medium';
}
return type;
},
getAlarmCount: function() {
return document.querySelectorAll('#alarms .alarm-item').length;
},
resizeAnalogClock: function cv_resizeAnalogClock() {
var type = this.calAnalogClockType(this.getAlarmCount());
this.container.className = type;
document.getElementById('alarms').className = 'count' + type;
},
show: function cv_show(mode) {
var isAnalog = false;
var previous, hiding, showing;
if (location.hash !== '#alarm-panel') {
location.hash = '#alarm-panel';
}
// The clock display mode is either
//
// - Explicitly passed as the mode param
// - Set as a property of ClockView
// - Default to "analog"
//
mode = mode || this.mode || 'analog';
isAnalog = mode === 'analog';
// Determine what to hide and what to show
previous = isAnalog ? 'digital' : 'analog';
hiding = isAnalog ? this.digital : this.analog;
showing = isAnalog ? this.analog : this.digital;
// Clear any previously created timeouts.
if (this.timeouts[previous]) {
clearTimeout(
this.timeouts[previous]
);
}
hiding.classList.remove('visible');
showing.classList.add('visible');
// Update the locally stored `mode`.
this.mode = mode;
// This MUST be called after this.mode is set
// to ensure that the correct mode is used for
// updating the clockface
this.update({
needsResize: true
});
}
};
return ClockView;
});

View file

@ -0,0 +1,11 @@
<li class="alarm-cell">
<label class="alarmList alarmEnable">
<input class="input-enable" data-id="" type="checkbox">
<span></span>
</label>
<a href="#alarm-edit-panel" class="alarm-item" data-id="">
<span class="time"></span>
<span class="label"></span>
<span class="repeat"></span>
</a>
</li>

View file

@ -0,0 +1,22 @@
define(function(require) {
'use strict';
var Panel = require('panel');
var ClockView = require('panels/alarm/clock_view');
var AlarmListPanel = require('panels/alarm/alarm_list');
var ActiveAlarm = require('panels/alarm/active_alarm');
var html = require('text!panels/alarm/panel.html');
function AlarmPanel() {
Panel.apply(this, arguments);
this.element.innerHTML = html;
ClockView.init();
this.alarmListPanel = new AlarmListPanel(document.getElementById('alarms'));
this.activeAlarm = new ActiveAlarm();
}
AlarmPanel.prototype = Object.create(Panel.prototype);
return AlarmPanel;
});

View file

@ -0,0 +1,26 @@
<div id="clock-view">
<div id="analog-clock">
<div id="analog-clock-container" role="img">
<div id="analog-clock-face">
<div id="analog-clock-hands">
<div class="analog-clock-hand" id="secondhand"></div>
<div class="analog-clock-hand" id="minutehand"></div>
<div class="analog-clock-hand" id="hourhand"></div>
</div>
</div>
</div>
</div>
<div id="digital-clock">
<div id="digital-clock-face">
<span id="clock-time"></span>
</div>
</div>
<div id="clock-day-date"></div>
<!-- create new alarm icon -->
<button id="alarm-new" data-l10n-id="newAlarmButton"></button>
<!-- list of exisiting alarms, to be populated -->
<ul id="alarms"></ul>
</div>
<section id="banner-countdown" role="status">
<!-- this will be replaced dynamically -->
</section>

View file

@ -0,0 +1,103 @@
'use strict';
define(function(require) {
var objectMap = {}; // Map of object handlers.
var responseMap = {}; // Map of callbacks awaiting function results.
var sequenceNumber = 0; // ID for matching requests to responses.
var PostMessageProxy = {
/**
* Create a PostMessageProxy. This returns an object that you can
* call like any other JavaScript object, by proxying methods
* through to another window.
*
* When you call a method, rather than returning its return value,
* it reutrns a Promise that you can resolve to get the return
* value from the remote function.
*
* @param whichWindow The window (usually window.parent or a child)
* @param objectId A string to identify the object you're proxying
*/
create: function(whichWindow, objectId) {
return new Proxy({ window: whichWindow }, {
get: function(target, name) {
if (name in target) {
return target[name];
}
return function() {
var args = Array.slice(arguments);
return new Promise((resolve, reject) => {
var responseId = ++sequenceNumber;
responseMap[responseId] = {
resolve: resolve,
reject: reject
};
target.window.postMessage({
postMessageProxy: objectId,
responseId: responseId,
fn: name,
args: args
}, '*');
});
};
}
});
},
/**
* On the other window, call PostMessateProxy.receive() to hook up
* an object that processes messages from a proxy in another window.
*/
receive: function(objectId, obj) {
objectMap[objectId] = obj;
},
/**
* Handle 'message' events from postMessage, both when receiving a
* message to invoke a function call, and receiving a message with
* the return value of that function call. Both callbacks are
* handled here so that we only bind one listener for each
* relevant window.
*/
_messageHandler: function(evt) {
if (!evt.source || evt.origin !== window.location.origin || !evt.data) return;
var data = evt.data;
if (data.postMessageProxy) {
// Remote side (calling a function):
var obj = objectMap[data.postMessageProxy];
var { fn, args, responseId } = data;
try {
evt.source.postMessage({
postMessageProxyResult: responseId,
result: obj[fn].apply(obj, args)
}, window.location.origin);
} catch(e) {
evt.source.postMessage({
postMessageProxyResult: responseId,
exception: e.toString(),
stack: e.stack
}, evt.origin);
}
} else if (data.postMessageProxyResult) {
// Local side (return value):
if (responseMap[data.postMessageProxyResult]) {
var { resolve, reject } = responseMap[data.postMessageProxyResult];
delete responseMap[data.postMessageProxyResult];
if (data.exception) {
var e = new Error();
e.name = data.exception;
e.stack = data.stack;
reject(e);
} else {
resolve(data.result);
}
}
}
}
};
window.addEventListener('message', PostMessageProxy._messageHandler);
return PostMessageProxy;
});