PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/settings.js

http://github.com/philc/vimium
JavaScript | 278 lines | 187 code | 35 blank | 56 comment | 47 complexity | e65cb38cb835e4c2a71bc34551e7dbc6 MD5 | raw file
  1. // A "setting" is a stored key/value pair. An "option" is a setting which has a default value and whose value
  2. // can be changed on the options page.
  3. //
  4. // Option values which have never been changed by the user are in Settings.defaults.
  5. //
  6. // Settings whose values have been changed are:
  7. // 1. stored either in chrome.storage.sync or in chrome.storage.local (but never both), and
  8. // 2. cached in Settings.cache; on extension pages, Settings.cache uses localStorage (so it persists).
  9. //
  10. // In all cases except Settings.defaults, values are stored as jsonified strings.
  11. // If the current frame is the Vomnibar or the HUD, then we'll need our Chrome stubs for the tests.
  12. // We use "try" because this fails within iframes on Firefox (where failure doesn't actually matter).
  13. try { if (window.chrome == null) { window.chrome = window.top != null ? window.top.chrome : undefined; } } catch (error) {}
  14. let storageArea = (chrome.storage.sync != null) ? "sync" : "local";
  15. const Settings = {
  16. debug: false,
  17. storage: chrome.storage[storageArea],
  18. cache: {},
  19. isLoaded: false,
  20. onLoadedCallbacks: [],
  21. init() {
  22. if (Utils.isExtensionPage() && Utils.isExtensionPage(window.top)) {
  23. // On extension pages, we use localStorage (or a copy of it) as the cache.
  24. // For UIComponents (or other content of ours in an iframe within a regular page), we can't access
  25. // localStorage, so we check that the top level frame is also an extension page.
  26. this.cache = Utils.isBackgroundPage() ? localStorage : extend({}, localStorage);
  27. this.runOnLoadedCallbacks();
  28. }
  29. // Test chrome.storage.sync to see if it is enabled.
  30. // NOTE(mrmr1993, 2017-04-18): currently the API is defined in FF, but it is disabled behind a flag in
  31. // about:config. Every use sets chrome.runtime.lastError, so we use that to check whether we can use it.
  32. chrome.storage.sync.get(null, () => {
  33. if (chrome.runtime.lastError) {
  34. storageArea = "local";
  35. this.storage = chrome.storage[storageArea];
  36. }
  37. // Delay this initialisation until after the correct storage area is known. The significance of this is
  38. // that it delays the on-loaded callbacks.
  39. chrome.storage.local.get(null, localItems => {
  40. if (chrome.runtime.lastError) { localItems = {}; }
  41. return this.storage.get(null, syncedItems => {
  42. if (!chrome.runtime.lastError) {
  43. const object = extend(localItems, syncedItems);
  44. for (let key of Object.keys(object || {})) {
  45. const value = object[key];
  46. this.handleUpdateFromChromeStorage(key, value);
  47. }
  48. }
  49. chrome.storage.onChanged.addListener((changes, area) => {
  50. if (area === storageArea) { return this.propagateChangesFromChromeStorage(changes); }
  51. });
  52. this.runOnLoadedCallbacks();
  53. });
  54. });
  55. });
  56. },
  57. // Called after @cache has been initialized. On extension pages, this will be called twice, but that does
  58. // not matter because it's idempotent.
  59. runOnLoadedCallbacks() {
  60. this.log(`runOnLoadedCallbacks: ${this.onLoadedCallbacks.length} callback(s)`);
  61. this.isLoaded = true;
  62. while (0 < this.onLoadedCallbacks.length) { this.onLoadedCallbacks.pop()(); }
  63. },
  64. // Returns the value of callback if it can be executed immediately.
  65. // TODO(philc): This return value behavior is strange. Ideally this returns nil, as you would expect from a
  66. // potentially async function.
  67. onLoaded(callback) {
  68. if (this.isLoaded) {
  69. return callback();
  70. } else {
  71. this.onLoadedCallbacks.push(callback);
  72. }
  73. },
  74. shouldSyncKey(key) {
  75. return (key in this.defaults) && !["settingsVersion", "previousVersion"].includes(key);
  76. },
  77. propagateChangesFromChromeStorage(changes) {
  78. for (let key of Object.keys(changes || {})) {
  79. const change = changes[key];
  80. this.handleUpdateFromChromeStorage(key, change != null ? change.newValue : undefined);
  81. }
  82. },
  83. handleUpdateFromChromeStorage(key, value) {
  84. this.log(`handleUpdateFromChromeStorage: ${key}`);
  85. // Note: value here is either null or a JSONified string. Therefore, even falsy settings values (like
  86. // false, 0 or "") are truthy here. Only null is falsy.
  87. if (this.shouldSyncKey(key)) {
  88. if (!value || !(key in this.cache) || (this.cache[key] !== value)) {
  89. if (value == null) { value = JSON.stringify(this.defaults[key]); }
  90. this.set(key, JSON.parse(value), false);
  91. }
  92. }
  93. },
  94. get(key) {
  95. if (!this.isLoaded)
  96. console.log(`WARNING: Settings have not loaded yet; using the default value for ${key}.`);
  97. if (key in this.cache && (this.cache[key] != null))
  98. return JSON.parse(this.cache[key]);
  99. else
  100. return this.defaults[key];
  101. },
  102. set(key, value, shouldSetInSyncedStorage) {
  103. if (shouldSetInSyncedStorage == null) { shouldSetInSyncedStorage = true; }
  104. this.cache[key] = JSON.stringify(value);
  105. this.log(`set: ${key} (length=${this.cache[key].length}, shouldSetInSyncedStorage=${shouldSetInSyncedStorage})`);
  106. if (this.shouldSyncKey(key)) {
  107. if (shouldSetInSyncedStorage) {
  108. const setting = {}; setting[key] = this.cache[key];
  109. this.log(` chrome.storage.${storageArea}.set(${key})`);
  110. this.storage.set(setting);
  111. }
  112. if (Utils.isBackgroundPage() && (storageArea === "sync")) {
  113. // Remove options installed by the "copyNonDefaultsToChromeStorage-20150717" migration; see below.
  114. this.log(` chrome.storage.local.remove(${key})`);
  115. chrome.storage.local.remove(key);
  116. }
  117. }
  118. // NOTE(mrmr1993): In FF, |value| will be garbage collected when the page owning it is unloaded.
  119. // Any postUpdateHooks that can be called from the options page/exclusions popup should be careful not to
  120. // use |value| asynchronously, or else it may refer to a |DeadObject| and accesses will throw an error.
  121. this.performPostUpdateHook(key, value);
  122. },
  123. clear(key) {
  124. this.log(`clear: ${key}`);
  125. this.set(key, this.defaults[key]);
  126. },
  127. has(key) { return key in this.cache; },
  128. use(key, callback) {
  129. this.log(`use: ${key} (isLoaded=${this.isLoaded})`);
  130. this.onLoaded(() => callback(this.get(key)));
  131. },
  132. // For settings which require action when their value changes, add hooks to this object.
  133. postUpdateHooks: {},
  134. performPostUpdateHook(key, value) {
  135. if (this.postUpdateHooks[key])
  136. this.postUpdateHooks[key](value);
  137. },
  138. // Completely remove a settings value, e.g. after migration to a new setting. This should probably only be
  139. // called from the background page.
  140. nuke(key) {
  141. delete localStorage[key];
  142. chrome.storage.local.remove(key);
  143. if (chrome.storage.sync != null) {
  144. chrome.storage.sync.remove(key);
  145. }
  146. },
  147. // For development only.
  148. log(...args) {
  149. if (this.debug) { console.log("settings:", ...args); }
  150. },
  151. // Default values for all settings.
  152. defaults: {
  153. scrollStepSize: 60,
  154. smoothScroll: true,
  155. keyMappings: "# Insert your preferred key mappings here.",
  156. linkHintCharacters: "sadfjklewcmpgh",
  157. linkHintNumbers: "0123456789",
  158. filterLinkHints: false,
  159. hideHud: false,
  160. userDefinedLinkHintCss:
  161. `\
  162. div > .vimiumHintMarker {
  163. /* linkhint boxes */
  164. background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FFF785),
  165. color-stop(100%,#FFC542));
  166. border: 1px solid #E3BE23;
  167. }
  168. div > .vimiumHintMarker span {
  169. /* linkhint text */
  170. color: black;
  171. font-weight: bold;
  172. font-size: 12px;
  173. }
  174. div > .vimiumHintMarker > .matchingCharacter {
  175. }\
  176. `,
  177. // Default exclusion rules.
  178. exclusionRules:
  179. [
  180. // Disable Vimium on Gmail.
  181. { pattern: "https?://mail.google.com/*", passKeys: "" }
  182. ],
  183. // NOTE: If a page contains both a single angle-bracket link and a double angle-bracket link, then in
  184. // most cases the single bracket link will be "prev/next page" and the double bracket link will be
  185. // "first/last page", so we put the single bracket first in the pattern string so that it gets searched
  186. // for first.
  187. // "\bprev\b,\bprevious\b,\bback\b,<,‹,←,«,≪,<<"
  188. previousPatterns: "prev,previous,back,older,<,\u2039,\u2190,\xab,\u226a,<<",
  189. // "\bnext\b,\bmore\b,>,›,→,»,≫,>>"
  190. nextPatterns: "next,more,newer,>,\u203a,\u2192,\xbb,\u226b,>>",
  191. // default/fall back search engine
  192. searchUrl: "https://www.google.com/search?q=",
  193. // put in an example search engine
  194. searchEngines:
  195. `\
  196. w: https://www.wikipedia.org/w/index.php?title=Special:Search&search=%s Wikipedia
  197. # More examples.
  198. #
  199. # (Vimium supports search completion Wikipedia, as
  200. # above, and for these.)
  201. #
  202. # g: https://www.google.com/search?q=%s Google
  203. # l: https://www.google.com/search?q=%s&btnI I'm feeling lucky...
  204. # y: https://www.youtube.com/results?search_query=%s Youtube
  205. # gm: https://www.google.com/maps?q=%s Google maps
  206. # b: https://www.bing.com/search?q=%s Bing
  207. # d: https://duckduckgo.com/?q=%s DuckDuckGo
  208. # az: https://www.amazon.com/s/?field-keywords=%s Amazon
  209. # qw: https://www.qwant.com/?q=%s Qwant\
  210. `,
  211. newTabUrl: "about:newtab",
  212. grabBackFocus: false,
  213. regexFindMode: false,
  214. waitForEnterForFilteredHints: false, // Note: this defaults to true for new users; see below.
  215. settingsVersion: "",
  216. helpDialog_showAdvancedCommands: false,
  217. optionsPage_showAdvancedOptions: false,
  218. passNextKeyKeys: [],
  219. ignoreKeyboardLayout: false
  220. }
  221. };
  222. Settings.init();
  223. // Perform migration from old settings versions, if this is the background page.
  224. if (Utils.isBackgroundPage()) {
  225. Settings.applyMigrations = function() {
  226. if (!Settings.get("settingsVersion")) {
  227. // This is a new install. For some settings, we retain a legacy default behaviour for existing users but
  228. // use a non-default behaviour for new users.
  229. // For waitForEnterForFilteredHints, "true" gives a better UX; see #1950. However, forcing the change on
  230. // existing users would be unnecessarily disruptive. So, only new users default to "true".
  231. Settings.set("waitForEnterForFilteredHints", true);
  232. }
  233. // We use settingsVersion to coordinate any necessary schema changes.
  234. Settings.set("settingsVersion", Utils.getCurrentVersion());
  235. // Remove legacy key which was used to control storage migration. This was after 1.57 (2016-10-01), and
  236. // can be removed after 1.58 has been out for sufficiently long.
  237. Settings.nuke("copyNonDefaultsToChromeStorage-20150717");
  238. };
  239. Settings.onLoaded(Settings.applyMigrations.bind(Settings));
  240. }
  241. root = typeof exports !== 'undefined' && exports !== null ? exports : (window.root != null ? window.root : (window.root = {}));
  242. root.Settings = Settings;
  243. if (typeof exports === 'undefined' || exports === null) { extend(window, root); }