Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / settings / prefs / prefs.js
blobddb38be8ee478326abb67fec7a1cc1ffb9c149f1
1 /* Copyright 2015 The Chromium Authors. All rights reserved.
2  * Use of this source code is governed by a BSD-style license that can be
3  * found in the LICENSE file. */
5 /**
6  * @fileoverview
7  * 'cr-settings-prefs' models Chrome settings and preferences, listening for
8  * changes to Chrome prefs whitelisted in chrome.settingsPrivate.
9  * When changing prefs in this element's 'prefs' property via the UI, this
10  * element tries to set those preferences in Chrome. Whether or not the calls to
11  * settingsPrivate.setPref succeed, 'prefs' is eventually consistent with the
12  * Chrome pref store.
13  *
14  * Example:
15  *
16  *    <cr-settings-prefs prefs="{{prefs}}"></cr-settings-prefs>
17  *    <cr-settings-a11y-page prefs="{{prefs}}"></cr-settings-a11y-page>
18  *
19  * @group Chrome Settings Elements
20  * @element cr-settings-prefs
21  */
23 (function() {
24   'use strict';
26   /**
27    * Checks whether two values are recursively equal. Only compares serializable
28    * data (primitives, serializable arrays and serializable objects).
29    * @param {*} val1 Value to compare.
30    * @param {*} val2 Value to compare with val1.
31    * @return {boolean} True if the values are recursively equal.
32    */
33   function deepEqual(val1, val2) {
34     if (val1 === val2)
35       return true;
37     if (Array.isArray(val1) || Array.isArray(val2)) {
38       if (!Array.isArray(val1) || !Array.isArray(val2))
39         return false;
40       return arraysEqual(/** @type {!Array} */(val1),
41                          /** @type {!Array} */(val2));
42     }
44     if (val1 instanceof Object && val2 instanceof Object)
45       return objectsEqual(val1, val2);
47     return false;
48   }
50   /**
51    * @param {!Array} arr1
52    * @param {!Array} arr2
53    * @return {boolean} True if the arrays are recursively equal.
54    */
55   function arraysEqual(arr1, arr2) {
56     if (arr1.length != arr2.length)
57       return false;
59     for (var i = 0; i < arr1.length; i++) {
60       if (!deepEqual(arr1[i], arr2[i]))
61         return false;
62     }
64     return true;
65   }
67   /**
68    * @param {!Object} obj1
69    * @param {!Object} obj2
70    * @return {boolean} True if the objects are recursively equal.
71    */
72   function objectsEqual(obj1, obj2) {
73     var keys1 = Object.keys(obj1);
74     var keys2 = Object.keys(obj2);
75     if (keys1.length != keys2.length)
76       return false;
78     for (var i = 0; i < keys1.length; i++) {
79       var key = keys1[i];
80       if (!deepEqual(obj1[key], obj2[key]))
81         return false;
82     }
84     return true;
85   }
87   /**
88    * Returns a recursive copy of the value.
89    * @param {*} val Value to copy. Should be a primitive or only contain
90    *     serializable data (primitives, serializable arrays and
91    *     serializable objects).
92    * @return {*} A deep copy of the value.
93    */
94   function deepCopy(val) {
95     if (!(val instanceof Object))
96       return val;
97     return Array.isArray(val) ? deepCopyArray(/** @type {!Array} */(val)) :
98         deepCopyObject(val);
99   };
101   /**
102    * @param {!Array} arr
103    * @return {!Array} Deep copy of the array.
104    */
105   function deepCopyArray(arr) {
106     var copy = [];
107     for (var i = 0; i < arr.length; i++)
108       copy.push(deepCopy(arr[i]));
109     return copy;
110   }
112   /**
113    * @param {!Object} obj
114    * @return {!Object} Deep copy of the object.
115    */
116   function deepCopyObject(obj) {
117     var copy = {};
118     var keys = Object.keys(obj);
119     for (var i = 0; i < keys.length; i++) {
120       var key = keys[i];
121       copy[key] = deepCopy(obj[key]);
122     }
123     return copy;
124   }
126   Polymer({
127     is: 'cr-settings-prefs',
129     properties: {
130       /**
131        * Object containing all preferences, for use by Polymer controls.
132        */
133       prefs: {
134         type: Object,
135         notify: true,
136       },
138       /**
139        * Map of pref keys to values representing the state of the Chrome
140        * pref store as of the last update from the API.
141        * @type {Object<*>}
142        * @private
143        */
144       lastPrefValues_: {
145         type: Object,
146         value: function() { return {}; },
147       },
148     },
150     observers: [
151       'prefsChanged_(prefs.*)',
152     ],
154     settingsApi_: chrome.settingsPrivate,
156     /** @override */
157     ready: function() {
158       // Set window.mockApi to pass a custom settings API, i.e. for tests.
159       // TODO(michaelpg): don't use a global.
160       if (window.mockApi)
161         this.settingsApi_ = window.mockApi;
162       CrSettingsPrefs.isInitialized = false;
164       this.settingsApi_.onPrefsChanged.addListener(
165           this.onSettingsPrivatePrefsChanged_.bind(this));
166       this.settingsApi_.getAllPrefs(
167           this.onSettingsPrivatePrefsFetched_.bind(this));
168     },
170     /**
171      * Polymer callback for changes to this.prefs.
172      * @param {!{path: string, value: *}} change
173      * @private
174      */
175     prefsChanged_: function(change) {
176       if (!CrSettingsPrefs.isInitialized)
177         return;
179       var key = this.getPrefKeyFromPath_(change.path);
180       var prefStoreValue = this.lastPrefValues_[key];
182       var prefObj = /** @type {chrome.settingsPrivate.PrefObject} */(
183           this.get(key, this.prefs));
185       // If settingsPrivate already has this value, do nothing. (Otherwise,
186       // a change event from settingsPrivate could make us call
187       // settingsPrivate.setPref and potentially trigger an IPC loop.)
188       if (deepEqual(prefStoreValue, prefObj.value))
189         return;
191       this.settingsApi_.setPref(
192           key,
193           prefObj.value,
194           /* pageId */ '',
195           /* callback */ this.setPrefCallback_.bind(this, key));
196     },
198     /**
199      * Called when prefs in the underlying Chrome pref store are changed.
200      * @param {!Array<!chrome.settingsPrivate.PrefObject>} prefs
201      *     The prefs that changed.
202      * @private
203      */
204     onSettingsPrivatePrefsChanged_: function(prefs) {
205       if (CrSettingsPrefs.isInitialized)
206         this.updatePrefs_(prefs);
207     },
209     /**
210      * Called when prefs are fetched from settingsPrivate.
211      * @param {!Array<!chrome.settingsPrivate.PrefObject>} prefs
212      * @private
213      */
214     onSettingsPrivatePrefsFetched_: function(prefs) {
215       this.updatePrefs_(prefs);
217       CrSettingsPrefs.isInitialized = true;
218       document.dispatchEvent(new Event(CrSettingsPrefs.INITIALIZED));
219     },
221     /**
222      * Checks the result of calling settingsPrivate.setPref.
223      * @param {string} key The key used in the call to setPref.
224      * @param {boolean} success True if setting the pref succeeded.
225      * @private
226      */
227     setPrefCallback_: function(key, success) {
228       if (success)
229         return;
231       // Get the current pref value from chrome.settingsPrivate to ensure the
232       // UI stays up to date.
233       this.settingsApi_.getPref(key, function(pref) {
234         this.updatePrefs_([pref]);
235       }.bind(this));
236     },
238     /**
239      * Updates the prefs model with the given prefs.
240      * @param {!Array<!chrome.settingsPrivate.PrefObject>} newPrefs
241      * @private
242      */
243     updatePrefs_: function(newPrefs) {
244       // Use the existing prefs object or create it.
245       var prefs = this.prefs || {};
246       newPrefs.forEach(function(newPrefObj) {
247         // Use the PrefObject from settingsPrivate to create a copy in
248         // lastPrefValues_ at the pref's key.
249         this.lastPrefValues_[newPrefObj.key] = deepCopy(newPrefObj.value);
251         // Add the pref to |prefs|.
252         cr.exportPath(newPrefObj.key, newPrefObj, prefs);
253         // If this.prefs already exists, notify listeners of the change.
254         if (prefs == this.prefs)
255           this.notifyPath('prefs.' + newPrefObj.key, newPrefObj);
256       }, this);
257       if (!this.prefs)
258         this.prefs = prefs;
259     },
261     /**
262      * Given a 'property-changed' path, returns the key of the preference the
263      * path refers to. E.g., if the path of the changed property is
264      * 'prefs.search.suggest_enabled.value', the key of the pref that changed is
265      * 'search.suggest_enabled'.
266      * @param {string} path
267      * @return {string}
268      * @private
269      */
270     getPrefKeyFromPath_: function(path) {
271       // Skip the first token, which refers to the member variable (this.prefs).
272       var parts = path.split('.');
273       assert(parts.shift() == 'prefs');
275       for (let i = 1; i <= parts.length; i++) {
276         let key = parts.slice(0, i).join('.');
277         // The lastPrefValues_ keys match the pref keys.
278         if (this.lastPrefValues_.hasOwnProperty(key))
279           return key;
280       }
281       return '';
282     },
283   });
284 })();