diff --git a/background.html b/background.html deleted file mode 100644 index d05d55da..00000000 --- a/background.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/background.js b/background.js index bb658b1b..9ff19214 100644 --- a/background.js +++ b/background.js @@ -54,7 +54,9 @@ chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { case "getStyles": var styles = getStyles(request, sendResponse); // check if this is a main content frame style enumeration - if (request.matchUrl && !request.id && sender && sender.tab && sender.frameId == 0) { + if (request.matchUrl && !request.id + && sender && sender.tab && sender.frameId == 0 + && sender.tab.url == request.matchUrl) { updateIcon(sender.tab, styles); } return true; @@ -88,7 +90,7 @@ chrome.commands.onCommand.addListener(function(command) { break; case "styleDisableAll": disableAllStylesToggle(); - chrome.contextMenus.update("disableAll", {checked: prefs.getPref("disableAll")}); + chrome.contextMenus.update("disableAll", {checked: prefs.get("disableAll")}); break; } }); @@ -98,11 +100,11 @@ chrome.commands.onCommand.addListener(function(command) { runTryCatch(function() { chrome.contextMenus.create({ id: "show-badge", title: chrome.i18n.getMessage("menuShowBadge"), - type: "checkbox", contexts: ["browser_action"], checked: prefs.getPref("show-badge") + type: "checkbox", contexts: ["browser_action"], checked: prefs.get("show-badge") }, function() { var clearError = chrome.runtime.lastError }); chrome.contextMenus.create({ id: "disableAll", title: chrome.i18n.getMessage("disableAllStyles"), - type: "checkbox", contexts: ["browser_action"], checked: prefs.getPref("disableAll") + type: "checkbox", contexts: ["browser_action"], checked: prefs.get("disableAll") }, function() { var clearError = chrome.runtime.lastError }); }); @@ -110,16 +112,15 @@ chrome.contextMenus.onClicked.addListener(function(info, tab) { if (info.menuItemId == "disableAll") { disableAllStylesToggle(info.checked); } else { - prefs.setPref(info.menuItemId, info.checked); + prefs.set(info.menuItemId, info.checked); } }); function disableAllStylesToggle(newState) { if (newState === undefined || newState === null) { - newState = !prefs.getPref("disableAll"); + newState = !prefs.get("disableAll"); } - prefs.setPref("disableAll", newState); - notifyAllTabs({method: "styleDisableAll", disableAll: newState}); + prefs.set("disableAll", newState); } function getStyles(options, callback) { @@ -132,7 +133,7 @@ function getStyles(options, callback) { var asHash = "asHash" in options ? options.asHash : false; var callCallback = function() { - var styles = asHash ? {disableAll: prefs.getPref("disableAll", false)} : []; + var styles = asHash ? {disableAll: prefs.get("disableAll", false)} : []; cachedStyles.forEach(function(style) { if (enabled != null && fixBoolean(style.enabled) != enabled) { return; @@ -243,6 +244,10 @@ function sectionAppliesToUrl(section, url) { if (url.indexOf("http") != 0 && url.indexOf("file") != 0 && url.indexOf("chrome-extension") != 0 && url.indexOf("ftp") != 0) { return false; } + // other extensions can't be styled + if (url.indexOf("chrome-extension") == 0 && url.indexOf(chrome.extension.getURL("")) != 0) { + return false; + } if (!section.urls && !section.domains && !section.urlPrefixes && !section.regexps) { //console.log(section.id + " is global"); return true; @@ -401,7 +406,7 @@ chrome.tabs.onAttached.addListener(function(tabId, data) { if (tabData.url.indexOf(editFullUrl) == 0) { chrome.windows.get(tabData.windowId, {populate: true}, function(win) { // If there's only one tab in this window, it's been dragged to new window - prefs.setPref('openEditInWindow', win.tabs.length == 1); + prefs.set("openEditInWindow", win.tabs.length == 1); }); } }); diff --git a/edit.js b/edit.js index a3bd5032..777a6b3d 100644 --- a/edit.js +++ b/edit.js @@ -11,7 +11,7 @@ var propertyToCss = {urls: "url", urlPrefixes: "url-prefix", domains: "domain", var CssToProperty = {"url": "urls", "url-prefix": "urlPrefixes", "domain": "domains", "regexp": "regexps"}; // make querySelectorAll enumeration code readable -["forEach", "some", "indexOf"].forEach(function(method) { +["forEach", "some", "indexOf", "map"].forEach(function(method) { NodeList.prototype[method]= Array.prototype[method]; }); @@ -125,26 +125,23 @@ function initCodeMirror() { var isWindowsOS = navigator.appVersion.indexOf("Windows") > 0; // default option values - var userOptions = prefs.getPref("editor.options"); - var stylishOptions = { + shallowMerge(CM.defaults, { mode: 'css', lineNumbers: true, lineWrapping: true, foldGutter: true, gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter", "CodeMirror-lint-markers"], matchBrackets: true, - lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.getPref("editor.lintDelay")}, - lintReportDelay: prefs.getPref("editor.lintReportDelay"), + lint: {getAnnotations: CodeMirror.lint.css, delay: prefs.get("editor.lintDelay")}, + lintReportDelay: prefs.get("editor.lintReportDelay"), styleActiveLine: true, theme: "default", - keyMap: prefs.getPref("editor.keyMap"), + keyMap: prefs.get("editor.keyMap"), extraKeys: { // independent of current keyMap "Alt-PageDown": "nextEditor", "Alt-PageUp": "prevEditor" } - } - shallowMerge(stylishOptions, CM.defaults); - shallowMerge(userOptions, CM.defaults); + }, prefs.get("editor.options")); // additional commands CM.commands.jumpToLine = jumpToLine; @@ -158,11 +155,9 @@ function initCodeMirror() { // "basic" keymap only has basic keys by design, so we skip it var extraKeysCommands = {}; - if (userOptions && typeof userOptions.extraKeys == "object") { - Object.keys(userOptions.extraKeys).forEach(function(key) { - extraKeysCommands[userOptions.extraKeys[key]] = true; - }); - } + Object.keys(CM.defaults.extraKeys).forEach(function(key) { + extraKeysCommands[CM.defaults.extraKeys[key]] = true; + }); if (!extraKeysCommands.jumpToLine) { CM.keyMap.sublime["Ctrl-G"] = "jumpToLine"; CM.keyMap.emacsy["Ctrl-G"] = "jumpToLine"; @@ -224,8 +219,8 @@ function initCodeMirror() { }); } - // preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->loadPrefs() - var theme = prefs.getPref("editor.theme"); + // preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs() + var theme = prefs.get("editor.theme"); document.getElementById("cm-theme").href = theme == "default" ? "" : "codemirror/theme/" + theme + ".css"; // initialize global editor controls @@ -246,12 +241,11 @@ function initCodeMirror() { }); } document.getElementById("editor.keyMap").innerHTML = optionsHtmlFromArray(Object.keys(CM.keyMap).sort()); - var controlPrefs = {}; - document.querySelectorAll("#options *[data-option][id^='editor.']").forEach(function(option) { - controlPrefs[option.id] = CM.defaults[option.dataset.option]; - }); document.getElementById("options").addEventListener("change", acmeEventListener, false); - loadPrefs(controlPrefs); + setupLivePrefs( + document.querySelectorAll("#options *[data-option][id^='editor.']") + .map(function(option) { return option.id }) + ); }); hotkeyRerouter.setState(true); @@ -276,8 +270,8 @@ function acmeEventListener(event) { // use non-localized "default" internally if (!value || value == "default" || value == t("defaultTheme")) { value = "default"; - if (prefs.getPref(el.id) != value) { - prefs.setPref(el.id, value); + if (prefs.get(el.id) != value) { + prefs.set(el.id, value); } themeLink.href = ""; el.selectedIndex = 0; @@ -400,7 +394,7 @@ document.addEventListener("wheel", function(event) { chrome.tabs.query({currentWindow: true}, function(tabs) { var windowId = tabs[0].windowId; - if (prefs.getPref("openEditInWindow")) { + if (prefs.get("openEditInWindow")) { if (tabs.length == 1 && window.history.length == 1) { chrome.windows.getAll(function(windows) { if (windows.length > 1) { @@ -434,7 +428,7 @@ function goBackToManage(event) { window.onbeforeunload = function() { if (saveSizeOnClose) { - prefs.setPref("windowPosition", { + prefs.set("windowPosition", { left: screenLeft, top: screenTop, width: outerWidth, @@ -994,9 +988,9 @@ function beautify(event) { script.onload = doBeautify; } function doBeautify() { - var tabs = prefs.getPref("editor.indentWithTabs"); - var options = prefs.getPref("editor.beautify"); - options.indent_size = tabs ? 1 : prefs.getPref("editor.tabSize"); + var tabs = prefs.get("editor.indentWithTabs"); + var options = prefs.get("editor.beautify"); + options.indent_size = tabs ? 1 : prefs.get("editor.tabSize"); options.indent_char = tabs ? "\t" : " "; var section = getSectionForChild(event.target); @@ -1045,7 +1039,7 @@ function beautify(event) { document.querySelector(".beautify-options").addEventListener("change", function(event) { var value = event.target.selectedIndex > 0; options[event.target.dataset.option] = value; - prefs.setPref("editor.beautify", options); + prefs.set("editor.beautify", options); event.target.parentNode.setAttribute("newline", value.toString()); doBeautify(); }); @@ -1120,7 +1114,7 @@ function initWithStyle(style) { function add() { var sectionDiv = addSection(null, queue.shift()); maximizeCodeHeight(sectionDiv, !queue.length); - updateLintReport(getCodeMirrorForSection(sectionDiv), prefs.getPref("editor.lintDelay")); + updateLintReport(getCodeMirrorForSection(sectionDiv), prefs.get("editor.lintDelay")); } } @@ -1472,12 +1466,12 @@ function showToMozillaHelp() { } function showKeyMapHelp() { - var keyMap = mergeKeyMaps({}, prefs.getPref("editor.keyMap"), CodeMirror.defaults.extraKeys); + var keyMap = mergeKeyMaps({}, prefs.get("editor.keyMap"), CodeMirror.defaults.extraKeys); var keyMapSorted = Object.keys(keyMap) .map(function(key) { return {key: key, cmd: keyMap[key]} }) .concat([{key: "Shift-Ctrl-Wheel", cmd: "scrollWindow"}]) .sort(function(a, b) { return a.cmd < b.cmd || (a.cmd == b.cmd && a.key < b.key) ? -1 : 1 }); - showHelp(t("cm_keyMap") + ": " + prefs.getPref("editor.keyMap"), + showHelp(t("cm_keyMap") + ": " + prefs.get("editor.keyMap"), '' + '' + '' + @@ -1585,7 +1579,7 @@ function showCodeMirrorPopup(title, html, options) { var popup = showHelp(title, html); popup.classList.add("big"); - popup.codebox = CodeMirror(popup.querySelector(".contents"), shallowMerge(options, { + popup.codebox = CodeMirror(popup.querySelector(".contents"), shallowMerge({ mode: "css", lineNumbers: true, lineWrapping: true, @@ -1594,9 +1588,9 @@ function showCodeMirrorPopup(title, html, options) { matchBrackets: true, lint: {getAnnotations: CodeMirror.lint.css, delay: 0}, styleActiveLine: true, - theme: prefs.getPref("editor.theme"), - keyMap: prefs.getPref("editor.keyMap") - })); + theme: prefs.get("editor.theme"), + keyMap: prefs.get("editor.keyMap") + }, options)); popup.codebox.focus(); popup.codebox.on("focus", function() { hotkeyRerouter.setState(false) }); popup.codebox.on("blur", function() { hotkeyRerouter.setState(true) }); diff --git a/manage.js b/manage.js index 8c35cbb8..fa9d9dcf 100644 --- a/manage.js +++ b/manage.js @@ -110,7 +110,7 @@ function createStyleElement(style) { event.stopPropagation(); if (openWindow || openBackgroundTab || openForegroundTab) { if (openWindow) { - var options = prefs.getPref('windowPosition', {}); + var options = prefs.get("windowPosition"); options.url = url; chrome.windows.create(options); } else { @@ -475,12 +475,12 @@ document.addEventListener("DOMContentLoaded", function() { document.getElementById("search").addEventListener("input", searchStyles); searchStyles(true); // re-apply filtering on history Back - loadPrefs({ - "manage.onlyEnabled": false, - "manage.onlyEdited": false, - "show-badge": true, - "popup.stylesFirst": true - }); + setupLivePrefs([ + "manage.onlyEnabled", + "manage.onlyEdited", + "show-badge", + "popup.stylesFirst" + ]); initFilter("enabled-only", document.getElementById("manage.onlyEnabled")); initFilter("edited-only", document.getElementById("manage.onlyEdited")); }); diff --git a/manifest.json b/manifest.json index ecbd1eca..25d3f090 100644 --- a/manifest.json +++ b/manifest.json @@ -13,11 +13,12 @@ "tabs", "webNavigation", "contextMenus", + "storage", "http://userstyles.org/", "https://userstyles.org/" ], "background": { - "page": "background.html" + "scripts": ["messaging.js", "storage.js", "background.js"] }, "commands": { "openManage": { diff --git a/messaging.js b/messaging.js index ea30055b..0678f0db 100644 --- a/messaging.js +++ b/messaging.js @@ -8,11 +8,7 @@ function notifyAllTabs(request) { }); }); // notify all open popups - // use a shallow copy since the original `request` is still being processed - var reqPopup = {}; - for (var k in request) reqPopup[k] = request[k]; - reqPopup.reason = request.method; - reqPopup.method = "updatePopup"; + var reqPopup = shallowMerge({}, request, {method: "updatePopup", reason: request.method}); chrome.extension.sendMessage(reqPopup); } @@ -48,15 +44,20 @@ function updateIcon(tab, styles) { }); function stylesReceived(styles) { - var disableAll = "disableAll" in styles ? styles.disableAll : prefs.getPref("disableAll"); + var disableAll = "disableAll" in styles ? styles.disableAll : prefs.get("disableAll"); var postfix = styles.length == 0 || disableAll ? "w" : ""; chrome.browserAction.setIcon({ path: {19: "19" + postfix + ".png", 38: "38" + postfix + ".png"}, tabId: tab.id + }, function() { + // if the tab was just closed an error may occur, + // e.g. 'windowPosition' pref updated in edit.js::window.onbeforeunload + if (!chrome.runtime.lastError) { + var t = prefs.get("show-badge") && styles.length ? ("" + styles.length) : ""; + chrome.browserAction.setBadgeText({text: t, tabId: tab.id}); + chrome.browserAction.setBadgeBackgroundColor({color: disableAll ? "#aaa" : [0, 0, 0, 0]}); + } }); - var t = prefs.getPref("show-badge") && styles.length ? ("" + styles.length) : ""; - chrome.browserAction.setBadgeText({text: t, tabId: tab.id}); - chrome.browserAction.setBadgeBackgroundColor({color: disableAll ? "#aaa" : [0, 0, 0, 0]}); //console.log("Tab " + tab.id + " (" + tab.url + ") badge text set to '" + t + "'."); } } diff --git a/popup.js b/popup.js index d14cefe6..6cf7648f 100644 --- a/popup.js +++ b/popup.js @@ -3,7 +3,7 @@ writeStyleTemplate.className = "write-style-link"; var installed = document.getElementById("installed"); -if (!prefs.getPref("popup.stylesFirst")) { +if (!prefs.get("popup.stylesFirst")) { document.body.insertBefore(document.querySelector("body > .actions"), installed); } @@ -29,14 +29,14 @@ function updatePopUp(url) { var urlLink = writeStyleTemplate.cloneNode(true); urlLink.href = "edit.html?url-prefix=" + encodeURIComponent(url); urlLink.appendChild(document.createTextNode( // switchable; default="this URL" - !prefs.getPref("popup.breadcrumbs.usePath") + !prefs.get("popup.breadcrumbs.usePath") ? t("writeStyleForURL").replace(/ /g, "\u00a0") : /\/\/[^/]+\/(.*)/.exec(url)[1] )); urlLink.title = "url-prefix(\"$\")".replace("$", url); writeStyleLinks.push(urlLink); document.querySelector("#write-style").appendChild(urlLink) - if (prefs.getPref("popup.breadcrumbs")) { // switchable; default=enabled + if (prefs.get("popup.breadcrumbs")) { // switchable; default=enabled urlLink.addEventListener("mouseenter", function(event) { this.parentNode.classList.add("url()") }, false); urlLink.addEventListener("focus", function(event) { this.parentNode.classList.add("url()") }, false); urlLink.addEventListener("mouseleave", function(event) { this.parentNode.classList.remove("url()") }, false); @@ -63,7 +63,7 @@ function updatePopUp(url) { link.addEventListener("click", openLinkInTabOrWindow, false); container.appendChild(link); }); - if (prefs.getPref("popup.breadcrumbs")) { + if (prefs.get("popup.breadcrumbs")) { container.classList.add("breadcrumbs"); container.appendChild(container.removeChild(container.firstChild)); } @@ -71,7 +71,7 @@ function updatePopUp(url) { } function showStyles(styles) { - var enabledFirst = prefs.getPref("popup.enabledFirst"); + var enabledFirst = prefs.get("popup.enabledFirst"); styles.sort(function(a, b) { if (enabledFirst && a.enabled !== b.enabled) return !(a.enabled < b.enabled) ? -1 : 1; return a.name.localeCompare(b.name); @@ -146,9 +146,9 @@ function getId(event) { function openLinkInTabOrWindow(event) { event.preventDefault(); - if (prefs.getPref('openEditInWindow', false)) { + if (prefs.get("openEditInWindow", false)) { var options = {url: event.target.href} - var wp = prefs.getPref('windowPosition', {}); + var wp = prefs.get("windowPosition", {}); for (var k in wp) options[k] = wp[k]; chrome.windows.create(options); } else { @@ -185,10 +185,6 @@ function handleDelete(id) { } } -function handleDisableAll(disableAll) { - installed.classList.toggle("disabled", disableAll); -} - chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { if (request.method == "updatePopup") { switch (request.reason) { @@ -199,12 +195,6 @@ chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { case "styleDeleted": handleDelete(request.id); break; - case "prefChanged": - if (request.prefName == "disableAll") { - document.getElementById("disableAll").checked = request.value; - handleDisableAll(request.value); - } - break; } } }); @@ -213,9 +203,7 @@ chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { document.getElementById(id).addEventListener("click", openLink, false); }); -loadPrefs({"disableAll": false}); -handleDisableAll(prefs.getPref("disableAll")); document.getElementById("disableAll").addEventListener("change", function(event) { - notifyAllTabs({method: "styleDisableAll", disableAll: event.target.checked}); - handleDisableAll(event.target.checked); + installed.classList.toggle("disabled", prefs.get("disableAll")); }); +setupLivePrefs(["disableAll"]); diff --git a/storage.js b/storage.js index f05cc1bd..621e5395 100644 --- a/storage.js +++ b/storage.js @@ -136,96 +136,170 @@ function isCheckbox(el) { return el.nodeName.toLowerCase() == "input" && "checkbox" == el.type.toLowerCase(); } -function changePref(event) { - var el = event.target; - prefs.setPref(el.id, isCheckbox(el) ? el.checked : el.value); -} - -// Accepts a hash of pref name to default value -function loadPrefs(prefs) { - for (var id in prefs) { - var value = this.prefs.getPref(id, prefs[id]); - var el = document.getElementById(id); - if (isCheckbox(el)) { - el.checked = value; - } else { - el.value = value; +// 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) { + var localIDs = {}; + IDs.forEach(function(id) { + localIDs[id] = true; + updateElement(id).addEventListener("change", function() { + prefs.set(this.id, isCheckbox(this) ? this.checked : this.value); + }); + }); + chrome.extension.onMessage.addListener(function(request) { + if (request.prefName in localIDs) { + updateElement(request.prefName); } + }); + function updateElement(id) { + var el = document.getElementById(id); + el[isCheckbox(el) ? "checked" : "value"] = prefs.get(id); el.dispatchEvent(new Event("change", {bubbles: true, cancelable: true})); - el.addEventListener("change", changePref); + return el; } } -var prefs = { -// NB: localStorage["not_key"] is undefined, localStorage.getItem("not_key") is null +var prefs = chrome.extension.getBackgroundPage().prefs || new function Prefs() { + var me = this; - // 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 + var 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 + "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.onlyEnabled": false, // display only enabled styles + "manage.onlyEdited": false, // display only styles created locally - "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 - }, - "editor.lintDelay": 500, // lint gutter marker update delay, ms - "editor.lintReportDelay": 4500, // lint report update delay, ms + "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 + }, + "editor.lintDelay": 500, // lint gutter marker update delay, ms + "editor.lintReportDelay": 4500, // lint report update delay, ms + }; + var values = deepCopy(defaults); - NO_DEFAULT_PREFERENCE: "No default preference for '%s'", - UNHANDLED_DATA_TYPE: "Default '%s' is of type '%s' - what should be done with it?", + var syncTimeout; // see broadcast() function below - getPref: function(key, defaultValue) { - // Returns localStorage[key], defaultValue, this[key], or undefined - // as type of defaultValue, this[key], or localStorage[key] - var value = localStorage[key]; - if (value === undefined) { - return defaultValue === undefined ? shallowCopy(this[key]) : defaultValue; + Object.defineProperty(this, "readOnlyValues", {value: {}}); + + Prefs.prototype.get = function(key, defaultValue) { + if (key in values) { + return values[key]; } - switch (typeof (defaultValue === undefined ? this[key] : defaultValue)) { - case "boolean": return value.toLowerCase() === "true"; - case "number": return Number(value); - case "object": return JSON.parse(value); - case "string": break; - case "undefined": console.warn(this.NO_DEFAULT_PREFERENCE, key); break; - default: console.error(UNHANDLED_DATA_TYPE, key, typeof defaultValue); + if (defaultValue !== undefined) { + return defaultValue; + } + if (key in defaults) { + return defaults[key]; + } + console.warn("No default preference for '%s'", key); + }; + + Prefs.prototype.getAll = function(key) { + return deepCopy(values); + }; + + Prefs.prototype.set = function(key, value, options) { + var oldValue = deepCopy(values[key]); + values[key] = value; + defineReadonlyProperty(this.readOnlyValues, key, value); + if ((!options || !options.noBroadcast) && !equal(value, oldValue)) { + me.broadcast(key, value, options); + } + }; + + Prefs.prototype.remove = function(key) { me.set(key, undefined) }; + + Prefs.prototype.broadcast = function(key, value, options) { + var message = {method: "prefChanged", prefName: key, value: value}; + notifyAllTabs(message); + chrome.extension.sendMessage(message); + if (key == "disableAll") { + notifyAllTabs({method: "styleDisableAll", disableAll: value}); + } + if (!options || !options.noSync) { + clearTimeout(syncTimeout); + syncTimeout = setTimeout(function() { + chrome.storage.sync.set({"settings": values}); + }, 0); + } + }; + + Object.keys(defaults).forEach(function(key) { + me.set(key, defaults[key], {noBroadcast: true}); + }); + + chrome.storage.sync.get("settings", function(result) { + var synced = result.settings; + for (var key in defaults) { + if (synced && (key in synced)) { + me.set(key, synced[key], {noSync: true}); + } else { + var value = tryMigrating(key); + if (value !== undefined) { + me.set(key, value); + } + } + } + }); + + chrome.storage.onChanged.addListener(function(changes, area) { + if (area == "sync" && "settings" in changes) { + var synced = changes.settings.newValue; + if (synced) { + for (key in defaults) { + if (key in synced) { + me.set(key, synced[key], {noSync: true}); + } + } + } else { + // user manually deleted our settings, we'll recreate them + chrome.storage.sync.set({"settings": values}); + } + } + }); + + function tryMigrating(key) { + if (!(key in localStorage)) { + return undefined; + } + var value = localStorage[key]; + delete localStorage[key]; + localStorage["DEPRECATED: " + key] = value; + switch (typeof defaults[key]) { + case "boolean": + return value.toLowerCase() === "true"; + case "number": + return Number(value); + case "object": + try { + return JSON.parse(value); + } catch(e) { + console.log("Cannot migrate from localStorage %s = '%s': %o", key, value, e); + return undefined; + } } return value; - }, - setPref: function(key, value) { - var oldValue = localStorage[key]; - if (value === undefined || equal(value, this[key])) { - delete localStorage[key]; - } else { - localStorage[key] = typeof value == "string" ? value : JSON.stringify(value); - } - if (!equal(value, oldValue === undefined ? this[key] : oldValue)) { - var message = {method: "prefChanged", prefName: key, value: value}; - notifyAllTabs(message); - chrome.extension.sendMessage(message); - } - }, - removePref: function(key) { setPref(key, undefined) } + } }; function getCodeMirrorThemes(callback) { @@ -261,17 +335,42 @@ function sessionStorageHash(name) { return hash; } -function shallowCopy(obj) { - return typeof obj == "object" ? shallowMerge(obj, {}) : obj; +function deepCopy(obj) { + if (!obj || typeof obj != "object") { + return obj; + } else { + var emptyCopy = Object.create(Object.getPrototypeOf(obj)); + return deepMerge(emptyCopy, obj); + } } -function shallowMerge(from, to) { - if (typeof from == "object" && typeof to == "object") { - for (var k in from) { - to[k] = from[k]; +function deepMerge(target, obj1 /* plus any number of object arguments */) { + for (var i = 1; i < arguments.length; i++) { + var obj = arguments[i]; + for (var k in obj) { + // hasOwnProperty checking is not needed for our non-OOP stuff + var value = obj[k]; + if (!value || typeof value != "object") { + target[k] = value; + } else if (k in target) { + deepMerge(target[k], value); + } else { + target[k] = deepCopy(value); + } } } - return to; + return target; +} + +function shallowMerge(target, obj1 /* plus any number of object arguments */) { + for (var i = 1; i < arguments.length; i++) { + var obj = arguments[i]; + for (var k in obj) { + target[k] = obj[k]; + // hasOwnProperty checking is not needed for our non-OOP stuff + } + } + return target; } function equal(a, b) { @@ -288,3 +387,9 @@ function equal(a, b) { } return true; } + +function defineReadonlyProperty(obj, key, value) { + var copy = deepCopy(value); + Object.freeze(copy); + Object.defineProperty(obj, key, {value: copy, configurable: true}) +}