295 lines
9.0 KiB
JavaScript
295 lines
9.0 KiB
JavaScript
/* global prefs: true, contextMenus */
|
|
'use strict';
|
|
|
|
// eslint-disable-next-line no-var
|
|
var prefs = new function Prefs() {
|
|
const defaults = {
|
|
'openEditInWindow': false, // new editor opens in a own browser window
|
|
'windowPosition': {}, // detached window position
|
|
'show-badge': true, // display text on popup menu icon
|
|
'disableAll': false, // boss key
|
|
|
|
'popup.breadcrumbs': true, // display 'New style' links as URL breadcrumbs
|
|
'popup.breadcrumbs.usePath': false, // use URL path for 'this URL'
|
|
'popup.enabledFirst': true, // display enabled styles before disabled styles
|
|
'popup.stylesFirst': true, // display enabled styles before disabled styles
|
|
|
|
'manage.onlyEnabled': false, // display only enabled styles
|
|
'manage.onlyEdited': false, // display only styles created locally
|
|
'manage.newUI': true, // use the new compact layout
|
|
'manage.newUI.favicons': true, // show favicons for the sites in applies-to
|
|
'manage.newUI.targets': 3, // max number of applies-to targets visible: 0 = none
|
|
|
|
'editor.options': {}, // CodeMirror.defaults.*
|
|
'editor.lineWrapping': true, // word wrap
|
|
'editor.smartIndent': true, // 'smart' indent
|
|
'editor.indentWithTabs': false, // smart indent with tabs
|
|
'editor.tabSize': 4, // tab width, in spaces
|
|
'editor.keyMap': navigator.appVersion.indexOf('Windows') > 0 ? 'sublime' : 'default',
|
|
'editor.theme': 'default', // CSS theme
|
|
'editor.beautify': { // CSS beautifier
|
|
selector_separator_newline: true,
|
|
newline_before_open_brace: false,
|
|
newline_after_open_brace: true,
|
|
newline_between_properties: true,
|
|
newline_before_close_brace: true,
|
|
newline_between_rules: false,
|
|
end_with_newline: false,
|
|
space_around_selector_separator: true,
|
|
},
|
|
'editor.lintDelay': 500, // lint gutter marker update delay, ms
|
|
'editor.lintReportDelay': 4500, // lint report update delay, ms
|
|
'editor.matchHighlight': 'token', // token = token/word under cursor even if nothing is selected
|
|
// selection = only when something is selected
|
|
// '' (empty string) = disabled
|
|
|
|
'badgeDisabled': '#8B0000', // badge background color when disabled
|
|
'badgeNormal': '#006666', // badge background color
|
|
|
|
'popupWidth': 246, // popup width in pixels
|
|
|
|
'updateInterval': 0 // user-style automatic update interval, hour
|
|
};
|
|
const values = deepCopy(defaults);
|
|
|
|
const affectsIcon = [
|
|
'show-badge',
|
|
'disableAll',
|
|
'badgeDisabled',
|
|
'badgeNormal',
|
|
];
|
|
|
|
// coalesce multiple pref changes in broadcast
|
|
let broadcastPrefs = {};
|
|
|
|
Object.defineProperty(this, 'readOnlyValues', {value: {}});
|
|
|
|
Object.assign(Prefs.prototype, {
|
|
|
|
get(key, defaultValue) {
|
|
if (key in values) {
|
|
return values[key];
|
|
}
|
|
if (defaultValue !== undefined) {
|
|
return defaultValue;
|
|
}
|
|
if (key in defaults) {
|
|
return defaults[key];
|
|
}
|
|
console.warn("No default preference for '%s'", key);
|
|
},
|
|
|
|
getAll() {
|
|
return deepCopy(values);
|
|
},
|
|
|
|
set(key, value, {noBroadcast, noSync} = {}) {
|
|
const oldValue = values[key];
|
|
switch (typeof defaults[key]) {
|
|
case typeof value:
|
|
break;
|
|
case 'string':
|
|
value = String(value);
|
|
break;
|
|
case 'number':
|
|
value |= 0;
|
|
break;
|
|
case 'boolean':
|
|
value = value === true || value === 'true';
|
|
break;
|
|
}
|
|
values[key] = value;
|
|
defineReadonlyProperty(this.readOnlyValues, key, value);
|
|
if (BG && BG != window) {
|
|
BG.prefs.set(key, BG.deepCopy(value), {noBroadcast, noSync});
|
|
} else {
|
|
localStorage[key] = typeof defaults[key] == 'object'
|
|
? JSON.stringify(value)
|
|
: value;
|
|
if (!noBroadcast && !equal(value, oldValue)) {
|
|
this.broadcast(key, value, {noSync});
|
|
}
|
|
}
|
|
},
|
|
|
|
remove: key => this.set(key, undefined),
|
|
|
|
reset: key => this.set(key, deepCopy(defaults[key])),
|
|
|
|
broadcast(key, value, {noSync} = {}) {
|
|
broadcastPrefs[key] = value;
|
|
debounce(doBroadcast);
|
|
if (!noSync) {
|
|
debounce(doSyncSet);
|
|
}
|
|
},
|
|
});
|
|
|
|
// Unlike sync, HTML5 localStorage is ready at browser startup
|
|
// so we'll mirror the prefs to avoid using the wrong defaults
|
|
// during the startup phase
|
|
for (const key in defaults) {
|
|
const defaultValue = defaults[key];
|
|
let value = localStorage[key];
|
|
if (typeof value == 'string') {
|
|
switch (typeof defaultValue) {
|
|
case 'boolean':
|
|
value = value.toLowerCase() === 'true';
|
|
break;
|
|
case 'number':
|
|
value |= 0;
|
|
break;
|
|
case 'object':
|
|
value = tryJSONparse(value) || defaultValue;
|
|
break;
|
|
}
|
|
} else {
|
|
value = defaultValue;
|
|
}
|
|
this.set(key, value, {noBroadcast: true});
|
|
}
|
|
|
|
getSync().get('settings', ({settings: synced} = {}) => {
|
|
if (synced) {
|
|
for (const key in defaults) {
|
|
if (key == 'popupWidth' && synced[key] != values.popupWidth) {
|
|
// this is a fix for the period when popupWidth wasn't synced
|
|
// TODO: remove it in a couple of months
|
|
continue;
|
|
}
|
|
if (key in synced) {
|
|
this.set(key, synced[key], {noSync: true});
|
|
}
|
|
}
|
|
}
|
|
if (typeof contextMenus !== 'undefined') {
|
|
for (const id in contextMenus) {
|
|
if (typeof values[id] == 'boolean') {
|
|
this.broadcast(id, values[id], {noSync: true});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
chrome.storage.onChanged.addListener((changes, area) => {
|
|
if (area == 'sync' && 'settings' in changes) {
|
|
const synced = changes.settings.newValue;
|
|
if (synced) {
|
|
for (const key in defaults) {
|
|
if (key in synced) {
|
|
this.set(key, synced[key], {noSync: true});
|
|
}
|
|
}
|
|
} else {
|
|
// user manually deleted our settings, we'll recreate them
|
|
getSync().set({'settings': values});
|
|
}
|
|
}
|
|
});
|
|
|
|
chrome.runtime.onMessage.addListener(msg => {
|
|
if (msg.prefs) {
|
|
for (const id in msg.prefs) {
|
|
this.set(id, msg.prefs[id], {noBroadcast: true, noSync: true});
|
|
}
|
|
}
|
|
});
|
|
|
|
function doBroadcast() {
|
|
const affects = {all: 'disableAll' in broadcastPrefs};
|
|
if (!affects.all) {
|
|
for (const key in broadcastPrefs) {
|
|
affects.icon = affects.icon || affectsIcon.includes(key);
|
|
affects.popup = affects.popup || key.startsWith('popup');
|
|
affects.editor = affects.editor || key.startsWith('editor');
|
|
affects.manager = affects.manager || key.startsWith('manage');
|
|
}
|
|
}
|
|
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
|
|
broadcastPrefs = {};
|
|
}
|
|
|
|
function doSyncSet() {
|
|
getSync().set({'settings': values});
|
|
}
|
|
|
|
// Polyfill for Firefox < 53 https://bugzilla.mozilla.org/show_bug.cgi?id=1220494
|
|
function getSync() {
|
|
if ('sync' in chrome.storage) {
|
|
return chrome.storage.sync;
|
|
}
|
|
const crappyStorage = {};
|
|
return {
|
|
get(key, callback) {
|
|
callback(crappyStorage[key] || {});
|
|
},
|
|
set(source, callback) {
|
|
for (const property in source) {
|
|
if (source.hasOwnProperty(property)) {
|
|
crappyStorage[property] = source[property];
|
|
}
|
|
}
|
|
callback();
|
|
}
|
|
};
|
|
}
|
|
|
|
function defineReadonlyProperty(obj, key, value) {
|
|
const copy = deepCopy(value);
|
|
if (typeof copy == 'object') {
|
|
Object.freeze(copy);
|
|
}
|
|
Object.defineProperty(obj, key, {value: copy, configurable: true});
|
|
}
|
|
|
|
function equal(a, b) {
|
|
if (!a || !b || typeof a != 'object' || typeof b != 'object') {
|
|
return a === b;
|
|
}
|
|
if (Object.keys(a).length != Object.keys(b).length) {
|
|
return false;
|
|
}
|
|
for (const k in a) {
|
|
if (typeof a[k] == 'object') {
|
|
if (!equal(a[k], b[k])) {
|
|
return false;
|
|
}
|
|
} else if (a[k] !== b[k]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}();
|
|
|
|
|
|
// Accepts an array of pref names (values are fetched via prefs.get)
|
|
// and establishes a two-way connection between the document elements and the actual prefs
|
|
function setupLivePrefs(IDs) {
|
|
const localIDs = {};
|
|
IDs.forEach(function(id) {
|
|
localIDs[id] = true;
|
|
updateElement(id).addEventListener('change', function() {
|
|
prefs.set(this.id, isCheckbox(this) ? this.checked : this.value);
|
|
});
|
|
});
|
|
chrome.runtime.onMessage.addListener(msg => {
|
|
if (msg.prefs) {
|
|
for (const prefName in msg.prefs) {
|
|
if (prefName in localIDs) {
|
|
updateElement(prefName, msg.prefs[prefName]);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
function updateElement(id, value) {
|
|
const el = document.getElementById(id);
|
|
el[isCheckbox(el) ? 'checked' : 'value'] = value || prefs.get(id);
|
|
el.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
|
return el;
|
|
}
|
|
function isCheckbox(el) {
|
|
return el.localName == 'input' && el.type == 'checkbox';
|
|
}
|
|
}
|