Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
590 lines
18 KiB
JavaScript
Executable file
590 lines
18 KiB
JavaScript
Executable file
'use strict';
|
|
/* global SettingsCache, SettingsListener */
|
|
|
|
(function(exports) {
|
|
|
|
/**
|
|
* Accessibility enables and disables the screenreader after the user
|
|
* gestures using the hardware buttons of the phone. To toggle the setting.
|
|
* the user must press volume up, then volume down three times in a row.
|
|
* @class Accessibility
|
|
* @requires SettingsListener
|
|
*/
|
|
function Accessibility() {}
|
|
|
|
Accessibility.prototype = {
|
|
|
|
/**
|
|
* How fast the autorepeat is.
|
|
* @type {Number}
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
REPEAT_INTERVAL: 600000,
|
|
|
|
/**
|
|
* Maximum interval between initial and final TOGGLE_SCREEN_READER_COUNT
|
|
* volume button presses.
|
|
* @type {Number}
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
REPEAT_BUTTON_PRESS: 15000000,
|
|
|
|
/**
|
|
* Number of times the buttons need to be pressed before the screen reader
|
|
* setting is toggled.
|
|
* @type {Number}
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
TOGGLE_SCREEN_READER_COUNT: 6,
|
|
|
|
/**
|
|
* Cap the full range of contrast. Actual -1 is completely gray, and 1
|
|
* makes things hard to see. This value is the max/min contrast.
|
|
*/
|
|
CONTRAST_CAP: 0.6,
|
|
|
|
/**
|
|
* Current counter for button presses in short succession.
|
|
* @type {Number}
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
counter: 0,
|
|
|
|
/**
|
|
* Next expected event.
|
|
* @type {Object}
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
expectedEvent: {
|
|
type: 'volumeup',
|
|
timeStamp: 0
|
|
},
|
|
|
|
/**
|
|
* Expected complete time stamp.
|
|
* @type {Number}
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
expectedCompleteTimeStamp: 0,
|
|
|
|
/**
|
|
* Accessibility settings to be observed.
|
|
* @type {Object} name: value pairs.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
settings: {
|
|
'accessibility.screenreader': false,
|
|
'accessibility.screenreader-volume': 1,
|
|
'accessibility.screenreader-rate': 0,
|
|
'accessibility.screenreader-captions': false,
|
|
'accessibility.colors.enable': false,
|
|
'accessibility.colors.invert': false,
|
|
'accessibility.colors.grayscale': false,
|
|
'accessibility.colors.contrast': '0.0'
|
|
},
|
|
|
|
/**
|
|
* Audio used by the screen reader.
|
|
* Note: Lazy-loaded when first needed
|
|
* @type {Object}
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
sounds: {
|
|
clickedAudio: null,
|
|
vcKeyAudio: null,
|
|
vcMoveAudio: null
|
|
},
|
|
|
|
/**
|
|
* URLs for screen reader audio files.
|
|
* @type {Object}
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
soundURLs: {
|
|
clickedAudio: './resources/sounds/screen_reader_clicked.ogg',
|
|
vcKeyAudio: './resources/sounds/screen_reader_virtual_cursor_key.ogg',
|
|
vcMoveAudio: './resources/sounds/screen_reader_virtual_cursor_move.ogg'
|
|
},
|
|
|
|
/**
|
|
* Start listening for events.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
start: function ar_init() {
|
|
|
|
this.screen = document.getElementById('screen');
|
|
|
|
this.speechSynthesizer = speechSynthesizer;
|
|
|
|
window.addEventListener('mozChromeEvent', this);
|
|
window.addEventListener('volumeup', this);
|
|
window.addEventListener('volumedown', this);
|
|
window.addEventListener('logohidden', this);
|
|
|
|
// Attach all observers.
|
|
Object.keys(this.settings).forEach(function attach(settingKey) {
|
|
SettingsCache.observe(settingKey, this.settings[settingKey],
|
|
function observe(aValue) {
|
|
this.settings[settingKey] = aValue;
|
|
switch (settingKey) {
|
|
case 'accessibility.screenreader':
|
|
// Show Accessibility panel if it is not already visible
|
|
if (aValue) {
|
|
SettingsListener.getSettingsLock().set({
|
|
'accessibility.screenreader-show-settings': true
|
|
});
|
|
}
|
|
this.screen.classList.toggle('screenreader', aValue);
|
|
break;
|
|
|
|
case 'accessibility.colors.enable':
|
|
SettingsListener.getSettingsLock().set({
|
|
'layers.effect.invert': aValue ?
|
|
this.settings['accessibility.colors.invert'] : false,
|
|
'layers.effect.grayscale': aValue ?
|
|
this.settings['accessibility.colors.grayscale'] : false,
|
|
'layers.effect.contrast': aValue ?
|
|
this.settings['accessibility.colors.contrast'] : '0.0'
|
|
});
|
|
break;
|
|
|
|
case 'accessibility.screenreader-captions':
|
|
this.speechSynthesizer.captions = aValue;
|
|
// If captions are displayed hide them.
|
|
if (!aValue) {
|
|
this.speechSynthesizer.hideSpeech(true);
|
|
}
|
|
break;
|
|
|
|
case 'accessibility.colors.invert':
|
|
case 'accessibility.colors.grayscale':
|
|
case 'accessibility.colors.contrast':
|
|
if (this.settings['accessibility.colors.enable']) {
|
|
var effect = settingKey.split('.').pop();
|
|
var gfxSetting = {};
|
|
if (effect === 'contrast') {
|
|
gfxSetting['layers.effect.contrast'] =
|
|
aValue * this.CONTRAST_CAP;
|
|
} else {
|
|
gfxSetting['layers.effect.' + effect] = aValue;
|
|
}
|
|
SettingsListener.getSettingsLock().set(gfxSetting);
|
|
}
|
|
break;
|
|
}
|
|
}.bind(this));
|
|
}, this);
|
|
},
|
|
|
|
/**
|
|
* Reset the expected event to defaults.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
reset: function ar_resetEvent() {
|
|
this.expectedEvent = {
|
|
type: 'volumeup',
|
|
timeStamp: 0
|
|
};
|
|
this.counter = 0;
|
|
},
|
|
|
|
/**
|
|
* Unset speaking flag and set the expected complete time stamp.
|
|
* @param {?Number} aExpectedCompleteTimeStamp Expected complete time
|
|
* stamp.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
resetSpeaking: function ar_resetSpeaking(aExpectedCompleteTimeStamp) {
|
|
this.isSpeaking = false;
|
|
this.expectedCompleteTimeStamp = aExpectedCompleteTimeStamp || 0;
|
|
},
|
|
|
|
/**
|
|
* Handle volumeup and volumedown events generated from HardwareButtons.
|
|
* @param {Object} aEvent a high-level key event object generated from
|
|
* HardwareButtons.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
handleVolumeButtonPress: function ar_handleVolumeButtonPress(aEvent) {
|
|
var type = aEvent.type;
|
|
var timeStamp = aEvent.timeStamp;
|
|
var expectedEvent = this.expectedEvent;
|
|
if (type !== expectedEvent.type || timeStamp > expectedEvent.timeStamp) {
|
|
this.reset();
|
|
if (type !== 'volumeup') {
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.expectedEvent = {
|
|
type: type === 'volumeup' ? 'volumedown' :
|
|
'volumeup',
|
|
timeStamp: timeStamp + this.REPEAT_INTERVAL
|
|
};
|
|
|
|
if (++this.counter < this.TOGGLE_SCREEN_READER_COUNT) {
|
|
return;
|
|
}
|
|
|
|
this.reset();
|
|
|
|
if (!this.isSpeaking && timeStamp > this.expectedCompleteTimeStamp) {
|
|
this.speechSynthesizer.cancel();
|
|
this.announceScreenReader(function onEnd() {
|
|
this.resetSpeaking(timeStamp + this.REPEAT_BUTTON_PRESS);
|
|
}.bind(this));
|
|
return;
|
|
}
|
|
|
|
this.speechSynthesizer.cancel();
|
|
this.resetSpeaking();
|
|
SettingsListener.getSettingsLock().set({
|
|
'accessibility.screenreader':
|
|
!this.settings['accessibility.screenreader']
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Play audio for a screen reader notification.
|
|
* @param {String} aSoundKey a key for the screen reader audio.
|
|
* XXX: When Bug 848954 lands we should be able to use Web Audio API.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
_playSound: function ar__playSound(aSoundKey) {
|
|
if (!this.sounds[aSoundKey]) {
|
|
this.sounds[aSoundKey] = new Audio(this.soundURLs[aSoundKey]);
|
|
this.sounds[aSoundKey].load();
|
|
}
|
|
var audio = this.sounds[aSoundKey].cloneNode(false);
|
|
audio.volume = this.volume;
|
|
audio.play();
|
|
},
|
|
|
|
/**
|
|
* Get current screen reader volume defined by the setting.
|
|
* @return {Number} Screen reader volume wihtin the [0, 1] interval.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
get volume() {
|
|
return this.settings['accessibility.screenreader-volume'];
|
|
},
|
|
|
|
/**
|
|
* Get current screen reader speech rate defined by the setting.
|
|
* @return {Number} Screen reader rate within the [0.2, 10] interval.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
get rate() {
|
|
var rate = this.settings['accessibility.screenreader-rate'];
|
|
return rate >= 0 ? rate + 1 : 1 / (Math.abs(rate) + 1);
|
|
},
|
|
|
|
/**
|
|
* Handle accessfu mozChromeEvent.
|
|
* @param {Object} accessfu details object.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
handleAccessFuOutput: function ar_handleAccessFuOutput(aDetails) {
|
|
var options = aDetails.options || {};
|
|
window.dispatchEvent(new CustomEvent('accessibility-action'));
|
|
switch (aDetails.eventType) {
|
|
case 'vc-change':
|
|
// Vibrate when the virtual cursor changes.
|
|
navigator.vibrate(options.pattern);
|
|
this._playSound(options.isKey ? 'vcKeyAudio' : 'vcMoveAudio');
|
|
break;
|
|
case 'action':
|
|
if (aDetails.data[0].string === 'clickAction') {
|
|
// If element is clicked, play 'click' sound instead of speech.
|
|
this._playSound('clickedAudio');
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
this.speak(aDetails.data, null, {
|
|
enqueue: options.enqueue
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Remove aria-hidden from the screen element to make content accessible to
|
|
* the screen reader.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
activateScreen: function ar_activateScreen() {
|
|
// Screen reader will not say anything until the splash animation is
|
|
// hidden and the aria-hidden attribute is removed from #screen.
|
|
this.screen.removeAttribute('aria-hidden');
|
|
window.removeEventListener('logohidden', this);
|
|
},
|
|
|
|
/**
|
|
* Handle event.
|
|
* @param {Object} aEvent mozChromeEvent/logohidden/volumeup/volumedown.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
handleEvent: function ar_handleEvent(aEvent) {
|
|
switch (aEvent.type) {
|
|
case 'logohidden':
|
|
this.activateScreen();
|
|
break;
|
|
case 'mozChromeEvent':
|
|
switch (aEvent.detail.type) {
|
|
case 'accessibility-output':
|
|
this.handleAccessFuOutput(JSON.parse(aEvent.detail.details));
|
|
break;
|
|
}
|
|
break;
|
|
case 'volumeup':
|
|
case 'volumedown':
|
|
this.handleVolumeButtonPress(aEvent);
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Based on whether the screen reader is currently enabled, announce the
|
|
* instructions of how to enable/disable it.
|
|
* @param {Function} aCallback A callback after the speech synthesis is
|
|
* completed.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
announceScreenReader: function ar_announceScreenReader(aCallback) {
|
|
var enabled = this.settings['accessibility.screenreader'];
|
|
this.isSpeaking = true;
|
|
this.speak({
|
|
string: enabled ? 'disableScreenReaderSteps' : 'enableScreenReaderSteps'
|
|
}, aCallback, {enqueue: false});
|
|
},
|
|
|
|
/**
|
|
* Use speechSynthesis to speak screen reader utterances.
|
|
* @param {?Array} aData Speech data before it is localized.
|
|
* @param {?Function} aCallback aCallback A callback after the speech
|
|
* synthesis is completed.
|
|
* @param {?Object} aOptions = {} Speech options such as enqueue etc.
|
|
* @memberof Accessibility.prototype
|
|
*/
|
|
speak: function ar_speak(aData, aCallback, aOptions = {}) {
|
|
this.speechSynthesizer.speak(aData, aOptions, this.rate, this.volume,
|
|
aCallback);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A speech synthesizer component that handles speech localization and
|
|
* pronunciation.
|
|
* @type {Object}
|
|
*/
|
|
var speechSynthesizer = {
|
|
|
|
/**
|
|
* A delay before hiding screen reader caption.
|
|
* @type {Number}
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
CAPTIONS_DELAY: 1500,
|
|
|
|
/**
|
|
* A flag used for speech captions rendering.
|
|
* @type {Boolean}
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
captions: false,
|
|
|
|
/**
|
|
* Screen container getter.
|
|
* @type {Object}
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
get screen() {
|
|
delete this.screen;
|
|
this.screen = document.getElementById('screen');
|
|
return this.screen;
|
|
},
|
|
|
|
/**
|
|
* Speech Synthesis
|
|
* @type {Object}
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
get speech() {
|
|
delete this.speech;
|
|
// If there are no voices bundled, consider speech synthesis unavailable.
|
|
if (!window.speechSynthesis ||
|
|
window.speechSynthesis.getVoices().length === 0) {
|
|
this.speech = null;
|
|
}
|
|
this.speech = window.speechSynthesis;
|
|
return this.speech;
|
|
},
|
|
|
|
/**
|
|
* Speech utterance
|
|
* @type {Object}
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
get utterance() {
|
|
delete this.utterance;
|
|
this.utterance = window.SpeechSynthesisUtterance;
|
|
return this.utterance;
|
|
},
|
|
|
|
/**
|
|
* Cancle speech if the screen reader is speaking.
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
cancel: function ss_cancel() {
|
|
if (this.speech) {
|
|
this.speech.cancel();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Localize speech data.
|
|
* @param {Object} aDetails Speech data object.
|
|
* @return {String} Localized speech data.
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
localize: function ss_localize(aDetails) {
|
|
if (!aDetails || typeof aDetails === 'string') {
|
|
return aDetails;
|
|
}
|
|
var string = aDetails.string;
|
|
var data = {
|
|
count: aDetails.count
|
|
};
|
|
if (!string) {
|
|
return '';
|
|
} else {
|
|
string = 'accessibility-' + string;
|
|
}
|
|
|
|
if (aDetails.args) {
|
|
data = aDetails.args.reduce(function(aData, val, index) {
|
|
aData[index] = val;
|
|
return aData;
|
|
}, data);
|
|
}
|
|
return navigator.mozL10n.get(string, data);
|
|
},
|
|
|
|
/**
|
|
* Build a complete utterance string by localizing an array of speech data.
|
|
* @param {?Array} aData Speech data.
|
|
* @return {String} A complete localized string from speech array data.
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
buildUtterance: function ss_buildUtterance(aData) {
|
|
if (!Array.isArray(aData)) {
|
|
aData = [aData];
|
|
}
|
|
var words = [], localize = this.localize;
|
|
aData.reduce(function(words, details) {
|
|
var localized = localize(details);
|
|
if (localized) {
|
|
var word = localized.trim();
|
|
if (word) {
|
|
words.push(word);
|
|
}
|
|
}
|
|
return words;
|
|
}, words);
|
|
|
|
return words.join(' ');
|
|
},
|
|
|
|
/**
|
|
* Show caption for the screen reader.
|
|
* @param {String} aUtterance Current screen reader caption.
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
showSpeech: function ar_showSpeech(aUtterance) {
|
|
if (!this.captionsBox) {
|
|
this.captionsBox = document.createElement('div');
|
|
this.captionsBox.id = 'accessibility-captions-box';
|
|
this.captionsBox.setAttribute('data-z-index-level',
|
|
'accessibility-captions');
|
|
this.captionsBox.setAttribute('aria-hidden', true);
|
|
this.screen.appendChild(this.captionsBox);
|
|
}
|
|
window.clearTimeout(this.captionsHideTimeout);
|
|
this.captionsHideTimeout = null;
|
|
this.captionsBox.textContent = aUtterance;
|
|
this.captionsBox.classList.add('visible');
|
|
},
|
|
|
|
/**
|
|
* Hide current screen reader caption.
|
|
* @param {Boolean} aImmediately A flag to hide captionsBox immediately.
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
hideSpeech: function ar_hideSpeech(aImmediately) {
|
|
if (!this.captionsBox) {
|
|
return;
|
|
}
|
|
// Hide the caption after CAPTIONS_DELAY.
|
|
if (aImmediately) {
|
|
this.captionsBox.classList.remove('visible');
|
|
} else {
|
|
this.captionsHideTimeout = window.setTimeout(function() {
|
|
this.captionsBox.classList.remove('visible');
|
|
}.bind(this), this.CAPTIONS_DELAY);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Utter a message with a speechSynthesizer.
|
|
* @param {?Array} aData A messages array to be localized.
|
|
* @param {JSON} aOptions Options to be used when speaking. For example: {
|
|
* enqueue: false
|
|
* }
|
|
* @param {Number} aRate Speech rate.
|
|
* @param {Number} aVolume Speech volume.
|
|
* @param {Function} aCallback A callback after the speech synthesis is
|
|
* completed.
|
|
* @memberof speechSynthesizer
|
|
*/
|
|
speak: function ss_speak(aData, aOptions, aRate, aVolume, aCallback) {
|
|
if (!this.speech || !this.utterance) {
|
|
if (aCallback) {
|
|
aCallback();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!aOptions.enqueue) {
|
|
this.cancel();
|
|
}
|
|
|
|
var sentence = this.buildUtterance(aData);
|
|
if (!sentence) {
|
|
if (aCallback) {
|
|
aCallback();
|
|
}
|
|
return;
|
|
}
|
|
|
|
var utterance = new this.utterance(sentence);
|
|
utterance.volume = aVolume;
|
|
utterance.rate = aRate;
|
|
utterance.addEventListener('end', function() {
|
|
if (this.captions) {
|
|
this.hideSpeech();
|
|
}
|
|
if (aCallback) {
|
|
aCallback();
|
|
}
|
|
}.bind(this));
|
|
|
|
if (this.captions) {
|
|
this.showSpeech(sentence);
|
|
}
|
|
this.speech.speak(utterance);
|
|
}
|
|
};
|
|
|
|
exports.Accessibility = Accessibility;
|
|
|
|
}(window));
|