blob: d782d52f1d0da045a3f65f8c4363707a42228e3e [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Constructor for lib.PreferenceManager objects.
*
* These objects deal with persisting changes to stable storage and notifying
* consumers when preferences change.
*
* It is intended that the backing store could be something other than HTML5
* storage, but there aren't any use cases at the moment. In the future there
* may be a chrome api to store sync-able name/value pairs, and we'd want
* that.
*
* @param {string} opt_prefix The optional prefix to be used for all preference
* names. The '/' character should be used to separate levels of heirarchy,
* if you're going to have that kind of thing. If provided, the prefix
* should start with a '/'. If not provided, it defaults to '/'.
* @param {Storage} opt_storage The storage object to use as a backing store.
* Defaults to window.localStorage.
* @param {EventTarget} opt_eventsource The object that will generate 'storage'
* events for the provided storage object. These events tell us when
* another page has made a modification to the stored data.
*/
lib.PreferenceManager = function(opt_prefix, opt_storage, opt_eventsource) {
var prefix = opt_prefix || '';
if (prefix.substr(prefix.length - 1) != '/')
prefix += '/';
this.prefix_ = prefix;
this.storage_ = opt_storage || window.localStorage;
this.eventSource_ = opt_eventsource || window;
this.prefDefs_ = {};
this.globalObservers_ = [];
// Pending set operations managed by the setLater method.
this.pendingSets_ = {};
this.eventSource_.addEventListener('storage',
this.onStorageEvent_.bind(this), false);
};
/**
* Define a preference.
*
* This registers a name, default value, and onChange handler for a preference.
*
* @param {string} key The name of the preference. This will be prefixed by
* the prefix of this PreferenceManager before written to local storage.
* @param {string|number|boolean|Object|Array|null} value The default value of
* this preference. Anything that can be represented in JSON is a valid
* default value.
*/
lib.PreferenceManager.prototype.definePreference = function(
key, value, opt_onChange) {
var observers = opt_onChange ? [opt_onChange] : [];
this.prefDefs_[key] = { defaultValue: value, observers: observers };
};
/**
* Define multiple preferences with a single function call.
*
* @param {Array} defaults An array of 3-element arrays. Each three element
* array should contain the [key, value, onChange] parameters for a
* preference.
*/
lib.PreferenceManager.prototype.definePreferences = function(defaults) {
for (var i = 0; i < defaults.length; i++) {
this.definePreference(defaults[i][0], defaults[i][1], defaults[i][2]);
}
};
/**
* Define multiple preferences with a single function call.
*
* @param {Array} defaults An array of 3-element arrays. Each three element
* array should contain the [key, value, onChange] parameters for a
* preference.
*/
lib.PreferenceManager.prototype.observePreferences = function(global, map) {
if (global)
this.globalObservers_.push(global);
if (!map)
return;
for (var key in map) {
if (!(key in this.prefDefs_))
throw new Error('Unknown preference: ' + key);
var prefDef = this.prefDefs_[key];
prefDef.observers.push(map[key]);
}
};
/**
* Dispatch the onChange handler for all known preferences.
*
* This can be used if you've changed a preference manager out from under
* a live object (say to switch to a different prefix), in order to get
* the application state in sync with the backing store.
*/
lib.PreferenceManager.prototype.notifyAll = function() {
for (var key in this.prefDefs_) {
this.notifyChange_(key);
}
};
/**
* Dispatch the onChange handler for a given preference.
*
* @param {string} key The preference to notify for, minus the prefix of
* this PreferenceManager.
*/
lib.PreferenceManager.prototype.notifyChange_ = function(key) {
for (var i = 0; i < this.globalObservers_.length; i++)
this.globalObservers_[i](key, this.get(key));
var observers = this.prefDefs_[key].observers;
for (var i = 0; i < observers.length; i++)
observers[i](this.get(key), key, this);
};
/**
* Reset a preference to its default state.
*
* This will dispatch the onChange handler if the preference value actually
* changes.
*
* @param {string} key The preference to reset.
*/
lib.PreferenceManager.prototype.reset = function(key) {
var oldValue = this.get(key);
this.storage_.removeItem(this.prefix_ + key);
if (oldValue != this.get(key))
this.notifyChange_(key);
};
lib.PreferenceManager.prototype.resetAll = function() {
for (var key in this.prefDefs_) {
this.reset(key);
}
};
/**
* Set a preference to a specific value.
*
* This will dispatch the onChange handler if the preference value actually
* changes.
*
* @param {string} key The preference to set.
* @param {*} value The value to set. Anything that can be represented in
* JSON is a valid value.
*/
lib.PreferenceManager.prototype.set = function(key, value) {
var prefDef = this.prefDefs_[key];
if (!prefDef)
throw new Error('Request to set unknown pref: ' + key);
var oldValue = this.get(key);
this.storage_.setItem(this.prefix_ + key, JSON.stringify(value));
if (oldValue != this.get(key))
this.notifyChange_(key);
};
/**
* Set a preference after a short delay, in order to debounce multiple sets.
*
* If this function is invoked multiple times before the delay expires, the
* pref will be saved just once, with the most recent value.
*
* @param {string} key The preference to set.
* @param {*} value The value to set. Anything that can be represented in
* JSON is a valid value.
* @param {integer} opt_delay_ms Optional amount of time to wait, in
* milliseconds. Defaults to 500ms.
*/
lib.PreferenceManager.prototype.setLater = function(key, value, opt_delay_ms) {
var delay_ms = opt_delay_ms || 500;
if (!(key in this.pendingSets_)) {
setTimeout(function() {
this.set(key, this.pendingSets_[key]);
delete this.pendingSets_[key];
}.bind(this),
delay_ms);
}
this.pendingSets_[key] = value;
};
/**
* Get the value of a preference.
*
* @param {string} key The preference to get.
*/
lib.PreferenceManager.prototype.get = function(key) {
var rv = this.storage_.getItem(this.prefix_ + key);
if (rv === null) {
var prefDef = this.prefDefs_[key];
if (!prefDef) {
console.warn('Request for unknown pref: ' + key);
return null;
}
return prefDef.defaultValue;
}
return JSON.parse(rv);
};
/**
* List all preference keys that start with a given prefix.
*
* The prefix will be added to the prefix of this PreferenceManager.
*
* @param {string} opt_prefix Optional prefix.
*/
lib.PreferenceManager.prototype.listKeys = function(opt_prefix) {
var rv = [];
for (var i = 0; i < this.storage_.length; i++) {
var key = this.storage_.key(i);
if (this.prefix_) {
// First check that the key starts with the prefix of this preference
// manager.
if (key.substr(0, this.prefix_.length) != this.prefix_)
continue;
key = key.substr(this.prefix_.length);
}
if (opt_prefix) {
// Then check that it also starts with the prefix requested for this
// listing.
if (key.substr(0, opt_prefix.length) != opt_prefix)
continue;
key = key.substr(opt_prefix.length);
}
rv.push(key);
}
return rv.sort();
};
/**
* Called when a key in the storage changes.
*/
lib.PreferenceManager.prototype.onStorageEvent_ = function(e) {
if (e.storageArea != this.storage_)
return;
var key = e.key;
if (this.prefix_) {
if (key.substr(0, this.prefix_.length) != this.prefix_)
return;
key = key.substr(this.prefix_.length);
}
if (key in this.prefDefs_) {
// Sometimes we'll get notified about prefs that are no longer defined.
this.notifyChange_(key);
}
};