VulpesOS/gaia/tv_apps/smart-system/js/interactive_notifications.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

337 lines
10 KiB
JavaScript
Executable file

/* global IACHandler, SimpleKeyNavigation, KeyEvent, focusManager */
(function(exports) {
'use strict';
function $(id) {
return document.getElementById(id);
}
var TYPE = {
'NORMAL': 'notification',
'ALERT': 'alert-notification'
};
// =============== IAC Connector ====================================
/**
* IACConnector bridges notifications from IAC to InteractiveNotifications.
* It only handles bridge staffes. All queuing features and priority features
* are handled by InteractiveNotifications.
**/
function IACConnector() {
}
var iacProto = IACConnector.prototype;
iacProto.start = function iac_start(notifier) {
window.addEventListener('iac-interactivenotifications', this);
this.notifier = notifier;
};
iacProto.stop = function iac_start() {
window.removeEventListener('iac-interactivenotifications', this);
};
iacProto.handleEvent = function iac_handleEvent(evt) {
var port = IACHandler.getPort('interactivenotifications');
var detail = evt.detail;
detail.message.onClosed = (function(button) {
port.postMessage({
'action': 'notification-closed',
'id': detail.id,
'type': detail.type,
'message': detail.message,
'button': button
});
}).bind(this);
this.notifier.showNotification(detail.type, detail.message);
};
// =============== Interacive Notifications ==========================
function InteractiveNotifications() {
this._activeMessage = null;
this._activeType = null;
this._activeTimeout = 0;
this._pendingMessages = {};
this._pendingMessages[TYPE.NORMAL] = [];
this._pendingMessages[TYPE.ALERT] = [];
this._banner = $('notification-container');
this._banner.setAttribute('aria-hidden', 'true');
this._banner.tabIndex = -1;
focusManager.addUI(this);
}
InteractiveNotifications.TYPE = Object.freeze(TYPE);
var proto = InteractiveNotifications.prototype;
proto.start = function in_start() {
this._iacConnector = new IACConnector();
this._iacConnector.start(this);
// This handles left and right. We start this module only when we really
// need it.
this._keyNavigator = new SimpleKeyNavigation();
this.initEvents();
};
proto.stop = function in_stop() {
this._iacConnector.stop();
};
proto.getAutoHideTimeout = function in_getAutoHideTimeout(type, hasButtons) {
if (TYPE.NORMAL === type) {
return hasButtons ? 8000 : 5000;
} else {
return 0;
}
};
proto.initEvents = function in_initEvents() {
var self = this;
['notification-button-0', 'notification-button-1'].forEach(function(b) {
$(b).addEventListener('click', self);
});
// We don't need mozbrowserkeyXXX here because we change the focus back to
// system app while a notification is shown. And there is no need to handle
// back key in other cases.
window.addEventListener('keyup', this);
this._banner.addEventListener('hidden', this);
this._banner.on('opened', this._focusNotification.bind(this));
};
proto.handleEvent = function in_handleEvent(e) {
switch(e.type) {
case 'click':
switch(e.target.id) {
case 'notification-button-0':
case 'notification-button-1':
this.hideNotification(this._activeType, this._activeMessage,
e.target.dataset.buttonId);
break;
}
break;
case 'keyup':
this._handleKeyEvent(e);
break;
case 'hidden':
this.onHide();
this._banner.classList.add('hidden');
this._showPendings();
break;
}
};
proto._handleKeyEvent = function in_handleKeyEvent(e) {
switch(e.keyCode) {
case KeyEvent.DOM_VK_BACK_SPACE:
if (this._activeMessage) {
this.hideNotification(this._activeType, this._activeMessage);
}
break;
}
},
proto._updateNotificationUI = function in_updateNotificationUI(type, msg) {
var banner = this._banner;
var title = $('notification-title');
var text = $('notification-body');
var buttonGroup = $('notification-button-group');
var buttonImg;
var buttons = [$('notification-button-0'), $('notification-button-1')];
banner.dataset.type = type;
// XXX: We don't have default icon for notification. Change to correct one
// once we have it.
banner.style.backgroundImage = msg.icon ? 'url("' + msg.icon + '")':
'url("/style/icons/system_84.png")';
title.textContent = msg.title ? msg.title : '';
if (msg.title && msg.title.id) {
navigator.mozL10n.setAttributes(
title,
msg.title.id,
msg.title.args
);
} else {
title.removeAttribute('data-l10n-id');
title.removeAttribute('data-l10n-args');
title.textContent = msg.title || '';
}
if (msg.text && msg.text.id) {
navigator.mozL10n.setAttributes(
text,
msg.text.id,
msg.text.args
);
} else {
text.removeAttribute('data-l10n-id');
text.removeAttribute('data-l10n-args');
text.textContent = msg.text || '';
}
banner.classList[msg.title ? 'add' : 'remove']('has-title');
if (msg.buttons && msg.buttons.length) {
buttonGroup.classList.remove('hidden');
// We only have two buttons at most.
for (var i = 0; i < buttons.length; i++) {
if (msg.buttons[i]) {
buttons[i].dataset.buttonId = msg.buttons[i].id;
buttons[i].innerHTML = '';
if (msg.buttons[i].icon) {
buttonImg = document.createElement('img');
buttonImg.src = msg.buttons[i].icon;
buttons[i].appendChild(buttonImg);
}
if (msg.buttons[i].label) {
buttons[i].textContent = msg.buttons[i].label;
}
buttons[i].classList.remove('hidden');
} else {
buttons[i].classList.add('hidden');
}
}
} else {
buttonGroup.classList.add('hidden');
}
banner.classList.remove('hidden');
banner.flyOpen();
};
proto._focusNotification = function in_focusNotification() {
this._banner.removeAttribute('aria-hidden');
if (this._activeMessage.buttons && this._activeMessage.buttons.length > 0) {
// KeyNavigator will auto-focus the first item at the start. It calls
// focus() for us when the focus switches to another button.
this._keyNavigator.start([$('notification-button-0'),
$('notification-button-1')],
SimpleKeyNavigation.DIRECTION.HORIZONTAL,
{target: this._banner});
}
focusManager.focus();
};
proto.focus = function in_focus() {
if (this.isFocusable()) {
document.activeElement.blur();
this._activeMessage.buttons ?
this._keyNavigator.focus() : this._banner.focus();
}
};
proto.isFocusable = function in_isFocusable() {
return !!this._activeMessage &&
this._activeMessage.buttons && this._activeMessage.buttons.length;
};
proto.getElement = function in_getElement() {
if (this.isFocusable()) {
return this._banner;
}
};
proto.showNotification = function in_showNotification(type, msg) {
if (type === TYPE.ALERT &&
this._activeType === TYPE.ALERT) {
// already have one, just pending it.
this._pendingMessages[TYPE.ALERT].push(msg);
} else if (type === TYPE.NORMAL &&
(this._activeType === TYPE.NORMAL ||
this._activeType === TYPE.ALERT)) {
// already have one, just pending it.
this._pendingMessages[TYPE.NORMAL].push(msg);
} else if (type === TYPE.ALERT && this._activeMessage) {
// type === alert and _activeType is null or normal.
// We show alert anyway and hide the normal one if one is shown.
window.clearTimeout(this._activeTimeout);
this._activeTimeout = 0;
this._pendingMessages[TYPE.ALERT].push(msg);
this.hideNotification(this._activeType, this._activeMessage);
} else {
var timeout = msg.timeout;
if (!timeout) {
timeout = this.getAutoHideTimeout(type, msg.buttons &&
msg.buttons.length > 0);
}
this._activeType = type;
this._activeMessage = msg;
this._updateNotificationUI(type, msg);
if (timeout > 0) {
this._activeTimeout = window.setTimeout((function autoHide() {
this._activeTimeout = 0;
this.hideNotification(type, msg);
}).bind(this), timeout);
}
}
};
proto._showPendings = function in_showPendings() {
var type, msg;
if (this._pendingMessages[TYPE.ALERT].length) {
type = TYPE.ALERT;
msg = this._pendingMessages[type].shift();
} else if (this._pendingMessages[TYPE.NORMAL].length) {
type = TYPE.NORMAL;
msg = this._pendingMessages[type].shift();
}
if (type && msg) {
this.showNotification(type, msg);
return true;
} else {
return false;
}
};
proto.hasPendings = function in_hasPendings() {
return this._pendingMessages[TYPE.ALERT].length ||
this._pendingMessages[TYPE.NORMAL].length;
};
proto.onHide = function in_onHide() {
if (this._activeMessage.onClosed) {
this._activeMessage.onClosed(this._activeMessage.closedBy);
}
if (this._activeMessage.buttons &&
this._activeMessage.buttons.length > 0) {
// We need to stop KeyNavigator while we don't need it.
this._keyNavigator.stop();
}
if (!this.hasPendings()) {
focusManager.focus();
}
this._activeType = null;
this._activeMessage = null;
};
proto.hideNotification = function in_hideNotification(type, msg, button) {
if (this._activeType === type && this._activeMessage === msg) {
this._banner.setAttribute('aria-hidden', 'true');
this._activeMessage.closedBy = button;
this._banner.hide();
} else {
var queue = this._pendingMessages[type];
var idx = queue.indexOf(msg);
if (idx > -1) {
var removed = queue.splice(idx, 1)[0];
if (removed.onClosed) {
removed.onClosed(button);
}
}
}
focusManager.focus();
};
exports.InteractiveNotifications = InteractiveNotifications;
}(window));