VulpesOS/assets/webapps/system.gaiamobile.org/shared/js/keyboard_helper.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

822 lines
24 KiB
JavaScript

'use strict';
/* global InputAppList, ManifestHelper */
/**
* Helper object to find all installed keyboard apps and layouts.
*
* (Need mozApps.mgmt and settings permission)
*/
(function(exports) {
/* jshint validthis: true */
/**
* The set of "basic keyboard" types
*/
var BASE_TYPES = new Set([
'text', 'url', 'email', 'password', 'number', 'option'
]);
/**
* The keys stored in the settings database
*/
var SETTINGS_KEYS = {
ENABLED: 'keyboard.enabled-layouts',
DEFAULT: 'keyboard.default-layouts'
};
var DEPRECATE_KEYBOARD_SETTINGS = {
en: 'keyboard.layouts.english',
'en-Dvorak': 'keyboard.layouts.dvorak',
cs: 'keyboard.layouts.czech',
fr: 'keyboard.layouts.french',
de: 'keyboard.layouts.german',
hu: 'keyboard.layouts.hungarian',
nb: 'keyboard.layouts.norwegian',
my: 'keyboard.layouts.myanmar',
sl: 'keyboard.layouts.slovak',
tr: 'keyboard.layouts.turkish',
ro: 'keyboard.layouts.romanian',
ru: 'keyboard.layouts.russian',
ar: 'keyboard.layouts.arabic',
he: 'keyboard.layouts.hebrew',
'zh-Hant-Zhuyin': 'keyboard.layouts.zhuyin',
'zh-Hans-Pinyin': 'keyboard.layouts.pinyin',
el: 'keyboard.layouts.greek',
'jp-kanji': 'keyboard.layouts.japanese',
pl: 'keyboard.layouts.polish',
'pt-BR': 'keyboard.layouts.portuguese',
sr: 'keyboard.layouts.serbian',
es: 'keyboard.layouts.spanish',
ca: 'keyboard.layouts.catalan'
};
var MULTI_LAYOUT_MAP = {
sr: ['sr-Cyrl', 'sr-Latn']
};
// In order to provide default defaults, we need to know the default keyboard
var defaultKeyboardManifestURL =
'app://keyboard.gaiamobile.org/manifest.webapp';
// Stores a local copy of whatever is in the settings database
var currentSettings = {
defaultLayouts: {}
};
// until we read otherwise, asssume the default keyboards are en and number
currentSettings.defaultLayouts[defaultKeyboardManifestURL] = {
en: true,
number: true
};
// and also assume that the defaults are the enabled
currentSettings.enabledLayouts = map2dClone(currentSettings.defaultLayouts);
/**
* helper function for reading a value in one of the currentSettings
*/
function map2dIs(manifestURL, layoutId) {
// force boolean true or false
return !!(this[manifestURL] && this[manifestURL][layoutId]);
}
/**
* helper function for setting a value to true in one of the currentSettings
*/
function map2dSet(manifestURL, layoutId) {
if (!this[manifestURL]) {
this[manifestURL] = {};
}
this[manifestURL][layoutId] = true;
}
/**
* helper function for setting a value to false in one of the currentSettings
* deletes the appropriate keys
*/
function map2dUnset(manifestURL, layoutId) {
if (!this[manifestURL]) {
return;
}
delete this[manifestURL][layoutId];
if (!Object.keys(this[manifestURL]).length) {
delete this[manifestURL];
}
}
/**
* helper function for cloning one of the currentSettings
*/
function map2dClone(obj) {
var result = {};
for (var key1 in obj) {
result[key1] = {};
for (var key2 in obj[key1]) {
result[key1][key2] = obj[key1][key2];
}
}
return result;
}
// callbacks when something changes
var watchQueries = [];
// holds the result of keyboard_layouts.json
var defaultLayoutConfig;
/**
* Sends any watchers the result of their query.
*/
function kh_updateWatchers(reason) {
if (watchQueries.length) {
// we do a getApps in order to make sure that we have the most recent
// app data before calling getLayouts
KeyboardHelper.getApps(function withApps() {
watchQueries.forEach(function eachWatchQuery(watch) {
KeyboardHelper.getLayouts(watch.query, function withLayouts(layouts) {
watch.callback(layouts, reason);
});
});
});
}
}
// callbacks waiting to know when settings are loaded
var waitingForSettings = [];
var loadedSettings = new Set();
/**
* Tracks the number of settings loaded and calls the callbacks
*/
function kh_loadedSetting(setting) {
loadedSettings.add(setting);
if (loadedSettings.size >= Object.keys(SETTINGS_KEYS).length) {
waitingForSettings.forEach(function(callback) {
callback();
});
waitingForSettings = [];
kh_updateWatchers({ settings: true });
}
}
/**
* Call a callback when the settings have loaded, or immediately if already
* loaded.
*/
function kh_withSettings(callback) {
if (loadedSettings.size >= Object.keys(SETTINGS_KEYS).length) {
callback();
} else {
waitingForSettings.push(callback);
}
}
/**
* Internal helper to read the settings again
*/
function kh_getSettings() {
loadedSettings = new Set();
var lock = window.navigator.mozSettings.createLock();
lock.get(SETTINGS_KEYS.DEFAULT).onsuccess = kh_parseDefault;
lock.get(SETTINGS_KEYS.ENABLED).onsuccess = kh_parseEnabled;
}
/**
* Parse the result from the settings query for default layouts
*/
function kh_parseDefault() {
var value = this.result[SETTINGS_KEYS.DEFAULT];
if (value) {
currentSettings.defaultLayouts = value;
}
kh_loadedSetting(SETTINGS_KEYS.DEFAULT);
}
/**
* Parse the result from the settings query for enabled layouts
*/
function kh_parseEnabled() {
var value = this.result[SETTINGS_KEYS.ENABLED];
var needsSave = false;
if (!value) {
currentSettings.enabledLayouts = map2dClone(currentSettings.defaultLayouts);
needsSave = true;
} else {
var type = typeof value;
// deal with the old string format - might be able to be removed once test
// devices have been living with this new format for a bit.
if (type === 'string') {
try {
currentSettings.enabledLayouts = {};
var oldSettings = JSON.parse(value);
oldSettings.forEach(function(layout) {
if (layout.enabled) {
var manifestURL = layout.manifestURL;
if (!manifestURL) {
manifestURL = layout.appOrigin + '/manifest.webapp';
}
map2dSet.call(currentSettings.enabledLayouts, manifestURL,
layout.layoutId);
}
});
} catch (e) {
currentSettings.enabledLayouts =
map2dClone(currentSettings.defaultLayouts);
needsSave = true;
}
} else {
// if it wasn't a string, assume its our object
currentSettings.enabledLayouts = value;
}
}
kh_loadedSetting(SETTINGS_KEYS.ENABLED);
if (needsSave) {
KeyboardHelper.saveToSettings();
}
}
/**
* Parse and migrate the result for the deprecated settings (v1.1) for enabled
* layouts.
*/
function kh_migrateDeprecatedSettings(deprecatedSettings) {
/* jshint loopfunc: true */
var settingEntry = DEPRECATE_KEYBOARD_SETTINGS.en;
// No need to do migration if the deprecated settings are not available
if (deprecatedSettings[settingEntry] === undefined) {
return;
}
// reset the enabled layouts
currentSettings.enabledLayouts[defaultKeyboardManifestURL] = {
number: true
};
var hasEnabledLayout = false;
for (var key in DEPRECATE_KEYBOARD_SETTINGS) {
settingEntry = DEPRECATE_KEYBOARD_SETTINGS[key];
// this layout was set as enabled in the old settings
if (deprecatedSettings[settingEntry]) {
hasEnabledLayout = true;
if (key in MULTI_LAYOUT_MAP) {
MULTI_LAYOUT_MAP[key].forEach(function enableLayout(layoutId) {
map2dSet.call(currentSettings.enabledLayouts,
defaultKeyboardManifestURL, layoutId);
});
} else {
map2dSet.call(currentSettings.enabledLayouts,
defaultKeyboardManifestURL, key);
}
}
}
// None of the layouts has been set enabled, so enable English by default
if (!hasEnabledLayout) {
map2dSet.call(currentSettings.enabledLayouts,
defaultKeyboardManifestURL, 'en');
}
// Clean up all the deprecated settings
var deprecatedSettingsQuery = {};
for (key in DEPRECATE_KEYBOARD_SETTINGS) {
// the deprecated setting entry, e.g. keyboard.layout.english
settingEntry = DEPRECATE_KEYBOARD_SETTINGS[key];
// Set the default value for each entry
deprecatedSettingsQuery[settingEntry] = null;
}
window.navigator.mozSettings.createLock().set(deprecatedSettingsQuery);
KeyboardHelper.saveToSettings();
}
/**
* JSON loader
*/
function kh_loadJSON(href, callback) {
if (!callback) {
return;
}
var xhr = new XMLHttpRequest();
xhr.onerror = function() {
console.error('Failed to fetch file: ' + href, xhr.statusText);
};
xhr.onload = function() {
callback(xhr.response);
};
xhr.open('GET', href, true); // async
xhr.responseType = 'json';
xhr.send();
}
//
// getSettings: Query the value of multiple settings at once.
//
// settings is an object whose property names are the settings to query
// and whose property values are the default values to use if no such
// setting is found. This function will create a setting lock and
// request the value of each of the specified settings. Once it receives
// a response to all of the queries, it passes all the settings values to
// the specified callback function. The argument to the callback function
// is an object just like the settings object, where the property name is
// the setting name and the property value is the setting value (or the
// default value if the setting was not found).
//
function kh_getMultiSettings(settings, callback) {
var results = {};
try {
var lock = navigator.mozSettings.createLock();
}
catch (e) {
// If settings is broken, just return the default values
console.warn('Exception in mozSettings.createLock():', e,
'\nUsing default values');
for (var p in settings) {
results[p] = settings[p];
}
callback(results);
}
var settingNames = Object.keys(settings);
var numSettings = settingNames.length;
var numResults = 0;
for (var i = 0; i < numSettings; i++) {
requestSetting(settingNames[i]);
}
function requestSetting(name) {
var request;
try {
request = lock.get(name);
}
catch (e) {
console.warn('Exception querying setting', name, ':', e,
'\nUsing default value');
recordResult(name, settings[name]);
return;
}
request.onsuccess = function() {
var value = request.result[name];
if (value === undefined) {
value = settings[name]; // Use the default value
}
recordResult(name, value);
};
request.onerror = function(evt) {
console.warn('Error querying setting', name, ':', evt.error);
recordResult(name, settings[name]);
};
}
function recordResult(name, value) {
results[name] = value;
numResults++;
if (numResults === numSettings) {
callback(results);
}
}
}
/**
* Creates an object that describes a keyboard layout. The objects prototype
* will give it accessors for 'default' and 'enabled' that will read or change
* the proper setting from currentSettings.
*
* @constructor
*/
function KeyboardLayout(options) {
for (var key in options) {
this[key] = options[key];
}
}
Object.defineProperties(KeyboardLayout.prototype, {
'default': {
get: function kh_getLayoutIsDefault() {
return map2dIs.call(
currentSettings.defaultLayouts, this.app.manifestURL, this.layoutId
);
}
},
enabled: {
get: function kh_getLayoutIsEnabled() {
return map2dIs.call(
currentSettings.enabledLayouts, this.app.manifestURL, this.layoutId
);
},
set: function kh_setLayoutIsDefault(value) {
var method = value ? map2dSet : map2dUnset;
method.call(currentSettings.enabledLayouts,
this.app.manifestURL, this.layoutId);
}
}
});
/**
* Exposed as KeyboardHelper.settings this gives us a fairly safe way to read
* and write to the settings data structures directly.
* XXX: is this really used anywhere?
*/
var kh_SettingsHelper = {};
Object.defineProperties(kh_SettingsHelper, {
'default': {
get: function() {
return map2dClone(currentSettings.defaultLayouts);
},
set: function(value) {
currentSettings.defaultLayouts = map2dClone(value);
},
enumerable: true
},
'enabled': {
get: function() {
return map2dClone(currentSettings.enabledLayouts);
},
set: function(value) {
currentSettings.enabledLayouts = map2dClone(value);
},
enumerable: true
}
});
var KeyboardHelper = exports.KeyboardHelper = {
settings: kh_SettingsHelper,
// bug 1035117: define the fallback layout for a group if no layout has been
// selected for that group (if it's not enforced in settings)
// please see the bug and its related UX spec for the sense of 'fallback'
fallbackLayoutNames: {
password: 'en'
},
fallbackLayouts: {},
// InputAppList manages the current input apps for us.
inputAppList: null,
/**
* Listen for changes in settings or apps and read the deafault settings
*/
init: function kh_init() {
watchQueries = [];
this.inputAppList = new InputAppList();
this.inputAppList.onupdate =
kh_updateWatchers.bind(undefined, { apps: true });
this.inputAppList.start();
// load the current settings, and watch for changes to settings
var settings = window.navigator.mozSettings;
if (settings) {
kh_getSettings();
settings.addObserver(SETTINGS_KEYS.ENABLED, kh_getSettings);
settings.addObserver(SETTINGS_KEYS.DEFAULT, kh_getSettings);
// read deprecated settings
var deprecatedSettingsQuery = {};
for (var key in DEPRECATE_KEYBOARD_SETTINGS) {
// the deprecated setting entry, e.g. keyboard.layout.english
var settingEntry = DEPRECATE_KEYBOARD_SETTINGS[key];
// Set the default value for each entry
deprecatedSettingsQuery[settingEntry] = undefined;
}
kh_getMultiSettings(deprecatedSettingsQuery,
kh_migrateDeprecatedSettings);
}
window.addEventListener('applicationinstallsuccess', this);
},
/**
* Handles the application changed events. Clears the cache and updates
* any listening watchers.
*/
handleEvent: function(event) {
kh_updateWatchers({ apps: true });
},
/**
* If this is a basic keyboard type
*/
isKeyboardType: function kh_isKeyboardType(type) {
return BASE_TYPES.has(type);
},
/**
* Enables or disables a layout based on manifest URL and layoutId
*/
setLayoutEnabled: function kh_setLayoutEnabled(manifestURL, layoutId,
enabled) {
var method = enabled ? map2dSet : map2dUnset;
method.call(currentSettings.enabledLayouts, manifestURL, layoutId);
},
/**
* Returns true if the layout specified by manifest URL and layoutId is
* enabled.
*/
getLayoutEnabled: function kh_getLayoutEnabled(manifestURL, layoutId) {
return map2dIs.call(currentSettings.enabledLayouts, manifestURL, layoutId);
},
/**
* set/unset a layout as default based on manifest URL and layoutId
*/
setLayoutIsDefault: function kh_setLayoutIsDefault(manifestURL, layoutId,
enabled) {
var method = enabled ? map2dSet : map2dUnset;
method.call(currentSettings.defaultLayouts, manifestURL, layoutId);
},
/**
* Returns true if the layout specified by manifest URL and layoutId
* is default.
*/
getLayoutIsDefault: function kh_getLayoutIsDefault(manifestURL, layoutId) {
return map2dIs.call(currentSettings.defaultLayouts, manifestURL, layoutId);
},
/**
* Checks the currently enabled layouts to ensure there is at least one
* 'text', 'url' and 'number' keyboard enabled. If there is not, it will
* enable the corresponding default keyboard.
*
* Only calls the callback if a default was enabled, and passes an array
* of layouts that were enabled.
*/
checkDefaults: function kh_checkDefaults(callback) {
var layoutsEnabled = [];
var missingTypes = [];
['text', 'url', 'number'].forEach(function eachType(type) {
// getLayouts is sync when we already have data
var enabled;
this.getLayouts({ type: type, enabled: true }, function(layouts) {
enabled = layouts.length;
});
if (!enabled) {
missingTypes.push(type);
this.getLayouts({ type: type, 'default': true }, function(layouts) {
if (layouts[0]) {
layouts[0].enabled = true;
layoutsEnabled.push(layouts[0]);
}
});
}
}, this);
if (layoutsEnabled.length) {
if (callback) {
callback(layoutsEnabled, missingTypes);
}
}
},
/**
* Saves the current state to settings. None of the changes made will be
* saved automatically by KeyboardHelper. This allows someone to batch
* whatever settings changes are nessecary, then call save.
*/
saveToSettings: function ke_saveToSettings() {
var toSet = {};
toSet[SETTINGS_KEYS.ENABLED] = currentSettings.enabledLayouts;
toSet[SETTINGS_KEYS.DEFAULT] = currentSettings.defaultLayouts;
window.navigator.mozSettings.createLock().set(toSet);
},
/**
* Get a list of current keyboard applications.
*/
getApps: function kh_getApps(callback) {
// every time we get a list of apps, clean up the settings
var cleanupSettings = function(inputApps) {
Object.keys(currentSettings.enabledLayouts)
.concat(Object.keys(currentSettings.defaultLayouts))
.forEach(function(manifestURL) {
// if the manifestURL doesn't exist in the list of apps, delete it
// from the settings maps
if (!inputApps.some(function(inputApp) {
return (inputApp.domApp.manifestURL === manifestURL);
})) {
delete currentSettings.enabledLayouts[manifestURL];
delete currentSettings.defaultLayouts[manifestURL];
}
});
};
// XXX We have to preserve the original getApps() behavior here,
// i.e. call the sync callback when we already have the data.
if (this.inputAppList.ready) {
var inputApps = this.inputAppList.getListSync();
cleanupSettings(inputApps);
callback(inputApps);
return;
}
this.inputAppList.getList().then(function(inputApps) {
cleanupSettings(inputApps);
callback(inputApps);
})['catch'](function(e) { // workaround gjslint error
console.error(e);
});
},
/**
* Requests an array of layouts matching an optional set of search params.
* Calls the callback when data is received, or immediately if already
* cached locally.
*
* options.default - When true, only default layouts are returned.
* options.enabled - When true, only enabled layouts are returned.
* options.type - A string, or array of strings. Layouts filtered to type.
*/
getLayouts: function kh_getLayouts(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
function withApps(inputApps) {
/* jshint loopfunc: true */
// we'll delete keys in this active copy (= the purpose of copying)
var fallbackLayoutNames = {};
for (var group in this.fallbackLayoutNames) {
fallbackLayoutNames[group] = this.fallbackLayoutNames[group];
}
this.fallbackLayouts = {};
var layouts = inputApps.reduce(function eachApp(result, inputApp) {
var domApp = inputApp.domApp;
var manifest = new ManifestHelper(domApp.manifest);
var inputs = inputApp.getInputs();
for (var layoutId in inputs) {
var inputManifest = inputs[layoutId];
if (!inputManifest.types) {
console.warn(domApp.manifestURL, layoutId, 'did not declare type.');
continue;
}
var layout = new KeyboardLayout({
app: domApp,
inputApp: inputApp,
manifest: manifest,
inputManifest: inputManifest,
layoutId: layoutId
});
// bug 1035117: insert a fallback layout regardless of its
// and enabledness
// XXX: we only do this for built-in keyboard?
if (domApp.manifestURL === defaultKeyboardManifestURL) {
for (var group in fallbackLayoutNames) {
if (layoutId === fallbackLayoutNames[group]) {
this.fallbackLayouts[group] = layout;
delete fallbackLayoutNames[group];
}
}
}
if (options['default'] && !layout['default']) {
continue;
}
if (options.enabled && !layout.enabled) {
continue;
}
if (options.type) {
options.type = [].concat(options.type);
// check for any type in our options to be any type in the layout
if (!options.type.some(function(type) {
return inputManifest.types.indexOf(type) !== -1;
})) {
continue;
}
}
result.push(layout);
}
// sort the default query by most specific keyboard
if (options['default']) {
result.sort(function(a, b) {
return a.inputManifest.types.length - b.inputManifest.types.length;
});
}
return result;
}.bind(this), []);
kh_withSettings(callback.bind(null, layouts));
}
this.getApps(withApps.bind(this));
},
/**
* Clears all watch queries
*/
stopWatching: function() {
watchQueries = [];
},
/**
* Calls the callback whenever settings or apps are changed. The query is
* passed onto getLayouts. The callback will be called as soon as the current
* settings and apps are loaded as well.
*
* The callback will be passed an array of layouts, and a reason object.
* I.E. function callback(layouts, reason) {}
* reason.apps will be true when apps have changed, and reason.settings
* will be true if settings changed. Both will be true on the first
* callback.
*/
watchLayouts: function(query, callback) {
if (typeof query === 'function') {
callback = query;
query = {};
}
var watch = {
query: query,
callback: callback
};
watchQueries.push(watch);
this.getLayouts(query, function initialCall(layouts) {
watch.layouts = layouts;
callback(layouts, { apps: true, settings: true });
});
},
// Read keyboard_layouts for language -> layouts mapping
getDefaultLayoutConfig: function kh_getDefaultLayoutConfig(callback) {
if (!callback) {
return;
}
if (defaultLayoutConfig) {
callback(defaultLayoutConfig);
} else {
var KEYBOARDS = '/shared/resources/keyboard_layouts.json';
kh_loadJSON(KEYBOARDS, function loadKeyboardLayouts(data) {
if (data) {
defaultLayoutConfig = data;
callback(defaultLayoutConfig);
}
});
}
},
// Change the default layouts set according to the language
// language: the current system language, used to look up the mapping table
// reset: if set as true, will reset the current enabled layouts
changeDefaultLayouts: function kh_changeDefaultLayouts(language, reset) {
this.getDefaultLayoutConfig(function gotDefaultLayouts(keyboards) {
var newKbLayouts = keyboards.layout[language];
// XXX: change this so that it could support multiple built-in
// keyboard apps
var kbManifestURL = defaultKeyboardManifestURL;
// reset the set of default layouts
currentSettings.defaultLayouts = {};
// set the language-independent default layouts
var langIndependentLayouts = keyboards.langIndependentLayouts;
for (var i = langIndependentLayouts.length - 1; i >= 0; i--) {
this.setLayoutIsDefault(kbManifestURL,
langIndependentLayouts[i].layoutId,
true);
}
if (reset) {
// reset the set of default layouts
currentSettings.enabledLayouts =
map2dClone(currentSettings.defaultLayouts);
}
// Enable the language specific keyboard layout group
for (i = newKbLayouts.length - 1; i >= 0; i--) {
this.setLayoutIsDefault(kbManifestURL, newKbLayouts[i].layoutId, true);
this.setLayoutEnabled(kbManifestURL, newKbLayouts[i].layoutId, true);
}
this.saveToSettings(); // save changes to settings
}.bind(this));
}
};
KeyboardHelper.init();
}(window));