diff options
author | bauerb@chromium.org <bauerb@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-23 15:18:06 +0000 |
---|---|---|
committer | bauerb@chromium.org <bauerb@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-11-23 15:18:06 +0000 |
commit | 13663571e9be13a79b86f15bf15346d12d7e5ebd (patch) | |
tree | 8fab15a7e8978dfa5dbf142b4c4ad4d29f51e3d4 | |
parent | 50bce4993a664114addf41c99ff61dfca19fab45 (diff) | |
download | chromium_src-13663571e9be13a79b86f15bf15346d12d7e5ebd.zip chromium_src-13663571e9be13a79b86f15bf15346d12d7e5ebd.tar.gz chromium_src-13663571e9be13a79b86f15bf15346d12d7e5ebd.tar.bz2 |
Add sample extension that allows setting plugin-specific content settings.
BUG=64155
TEST=none
Review URL: http://codereview.chromium.org/8396001
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@111352 0039d316-1c4b-4281-b951-d872f2087c98
36 files changed, 5305 insertions, 2 deletions
diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings.zip b/chrome/common/extensions/docs/examples/extensions/plugin_settings.zip Binary files differnew file mode 100644 index 0000000..ce75a5c --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings.zip diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/_locales/en/messages.json b/chrome/common/extensions/docs/examples/extensions/plugin_settings/_locales/en/messages.json new file mode 100644 index 0000000..95524bd --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/_locales/en/messages.json @@ -0,0 +1,23 @@ +{ + "extName": { + "message": "Per-plugin content settings" + }, + "extDescription": { + "message": "Customize your content setting for different plug-ins." + }, + "patternColumnHeader": { + "message": "Hostname Pattern" + }, + "settingColumnHeader": { + "message": "Behavior" + }, + "allowRule": { + "message": "Allow" + }, + "blockRule": { + "message": "Block" + }, + "addNewPattern": { + "message": "Add a new hostname pattern" + } +}
\ No newline at end of file diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/bunny128.png b/chrome/common/extensions/docs/examples/extensions/plugin_settings/bunny128.png Binary files differnew file mode 100644 index 0000000..917371d --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/bunny128.png diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/bunny48.png b/chrome/common/extensions/docs/examples/extensions/plugin_settings/bunny48.png Binary files differnew file mode 100644 index 0000000..30434e4 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/bunny48.png diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/css/plugin_list.css b/chrome/common/extensions/docs/examples/extensions/plugin_settings/css/plugin_list.css new file mode 100644 index 0000000..225bd3c --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/css/plugin_list.css @@ -0,0 +1,85 @@ +/* +Copyright (c) 2011 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +*/ + +body { + font-family: Helvetica, sans-serif; + background-color: white; + color: black; + margin: 10px; +} + +.plugin-list { + border: 1px solid #d9d9d9; + border-radius: 2px; + width: 517px; +} + +.plugin-list > li { + padding: 3px; +} + +.plugin-name { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + font-weight: bold; +} + +.num-rules:before { + content: ' '; +} + +.plugin-show-details .num-rules { + display: none; +} + +.plugin-description { + display: inline-block; + -webkit-padding-start: 7px; + width: auto; + overflow: hidden; + text-overflow: ellipsis; + font-size: 95%; +} + +.plugin-details { + -webkit-transition: height .5s ease-in-out; + background: #f5f8f8; + border: 1px solid #b2b2b2; + border-radius: 5px; + padding: 5px; + height: 0; + width: 500px; + opacity: 0; +} + +.plugin-measure-details .plugin-details { + -webkit-transition: none; + height: auto; + visibility: hidden; +} + +li.plugin-show-details { + height: auto; +} + +.plugin-show-details .plugin-description { + height: auto; +} + +.plugin-show-details .plugin-details { + opacity: 1; + height: auto; +} + +.column-headers { + -webkit-margin-start: 17px; + display: -webkit-box; +} + +.column-headers > div { + font-weight: bold; +} diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/css/rule_list.css b/chrome/common/extensions/docs/examples/extensions/plugin_settings/css/rule_list.css new file mode 100644 index 0000000..b67157b --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/css/rule_list.css @@ -0,0 +1,33 @@ +/* +Copyright (c) 2011 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +*/ + +.rule-pattern { + -webkit-box-flex: 1; + -webkit-margin-end: 10px; + -webkit-margin-start: 14px; +} + +.rule-behavior { + display: inline-block; + width: 120px; +} + +select.rule-behavior { + vertical-align: middle; +} + +.rule-list { + border: 1px solid #d9d9d9; + border-radius: 2px; +} + +.pattern-column-header { + -webkit-box-flex: 1; +} + +.setting-column-header { + width: 145px; +} diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/button.css b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/button.css new file mode 100644 index 0000000..5564f42 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/button.css @@ -0,0 +1,43 @@ +button, +input[type='button'], +input[type='submit'] { + -webkit-border-radius: 2px; + -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.1); + -webkit-user-select: none; + background: -webkit-linear-gradient(#fafafa, #f4f4f4 40%, #e5e5e5); + border: 1px solid #aaa; + color: #444; + font-size: inherit; + margin-bottom: 0px; + min-width: 4em; + padding: 3px 12px 3px 12px; +} + +button:hover, +input[type='button']:hover, +input[type='submit']:hover { + -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.2); + background: #ebebeb -webkit-linear-gradient(#fefefe, #f8f8f8 40%, #e9e9e9); + border-color: #999; + color: #222; +} + +button:active, +input[type='button']:active, +input[type='submit']:active { + -webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.2); + background: #ebebeb -webkit-linear-gradient(#f4f4f4, #efefef 40%, #dcdcdc); + color: #333; +} + +button[disabled], +input[type='button'][disabled], +input[type='submit'][disabled], +button[disabled]:hover, +input[type='button'][disabled]:hover, +input[type='submit'][disabled]:hover { + -webkit-box-shadow: none; + background: -webkit-linear-gradient(#fafafa, #f4f4f4 40%, #e5e5e5); + border-color: #aaa; + color: #888; +} diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/chrome_shared.css b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/chrome_shared.css new file mode 100644 index 0000000..fa70aa2 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/chrome_shared.css @@ -0,0 +1,257 @@ +/* Styles common to WebUI pages that share the options pages style */ +body { + cursor: default; + font-size: 13px; +} + +a:link { + color: rgb(63, 110, 194); +} + +a:active { + color: rgb(37, 64, 113); +} + +#navbar-content-title { + -webkit-padding-end: 24px; + -webkit-user-select: none; + color: #53637d; + cursor: pointer; + font-size: 200%; + font-weight: normal; + margin: 0; + padding-bottom: 14px; + padding-top: 13px; + text-align: end; + text-shadow: white 0 1px 2px; +} + +#main-content { + display: -webkit-box; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; +} + +#navbar { + margin: 0; +} + +#navbar-container { + -webkit-border-end: 1px solid #c6c9ce; + background: -webkit-linear-gradient(rgba(234, 238, 243, 0.2), #eaeef3), + -webkit-linear-gradient(left, #eaeef3, #eaeef3 97%, #d3d7db); + position: fixed; + bottom: 0; + /* We set both left and right for the sake of RTL. */ + left: 0; + right: 0; + top: 0; + width: 216px; + z-index: 2; +} + +html[dir='rtl'] #navbar-container { + background: -webkit-linear-gradient(rgba(234, 238, 243, 0), #EAEEF3), + -webkit-linear-gradient(right, #EAEEF3, #EAEEF3 97%, #D3D7DB); +} + +html.hide-menu #navbar-container { + display: none; +} + +#navbar-container > ul { + -webkit-user-select: none; + list-style-type: none; + margin: 0; + padding: 0; +} + +.navbar-item { + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; + color: #426dc9; + cursor: pointer; + display: block; + font-size: 105%; + outline: none; + padding: 7px 0; + text-align: end; + text-shadow: white 0 1px 1px; + -webkit-padding-end: 24px; +} + +.navbar-item:focus { + border-bottom: 1px solid #8faad9; + border-top: 1px solid #8faad9; +} + +.navbar-item-selected { + -webkit-box-shadow: 0px 1px 0px #f7f7f7; + background: -webkit-linear-gradient(left, #bbcee9, #bbcee9 97%, #aabedc); + border-bottom: 1px solid #8faad9; + border-top: 1px solid #8faad9; + color: black; + text-shadow: #bbcee9 0 1px 1px; +} + +#mainview { + -webkit-box-align: stretch; + -webkit-padding-start: 216px; + margin: 0; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + z-index: 1; +} + +html.hide-menu #mainview { + -webkit-padding-start: 0; +} + +#mainview-content { + min-height: 100%; + position: relative; +} + +#page-container { + box-sizing: border-box; + max-width: 888px; + min-width: 600px; + padding: 0 24px; +} + +div.checkbox, +div.radio { + margin: 5px 0; + color: #444; +} + +div.disabled { + color: #888; +} + +/* TEXT */ +input[type='password'], +input[type='text'], +input[type='url'], +input:not([type]) { + -webkit-border-radius: 2px; + border: 1px solid #aaa; + font-size: inherit; + padding: 3px; +} + +/* CHECKBOX, RADIO */ +input[type=checkbox], +input[type=radio] { + margin-left: 0; + margin-right: 0; + position: relative; + top: 1px; +} + +/* Checkbox and radio buttons have different sizes on different platforms. The + * following rules have platform specific tweaks. + * TODO(arv): Test the vertical position on Linux and CrOS as well. + */ + +label > input[type=checkbox], +label > input[type=radio] { + opacity: 0.7; + margin-top: 1px; +} + +html[os=mac] label > input[type=checkbox], +html[os=mac] label > input[type=radio] { + margin-top: 2px; +} + +html[os=chromeos] label > input[type=checkbox], +html[os=chromeos] label > input[type=radio] { + top: 2px; +} + +/* Checkbox and radio hover visuals. + * Their appearance when checked is set to be the same. + */ +label:hover > input[type=checkbox]:not([disabled]), +label:hover > input[type=radio]:not([disabled]), +label > input:not([disabled]):checked { + opacity: 1; +} + +label:hover > input[type=checkbox]:not([disabled]) ~ span, +label:hover > input[type=radio]:not([disabled]) ~ span, +label > input:not([disabled]):checked ~ span { + color: #222; +} + +/* This will 'disable' the label associated with any input whose next sibling is + * the span containing the label (usually a checkbox or radio). + */ +label > input[disabled] ~ span { + color: #888; +} + +/* Elements that need to be LTR even in an RTL context, but should align + * right. (Namely, URLs, search engine names, etc.) + */ +html[dir='rtl'] .weakrtl { + direction: ltr; + text-align: right; +} + +/* Input fields in search engine table need to be weak-rtl. Since those input + * fields are generated for all cr.ListItem elements (and we only want weakrtl + * on some), the class needs to be on the enclosing div. + */ +html[dir='rtl'] div.weakrtl input { + direction: ltr; + text-align: right; +} + +html[dir='rtl'] .favicon-cell.weakrtl { + -webkit-padding-end: 22px; + -webkit-padding-start: 0; +} + +/* weakrtl for selection drop downs needs to account for the fact that + * Webkit does not honor the text-align attribute for the select element. + * (See Webkit bug #40216) + */ +html[dir='rtl'] select.weakrtl { + direction: rtl; +} + +html[dir='rtl'] select.weakrtl option { + direction: ltr; +} + +/* WebKit does not honor alignment for text specified via placeholder attrib. + * This CSS is a workaround. Please remove once WebKit bug is fixed. + * https://bugs.webkit.org/show_bug.cgi?id=63367 + */ +html[dir='rtl'] input.weakrtl::-webkit-input-placeholder, +html[dir='rtl'] .weakrtl input::-webkit-input-placeholder { + direction: rtl; +} + +.page h1 { + -webkit-padding-end: 24px; + -webkit-user-select: none; + border-bottom: 1px solid #eeeeee; + color: #53637d; + font-size: 200%; + font-weight: normal; + margin: 0; + padding-bottom: 4px; + padding-top: 13px; + text-shadow: white 0 1px 2px; +} + + diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/list.css b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/list.css new file mode 100644 index 0000000..75d2a92 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/list.css @@ -0,0 +1,89 @@ + +list, +grid { + display: block; + outline: none; + overflow: auto; + position: relative; /* Make sure that item offsets are relative to the + list. */ +} + +list > *, +grid > * { + -webkit-user-select: none; + background-color: rgba(255,255,255,0); + border: 1px solid rgba(255,255,255,0); /* transparent white */ + border-radius: 2px; + cursor: default; + line-height: 20px; + margin: -1px 0; + overflow: hidden; + padding: 0px 3px; + position: relative; /* to allow overlap */ + text-overflow: ellipsis; + white-space: pre; +} + +list > * { + display: block; +} + +grid > * { + display: inline-block; +} + +list > [lead], +grid > [lead] { + border-color: transparent; +} + +list:focus > [lead], +grid:focus > [lead] { + border-color: hsl(214, 91%, 65%); + z-index: 2; +} + +list > [anchor], +grid > [anchor] { + +} + +list:not([disabled]) > :hover, +grid:not([disabled]) > :hover { + border-color: hsl(214, 91%, 85%); + z-index: 1; + background-color: hsl(214, 91%, 97%); +} + +list > [selected], +grid > [selected] { + border-color: hsl(0, 0%, 85%); + background-color: hsl(0,0%,90%); + z-index: 2; + background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0.8), + rgba(255, 255, 255, 0)); +} + +list:focus > [selected], +grid:focus > [selected] { + background-color: hsl(214,91%,89%); + border-color: hsl(214, 91%, 65%); +} + +list:focus > [lead][selected], +list > [selected]:hover, +grid:focus > [lead][selected], +grid > [selected]:hover { + background-color: hsl(214, 91%, 87%); + border-color: hsl(214, 91%, 65%); +} + +list > .spacer, +grid > .spacer { + border: 0; + box-sizing: border-box; + display: block; + overflow: hidden; + visibility: hidden; + margin: 0; +} diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/select.css b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/select.css new file mode 100644 index 0000000..ba951e5 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/select.css @@ -0,0 +1,51 @@ +/* Copyright (c) 2011 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * This is the generic select css used on various WebUI implementations. + */ + +select { + -webkit-appearance: button; + -webkit-border-radius: 2px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -webkit-padding-end: 20px; + -webkit-padding-start: 2px; + -webkit-user-select: none; + background-image: url("../images/select.png"), + -webkit-linear-gradient(#fafafa, #f4f4f4 40%, #e5e5e5); + background-position: center right; + background-repeat: no-repeat; + border: 1px solid #aaa; + color: #555; + font-size: inherit; + margin: 0; + overflow: hidden; + padding-top: 2px; + padding-bottom: 2px; + text-overflow: ellipsis; + white-space: nowrap; +} + +html[dir='rtl'] select { + background-position: center left; +} + + +select:disabled { + color: graytext; +} + +select:enabled:hover { + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + background-image: url("../images/select.png"), + -webkit-linear-gradient(#fefefe, #f8f8f8 40%, #e9e9e9); + color: #333; +} + +select:enabled:active { + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.2); + background-image: url("../images/select.png"), + -webkit-linear-gradient(#f4f4f4, #efefef 40%, #dcdcdc); + color: #444; +}
\ No newline at end of file diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/images/select.png b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/images/select.png Binary files differnew file mode 100644 index 0000000..c20d304 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/images/select.png diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr.js new file mode 100644 index 0000000..2a370a1 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr.js @@ -0,0 +1,381 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const cr = (function() { + + /** + * Whether we are using a Mac or not. + * @type {boolean} + */ + const isMac = /Mac/.test(navigator.platform); + + /** + * Whether this is on the Windows platform or not. + * @type {boolean} + */ + const isWindows = /Win/.test(navigator.platform); + + /** + * Whether this is on chromeOS or not. + * @type {boolean} + */ + const isChromeOS = /CrOS/.test(navigator.userAgent); + + /** + * Whether this is on touchui build or not. + * @type {boolean} + */ + const isTouch = /Touch/.test(navigator.userAgent); + + /** + * Whether this is on vanilla Linux (not chromeOS). + * @type {boolean} + */ + const isLinux = /Linux/.test(navigator.userAgent); + + /** + * Whether this uses GTK or not. + * @type {boolean} + */ + const isGTK = /GTK/.test(chrome.toolkit); + + /** + * Whether this uses the views toolkit or not. + * @type {boolean} + */ + const isViews = /views/.test(chrome.toolkit); + + /** + * Sets the os and toolkit attributes in the <html> element so that platform + * specific css rules can be applied. + */ + function enablePlatformSpecificCSSRules() { + if (isMac) + doc.documentElement.setAttribute('os', 'mac'); + if (isWindows) + doc.documentElement.setAttribute('os', 'windows'); + if (isChromeOS) + doc.documentElement.setAttribute('os', 'chromeos'); + if (isLinux) + doc.documentElement.setAttribute('os', 'linux'); + if (isGTK) + doc.documentElement.setAttribute('toolkit', 'gtk'); + if (isViews) + doc.documentElement.setAttribute('toolkit', 'views'); + } + + /** + * Builds an object structure for the provided namespace path, + * ensuring that names that already exist are not overwritten. For + * example: + * "a.b.c" -> a = {};a.b={};a.b.c={}; + * @param {string} name Name of the object that this file defines. + * @param {*=} opt_object The object to expose at the end of the path. + * @param {Object=} opt_objectToExportTo The object to add the path to; + * default is {@code window}. + * @private + */ + function exportPath(name, opt_object, opt_objectToExportTo) { + var parts = name.split('.'); + var cur = opt_objectToExportTo || window /* global */; + + for (var part; parts.length && (part = parts.shift());) { + if (!parts.length && opt_object !== undefined) { + // last part and we have an object; use it + cur[part] = opt_object; + } else if (part in cur) { + cur = cur[part]; + } else { + cur = cur[part] = {}; + } + } + return cur; + }; + + // cr.Event is called CrEvent in here to prevent naming conflicts. We also + // store the original Event in case someone does a global alias of cr.Event. + const DomEvent = Event; + + /** + * Creates a new event to be used with cr.EventTarget or DOM EventTarget + * objects. + * @param {string} type The name of the event. + * @param {boolean=} opt_bubbles Whether the event bubbles. Default is false. + * @param {boolean=} opt_preventable Whether the default action of the event + * can be prevented. + * @constructor + * @extends {DomEvent} + */ + function CrEvent(type, opt_bubbles, opt_preventable) { + var e = cr.doc.createEvent('Event'); + e.initEvent(type, !!opt_bubbles, !!opt_preventable); + e.__proto__ = CrEvent.prototype; + return e; + } + + CrEvent.prototype = { + __proto__: DomEvent.prototype + }; + + /** + * Fires a property change event on the target. + * @param {EventTarget} target The target to dispatch the event on. + * @param {string} propertyName The name of the property that changed. + * @param {*} newValue The new value for the property. + * @param {*} oldValue The old value for the property. + */ + function dispatchPropertyChange(target, propertyName, newValue, oldValue) { + var e = new CrEvent(propertyName + 'Change'); + e.propertyName = propertyName; + e.newValue = newValue; + e.oldValue = oldValue; + target.dispatchEvent(e); + } + + /** + * The kind of property to define in {@code defineProperty}. + * @enum {number} + */ + const PropertyKind = { + /** + * Plain old JS property where the backing data is stored as a "private" + * field on the object. + */ + JS: 'js', + + /** + * The property backing data is stored as an attribute on an element. + */ + ATTR: 'attr', + + /** + * The property backing data is stored as an attribute on an element. If the + * element has the attribute then the value is true. + */ + BOOL_ATTR: 'boolAttr' + }; + + /** + * Helper function for defineProperty that returns the getter to use for the + * property. + * @param {string} name + * @param {cr.PropertyKind} kind + * @return {function():*} The getter for the property. + */ + function getGetter(name, kind) { + switch (kind) { + case PropertyKind.JS: + var privateName = name + '_'; + return function() { + return this[privateName]; + }; + case PropertyKind.ATTR: + return function() { + return this.getAttribute(name); + }; + case PropertyKind.BOOL_ATTR: + return function() { + return this.hasAttribute(name); + }; + } + } + + /** + * Helper function for defineProperty that returns the setter of the right + * kind. + * @param {string} name The name of the property we are defining the setter + * for. + * @param {cr.PropertyKind} kind The kind of property we are getting the + * setter for. + * @param {function(*):void} opt_setHook A function to run after the property + * is set, but before the propertyChange event is fired. + * @return {function(*):void} The function to use as a setter. + */ + function getSetter(name, kind, opt_setHook) { + switch (kind) { + case PropertyKind.JS: + var privateName = name + '_'; + return function(value) { + var oldValue = this[privateName]; + if (value !== oldValue) { + this[privateName] = value; + if (opt_setHook) + opt_setHook.call(this, value, oldValue); + dispatchPropertyChange(this, name, value, oldValue); + } + }; + + case PropertyKind.ATTR: + return function(value) { + var oldValue = this[name]; + if (value !== oldValue) { + if (value == undefined) + this.removeAttribute(name); + else + this.setAttribute(name, value); + if (opt_setHook) + opt_setHook.call(this, value, oldValue); + dispatchPropertyChange(this, name, value, oldValue); + } + }; + + case PropertyKind.BOOL_ATTR: + return function(value) { + var oldValue = this[name]; + if (value !== oldValue) { + if (value) + this.setAttribute(name, name); + else + this.removeAttribute(name); + if (opt_setHook) + opt_setHook.call(this, value, oldValue); + dispatchPropertyChange(this, name, value, oldValue); + } + }; + } + } + + /** + * Defines a property on an object. When the setter changes the value a + * property change event with the type {@code name + 'Change'} is fired. + * @param {!Object} obj The object to define the property for. + * @param {string} name The name of the property. + * @param {cr.PropertyKind=} opt_kind What kind of underlying storage to use. + * @param {function(*):void} opt_setHook A function to run after the + * property is set, but before the propertyChange event is fired. + */ + function defineProperty(obj, name, opt_kind, opt_setHook) { + if (typeof obj == 'function') + obj = obj.prototype; + + var kind = opt_kind || PropertyKind.JS; + + if (!obj.__lookupGetter__(name)) { + obj.__defineGetter__(name, getGetter(name, kind)); + } + + if (!obj.__lookupSetter__(name)) { + obj.__defineSetter__(name, getSetter(name, kind, opt_setHook)); + } + } + + /** + * Counter for use with createUid + */ + var uidCounter = 1; + + /** + * @return {number} A new unique ID. + */ + function createUid() { + return uidCounter++; + } + + /** + * Returns a unique ID for the item. This mutates the item so it needs to be + * an object + * @param {!Object} item The item to get the unique ID for. + * @return {number} The unique ID for the item. + */ + function getUid(item) { + if (item.hasOwnProperty('uid')) + return item.uid; + return item.uid = createUid(); + } + + /** + * Dispatches a simple event on an event target. + * @param {!EventTarget} target The event target to dispatch the event on. + * @param {string} type The type of the event. + * @param {boolean=} opt_bubbles Whether the event bubbles or not. + * @param {boolean=} opt_cancelable Whether the default action of the event + * can be prevented. + * @return {boolean} If any of the listeners called {@code preventDefault} + * during the dispatch this will return false. + */ + function dispatchSimpleEvent(target, type, opt_bubbles, opt_cancelable) { + var e = new cr.Event(type, opt_bubbles, opt_cancelable); + return target.dispatchEvent(e); + } + + /** + * @param {string} name + * @param {!Function} fun + */ + function define(name, fun) { + var obj = exportPath(name); + var exports = fun(); + for (var propertyName in exports) { + // Maybe we should check the prototype chain here? The current usage + // pattern is always using an object literal so we only care about own + // properties. + var propertyDescriptor = Object.getOwnPropertyDescriptor(exports, + propertyName); + if (propertyDescriptor) + Object.defineProperty(obj, propertyName, propertyDescriptor); + } + } + + /** + * Document used for various document related operations. + * @type {!Document} + */ + var doc = document; + + + /** + * Allows you to run func in the context of a different document. + * @param {!Document} document The document to use. + * @param {function():*} func The function to call. + */ + function withDoc(document, func) { + var oldDoc = doc; + doc = document; + try { + func(); + } finally { + doc = oldDoc; + } + } + + /** + * Adds a {@code getInstance} static method that always return the same + * instance object. + * @param {!Function} ctor The constructor for the class to add the static + * method to. + */ + function addSingletonGetter(ctor) { + ctor.getInstance = function() { + return ctor.instance_ || (ctor.instance_ = new ctor()); + }; + } + + return { + addSingletonGetter: addSingletonGetter, + isChromeOS: isChromeOS, + isMac: isMac, + isWindows: isWindows, + isLinux: isLinux, + isViews: isViews, + isTouch: isTouch, + enablePlatformSpecificCSSRules: enablePlatformSpecificCSSRules, + define: define, + defineProperty: defineProperty, + PropertyKind: PropertyKind, + createUid: createUid, + getUid: getUid, + dispatchSimpleEvent: dispatchSimpleEvent, + dispatchPropertyChange: dispatchPropertyChange, + + /** + * The document that we are currently using. + * @type {!Document} + */ + get doc() { + return doc; + }, + withDoc: withDoc, + Event: CrEvent + }; +})(); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/event_target.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/event_target.js new file mode 100644 index 0000000..5bcb41d --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/event_target.js @@ -0,0 +1,104 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This contains an implementation of the EventTarget interface + * as defined by DOM Level 2 Events. + */ + +cr.define('cr', function() { + + /** + * Creates a new EventTarget. This class implements the DOM level 2 + * EventTarget interface and can be used wherever those are used. + * @constructor + */ + function EventTarget() { + } + + EventTarget.prototype = { + + /** + * Adds an event listener to the target. + * @param {string} type The name of the event. + * @param {!Function|{handleEvent:Function}} handler The handler for the + * event. This is called when the event is dispatched. + */ + addEventListener: function(type, handler) { + if (!this.listeners_) + this.listeners_ = Object.create(null); + if (!(type in this.listeners_)) { + this.listeners_[type] = [handler]; + } else { + var handlers = this.listeners_[type]; + if (handlers.indexOf(handler) < 0) + handlers.push(handler); + } + }, + + /** + * Removes an event listener from the target. + * @param {string} type The name of the event. + * @param {!Function|{handleEvent:Function}} handler The handler for the + * event. + */ + removeEventListener: function(type, handler) { + if (!this.listeners_) + return; + if (type in this.listeners_) { + var handlers = this.listeners_[type]; + var index = handlers.indexOf(handler); + if (index >= 0) { + // Clean up if this was the last listener. + if (handlers.length == 1) + delete this.listeners_[type]; + else + handlers.splice(index, 1); + } + } + }, + + /** + * Dispatches an event and calls all the listeners that are listening to + * the type of the event. + * @param {!cr.event.Event} event The event to dispatch. + * @return {boolean} Whether the default action was prevented. If someone + * calls preventDefault on the event object then this returns false. + */ + dispatchEvent: function(event) { + if (!this.listeners_) + return true; + + // Since we are using DOM Event objects we need to override some of the + // properties and methods so that we can emulate this correctly. + var self = this; + event.__defineGetter__('target', function() { + return self; + }); + event.preventDefault = function() { + this.returnValue = false; + }; + + var type = event.type; + var prevented = 0; + if (type in this.listeners_) { + // Clone to prevent removal during dispatch + var handlers = this.listeners_[type].concat(); + for (var i = 0, handler; handler = handlers[i]; i++) { + if (handler.handleEvent) + prevented |= handler.handleEvent.call(handler, event) === false; + else + prevented |= handler.call(this, event) === false; + } + } + + return !prevented && event.returnValue; + } + }; + + // Export + return { + EventTarget: EventTarget + }; +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui.js new file mode 100644 index 0000000..ea286b2 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui.js @@ -0,0 +1,161 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('cr.ui', function() { + + /** + * Decorates elements as an instance of a class. + * @param {string|!Element} source The way to find the element(s) to decorate. + * If this is a string then {@code querySeletorAll} is used to find the + * elements to decorate. + * @param {!Function} constr The constructor to decorate with. The constr + * needs to have a {@code decorate} function. + */ + function decorate(source, constr) { + var elements; + if (typeof source == 'string') + elements = cr.doc.querySelectorAll(source); + else + elements = [source]; + + for (var i = 0, el; el = elements[i]; i++) { + if (!(el instanceof constr)) + constr.decorate(el); + } + } + + /** + * Helper function for creating new element for define. + */ + function createElementHelper(tagName, opt_bag) { + // Allow passing in ownerDocument to create in a different document. + var doc; + if (opt_bag && opt_bag.ownerDocument) + doc = opt_bag.ownerDocument; + else + doc = cr.doc; + return doc.createElement(tagName); + } + + /** + * Creates the constructor for a UI element class. + * + * Usage: + * <pre> + * var List = cr.ui.define('list'); + * List.prototype = { + * __proto__: HTMLUListElement.prototype, + * decorate: function() { + * ... + * }, + * ... + * }; + * </pre> + * + * @param {string|Function} tagNameOrFunction The tagName or + * function to use for newly created elements. If this is a function it + * needs to return a new element when called. + * @return {function(Object=):Element} The constructor function which takes + * an optional property bag. The function also has a static + * {@code decorate} method added to it. + */ + function define(tagNameOrFunction) { + var createFunction, tagName; + if (typeof tagNameOrFunction == 'function') { + createFunction = tagNameOrFunction; + tagName = ''; + } else { + createFunction = createElementHelper; + tagName = tagNameOrFunction; + } + + /** + * Creates a new UI element constructor. + * @param {Object=} opt_propertyBag Optional bag of properties to set on the + * object after created. The property {@code ownerDocument} is special + * cased and it allows you to create the element in a different + * document than the default. + * @constructor + */ + function f(opt_propertyBag) { + var el = createFunction(tagName, opt_propertyBag); + f.decorate(el); + for (var propertyName in opt_propertyBag) { + el[propertyName] = opt_propertyBag[propertyName]; + } + return el; + } + + /** + * Decorates an element as a UI element class. + * @param {!Element} el The element to decorate. + */ + f.decorate = function(el) { + el.__proto__ = f.prototype; + el.decorate(); + }; + + return f; + } + + /** + * Input elements do not grow and shrink with their content. This is a simple + * (and not very efficient) way of handling shrinking to content with support + * for min width and limited by the width of the parent element. + * @param {HTMLElement} el The element to limit the width for. + * @param {number} parentEl The parent element that should limit the size. + * @param {number} min The minimum width. + */ + function limitInputWidth(el, parentEl, min) { + // Needs a size larger than borders + el.style.width = '10px'; + var doc = el.ownerDocument; + var win = doc.defaultView; + var computedStyle = win.getComputedStyle(el); + var parentComputedStyle = win.getComputedStyle(parentEl); + var rtl = computedStyle.direction == 'rtl'; + + // To get the max width we get the width of the treeItem minus the position + // of the input. + var inputRect = el.getBoundingClientRect(); // box-sizing + var parentRect = parentEl.getBoundingClientRect(); + var startPos = rtl ? parentRect.right - inputRect.right : + inputRect.left - parentRect.left; + + // Add up border and padding of the input. + var inner = parseInt(computedStyle.borderLeftWidth, 10) + + parseInt(computedStyle.paddingLeft, 10) + + parseInt(computedStyle.paddingRight, 10) + + parseInt(computedStyle.borderRightWidth, 10); + + // We also need to subtract the padding of parent to prevent it to overflow. + var parentPadding = rtl ? parseInt(parentComputedStyle.paddingLeft, 10) : + parseInt(parentComputedStyle.paddingRight, 10); + + var max = parentEl.clientWidth - startPos - inner - parentPadding; + + function limit() { + if (el.scrollWidth > max) { + el.style.width = max + 'px'; + } else { + el.style.width = 0; + var sw = el.scrollWidth; + if (sw < min) { + el.style.width = min + 'px'; + } else { + el.style.width = sw + 'px'; + } + } + } + + el.addEventListener('input', limit); + limit(); + } + + return { + decorate: decorate, + define: define, + limitInputWidth: limitInputWidth + }; +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/array_data_model.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/array_data_model.js new file mode 100644 index 0000000..f12bd1d --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/array_data_model.js @@ -0,0 +1,363 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview This is a data model representin + */ + +cr.define('cr.ui', function() { + const EventTarget = cr.EventTarget; + const Event = cr.Event; + + /** + * A data model that wraps a simple array and supports sorting by storing + * initial indexes of elements for each position in sorted array. + * @param {!Array} array The underlying array. + * @constructor + * @extends {EventTarget} + */ + function ArrayDataModel(array) { + this.array_ = array; + this.indexes_ = []; + this.compareFunctions_ = {}; + + for (var i = 0; i < array.length; i++) { + this.indexes_.push(i); + } + } + + ArrayDataModel.prototype = { + __proto__: EventTarget.prototype, + + /** + * The length of the data model. + * @type {number} + */ + get length() { + return this.array_.length; + }, + + /** + * Returns the item at the given index. + * This implementation returns the item at the given index in the sorted + * array. + * @param {number} index The index of the element to get. + * @return {*} The element at the given index. + */ + item: function(index) { + if (index >= 0 && index < this.length) + return this.array_[this.indexes_[index]]; + return undefined; + }, + + /** + * Returns compare function set for given field. + * @param {string} field The field to get compare function for. + * @return {function(*, *): number} Compare function set for given field. + */ + compareFunction: function(field) { + return this.compareFunctions_[field]; + }, + + /** + * Sets compare function for given field. + * @param {string} field The field to set compare function. + * @param {function(*, *): number} Compare function to set for given field. + */ + setCompareFunction: function(field, compareFunction) { + if (!this.compareFunctions_) { + this.compareFunctions_ = {}; + } + this.compareFunctions_[field] = compareFunction; + }, + + /** + * Returns current sort status. + * @return {!Object} Current sort status. + */ + get sortStatus() { + if (this.sortStatus_) { + return this.createSortStatus( + this.sortStatus_.field, this.sortStatus_.direction); + } else { + return this.createSortStatus(null, null); + } + }, + + /** + * Returns the first matching item. + * @param {*} item The item to find. + * @param {number=} opt_fromIndex If provided, then the searching start at + * the {@code opt_fromIndex}. + * @return {number} The index of the first found element or -1 if not found. + */ + indexOf: function(item, opt_fromIndex) { + return this.array_.indexOf(item, opt_fromIndex); + }, + + /** + * Returns an array of elements in a selected range. + * @param {number=} opt_from The starting index of the selected range. + * @param {number=} opt_to The ending index of selected range. + * @return {Array} An array of elements in the selected range. + */ + slice: function(opt_from, opt_to) { + return this.array_.slice.apply(this.array_, arguments); + }, + + /** + * This removes and adds items to the model. + * This dispatches a splice event. + * This implementation runs sort after splice and creates permutation for + * the whole change. + * @param {number} index The index of the item to update. + * @param {number} deleteCount The number of items to remove. + * @param {...*} The items to add. + * @return {!Array} An array with the removed items. + */ + splice: function(index, deleteCount, var_args) { + var addCount = arguments.length - 2; + var newIndexes = []; + var deletePermutation = []; + var deleted = 0; + for (var i = 0; i < this.indexes_.length; i++) { + var oldIndex = this.indexes_[i]; + if (oldIndex < index) { + newIndexes.push(oldIndex); + deletePermutation.push(i - deleted); + } else if (oldIndex >= index + deleteCount) { + newIndexes.push(oldIndex - deleteCount + addCount); + deletePermutation.push(i - deleted); + } else { + deletePermutation.push(-1); + deleted++; + } + } + for (var i = 0; i < addCount; i++) { + newIndexes.push(index + i); + } + this.indexes_ = newIndexes; + + var arr = this.array_; + + // TODO(arv): Maybe unify splice and change events? + var spliceEvent = new Event('splice'); + spliceEvent.index = index; + spliceEvent.removed = arr.slice(index, index + deleteCount); + spliceEvent.added = Array.prototype.slice.call(arguments, 2); + + var rv = arr.splice.apply(arr, arguments); + + var status = this.sortStatus; + // if sortStatus.field is null, this restores original order. + var sortPermutation = this.doSort_(this.sortStatus.field, + this.sortStatus.direction); + if (sortPermutation) { + var splicePermutation = deletePermutation.map(function(element) { + return element != -1 ? sortPermutation[element] : -1; + }); + this.dispatchPermutedEvent_(splicePermutation); + } else { + this.dispatchPermutedEvent_(deletePermutation); + } + + this.dispatchEvent(spliceEvent); + + // If real sorting is needed, we should first call prepareSort (data may + // change), and then sort again. + // Still need to finish the sorting above (including events), so + // list will not go to inconsistent state. + if (status.field) { + setTimeout(this.sort.bind(this, status.field, status.direction), 0); + } + return rv; + }, + + /** + * Appends items to the end of the model. + * + * This dispatches a splice event. + * + * @param {...*} The items to append. + * @return {number} The new length of the model. + */ + push: function(var_args) { + var args = Array.prototype.slice.call(arguments); + args.unshift(this.length, 0); + this.splice.apply(this, args); + return this.length; + }, + + /** + * Use this to update a given item in the array. This does not remove and + * reinsert a new item. + * This dispatches a change event. + * This runs sort after updating. + * @param {number} index The index of the item to update. + */ + updateIndex: function(index) { + if (index < 0 || index >= this.length) + throw Error('Invalid index, ' + index); + + // TODO(arv): Maybe unify splice and change events? + var e = new Event('change'); + e.index = index; + this.dispatchEvent(e); + + if (this.sortStatus.field) { + var status = this.sortStatus; + var sortPermutation = this.doSort_(this.sortStatus.field, + this.sortStatus.direction); + if (sortPermutation) + this.dispatchPermutedEvent_(sortPermutation); + // We should first call prepareSort (data may change), and then sort. + // Still need to finish the sorting above (including events), so + // list will not go to inconsistent state. + setTimeout(this.sort.bind(this, status.field, status.direction), 0); + } + }, + + /** + * Creates sort status with given field and direction. + * @param {string} field Sort field. + * @param {string} direction Sort direction. + * @return {!Object} Created sort status. + */ + createSortStatus: function(field, direction) { + return { + field: field, + direction: direction + }; + }, + + /** + * Called before a sort happens so that you may fetch additional data + * required for the sort. + * + * @param {string} field Sort field. + * @param {function()} callback The function to invoke when preparation + * is complete. + */ + prepareSort: function(field, callback) { + callback(); + }, + + /** + * Sorts data model according to given field and direction and dispathes + * sorted event. + * @param {string} field Sort field. + * @param {string} direction Sort direction. + */ + sort: function(field, direction) { + var self = this; + + this.prepareSort(field, function() { + var sortPermutation = self.doSort_(field, direction); + if (sortPermutation) + self.dispatchPermutedEvent_(sortPermutation); + self.dispatchSortEvent_(); + }); + }, + + /** + * Sorts data model according to given field and direction. + * @param {string} field Sort field. + * @param {string} direction Sort direction. + */ + doSort_: function(field, direction) { + var compareFunction = this.sortFunction_(field, direction); + var positions = []; + for (var i = 0; i < this.length; i++) { + positions[this.indexes_[i]] = i; + } + this.indexes_.sort(compareFunction); + this.sortStatus_ = this.createSortStatus(field, direction); + var sortPermutation = []; + var changed = false; + for (var i = 0; i < this.length; i++) { + if (positions[this.indexes_[i]] != i) + changed = true; + sortPermutation[positions[this.indexes_[i]]] = i; + } + if (changed) + return sortPermutation; + return null; + }, + + dispatchSortEvent_: function() { + var e = new Event('sorted'); + this.dispatchEvent(e); + }, + + dispatchPermutedEvent_: function(permutation) { + var e = new Event('permuted'); + e.permutation = permutation; + e.newLength = this.length; + this.dispatchEvent(e); + }, + + /** + * Creates compare function for the field. + * Returns the function set as sortFunction for given field + * or default compare function + * @param {string} field Sort field. + * @param {function(*, *): number} Compare function. + */ + createCompareFunction_: function(field) { + var compareFunction = + this.compareFunctions_ ? this.compareFunctions_[field] : null; + var defaultValuesCompareFunction = this.defaultValuesCompareFunction; + if (compareFunction) { + return compareFunction; + } else { + return function(a, b) { + return defaultValuesCompareFunction.call(null, a[field], b[field]); + } + } + return compareFunction; + }, + + /** + * Creates compare function for given field and direction. + * @param {string} field Sort field. + * @param {string} direction Sort direction. + * @param {function(*, *): number} Compare function. + */ + sortFunction_: function(field, direction) { + var compareFunction = null; + if (field !== null) + compareFunction = this.createCompareFunction_(field); + var dirMultiplier = direction == 'desc' ? -1 : 1; + + return function(index1, index2) { + var item1 = this.array_[index1]; + var item2 = this.array_[index2]; + + var compareResult = 0; + if (typeof(compareFunction) === 'function') + compareResult = compareFunction.call(null, item1, item2); + if (compareResult != 0) + return dirMultiplier * compareResult; + return dirMultiplier * this.defaultValuesCompareFunction(index1, + index2); + }.bind(this); + }, + + /** + * Default compare function. + */ + defaultValuesCompareFunction: function(a, b) { + // We could insert i18n comparisons here. + if (a < b) + return -1; + if (a > b) + return 1; + return 0; + } + }; + + return { + ArrayDataModel: ArrayDataModel + }; +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list.js new file mode 100644 index 0000000..291d5ff --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list.js @@ -0,0 +1,971 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// require: array_data_model.js +// require: list_selection_model.js +// require: list_selection_controller.js +// require: list_item.js + +/** + * @fileoverview This implements a list control. + */ + +cr.define('cr.ui', function() { + const ListSelectionModel = cr.ui.ListSelectionModel; + const ListSelectionController = cr.ui.ListSelectionController; + const ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Whether a mouse event is inside the element viewport. This will return + * false if the mouseevent was generated over a border or a scrollbar. + * @param {!HTMLElement} el The element to test the event with. + * @param {!Event} e The mouse event. + * @param {boolean} Whether the mouse event was inside the viewport. + */ + function inViewport(el, e) { + var rect = el.getBoundingClientRect(); + var x = e.clientX; + var y = e.clientY; + return x >= rect.left + el.clientLeft && + x < rect.left + el.clientLeft + el.clientWidth && + y >= rect.top + el.clientTop && + y < rect.top + el.clientTop + el.clientHeight; + } + + /** + * Creates an item (dataModel.item(0)) and measures its height. + * @param {!List} list The list to create the item for. + * @param {ListItem=} opt_item The list item to use to do the measuring. If + * this is not provided an item will be created based on the first value + * in the model. + * @return {{height: number, marginVertical: number, width: number, + * marginHorizontal: number}} The height and width of the item, taking + * margins into account, and the height and width of the margins + * themselves. + */ + function measureItem(list, opt_item) { + var dataModel = list.dataModel; + if (!dataModel || !dataModel.length) + return 0; + var item = opt_item || list.createItem(dataModel.item(0)); + if (!opt_item) + list.appendChild(item); + + var rect = item.getBoundingClientRect(); + var cs = getComputedStyle(item); + var mt = parseFloat(cs.marginTop); + var mb = parseFloat(cs.marginBottom); + var ml = parseFloat(cs.marginLeft); + var mr = parseFloat(cs.marginRight); + var h = rect.height; + var w = rect.width; + var mh = 0; + var mv = 0; + + // Handle margin collapsing. + if (mt < 0 && mb < 0) { + mv = Math.min(mt, mb); + } else if (mt >= 0 && mb >= 0) { + mv = Math.max(mt, mb); + } else { + mv = mt + mb; + } + h += mv; + + if (ml < 0 && mr < 0) { + mh = Math.min(ml, mr); + } else if (ml >= 0 && mr >= 0) { + mh = Math.max(ml, mr); + } else { + mh = ml + mr; + } + w += mh; + + if (!opt_item) + list.removeChild(item); + return { + height: Math.max(0, h), marginVertical: mv, + width: Math.max(0, w), marginHorizontal: mh}; + } + + function getComputedStyle(el) { + return el.ownerDocument.defaultView.getComputedStyle(el); + } + + /** + * Creates a new list element. + * @param {Object=} opt_propertyBag Optional properties. + * @constructor + * @extends {HTMLUListElement} + */ + var List = cr.ui.define('list'); + + List.prototype = { + __proto__: HTMLUListElement.prototype, + + /** + * Measured size of list items. This is lazily calculated the first time it + * is needed. Note that lead item is allowed to have a different height, to + * accommodate lists where a single item at a time can be expanded to show + * more detail. + * @type {{height: number, marginVertical: number, width: number, + * marginHorizontal: number}} + * @private + */ + measured_: undefined, + + /** + * The height of the lead item, which is allowed to have a different height + * than other list items to accommodate lists where a single item at a time + * can be expanded to show more detail. It is explicitly set by client code + * when the height of the lead item is changed with {@code set + * leadItemHeight}, and presumed equal to {@code itemHeight_} otherwise. + * @type {number} + * @private + */ + leadItemHeight_: 0, + + /** + * Whether or not the list is autoexpanding. If true, the list resizes + * its height to accomadate all children. + * @type {boolean} + * @private + */ + autoExpands_: false, + + /** + * Function used to create grid items. + * @type {function(): !ListItem} + * @private + */ + itemConstructor_: cr.ui.ListItem, + + /** + * Function used to create grid items. + * @type {function(): !ListItem} + */ + get itemConstructor() { + return this.itemConstructor_; + }, + set itemConstructor(func) { + if (func != this.itemConstructor_) { + this.itemConstructor_ = func; + this.cachedItems_ = {}; + this.redraw(); + } + }, + + dataModel_: null, + + /** + * The data model driving the list. + * @type {ArrayDataModel} + */ + set dataModel(dataModel) { + if (this.dataModel_ != dataModel) { + if (!this.boundHandleDataModelPermuted_) { + this.boundHandleDataModelPermuted_ = + this.handleDataModelPermuted_.bind(this); + this.boundHandleDataModelChange_ = + this.handleDataModelChange_.bind(this); + } + + if (this.dataModel_) { + this.dataModel_.removeEventListener( + 'permuted', + this.boundHandleDataModelPermuted_); + this.dataModel_.removeEventListener('change', + this.boundHandleDataModelChange_); + } + + this.dataModel_ = dataModel; + + this.cachedItems_ = {}; + this.selectionModel.clear(); + if (dataModel) + this.selectionModel.adjustLength(dataModel.length); + + if (this.dataModel_) { + this.dataModel_.addEventListener( + 'permuted', + this.boundHandleDataModelPermuted_); + this.dataModel_.addEventListener('change', + this.boundHandleDataModelChange_); + } + + this.redraw(); + } + }, + + get dataModel() { + return this.dataModel_; + }, + + /** + * The selection model to use. + * @type {cr.ui.ListSelectionModel} + */ + get selectionModel() { + return this.selectionModel_; + }, + set selectionModel(sm) { + var oldSm = this.selectionModel_; + if (oldSm == sm) + return; + + if (!this.boundHandleOnChange_) { + this.boundHandleOnChange_ = this.handleOnChange_.bind(this); + this.boundHandleLeadChange_ = this.handleLeadChange_.bind(this); + } + + if (oldSm) { + oldSm.removeEventListener('change', this.boundHandleOnChange_); + oldSm.removeEventListener('leadIndexChange', + this.boundHandleLeadChange_); + } + + this.selectionModel_ = sm; + this.selectionController_ = this.createSelectionController(sm); + + if (sm) { + sm.addEventListener('change', this.boundHandleOnChange_); + sm.addEventListener('leadIndexChange', this.boundHandleLeadChange_); + } + }, + + /** + * Whether or not the list auto-expands. + * @type {boolean} + */ + get autoExpands() { + return this.autoExpands_; + }, + set autoExpands(autoExpands) { + if (this.autoExpands_ == autoExpands) + return; + this.autoExpands_ = autoExpands; + this.redraw(); + }, + + /** + * Convenience alias for selectionModel.selectedItem + * @type {cr.ui.ListItem} + */ + get selectedItem() { + var dataModel = this.dataModel; + if (dataModel) { + var index = this.selectionModel.selectedIndex; + if (index != -1) + return dataModel.item(index); + } + return null; + }, + set selectedItem(selectedItem) { + var dataModel = this.dataModel; + if (dataModel) { + var index = this.dataModel.indexOf(selectedItem); + this.selectionModel.selectedIndex = index; + } + }, + + /** + * The height of the lead item. + * If set to 0, resets to the same height as other items. + * @type {number} + */ + get leadItemHeight() { + return this.leadItemHeight_ || this.getItemHeight_(); + }, + set leadItemHeight(height) { + if (height) { + var size = this.getItemSize_(); + this.leadItemHeight_ = Math.max(0, height + size.marginVertical); + } else { + this.leadItemHeight_ = 0; + } + }, + + /** + * Convenience alias for selectionModel.selectedItems + * @type {!Array<cr.ui.ListItem>} + */ + get selectedItems() { + var indexes = this.selectionModel.selectedIndexes; + var dataModel = this.dataModel; + if (dataModel) { + return indexes.map(function(i) { + return dataModel.item(i); + }); + } + return []; + }, + + /** + * The HTML elements representing the items. This is just all the list item + * children but subclasses may override this to filter out certain elements. + * @type {HTMLCollection} + */ + get items() { + return Array.prototype.filter.call(this.children, function(child) { + return !child.classList.contains('spacer'); + }); + }, + + batchCount_: 0, + + /** + * When making a lot of updates to the list, the code could be wrapped in + * the startBatchUpdates and finishBatchUpdates to increase performance. Be + * sure that the code will not return without calling endBatchUpdates or the + * list will not be correctly updated. + */ + startBatchUpdates: function() { + this.batchCount_++; + }, + + /** + * See startBatchUpdates. + */ + endBatchUpdates: function() { + this.batchCount_--; + if (this.batchCount_ == 0) + this.redraw(); + }, + + /** + * Initializes the element. + */ + decorate: function() { + // Add fillers. + this.beforeFiller_ = this.ownerDocument.createElement('div'); + this.afterFiller_ = this.ownerDocument.createElement('div'); + this.beforeFiller_.className = 'spacer'; + this.afterFiller_.className = 'spacer'; + this.appendChild(this.beforeFiller_); + this.appendChild(this.afterFiller_); + + var length = this.dataModel ? this.dataModel.length : 0; + this.selectionModel = new ListSelectionModel(length); + + this.addEventListener('dblclick', this.handleDoubleClick_); + this.addEventListener('mousedown', this.handleMouseDownUp_); + this.addEventListener('mouseup', this.handleMouseDownUp_); + this.addEventListener('keydown', this.handleKeyDown); + this.addEventListener('focus', this.handleElementFocus_, true); + this.addEventListener('blur', this.handleElementBlur_, true); + this.addEventListener('scroll', this.redraw.bind(this)); + this.setAttribute('role', 'listbox'); + + // Make list focusable + if (!this.hasAttribute('tabindex')) + this.tabIndex = 0; + }, + + /** + * @return {number} The height of an item, measuring it if necessary. + * @private + */ + getItemHeight_: function() { + return this.getItemSize_().height; + }, + + /** + * @return {number} The width of an item, measuring it if necessary. + * @private + */ + getItemWidth_: function() { + return this.getItemSize_().width; + }, + + /** + * @return {{height: number, width: number}} The height and width + * of an item, measuring it if necessary. + * @private + */ + getItemSize_: function() { + if (!this.measured_ || !this.measured_.height) { + this.measured_ = measureItem(this); + } + return this.measured_; + }, + + /** + * Callback for the double click event. + * @param {Event} e The mouse event object. + * @private + */ + handleDoubleClick_: function(e) { + if (this.disabled) + return; + + var target = this.getListItemAncestor(e.target); + if (target) + this.activateItemAtIndex(this.getIndexOfListItem(target)); + }, + + /** + * Callback for mousedown and mouseup events. + * @param {Event} e The mouse event object. + * @private + */ + handleMouseDownUp_: function(e) { + if (this.disabled) + return; + + var target = e.target; + + // If the target was this element we need to make sure that the user did + // not click on a border or a scrollbar. + if (target == this && !inViewport(target, e)) + return; + + target = this.getListItemAncestor(target); + + var index = target ? this.getIndexOfListItem(target) : -1; + this.selectionController_.handleMouseDownUp(e, index); + }, + + /** + * Called when an element in the list is focused. Marks the list as having + * a focused element, and dispatches an event if it didn't have focus. + * @param {Event} e The focus event. + * @private + */ + handleElementFocus_: function(e) { + if (!this.hasElementFocus) { + this.hasElementFocus = true; + // Force styles based on hasElementFocus to take effect. + this.forceRepaint_(); + } + }, + + /** + * Called when an element in the list is blurred. If focus moves outside + * the list, marks the list as no longer having focus and dispatches an + * event. + * @param {Event} e The blur event. + * @private + */ + handleElementBlur_: function(e) { + // When the blur event happens we do not know who is getting focus so we + // delay this a bit until we know if the new focus node is outside the + // list. + var list = this; + var doc = e.target.ownerDocument; + window.setTimeout(function() { + var activeElement = doc.activeElement; + if (!list.contains(activeElement)) { + list.hasElementFocus = false; + // Force styles based on hasElementFocus to take effect. + list.forceRepaint_(); + } + }); + }, + + /** + * Forces a repaint of the list. Changing custom attributes, even if there + * are style rules depending on them, doesn't cause a repaint + * (<https://bugs.webkit.org/show_bug.cgi?id=12519>), so this can be called + * to force the list to repaint. + * @private + */ + forceRepaint_: function(e) { + var dummyElement = document.createElement('div'); + this.appendChild(dummyElement); + this.removeChild(dummyElement); + }, + + /** + * Returns the list item element containing the given element, or null if + * it doesn't belong to any list item element. + * @param {HTMLElement} element The element. + * @return {ListItem} The list item containing |element|, or null. + */ + getListItemAncestor: function(element) { + var container = element; + while (container && container.parentNode != this) { + container = container.parentNode; + } + return container; + }, + + /** + * Handle a keydown event. + * @param {Event} e The keydown event. + * @return {boolean} Whether the key event was handled. + */ + handleKeyDown: function(e) { + if (this.disabled) + return; + + return this.selectionController_.handleKeyDown(e); + }, + + /** + * Callback from the selection model. We dispatch {@code change} events + * when the selection changes. + * @param {!cr.Event} e Event with change info. + * @private + */ + handleOnChange_: function(ce) { + ce.changes.forEach(function(change) { + var listItem = this.getListItemByIndex(change.index); + if (listItem) + listItem.selected = change.selected; + }, this); + + cr.dispatchSimpleEvent(this, 'change'); + }, + + /** + * Handles a change of the lead item from the selection model. + * @property {Event} pe The property change event. + * @private + */ + handleLeadChange_: function(pe) { + var element; + if (pe.oldValue != -1) { + if ((element = this.getListItemByIndex(pe.oldValue))) + element.lead = false; + } + + if (pe.newValue != -1) { + if ((element = this.getListItemByIndex(pe.newValue))) + element.lead = true; + this.scrollIndexIntoView(pe.newValue); + // If the lead item has a different height than other items, then we + // may run into a problem that requires a second attempt to scroll + // it into view. The first scroll attempt will trigger a redraw, + // which will clear out the list and repopulate it with new items. + // During the redraw, the list may shrink temporarily, which if the + // lead item is the last item, will move the scrollTop up since it + // cannot extend beyond the end of the list. (Sadly, being scrolled to + // the bottom of the list is not "sticky.") So, we set a timeout to + // rescroll the list after this all gets sorted out. This is perhaps + // not the most elegant solution, but no others seem obvious. + var self = this; + window.setTimeout(function() { + self.scrollIndexIntoView(pe.newValue); + }); + } + }, + + /** + * This handles data model 'permuted' event. + * this event is dispatched as a part of sort or splice. + * We need to + * - adjust the cache. + * - adjust selection. + * - redraw. + * - scroll the list to show selection. + * It is important that the cache adjustment happens before selection model + * adjustments. + * @param {Event} e The 'permuted' event. + */ + handleDataModelPermuted_: function(e) { + var newCachedItems = {}; + for (var index in this.cachedItems_) { + if (e.permutation[index] != -1) + newCachedItems[e.permutation[index]] = this.cachedItems_[index]; + else + delete this.cachedItems_[index]; + } + this.cachedItems_ = newCachedItems; + + this.startBatchUpdates(); + + var sm = this.selectionModel; + sm.adjustLength(e.newLength); + sm.adjustToReordering(e.permutation); + + this.endBatchUpdates(); + + if (sm.leadIndex != -1) + this.scrollIndexIntoView(sm.leadIndex); + }, + + handleDataModelChange_: function(e) { + if (e.index >= this.firstIndex_ && e.index < this.lastIndex_) { + if (this.cachedItems_[e.index]) + delete this.cachedItems_[e.index]; + this.redraw(); + } + }, + + /** + * @param {number} index The index of the item. + * @return {number} The top position of the item inside the list, not taking + * into account lead item. May vary in the case of multiple columns. + */ + getItemTop: function(index) { + return index * this.getItemHeight_(); + }, + + /** + * @param {number} index The index of the item. + * @return {number} The row of the item. May vary in the case + * of multiple columns. + */ + getItemRow: function(index) { + return index; + }, + + /** + * @param {number} row The row. + * @return {number} The index of the first item in the row. + */ + getFirstItemInRow: function(row) { + return row; + }, + + /** + * Ensures that a given index is inside the viewport. + * @param {number} index The index of the item to scroll into view. + * @return {boolean} Whether any scrolling was needed. + */ + scrollIndexIntoView: function(index) { + var dataModel = this.dataModel; + if (!dataModel || index < 0 || index >= dataModel.length) + return false; + + var itemHeight = this.getItemHeight_(); + var scrollTop = this.scrollTop; + var top = this.getItemTop(index); + var leadIndex = this.selectionModel.leadIndex; + + // Adjust for the lead item if it is above the given index. + if (leadIndex > -1 && leadIndex < index) + top += this.leadItemHeight - itemHeight; + else if (leadIndex == index) + itemHeight = this.leadItemHeight; + + if (top < scrollTop) { + this.scrollTop = top; + return true; + } else { + var clientHeight = this.clientHeight; + var cs = getComputedStyle(this); + var paddingY = parseInt(cs.paddingTop, 10) + + parseInt(cs.paddingBottom, 10); + + if (top + itemHeight > scrollTop + clientHeight - paddingY) { + this.scrollTop = top + itemHeight - clientHeight + paddingY; + return true; + } + } + + return false; + }, + + /** + * @return {!ClientRect} The rect to use for the context menu. + */ + getRectForContextMenu: function() { + // TODO(arv): Add trait support so we can share more code between trees + // and lists. + var index = this.selectionModel.selectedIndex; + var el = this.getListItemByIndex(index); + if (el) + return el.getBoundingClientRect(); + return this.getBoundingClientRect(); + }, + + /** + * Takes a value from the data model and finds the associated list item. + * @param {*} value The value in the data model that we want to get the list + * item for. + * @return {ListItem} The first found list item or null if not found. + */ + getListItem: function(value) { + var dataModel = this.dataModel; + if (dataModel) { + var index = dataModel.indexOf(value); + return this.getListItemByIndex(index); + } + return null; + }, + + /** + * Find the list item element at the given index. + * @param {number} index The index of the list item to get. + * @return {ListItem} The found list item or null if not found. + */ + getListItemByIndex: function(index) { + return this.cachedItems_[index] || null; + }, + + /** + * Find the index of the given list item element. + * @param {ListItem} item The list item to get the index of. + * @return {number} The index of the list item, or -1 if not found. + */ + getIndexOfListItem: function(item) { + var index = item.listIndex; + if (this.cachedItems_[index] == item) { + return index; + } + return -1; + }, + + /** + * Creates a new list item. + * @param {*} value The value to use for the item. + * @return {!ListItem} The newly created list item. + */ + createItem: function(value) { + var item = new this.itemConstructor_(value); + item.label = value; + if (typeof item.decorate == 'function') + item.decorate(); + return item; + }, + + /** + * Creates the selection controller to use internally. + * @param {cr.ui.ListSelectionModel} sm The underlying selection model. + * @return {!cr.ui.ListSelectionController} The newly created selection + * controller. + */ + createSelectionController: function(sm) { + return new ListSelectionController(sm); + }, + + /** + * Return the heights (in pixels) of the top of the given item index within + * the list, and the height of the given item itself, accounting for the + * possibility that the lead item may be a different height. + * @param {number} index The index to find the top height of. + * @return {{top: number, height: number}} The heights for the given index. + * @private + */ + getHeightsForIndex_: function(index) { + var itemHeight = this.getItemHeight_(); + var top = this.getItemTop(index); + if (this.selectionModel.leadIndex > -1 && + this.selectionModel.leadIndex < index) { + top += this.leadItemHeight - itemHeight; + } else if (this.selectionModel.leadIndex == index) { + itemHeight = this.leadItemHeight; + } + return {top: top, height: itemHeight}; + }, + + /** + * Find the index of the list item containing the given y offset (measured + * in pixels from the top) within the list. In the case of multiple columns, + * returns the first index in the row. + * @param {number} offset The y offset in pixels to get the index of. + * @return {number} The index of the list item. + * @private + */ + getIndexForListOffset_: function(offset) { + var itemHeight = this.getItemHeight_(); + var leadIndex = this.selectionModel.leadIndex; + var leadItemHeight = this.leadItemHeight; + if (leadIndex < 0 || leadItemHeight == itemHeight) { + // Simple case: no lead item or lead item height is not different. + return this.getFirstItemInRow(Math.floor(offset / itemHeight)); + } + var leadTop = this.getItemTop(leadIndex); + // If the given offset is above the lead item, it's also simple. + if (offset < leadTop) + return this.getFirstItemInRow(Math.floor(offset / itemHeight)); + // If the lead item contains the given offset, we just return its index. + if (offset < leadTop + leadItemHeight) + return this.getFirstItemInRow(this.getItemRow(leadIndex)); + // The given offset must be below the lead item. Adjust and recalculate. + offset -= leadItemHeight - itemHeight; + return this.getFirstItemInRow(Math.floor(offset / itemHeight)); + }, + + /** + * Return the number of items that occupy the range of heights between the + * top of the start item and the end offset. + * @param {number} startIndex The index of the first visible item. + * @param {number} endOffset The y offset in pixels of the end of the list. + * @return {number} The number of list items visible. + * @private + */ + countItemsInRange_: function(startIndex, endOffset) { + var endIndex = this.getIndexForListOffset_(endOffset); + return endIndex - startIndex + 1; + }, + + /** + * Calculates the number of items fitting in viewport given the index of + * first item and heights. + * @param {number} itemHeight The height of the item. + * @param {number} firstIndex Index of the first item in viewport. + * @param {number} scrollTop The scroll top position. + * @return {number} The number of items in view port. + */ + getItemsInViewPort: function(itemHeight, firstIndex, scrollTop) { + // This is a bit tricky. We take the minimum of the available items to + // show and the number we want to show, so as not to go off the end of the + // list. For the number we want to show, we take the maximum of the number + // that would fit without a differently-sized lead item, and with one. We + // do this so that if the size of the lead item changes without a scroll + // event to trigger redrawing the list, we won't end up with empty space. + var clientHeight = this.clientHeight; + return this.autoExpands_ ? this.dataModel.length : Math.min( + this.dataModel.length - firstIndex, + Math.max( + Math.ceil(clientHeight / itemHeight) + 1, + this.countItemsInRange_(firstIndex, scrollTop + clientHeight))); + }, + + /** + * Adds items to the list and {@code newCachedItems}. + * @param {number} firstIndex The index of first item, inclusively. + * @param {number} lastIndex The index of last item, exclusively. + * @param {Object.<string, ListItem>} cachedItems Old items cache. + * @param {Object.<string, ListItem>} newCachedItems New items cache. + */ + addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) { + var listItem; + var dataModel = this.dataModel; + + window.l = this; + for (var y = firstIndex; y < lastIndex; y++) { + var dataItem = dataModel.item(y); + listItem = cachedItems[y] || this.createItem(dataItem); + listItem.listIndex = y; + this.appendChild(listItem); + newCachedItems[y] = listItem; + } + }, + + /** + * Returns the height of after filler in the list. + * @param {number} lastIndex The index of item past the last in viewport. + * @param {number} itemHeight The height of the item. + * @return {number} The height of after filler. + */ + getAfterFillerHeight: function(lastIndex, itemHeight) { + return (this.dataModel.length - lastIndex) * itemHeight; + }, + + /** + * Redraws the viewport. + */ + redraw: function() { + if (this.batchCount_ != 0) + return; + + var dataModel = this.dataModel; + if (!dataModel) { + this.textContent = ''; + return; + } + + var scrollTop = this.scrollTop; + var clientHeight = this.clientHeight; + + var itemHeight = this.getItemHeight_(); + + // We cache the list items since creating the DOM nodes is the most + // expensive part of redrawing. + var cachedItems = this.cachedItems_ || {}; + var newCachedItems = {}; + + var desiredScrollHeight = this.getHeightsForIndex_(dataModel.length).top; + + var autoExpands = this.autoExpands_; + var firstIndex = autoExpands ? 0 : this.getIndexForListOffset_(scrollTop); + var itemsInViewPort = this.getItemsInViewPort(itemHeight, firstIndex, + scrollTop); + var lastIndex = firstIndex + itemsInViewPort; + + this.textContent = ''; + + this.beforeFiller_.style.height = + this.getHeightsForIndex_(firstIndex).top + 'px'; + this.appendChild(this.beforeFiller_); + + var sm = this.selectionModel; + var leadIndex = sm.leadIndex; + + this.addItems(firstIndex, lastIndex, cachedItems, newCachedItems); + + var afterFillerHeight = this.getAfterFillerHeight(lastIndex, itemHeight); + if (leadIndex >= lastIndex) + afterFillerHeight += this.leadItemHeight - itemHeight; + this.afterFiller_.style.height = afterFillerHeight + 'px'; + this.appendChild(this.afterFiller_); + + // We don't set the lead or selected properties until after adding all + // items, in case they force relayout in response to these events. + var listItem = null; + if (newCachedItems[leadIndex]) + newCachedItems[leadIndex].lead = true; + for (var y = firstIndex; y < lastIndex; y++) { + if (sm.getIndexSelected(y)) + newCachedItems[y].selected = true; + else if (y != leadIndex) + listItem = newCachedItems[y]; + } + + this.firstIndex_ = firstIndex; + this.lastIndex_ = lastIndex; + + this.cachedItems_ = newCachedItems; + + // Measure again in case the item height has changed due to a page zoom. + // + // The measure above is only done the first time but this measure is done + // after every redraw. It is done in a timeout so it will not trigger + // a reflow (which made the redraw speed 3 times slower on my system). + // By using a timeout the measuring will happen later when there is no + // need for a reflow. + if (listItem) { + var list = this; + window.setTimeout(function() { + if (listItem.parentNode == list) { + list.measured_ = measureItem(list, listItem); + } + }); + } + }, + + /** + * Invalidates list by removing cached items. + */ + invalidate: function() { + this.cachedItems_ = {}; + }, + + /** + * Redraws a single item. + * @param {number} index The row index to redraw. + */ + redrawItem: function(index) { + if (index >= this.firstIndex_ && index < this.lastIndex_) { + delete this.cachedItems_[index]; + this.redraw(); + } + }, + + /** + * Called when a list item is activated, currently only by a double click + * event. + * @param {number} index The index of the activated item. + */ + activateItemAtIndex: function(index) { + }, + }; + + cr.defineProperty(List, 'disabled', cr.PropertyKind.BOOL_ATTR); + + /** + * Whether the list or one of its descendents has focus. This is necessary + * because list items can contain controls that can be focused, and for some + * purposes (e.g., styling), the list can still be conceptually focused at + * that point even though it doesn't actually have the page focus. + */ + cr.defineProperty(List, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR); + + return { + List: List + } +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_item.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_item.js new file mode 100644 index 0000000..68431bc --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_item.js @@ -0,0 +1,75 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('cr.ui', function() { + + /** + * Creates a new list item element. + * @param {string} opt_label The text label for the item. + * @constructor + * @extends {HTMLLIElement} + */ + var ListItem = cr.ui.define('li'); + + ListItem.prototype = { + __proto__: HTMLLIElement.prototype, + + /** + * Plain text label. + * @type {string} + */ + get label() { + return this.textContent; + }, + set label(label) { + this.textContent = label; + }, + + /** + * This item's index in the containing list. + * @type {number} + */ + listIndex_: -1, + + /** + * Called when an element is decorated as a list item. + */ + decorate: function() { + this.setAttribute('role', 'listitem'); + }, + + /** + * Called when the selection state of this element changes. + */ + selectionChanged: function() { + }, + }; + + /** + * Whether the item is selected. Setting this does not update the underlying + * selection model. This is only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(ListItem, 'selected', cr.PropertyKind.BOOL_ATTR, + function() { + this.selectionChanged(); + }); + + /** + * Whether the item is the lead in a selection. Setting this does not update + * the underlying selection model. This is only used for display purpose. + * @type {boolean} + */ + cr.defineProperty(ListItem, 'lead', cr.PropertyKind.BOOL_ATTR); + + /** + * This item's index in the containing list. + * @type {number} + */ + cr.defineProperty(ListItem, 'listIndex'); + + return { + ListItem: ListItem + }; +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_selection_controller.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_selection_controller.js new file mode 100644 index 0000000..35101d1 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_selection_controller.js @@ -0,0 +1,289 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('cr.ui', function() { + /** + * Creates a selection controller that is to be used with lists. This is + * implemented for vertical lists but changing the behavior for horizontal + * lists or icon views is a matter of overriding {@code getIndexBefore}, + * {@code getIndexAfter}, {@code getIndexAbove} as well as + * {@code getIndexBelow}. + * + * @param {cr.ui.ListSelectionModel} selectionModel The selection model to + * interact with. + * + * @constructor + * @extends {!cr.EventTarget} + */ + function ListSelectionController(selectionModel) { + this.selectionModel_ = selectionModel; + } + + ListSelectionController.prototype = { + + /** + * The selection model we are interacting with. + * @type {cr.ui.ListSelectionModel} + */ + get selectionModel() { + return this.selectionModel_; + }, + + /** + * Returns the index below (y axis) the given element. + * @param {number} index The index to get the index below. + * @return {number} The index below or -1 if not found. + */ + getIndexBelow: function(index) { + if (index == this.getLastIndex()) + return -1; + return index + 1; + }, + + /** + * Returns the index above (y axis) the given element. + * @param {number} index The index to get the index above. + * @return {number} The index below or -1 if not found. + */ + getIndexAbove: function(index) { + return index - 1; + }, + + /** + * Returns the index before (x axis) the given element. This returns -1 + * by default but override this for icon view and horizontal selection + * models. + * + * @param {number} index The index to get the index before. + * @return {number} The index before or -1 if not found. + */ + getIndexBefore: function(index) { + return -1; + }, + + /** + * Returns the index after (x axis) the given element. This returns -1 + * by default but override this for icon view and horizontal selection + * models. + * + * @param {number} index The index to get the index after. + * @return {number} The index after or -1 if not found. + */ + getIndexAfter: function(index) { + return -1; + }, + + /** + * Returns the next list index. This is the next logical and should not + * depend on any kind of layout of the list. + * @param {number} index The index to get the next index for. + * @return {number} The next index or -1 if not found. + */ + getNextIndex: function(index) { + if (index == this.getLastIndex()) + return -1; + return index + 1; + }, + + /** + * Returns the prevous list index. This is the previous logical and should + * not depend on any kind of layout of the list. + * @param {number} index The index to get the previous index for. + * @return {number} The previous index or -1 if not found. + */ + getPreviousIndex: function(index) { + return index - 1; + }, + + /** + * @return {number} The first index. + */ + getFirstIndex: function() { + return 0; + }, + + /** + * @return {number} The last index. + */ + getLastIndex: function() { + return this.selectionModel.length - 1; + }, + + /** + * Called by the view when the user does a mousedown or mouseup on the list. + * @param {!Event} e The browser mousedown event. + * @param {number} index The index that was under the mouse pointer, -1 if + * none. + */ + handleMouseDownUp: function(e, index) { + var sm = this.selectionModel; + var anchorIndex = sm.anchorIndex; + var isDown = e.type == 'mousedown'; + + sm.beginChange(); + + if (index == -1) { + // On Mac we always clear the selection if the user clicks a blank area. + // On Windows, we only clear the selection if neither Shift nor Ctrl are + // pressed. + if (cr.isMac) { + sm.leadIndex = sm.anchorIndex = -1; + if (sm.multiple) + sm.unselectAll(); + } else if (!isDown && !e.shiftKey && !e.ctrlKey) + // Keep anchor and lead indexes. Note that this is intentionally + // different than on the Mac. + if (sm.multiple) + sm.unselectAll(); + } else { + if (sm.multiple && (cr.isMac ? e.metaKey : + (e.ctrlKey && !e.shiftKey))) { + // Selection is handled at mouseUp on windows/linux, mouseDown on mac. + if (cr.isMac? isDown : !isDown) { + // Toggle the current one and make it anchor index. + sm.setIndexSelected(index, !sm.getIndexSelected(index)); + sm.leadIndex = index; + sm.anchorIndex = index; + } + } else if (e.shiftKey && anchorIndex != -1 && anchorIndex != index) { + // Shift is done in mousedown. + if (isDown) { + sm.unselectAll(); + sm.leadIndex = index; + if (sm.multiple) + sm.selectRange(anchorIndex, index); + else + sm.setIndexSelected(index, true); + } + } else { + // Right click for a context menu needs to not clear the selection. + var isRightClick = e.button == 2; + + // If the index is selected this is handled in mouseup. + var indexSelected = sm.getIndexSelected(index); + if ((indexSelected && !isDown || !indexSelected && isDown) && + !(indexSelected && isRightClick)) { + sm.unselectAll(); + sm.setIndexSelected(index, true); + sm.leadIndex = index; + sm.anchorIndex = index; + } + } + } + + sm.endChange(); + }, + + /** + * Called by the view when it receives a keydown event. + * @param {Event} e The keydown event. + */ + handleKeyDown: function(e) { + const SPACE_KEY_CODE = 32; + var tagName = e.target.tagName; + // If focus is in an input field of some kind, only handle navigation keys + // that aren't likely to conflict with input interaction (e.g., text + // editing, or changing the value of a checkbox or select). + if (tagName == 'INPUT') { + var inputType = e.target.type; + // Just protect space (for toggling) for checkbox and radio. + if (inputType == 'checkbox' || inputType == 'radio') { + if (e.keyCode == SPACE_KEY_CODE) + return; + // Protect all but the most basic navigation commands in anything else. + } else if (e.keyIdentifier != 'Up' && e.keyIdentifier != 'Down') { + return; + } + } + // Similarly, don't interfere with select element handling. + if (tagName == 'SELECT') + return; + + var sm = this.selectionModel; + var newIndex = -1; + var leadIndex = sm.leadIndex; + var prevent = true; + + // Ctrl/Meta+A + if (sm.multiple && e.keyCode == 65 && + (cr.isMac && e.metaKey || !cr.isMac && e.ctrlKey)) { + sm.selectAll(); + e.preventDefault(); + return; + } + + // Space + if (e.keyCode == SPACE_KEY_CODE) { + if (leadIndex != -1) { + var selected = sm.getIndexSelected(leadIndex); + if (e.ctrlKey || !selected) { + sm.setIndexSelected(leadIndex, !selected || !sm.multiple); + return; + } + } + } + + switch (e.keyIdentifier) { + case 'Home': + newIndex = this.getFirstIndex(); + break; + case 'End': + newIndex = this.getLastIndex(); + break; + case 'Up': + newIndex = leadIndex == -1 ? + this.getLastIndex() : this.getIndexAbove(leadIndex); + break; + case 'Down': + newIndex = leadIndex == -1 ? + this.getFirstIndex() : this.getIndexBelow(leadIndex); + break; + case 'Left': + newIndex = leadIndex == -1 ? + this.getLastIndex() : this.getIndexBefore(leadIndex); + break; + case 'Right': + newIndex = leadIndex == -1 ? + this.getFirstIndex() : this.getIndexAfter(leadIndex); + break; + default: + prevent = false; + } + + if (newIndex != -1) { + sm.beginChange(); + + sm.leadIndex = newIndex; + if (e.shiftKey) { + var anchorIndex = sm.anchorIndex; + if (sm.multiple) + sm.unselectAll(); + if (anchorIndex == -1) { + sm.setIndexSelected(newIndex, true); + sm.anchorIndex = newIndex; + } else { + sm.selectRange(anchorIndex, newIndex); + } + } else if (e.ctrlKey && !cr.isMac) { + // Setting the lead index is done above. + // Mac does not allow you to change the lead. + } else { + if (sm.multiple) + sm.unselectAll(); + sm.setIndexSelected(newIndex, true); + sm.anchorIndex = newIndex; + } + + sm.endChange(); + + if (prevent) + e.preventDefault(); + } + } + }; + + return { + ListSelectionController: ListSelectionController + }; +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_selection_model.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_selection_model.js new file mode 100644 index 0000000..8c16d57 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_selection_model.js @@ -0,0 +1,275 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('cr.ui', function() { + const Event = cr.Event; + const EventTarget = cr.EventTarget; + + /** + * Creates a new selection model that is to be used with lists. + * + * @param {number=} opt_length The number items in the selection. + * + * @constructor + * @extends {!cr.EventTarget} + */ + function ListSelectionModel(opt_length) { + this.length_ = opt_length || 0; + // Even though selectedIndexes_ is really a map we use an array here to get + // iteration in the order of the indexes. + this.selectedIndexes_ = []; + } + + ListSelectionModel.prototype = { + __proto__: EventTarget.prototype, + + /** + * The number of items in the model. + * @type {number} + */ + get length() { + return this.length_; + }, + + /** + * @type {!Array} The selected indexes. + */ + get selectedIndexes() { + return Object.keys(this.selectedIndexes_).map(Number); + }, + set selectedIndexes(selectedIndexes) { + this.beginChange(); + this.unselectAll(); + for (var i = 0; i < selectedIndexes.length; i++) { + this.setIndexSelected(selectedIndexes[i], true); + } + if (selectedIndexes.length) { + this.leadIndex = this.anchorIndex = selectedIndexes[0]; + } else { + this.leadIndex = this.anchorIndex = -1; + } + this.endChange(); + }, + + /** + * Convenience getter which returns the first selected index. + * @type {number} + */ + get selectedIndex() { + for (var i in this.selectedIndexes_) { + return Number(i); + } + return -1; + }, + set selectedIndex(selectedIndex) { + this.beginChange(); + this.unselectAll(); + if (selectedIndex != -1) { + this.selectedIndexes = [selectedIndex]; + } else { + this.leadIndex = this.anchorIndex = -1; + } + this.endChange(); + }, + + /** + * Selects a range of indexes, starting with {@code start} and ends with + * {@code end}. + * @param {number} start The first index to select. + * @param {number} end The last index to select. + */ + selectRange: function(start, end) { + // Swap if starts comes after end. + if (start > end) { + var tmp = start; + start = end; + end = tmp; + } + + this.beginChange(); + + for (var index = start; index != end; index++) { + this.setIndexSelected(index, true); + } + this.setIndexSelected(end, true); + + this.endChange(); + }, + + /** + * Selects all indexes. + */ + selectAll: function() { + this.selectRange(0, this.length - 1); + }, + + /** + * Clears the selection + */ + clear: function() { + this.beginChange(); + this.length_ = 0; + this.anchorIndex = this.leadIndex = -1; + this.unselectAll(); + this.endChange(); + }, + + /** + * Unselects all selected items. + */ + unselectAll: function() { + this.beginChange(); + for (var i in this.selectedIndexes_) { + this.setIndexSelected(i, false); + } + this.endChange(); + }, + + /** + * Sets the selected state for an index. + * @param {number} index The index to set the selected state for. + * @param {boolean} b Whether to select the index or not. + */ + setIndexSelected: function(index, b) { + var oldSelected = index in this.selectedIndexes_; + if (oldSelected == b) + return; + + if (b) + this.selectedIndexes_[index] = true; + else + delete this.selectedIndexes_[index]; + + this.beginChange(); + + // Changing back? + if (index in this.changedIndexes_ && this.changedIndexes_[index] == !b) { + delete this.changedIndexes_[index]; + } else { + this.changedIndexes_[index] = b; + } + + // End change dispatches an event which in turn may update the view. + this.endChange(); + }, + + /** + * Whether a given index is selected or not. + * @param {number} index The index to check. + * @return {boolean} Whether an index is selected. + */ + getIndexSelected: function(index) { + return index in this.selectedIndexes_; + }, + + /** + * This is used to begin batching changes. Call {@code endChange} when you + * are done making changes. + */ + beginChange: function() { + if (!this.changeCount_) { + this.changeCount_ = 0; + this.changedIndexes_ = {}; + } + this.changeCount_++; + }, + + /** + * Call this after changes are done and it will dispatch a change event if + * any changes were actually done. + */ + endChange: function() { + this.changeCount_--; + if (!this.changeCount_) { + var indexes = Object.keys(this.changedIndexes_); + if (indexes.length) { + var e = new Event('change'); + e.changes = indexes.map(function(index) { + return { + index: index, + selected: this.changedIndexes_[index] + }; + }, this); + this.dispatchEvent(e); + } + this.changedIndexes_ = {}; + } + }, + + leadIndex_: -1, + + /** + * The leadIndex is used with multiple selection and it is the index that + * the user is moving using the arrow keys. + * @type {number} + */ + get leadIndex() { + return this.leadIndex_; + }, + set leadIndex(leadIndex) { + var li = Math.max(-1, Math.min(this.length_ - 1, leadIndex)); + if (li != this.leadIndex_) { + var oldLeadIndex = this.leadIndex_; + this.leadIndex_ = li; + cr.dispatchPropertyChange(this, 'leadIndex', li, oldLeadIndex); + } + }, + + anchorIndex_: -1, + + /** + * The anchorIndex is used with multiple selection. + * @type {number} + */ + get anchorIndex() { + return this.anchorIndex_; + }, + set anchorIndex(anchorIndex) { + var ai = Math.max(-1, Math.min(this.length_ - 1, anchorIndex)); + if (ai != this.anchorIndex_) { + var oldAnchorIndex = this.anchorIndex_; + this.anchorIndex_ = ai; + cr.dispatchPropertyChange(this, 'anchorIndex', ai, oldAnchorIndex); + } + }, + + /** + * Whether the selection model supports multiple selected items. + * @type {boolean} + */ + get multiple() { + return true; + }, + + /** + * Adjusts the selection after reordering of items in the table. + * @param {!Array.<number>} permutation The reordering permutation. + */ + adjustToReordering: function(permutation) { + var oldLeadIndex = this.leadIndex; + + var oldSelectedIndexes = this.selectedIndexes; + this.selectedIndexes = oldSelectedIndexes.map(function(oldIndex) { + return permutation[oldIndex]; + }).filter(function(index) { + return index != -1; + }); + + if (oldLeadIndex != -1) + this.leadIndex = permutation[oldLeadIndex]; + }, + + /** + * Adjusts selection model length. + * @param {number} length New selection model length. + */ + adjustLength: function(length) { + this.length_ = length; + } + }; + + return { + ListSelectionModel: ListSelectionModel + }; +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_single_selection_model.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_single_selection_model.js new file mode 100644 index 0000000..2cf43a92 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_single_selection_model.js @@ -0,0 +1,221 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('cr.ui', function() { + const Event = cr.Event; + const EventTarget = cr.EventTarget; + + /** + * Creates a new selection model that is to be used with lists. This only + * allows a single index to be selected. + * + * @param {number=} opt_length The number items in the selection. + * + * @constructor + * @extends {!cr.EventTarget} + */ + function ListSingleSelectionModel(opt_length) { + this.length_ = opt_length || 0; + this.selectedIndex = -1; + } + + ListSingleSelectionModel.prototype = { + __proto__: EventTarget.prototype, + + /** + * The number of items in the model. + * @type {number} + */ + get length() { + return this.length_; + }, + + /** + * @type {!Array} The selected indexes. + */ + get selectedIndexes() { + var i = this.selectedIndex; + return i != -1 ? [this.selectedIndex] : []; + }, + + /** + * Convenience getter which returns the first selected index. + * @type {number} + */ + get selectedIndex() { + return this.selectedIndex_; + }, + set selectedIndex(selectedIndex) { + var oldSelectedIndex = this.selectedIndex; + var i = Math.max(-1, Math.min(this.length_ - 1, selectedIndex)); + + if (i != oldSelectedIndex) { + this.beginChange(); + this.selectedIndex_ = i + this.endChange(); + } + }, + + /** + * Selects a range of indexes, starting with {@code start} and ends with + * {@code end}. + * @param {number} start The first index to select. + * @param {number} end The last index to select. + */ + selectRange: function(start, end) { + // Only select first index. + this.selectedIndex = Math.min(start, end); + }, + + /** + * Selects all indexes. + */ + selectAll: function() { + // Select all is not allowed on a single selection model + }, + + /** + * Clears the selection + */ + clear: function() { + this.beginChange(); + this.length_ = 0; + this.selectedIndex = this.anchorIndex = this.leadIndex = -1; + this.endChange(); + }, + + /** + * Unselects all selected items. + */ + unselectAll: function() { + this.selectedIndex = -1; + }, + + /** + * Sets the selected state for an index. + * @param {number} index The index to set the selected state for. + * @param {boolean} b Whether to select the index or not. + */ + setIndexSelected: function(index, b) { + // Only allow selection + var oldSelected = index == this.selectedIndex_; + if (oldSelected == b) + return; + + if (b) + this.selectedIndex = index; + else if (index == this.selectedIndex_) + this.selectedIndex = -1; + }, + + /** + * Whether a given index is selected or not. + * @param {number} index The index to check. + * @return {boolean} Whether an index is selected. + */ + getIndexSelected: function(index) { + return index == this.selectedIndex_; + }, + + /** + * This is used to begin batching changes. Call {@code endChange} when you + * are done making changes. + */ + beginChange: function() { + if (!this.changeCount_) { + this.changeCount_ = 0; + this.selectedIndexBefore_ = this.selectedIndex_; + } + this.changeCount_++; + }, + + /** + * Call this after changes are done and it will dispatch a change event if + * any changes were actually done. + */ + endChange: function() { + this.changeCount_--; + if (!this.changeCount_) { + if (this.selectedIndexBefore_ != this.selectedIndex_) { + var e = new Event('change'); + var indexes = [this.selectedIndexBefore_, this.selectedIndex_]; + e.changes = indexes.filter(function(index) { + return index != -1; + }).map(function(index) { + return { + index: index, + selected: index == this.selectedIndex_ + }; + }, this); + this.dispatchEvent(e); + } + } + }, + + leadIndex_: -1, + + /** + * The leadIndex is used with multiple selection and it is the index that + * the user is moving using the arrow keys. + * @type {number} + */ + get leadIndex() { + return this.leadIndex_; + }, + set leadIndex(leadIndex) { + var li = Math.max(-1, Math.min(this.length_ - 1, leadIndex)); + if (li != this.leadIndex_) { + var oldLeadIndex = this.leadIndex_; + this.leadIndex_ = li; + cr.dispatchPropertyChange(this, 'leadIndex', li, oldLeadIndex); + cr.dispatchPropertyChange(this, 'anchorIndex', li, oldLeadIndex); + } + }, + + /** + * The anchorIndex is used with multiple selection. + * @type {number} + */ + get anchorIndex() { + return this.leadIndex; + }, + set anchorIndex(anchorIndex) { + this.leadIndex = anchorIndex; + }, + + /** + * Whether the selection model supports multiple selected items. + * @type {boolean} + */ + get multiple() { + return false; + }, + + /** + * Adjusts the selection after reordering of items in the table. + * @param {!Array.<number>} permutation The reordering permutation. + */ + adjustToReordering: function(permutation) { + if (this.leadIndex != -1) + this.leadIndex = permutation[this.leadIndex]; + + var oldSelectedIndex = this.selectedIndex; + if (oldSelectedIndex != -1) { + this.selectedIndex = permutation[oldSelectedIndex]; + } + }, + + /** + * Adjusts selection model length. + * @param {number} length New selection model length. + */ + adjustLength: function(length) { + this.length_ = length; + } + }; + + return { + ListSingleSelectionModel: ListSingleSelectionModel + }; +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/util.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/util.js new file mode 100644 index 0000000..1efbf19 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/util.js @@ -0,0 +1,151 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * The global object. + * @type {!Object} + */ +const global = this; + +/** + * Alias for document.getElementById. + * @param {string} id The ID of the element to find. + * @return {HTMLElement} The found element or null if not found. + */ +function $(id) { + return document.getElementById(id); +} + +/** + * Calls chrome.send with a callback and restores the original afterwards. + * @param {string} name The name of the message to send. + * @param {!Array} params The parameters to send. + * @param {string} callbackName The name of the function that the backend calls. + * @param {!Function} The function to call. + */ +function chromeSend(name, params, callbackName, callback) { + var old = global[callbackName]; + global[callbackName] = function() { + // restore + global[callbackName] = old; + + var args = Array.prototype.slice.call(arguments); + return callback.apply(global, args); + }; + chrome.send(name, params); +} + +/** + * Generates a CSS url string. + * @param {string} s The URL to generate the CSS url for. + * @return {string} The CSS url string. + */ +function url(s) { + // http://www.w3.org/TR/css3-values/#uris + // Parentheses, commas, whitespace characters, single quotes (') and double + // quotes (") appearing in a URI must be escaped with a backslash + var s2 = s.replace(/(\(|\)|\,|\s|\'|\"|\\)/g, '\\$1'); + // WebKit has a bug when it comes to URLs that end with \ + // https://bugs.webkit.org/show_bug.cgi?id=28885 + if (/\\\\$/.test(s2)) { + // Add a space to work around the WebKit bug. + s2 += ' '; + } + return 'url("' + s2 + '")'; +} + +/** + * Parses query parameters from Location. + * @param {string} s The URL to generate the CSS url for. + * @return {object} Dictionary containing name value pairs for URL + */ +function parseQueryParams(location) { + var params = {}; + var query = unescape(location.search.substring(1)); + var vars = query.split("&"); + for (var i=0; i < vars.length; i++) { + var pair = vars[i].split("="); + params[pair[0]] = pair[1]; + } + return params; +} + +function findAncestorByClass(el, className) { + return findAncestor(el, function(el) { + if (el.classList) + return el.classList.contains(className); + return null; + }); +} + +/** + * Return the first ancestor for which the {@code predicate} returns true. + * @param {Node} node The node to check. + * @param {function(Node) : boolean} predicate The function that tests the + * nodes. + * @return {Node} The found ancestor or null if not found. + */ +function findAncestor(node, predicate) { + var last = false; + while (node != null && !(last = predicate(node))) { + node = node.parentNode; + } + return last ? node : null; +} + +function swapDomNodes(a, b) { + var afterA = a.nextSibling; + if (afterA == b) { + swapDomNodes(b, a); + return; + } + var aParent = a.parentNode; + b.parentNode.replaceChild(a, b); + aParent.insertBefore(b, afterA); +} + +/** + * Disables text selection and dragging. + */ +function disableTextSelectAndDrag() { + // Disable text selection. + document.onselectstart = function(e) { + e.preventDefault(); + } + + // Disable dragging. + document.ondragstart = function(e) { + e.preventDefault(); + } +} + +// Handle click on a link. If the link points to a chrome: or file: url, then +// call into the browser to do the navigation. +document.addEventListener('click', function(e) { + // Allow preventDefault to work. + if (!e.returnValue) + return; + + var el = e.target; + if (el.nodeType == Node.ELEMENT_NODE && + el.webkitMatchesSelector('A, A *')) { + while (el.tagName != 'A') { + el = el.parentElement; + } + + if ((el.protocol == 'file:' || el.protocol == 'about:') && + (e.button == 0 || e.button == 1)) { + chrome.send('navigateToUrl', [ + el.href, + el.target, + e.button, + e.altKey, + e.ctrlKey, + e.metaKey, + e.shiftKey + ]); + e.preventDefault(); + } + } +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/main.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/main.js new file mode 100644 index 0000000..1b61566 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/main.js @@ -0,0 +1,16 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +document.addEventListener('DOMContentLoaded', function() { + chrome.contentSettings.plugins.getResourceIdentifiers(function(r) { + if (chrome.extension.lastError) { + $('error').textContent = + "Error: " + chrome.extension.lastError.message; + return; + } + var pluginList = $('plugin-list'); + pluginSettings.ui.PluginList.decorate(pluginList); + pluginList.dataModel = new cr.ui.ArrayDataModel(r); + }); +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/plugin_list.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/plugin_list.js new file mode 100644 index 0000000..53d9da8 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/plugin_list.js @@ -0,0 +1,220 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('pluginSettings.ui', function() { + const List = cr.ui.List; + const ListItem = cr.ui.ListItem; + const ListSingleSelectionModel = cr.ui.ListSingleSelectionModel; + + /** + * Returns the item's height, like offsetHeight but such that it works better + * when the page is zoomed. See the similar calculation in @{code cr.ui.List}. + * This version also accounts for the animation done in this file. + * @param {Element} item The item to get the height of. + * @return {number} The height of the item, calculated with zooming in mind. + */ + function getItemHeight(item) { + var height = item.style.height; + // Use the fixed animation target height if set, in case the element is + // currently being animated and we'd get an intermediate height below. + if (height && height.substr(-2) == 'px') + return parseInt(height.substr(0, height.length - 2)); + return item.getBoundingClientRect().height; + } + + /** + * Creates a new plug-in list item element. + * @param {PluginList} list The plug-in list containing this item. + * @param {Object} info Information about the plug-in. + * @constructor + * @extends {cr.ui.ListItem} + */ + function PluginListItem(list, info) { + var el = cr.doc.createElement('li'); + el.list_ = list; + el.info_ = info; + el.__proto__ = PluginListItem.prototype; + el.decorate(); + return el; + } + + PluginListItem.prototype = { + __proto__: ListItem.prototype, + + /** + * The plug-in list containing this item. + * @type {PluginList} + * @private + */ + list_: null, + + /** + * Information about the plug-in. + * @type {Object} + * @private + */ + info_: null, + + /** + * The element containing details about the plug-in. + * @type {HTMLDivElemebt} + * @private + */ + detailsElement_: null, + + /** + * Initializes the element. + */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + var info = this.info_; + + var contentElement = this.ownerDocument.createElement('div'); + + var titleEl = this.ownerDocument.createElement('div'); + var nameEl = this.ownerDocument.createElement('span'); + nameEl.className = 'plugin-name'; + nameEl.textContent = info.description; + nameEl.title = info.description; + titleEl.appendChild(nameEl); + this.numRulesEl_ = this.ownerDocument.createElement('span'); + this.numRulesEl_.className = 'num-rules'; + titleEl.appendChild(this.numRulesEl_); + contentElement.appendChild(titleEl); + + this.detailsElement_ = this.ownerDocument.createElement('div'); + this.detailsElement_.className = 'plugin-details hidden'; + + var columnHeadersEl = this.ownerDocument.createElement('div'); + columnHeadersEl.className = 'column-headers'; + var patternColumnEl = this.ownerDocument.createElement('div'); + patternColumnEl.textContent = + chrome.i18n.getMessage("patternColumnHeader"); + patternColumnEl.className = 'pattern-column-header'; + var settingColumnEl = this.ownerDocument.createElement('div'); + settingColumnEl.textContent = + chrome.i18n.getMessage("settingColumnHeader"); + settingColumnEl.className = 'setting-column-header'; + columnHeadersEl.appendChild(patternColumnEl); + columnHeadersEl.appendChild(settingColumnEl); + this.detailsElement_.appendChild(columnHeadersEl); + contentElement.appendChild(this.detailsElement_); + + this.appendChild(contentElement); + + var settings = new pluginSettings.Settings(this.info_.id); + this.updateRulesCount_(settings); + settings.addEventListener('change', + this.updateRulesCount_.bind(this, settings)); + + // Create the rule list asynchronously, to make sure that it is already + // fully integrated in the DOM tree. + window.setTimeout(this.loadRules_.bind(this, settings), 0); + }, + + /** + * Create the list of content setting rules applying to this plug-in. + * @private + */ + loadRules_: function(settings) { + var rulesEl = this.ownerDocument.createElement('list'); + this.detailsElement_.appendChild(rulesEl); + + pluginSettings.ui.RuleList.decorate(rulesEl); + rulesEl.setPluginSettings(settings); + }, + + updateRulesCount_: function(settings) { + this.numRulesEl_.textContent = '(' + settings.getAll().length + ' rules)'; + }, + + /** + * Whether this item is expanded or not. + * @type {boolean} + */ + expanded_: false, + get expanded() { + return this.expanded_; + }, + set expanded(expanded) { + if (this.expanded_ == expanded) + return; + this.expanded_ = expanded; + if (expanded) { + var oldExpanded = this.list_.expandItem; + this.list_.expandItem = this; + this.detailsElement_.classList.remove('hidden'); + if (oldExpanded) + oldExpanded.expanded = false; + this.classList.add('plugin-show-details'); + } else { + if (this.list_.expandItem == this) { + this.list_.leadItemHeight = 0; + this.list_.expandItem = null; + } + this.style.height = ''; + this.detailsElement_.classList.add('hidden'); + this.classList.remove('plugin-show-details'); + } + }, + }; + + /** + * Creates a new plug-in list. + * @constructor + * @extends {cr.ui.List} + */ + var PluginList = cr.ui.define('list'); + + PluginList.prototype = { + __proto__: List.prototype, + + /** + * Initializes the element. + */ + decorate: function() { + List.prototype.decorate.call(this); + this.classList.add('plugin-list'); + var sm = new ListSingleSelectionModel(); + sm.addEventListener('change', this.handleSelectionChange_.bind(this)); + this.selectionModel = sm; + this.autoExpands = true; + }, + + /** + * Creates a new plug-in list item. + * @param {Object} info Information about the plug-in. + */ + createItem: function(info) { + return new PluginListItem(this, info); + }, + + /** + * Called when the selection changes. + * @private + */ + handleSelectionChange_: function(ce) { + ce.changes.forEach(function(change) { + var listItem = this.getListItemByIndex(change.index); + if (listItem) { + if (!change.selected) { + // TODO(bsmith) explain window timeout (from cookies_list.js) + window.setTimeout(function() { + if (!listItem.selected || !listItem.lead) + listItem.expanded = false; + }, 0); + } else if (listItem.lead) { + listItem.expanded = true; + } + } + }, this); + }, + }; + + return { + PluginList: PluginList, + PluginListItem: PluginListItem, + }; +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/plugin_settings.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/plugin_settings.js new file mode 100644 index 0000000..9929e89 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/plugin_settings.js @@ -0,0 +1,188 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('pluginSettings', function() { + const EventTarget = cr.EventTarget; + const Event = cr.Event; + + /** + * Creates a new content settings model. + * @param {string} plugin Identifies the plug-in for which this object stores + * settings. + * @constructor + * @extends {cr.EventTarget} + */ + function Settings(plugin) { + this.plugin_ = plugin; + } + + Settings.prototype = { + __proto__: cr.EventTarget.prototype, + + /** + * Identifies the plug-in for which this object stores settings. + * @type {string} + * @private + */ + plugin_: null, + + /** + * Clears all content settings, and recreates them from local storage. If a + * content setting can't be set (which shouldn't really happen, as it has + * been successfully set previously), it is removed from local storage as + * well. + * @param {function()} callback Called when the content settings have been + * recreated, or on error. + * @private + */ + recreateRules_: function(callback) { + chrome.contentSettings.plugins.clear({}, function() { + if (chrome.extension.lastError) { + console.error("Error clearing rules"); + callback(); + return; + } + var length = window.localStorage.length; + if (length == 0) { + callback(); + return; + } + var count = length; + var errors = []; + for (var i = 0; i < length; i++) { + var key = window.localStorage.key(i); + var keyArray = JSON.parse(key); + var plugin = keyArray[0]; + var pattern = keyArray[1]; + var setting = window.localStorage.getItem(key); + chrome.contentSettings.plugins.set({ + 'primaryPattern': pattern, + 'resourceIdentifier': { 'id': plugin }, + 'setting': setting, + }, function() { + if (chrome.extension.lastError) { + console.error('Error restoring [' + plugin_ + ', ' + + pattern + setting + ']: ' + + chrome.extension.lastError.message); + window.localStorage.removeItem(key); + } + count--; + if (count == 0) + callback(); + }); + } + }); + }, + + /** + * Creates a content setting rule and calls the passed in callback with the + * result. + * @param {string} pattern The content setting pattern for the rule. + * @param {string} setting The setting for the rule. + * @param {function(?string)} callback Called when the content settings have + * been updated, or on error. + */ + set: function(pattern, setting, callback) { + var plugin = this.plugin_; + chrome.contentSettings.plugins.set({ + 'primaryPattern': pattern, + 'resourceIdentifier': { 'id': plugin }, + 'setting': setting, + }, function() { + if (chrome.extension.lastError) { + callback(chrome.extension.lastError.message); + } else { + window.localStorage.setItem(JSON.stringify([plugin, pattern]), + setting); + callback(); + } + }); + }, + + /** + * Removes the content setting rule with a given pattern, and calls the + * passed in callback afterwards. + * @param {string} pattern The content setting pattern for the rule. + * @param {function()?} callback Called when the content settings have + * been updated. + */ + clear: function(pattern, callback) { + window.localStorage.removeItem( + JSON.stringify([this.plugin_, pattern])); + this.recreateRules_(callback); + }, + + /** + * Updates the content setting rule with a given pattern to a new pattern + * and setting and calls the passed in callback with the result. + * @param {string} oldPattern The old content setting pattern for the rule. + * @param {string} newPattern The new content setting pattern for the rule. + * @param {string} setting The setting for the rule. + * @param {function(?string)} callback Called when the content settings have + * been updated, or on error. + */ + update: function(oldPattern, newPattern, setting, callback) { + if (oldPattern == newPattern) { + // Avoid recreating all rules if only the setting changed. + this.set(newPattern, setting, callback); + return; + } + var oldSetting = this.get(oldPattern); + var settings = this; + // Remove the old rule. + this.clear(oldPattern, function() { + // Try to set the new rule. + settings.set(newPattern, setting, function(error) { + if (error) { + // If setting the new rule failed, restore the old rule. + settings.setInternal_(oldPattern, oldSetting, + function(restoreError) { + if (restoreError) { + console.error('Error restoring [' + settings.plugin_ + ', ' + + oldPattern + oldSetting + ']: ' + restoreError); + } + callback(error); + }); + } else { + callback(); + } + }); + }); + }, + + /** + * Returns the content setting for a given pattern. + * @param {string} pattern The content setting pattern for the rule. + * @return {string} The setting for the rule. + */ + get: function(primaryPattern) { + return window.localStorage.getItem( + JSON.stringify([this.plugin_, primaryPattern])); + }, + + /** + * @return {array} A list of all content setting rules for this plug-in. + */ + getAll: function() { + var length = window.localStorage.length; + var rules = []; + for (var i = 0; i < length; i++) { + var key = window.localStorage.key(i); + var keyArray = JSON.parse(key); + if (keyArray[0] == this.plugin_) { + rules.push({ + 'primaryPattern': keyArray[1], + 'setting': window.localStorage.getItem(key), + }); + } + } + return rules; + } + }; + + return { + Settings: Settings, + } +}); + diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/rule_list.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/rule_list.js new file mode 100644 index 0000000..db83cee --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/rule_list.js @@ -0,0 +1,392 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('pluginSettings.ui', function() { + const InlineEditableItemList = options.InlineEditableItemList; + const InlineEditableItem = options.InlineEditableItem; + const ArrayDataModel = cr.ui.ArrayDataModel; + + /** + * Creates a new rule list item. + * @param {RuleList} list The rule list containing this item. + * @param {Object} rule The content setting rule. + * @constructor + * @extends {options.InlineEditableItem} + */ + function RuleListItem(list, rule) { + var el = cr.doc.createElement('li'); + + el.dataItem_ = rule; + el.list_ = list; + el.__proto__ = RuleListItem.prototype; + el.decorate(); + + return el; + } + + RuleListItem.prototype = { + __proto__: InlineEditableItem.prototype, + + /** + * The content setting rule. + * @type {Object} + * @private + */ + dataItem_: null, + + /** + * The rule list containing this item. + * @type {RuleList} + * @private + */ + list_: null, + + /** + * The text input element for the pattern. + * @type {HTMLInputElement} + * @private + */ + input_: null, + + /** + * The popup button for the setting. + * @type {HTMLSelectElement} + * @private + */ + select_: null, + + /** + * The static text field containing the pattern. + * @type {HTMLDivElement} + * @private + */ + patternLabel_: null, + + /** + * The static text field containing the setting. + * @type {HTMLDivElement} + * @private + */ + settingLabel_: null, + + /** + * Called when an element is decorated as a list item. + */ + decorate: function() { + InlineEditableItem.prototype.decorate.call(this); + + this.isPlaceholder = !this.pattern; + var patternCell = this.createEditableTextCell(this.pattern); + patternCell.className = 'rule-pattern'; + patternCell.classList.add('weakrtl'); + this.contentElement.appendChild(patternCell); + var input = patternCell.querySelector('input'); + if (this.pattern) + this.patternLabel_ = patternCell.querySelector('.static-text'); + else + input.placeholder = chrome.i18n.getMessage("addNewPattern"); + + // TODO(stuartmorgan): Create an createEditableSelectCell abstracting + // this code. + // Setting label for display mode. |pattern| will be null for the 'add new + // exception' row. + if (this.pattern) { + var settingLabel = cr.doc.createElement('span'); + settingLabel.textContent = this.settingForDisplay(); + settingLabel.className = 'rule-behavior'; + settingLabel.setAttribute('displaymode', 'static'); + this.contentElement.appendChild(settingLabel); + this.settingLabel_ = settingLabel; + } + + // Setting select element for edit mode. + var select = cr.doc.createElement('select'); + var optionAllow = cr.doc.createElement('option'); + optionAllow.textContent = chrome.i18n.getMessage("allowRule"); + optionAllow.value = 'allow'; + select.appendChild(optionAllow); + + var optionBlock = cr.doc.createElement('option'); + optionBlock.textContent = chrome.i18n.getMessage("blockRule"); + optionBlock.value = 'block'; + select.appendChild(optionBlock); + + this.contentElement.appendChild(select); + select.className = 'rule-behavior'; + if (this.pattern) + select.setAttribute('displaymode', 'edit'); + + this.input_ = input; + this.select_ = select; + + this.updateEditables(); + + // Listen for edit events. + this.addEventListener('canceledit', this.onEditCancelled_); + this.addEventListener('commitedit', this.onEditCommitted_); + }, + + /** + * The pattern (e.g., a URL) for the rule. + * @type {string} + */ + get pattern() { + return this.dataItem_['primaryPattern']; + }, + set pattern(pattern) { + this.dataItem_['primaryPattern'] = pattern; + }, + + /** + * The setting (allow/block) for the rule. + * @type {string} + */ + get setting() { + return this.dataItem_['setting']; + }, + set setting(setting) { + this.dataItem_['setting'] = setting; + }, + + /** + * Gets a human-readable setting string. + * @type {string} + */ + settingForDisplay: function() { + var setting = this.setting; + if (setting == 'allow') + return chrome.i18n.getMessage("allowRule"); + else if (setting == 'block') + return chrome.i18n.getMessage("blockRule"); + }, + + /** + * Set the <input> to its original contents. Used when the user quits + * editing. + */ + resetInput: function() { + this.input_.value = this.pattern; + }, + + /** + * Copy the data model values to the editable nodes. + */ + updateEditables: function() { + this.resetInput(); + + var settingOption = + this.select_.querySelector('[value=\'' + this.setting + '\']'); + if (settingOption) + settingOption.selected = true; + }, + + /** @inheritDoc */ + get hasBeenEdited() { + var livePattern = this.input_.value; + var liveSetting = this.select_.value; + return livePattern != this.pattern || liveSetting != this.setting; + }, + + /** + * Called when committing an edit. + * @param {Event} e The end event. + * @private + */ + onEditCommitted_: function(e) { + var newPattern = this.input_.value; + var newSetting = this.select_.value; + + this.finishEdit(newPattern, newSetting); + }, + + /** + * Called when cancelling an edit; resets the control states. + * @param {Event} e The cancel event. + * @private + */ + onEditCancelled_: function() { + this.updateEditables(); + }, + + /** + * Editing is complete; update the model. + * @param {string} newPattern The pattern that the user entered. + * @param {string} newSetting The setting the user chose. + */ + finishEdit: function(newPattern, newSetting) { + this.patternLabel_.textContent = newPattern; + this.settingLabel_.textContent = this.settingForDisplay(); + var oldPattern = this.pattern; + this.pattern = newPattern; + this.setting = newSetting; + + this.list_.settings.update(oldPattern, newPattern, newSetting, + this.list_.settingsChangedCallback()); + } + }; + + /** + * Create a new list item to add a rule. + * @param {RuleList} list The rule list containing this item. + * @constructor + * @extends {AddRuleListItem} + */ + function AddRuleListItem(list) { + var el = cr.doc.createElement('div'); + el.dataItem_ = {}; + el.list_ = list; + el.__proto__ = AddRuleListItem.prototype; + el.decorate(); + + return el; + } + + AddRuleListItem.prototype = { + __proto__: RuleListItem.prototype, + + /** + * Initializes the element. + */ + decorate: function() { + RuleListItem.prototype.decorate.call(this); + + this.setting = 'allow'; + }, + + /** + * Clear the <input> and let the placeholder text show again. + */ + resetInput: function() { + this.input_.value = ''; + }, + + /** @inheritDoc */ + get hasBeenEdited() { + return this.input_.value != ''; + }, + + /** + * Editing is complete; update the model. As long as the pattern isn't + * empty, we'll just add it. + * @param {string} newPattern The pattern that the user entered. + * @param {string} newSetting The setting the user chose. + */ + finishEdit: function(newPattern, newSetting) { + this.resetInput(); + this.list_.settings.set(newPattern, newSetting, + this.list_.settingsChangedCallback()); + }, + }; + + /** + * Creates a new rule list. + * @constructor + * @extends {cr.ui.List} + */ + var RuleList = cr.ui.define('list'); + + RuleList.prototype = { + __proto__: InlineEditableItemList.prototype, + + /** + * The content settings model for this list. + * @type {Settings} + */ + settings: null, + + /** + * Called when an element is decorated as a list. + */ + decorate: function() { + InlineEditableItemList.prototype.decorate.call(this); + + this.classList.add('rule-list'); + + this.autoExpands = true; + this.reset(); + }, + + /** + * Creates an item to go in the list. + * @param {Object} entry The element from the data model for this row. + */ + createItem: function(entry) { + if (entry) { + return new RuleListItem(this, entry); + } else { + var addRuleItem = new AddRuleListItem(this); + addRuleItem.deletable = false; + return addRuleItem; + } + }, + + /** + * Sets the rules in the js model. + * @param {Object} entries A list of dictionaries of values, each dictionary + * represents a rule. + */ + setRules_: function(entries) { + var deleteCount = this.dataModel.length - 1; + + var args = [0, deleteCount]; + args.push.apply(args, entries); + this.dataModel.splice.apply(this.dataModel, args); + }, + + /** + * Called when the list of content setting rules has been changed. + * @param {?string} error The error message, if an error occurred. + * Otherwise, this is null. + * @private + */ + settingsChanged_: function(error) { + if (error) + $('error').textContent = 'Error: ' + error; + else + $('error').textContent = ''; + this.setRules_(this.settings.getAll()); + }, + + /** + * @return {function()} A bound callback to update the UI after the settings + * have been changed. + */ + settingsChangedCallback: function() { + return this.settingsChanged_.bind(this); + }, + + /** + * Binds this list to the content settings model. + * @param {Settings} settings The content settings model. + */ + setPluginSettings: function(settings) { + this.settings = settings; + this.settingsChanged_(); + }, + + /** + * Removes all rules from the js model. + */ + reset: function() { + // The null creates the Add New Rule row. + this.dataModel = new ArrayDataModel([null]); + }, + + /** @inheritDoc */ + deleteItemAtIndex: function(index) { + var listItem = this.getListItemByIndex(index); + if (listItem.undeletable) + return; + + this.settings.clear(listItem.pattern, this.settingsChangedCallback()); + }, + }; + + return { + RuleListItem: RuleListItem, + AddRuleListItem: AddRuleListItem, + RuleList: RuleList, + } +}); + diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/manifest.json b/chrome/common/extensions/docs/examples/extensions/plugin_settings/manifest.json new file mode 100644 index 0000000..c42e0ef --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/manifest.json @@ -0,0 +1,16 @@ +{
+ "name" : "__MSG_extName__",
+ "version" : "0.6",
+ "description" : "__MSG_extDescription__",
+ "options_page": "options.html",
+ "permissions": [
+ "contentSettings"
+ ],
+ "icons": {
+ "128": "bunny128.png",
+ "48": "bunny48.png"
+ },
+ "minimum_chrome_version": "16.0.912",
+ "content_security_policy": "default-src 'none'; script-src 'self'; img-src 'self'; style-src 'self'",
+ "default_locale": "en"
+}
diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/options.html b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options.html new file mode 100644 index 0000000..abfdd7e --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> +<link rel="stylesheet" href="domui/css/button.css"> +<link rel="stylesheet" href="domui/css/chrome_shared.css"> +<link rel="stylesheet" href="domui/css/list.css"> +<link rel="stylesheet" href="domui/css/select.css"> + +<link rel="stylesheet" href="options/css/list.css"> + +<link rel="stylesheet" href="css/plugin_list.css"> +<link rel="stylesheet" href="css/rule_list.css"> + +<script src="domui/js/cr.js"></script> +<script src="domui/js/cr/event_target.js"></script> +<script src="domui/js/cr/ui.js"></script> +<script src="domui/js/cr/ui/array_data_model.js"></script> +<script src="domui/js/cr/ui/list_item.js"></script> +<script src="domui/js/cr/ui/list_selection_controller.js"></script> +<script src="domui/js/cr/ui/list_selection_model.js"></script> +<script src="domui/js/cr/ui/list_single_selection_model.js"></script> +<script src="domui/js/cr/ui/list.js"></script> +<script src="domui/js/util.js"></script> + +<script src="options/js/deletable_item_list.js"></script> +<script src="options/js/inline_editable_list.js"></script> + +<script src="js/plugin_list.js" type="text/javascript"></script> +<script src="js/plugin_settings.js" type="text/javascript"></script> +<script src="js/rule_list.js" type="text/javascript"></script> + +<script src="js/main.js" type="text/javascript"></script> +</head> +<body> +<list id="plugin-list"></list> +<div id="error"></div> +</body> +</html> diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/css/list.css b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/css/list.css new file mode 100644 index 0000000..9cc716d --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/css/list.css @@ -0,0 +1,124 @@ +/* +Copyright (c) 2011 The Chromium Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. +*/ + +.raw-button, +.raw-button:hover, +.raw-button:active { + -webkit-box-shadow: none; + background-color: transparent; + background-repeat: no-repeat; + border: none; + min-width: 0; + padding: 1px 6px; +} + +list > * { + -webkit-box-align: center; + -webkit-transition: .15s background-color; + box-sizing: border-box; + border-radius: 0; + display: -webkit-box; + height: 32px; + border: none; + margin: 0; +} + +list:not([disabled]) > :hover { + background-color: #e4ecf7; +} + +/* TODO(stuartmorgan): Once this becomes the list style for other WebUI pages + * these rules can be simplified (since they wont need to override other rules). + */ + +list:not([hasElementFocus]) > [selected], +list:not([hasElementFocus]) > [lead][selected] { + background-color: #d0d0d0; + background-image: none; +} + +list[hasElementFocus] > [selected], +list[hasElementFocus] > [lead][selected], +list:not([hasElementFocus]) > [selected]:hover, +list:not([hasElementFocus]) > [selected][lead]:hover { + background-color: #bbcee9; + background-image: none; +} + +list[disabled] { + opacity: 0.6; +} + +list > .heading { + color: #666666; +} + +list > .heading:hover { + background-color: transparent; + border-color: transparent; +} + +list .deletable-item { + -webkit-box-align: center; +} + +list .deletable-item > :first-child { + -webkit-box-align: center; + -webkit-box-flex: 1; + -webkit-padding-end: 5px; + display: -webkit-box; +} + +list .close-button { + -webkit-transition: .15s opacity; + background-color: transparent; + /* TODO(stuartmorgan): Replace with real images once they are available. */ + background-image: url("../images/close_bar.png"); + border: none; + display: block; + height: 16px; + opacity: 1; + width: 16px; +} + +list > *:not(:hover):not([lead]) .close-button, +list > *:not(:hover):not([selected]) .close-button, +list:not([hasElementFocus]) > *:not(:hover) .close-button, +list[disabled] .close-button, +list .close-button[disabled] { + opacity: 0; + pointer-events: none; +} + +list .close-button:hover { + background-image: url("../images/close_bar_h.png"); +} + +list .close-button:active { + background-image: url("../images/close_bar_p.png"); +} + +list .static-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +list[inlineeditable] input { + box-sizing: border-box; + margin: 0; + width: 100%; +} + +list[inlineeditable] > :not([editing]) [displaymode="edit"], +list[inlineeditable] > [editing] [displaymode="static"] { + display: none; +} + +list > [editing] input:invalid { + /* TODO(stuartmorgan): Replace with validity badge */ + background-color: pink; +} diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/images/close_bar.png b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/images/close_bar.png Binary files differnew file mode 100644 index 0000000..912df05d --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/images/close_bar.png diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/images/close_bar_h.png b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/images/close_bar_h.png Binary files differnew file mode 100644 index 0000000..c5e1481 --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/images/close_bar_h.png diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/images/close_bar_p.png b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/images/close_bar_p.png Binary files differnew file mode 100644 index 0000000..cc5bbbe --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/images/close_bar_p.png diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/js/deletable_item_list.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/js/deletable_item_list.js new file mode 100644 index 0000000..4d2e68e --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/js/deletable_item_list.js @@ -0,0 +1,185 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + const List = cr.ui.List; + const ListItem = cr.ui.ListItem; + + /** + * Creates a deletable list item, which has a button that will trigger a call + * to deleteItemAtIndex(index) in the list. + */ + var DeletableItem = cr.ui.define('li'); + + DeletableItem.prototype = { + __proto__: ListItem.prototype, + + /** + * The element subclasses should populate with content. + * @type {HTMLElement} + * @private + */ + contentElement_: null, + + /** + * The close button element. + * @type {HTMLElement} + * @private + */ + closeButtonElement_: null, + + /** + * Whether or not this item can be deleted. + * @type {boolean} + * @private + */ + deletable_: true, + + /** @inheritDoc */ + decorate: function() { + ListItem.prototype.decorate.call(this); + + this.classList.add('deletable-item'); + + this.contentElement_ = this.ownerDocument.createElement('div'); + this.appendChild(this.contentElement_); + + this.closeButtonElement_ = this.ownerDocument.createElement('button'); + this.closeButtonElement_.classList.add('raw-button'); + this.closeButtonElement_.classList.add('close-button'); + this.closeButtonElement_.addEventListener('mousedown', + this.handleMouseDownUpOnClose_); + this.closeButtonElement_.addEventListener('mouseup', + this.handleMouseDownUpOnClose_); + this.closeButtonElement_.addEventListener('focus', + this.handleFocus_.bind(this)); + this.appendChild(this.closeButtonElement_); + }, + + /** + * Returns the element subclasses should add content to. + * @return {HTMLElement} The element subclasses should popuplate. + */ + get contentElement() { + return this.contentElement_; + }, + + /* Gets/sets the deletable property. An item that is not deletable doesn't + * show the delete button (although space is still reserved for it). + */ + get deletable() { + return this.deletable_; + }, + set deletable(value) { + this.deletable_ = value; + this.closeButtonElement_.disabled = !value; + }, + + /** + * Called when a focusable child element receives focus. Selects this item + * in the list selection model. + * @private + */ + handleFocus_: function() { + var list = this.parentNode; + var index = list.getIndexOfListItem(this); + list.selectionModel.selectedIndex = index; + list.selectionModel.anchorIndex = index; + }, + + /** + * Don't let the list have a crack at the event. We don't want clicking the + * close button to change the selection of the list. + * @param {Event} e The mouse down/up event object. + * @private + */ + handleMouseDownUpOnClose_: function(e) { + if (!e.target.disabled) + e.stopPropagation(); + }, + }; + + var DeletableItemList = cr.ui.define('list'); + + DeletableItemList.prototype = { + __proto__: List.prototype, + + /** @inheritDoc */ + decorate: function() { + List.prototype.decorate.call(this); + this.addEventListener('click', this.handleClick_); + this.addEventListener('keydown', this.handleKeyDown_); + }, + + /** + * Callback for onclick events. + * @param {Event} e The click event object. + * @private + */ + handleClick_: function(e) { + if (this.disabled) + return; + + var target = e.target; + if (target.classList.contains('close-button')) { + var listItem = this.getListItemAncestor(target); + var selected = this.selectionModel.selectedIndexes; + + // Check if the list item that contains the close button being clicked + // is not in the list of selected items. Only delete this item in that + // case. + var idx = this.getIndexOfListItem(listItem); + if (selected.indexOf(idx) == -1) { + this.deleteItemAtIndex(idx); + } else { + this.deleteSelectedItems_(); + } + } + }, + + /** + * Callback for keydown events. + * @param {Event} e The keydown event object. + * @private + */ + handleKeyDown_: function(e) { + // Map delete (and backspace on Mac) to item deletion (unless focus is + // in an input field, where it's intended for text editing). + if ((e.keyCode == 46 || (e.keyCode == 8 && cr.isMac)) && + e.target.tagName != 'INPUT') { + this.deleteSelectedItems_(); + // Prevent the browser from going back. + e.preventDefault(); + } + }, + + /** + * Deletes all the currently selected items that are deletable. + * @private + */ + deleteSelectedItems_: function() { + var selected = this.selectionModel.selectedIndexes; + // Reverse through the list of selected indexes to maintain the + // correct index values after deletion. + for (var j = selected.length - 1; j >= 0; j--) { + var index = selected[j]; + if (this.getListItemByIndex(index).deletable) + this.deleteItemAtIndex(index); + } + }, + + /** + * Called when an item should be deleted; subclasses are responsible for + * implementing. + * @param {number} index The index of the item that is being deleted. + */ + deleteItemAtIndex: function(index) { + }, + }; + + return { + DeletableItemList: DeletableItemList, + DeletableItem: DeletableItem, + }; +}); diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/js/inline_editable_list.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/js/inline_editable_list.js new file mode 100644 index 0000000..8aed93b --- /dev/null +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/js/inline_editable_list.js @@ -0,0 +1,414 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +cr.define('options', function() { + const DeletableItem = options.DeletableItem; + const DeletableItemList = options.DeletableItemList; + + /** + * Creates a new list item with support for inline editing. + * @constructor + * @extends {options.DeletableListItem} + */ + function InlineEditableItem() { + var el = cr.doc.createElement('div'); + InlineEditableItem.decorate(el); + return el; + } + + /** + * Decorates an element as a inline-editable list item. Note that this is + * a subclass of DeletableItem. + * @param {!HTMLElement} el The element to decorate. + */ + InlineEditableItem.decorate = function(el) { + el.__proto__ = InlineEditableItem.prototype; + el.decorate(); + }; + + InlineEditableItem.prototype = { + __proto__: DeletableItem.prototype, + + /** + * Whether or not this item can be edited. + * @type {boolean} + * @private + */ + editable_: true, + + /** + * Whether or not this is a placeholder for adding a new item. + * @type {boolean} + * @private + */ + isPlaceholder_: false, + + /** + * Fields associated with edit mode. + * @type {array} + * @private + */ + editFields_: null, + + /** + * Whether or not the current edit should be considered cancelled, rather + * than committed, when editing ends. + * @type {boolean} + * @private + */ + editCancelled_: true, + + /** + * The editable item corresponding to the last click, if any. Used to decide + * initial focus when entering edit mode. + * @type {HTMLElement} + * @private + */ + editClickTarget_: null, + + /** @inheritDoc */ + decorate: function() { + DeletableItem.prototype.decorate.call(this); + + this.editFields_ = []; + this.addEventListener('mousedown', this.handleMouseDown_); + this.addEventListener('keydown', this.handleKeyDown_); + this.addEventListener('leadChange', this.handleLeadChange_); + }, + + /** @inheritDoc */ + selectionChanged: function() { + this.updateEditState(); + }, + + /** + * Called when this element gains or loses 'lead' status. Updates editing + * mode accordingly. + * @private + */ + handleLeadChange_: function() { + this.updateEditState(); + }, + + /** + * Updates the edit state based on the current selected and lead states. + */ + updateEditState: function() { + if (this.editable) + this.editing = this.selected && this.lead; + }, + + /** + * Whether the user is currently editing the list item. + * @type {boolean} + */ + get editing() { + return this.hasAttribute('editing'); + }, + set editing(editing) { + if (this.editing == editing) + return; + + if (editing) + this.setAttribute('editing', ''); + else + this.removeAttribute('editing'); + + if (editing) { + this.editCancelled_ = false; + + cr.dispatchSimpleEvent(this, 'edit', true); + + var focusElement = this.editClickTarget_ || this.initialFocusElement; + this.editClickTarget_ = null; + + // When this is called in response to the selectedChange event, + // the list grabs focus immediately afterwards. Thus we must delay + // our focus grab. + var self = this; + if (focusElement) { + window.setTimeout(function() { + // Make sure we are still in edit mode by the time we execute. + if (self.editing) { + focusElement.focus(); + focusElement.select(); + } + }, 50); + } + } else { + if (!this.editCancelled_ && this.hasBeenEdited && + this.currentInputIsValid) { + if (this.isPlaceholder) + this.parentNode.focusPlaceholder = true; + + this.updateStaticValues_(); + cr.dispatchSimpleEvent(this, 'commitedit', true); + } else { + this.resetEditableValues_(); + cr.dispatchSimpleEvent(this, 'canceledit', true); + } + } + }, + + /** + * Whether the item is editable. + * @type {boolean} + */ + get editable() { + return this.editable_; + }, + set editable(editable) { + this.editable_ = editable; + if (!editable) + this.editing = false; + }, + + /** + * Whether the item is a new item placeholder. + * @type {boolean} + */ + get isPlaceholder() { + return this.isPlaceholder_; + }, + set isPlaceholder(isPlaceholder) { + this.isPlaceholder_ = isPlaceholder; + if (isPlaceholder) + this.deletable = false; + }, + + /** + * The HTML element that should have focus initially when editing starts, + * if a specific element wasn't clicked. + * Defaults to the first <input> element; can be overriden by subclasses if + * a different element should be focused. + * @type {HTMLElement} + */ + get initialFocusElement() { + return this.contentElement.querySelector('input'); + }, + + /** + * Whether the input in currently valid to submit. If this returns false + * when editing would be submitted, either editing will not be ended, + * or it will be cancelled, depending on the context. + * Can be overrided by subclasses to perform input validation. + * @type {boolean} + */ + get currentInputIsValid() { + return true; + }, + + /** + * Returns true if the item has been changed by an edit. + * Can be overrided by subclasses to return false when nothing has changed + * to avoid unnecessary commits. + * @type {boolean} + */ + get hasBeenEdited() { + return true; + }, + + /** + * Returns a div containing an <input>, as well as static text if + * isPlaceholder is not true. + * @param {string} text The text of the cell. + * @return {HTMLElement} The HTML element for the cell. + * @private + */ + createEditableTextCell: function(text) { + var container = this.ownerDocument.createElement('div'); + + if (!this.isPlaceholder) { + var textEl = this.ownerDocument.createElement('div'); + textEl.className = 'static-text'; + textEl.textContent = text; + textEl.setAttribute('displaymode', 'static'); + container.appendChild(textEl); + } + + var inputEl = this.ownerDocument.createElement('input'); + inputEl.type = 'text'; + inputEl.value = text; + if (!this.isPlaceholder) { + inputEl.setAttribute('displaymode', 'edit'); + inputEl.staticVersion = textEl; + } else { + // At this point |this| is not attached to the parent list yet, so give + // a short timeout in order for the attachment to occur. + var self = this; + window.setTimeout(function() { + var list = self.parentNode; + if (list && list.focusPlaceholder) { + list.focusPlaceholder = false; + if (list.shouldFocusPlaceholder()) + inputEl.focus(); + } + }, 50); + } + + inputEl.addEventListener('focus', this.handleFocus_.bind(this)); + container.appendChild(inputEl); + this.editFields_.push(inputEl); + + return container; + }, + + /** + * Resets the editable version of any controls created by createEditable* + * to match the static text. + * @private + */ + resetEditableValues_: function() { + var editFields = this.editFields_; + for (var i = 0; i < editFields.length; i++) { + var staticLabel = editFields[i].staticVersion; + if (!staticLabel && !this.isPlaceholder) + continue; + + if (editFields[i].tagName == 'INPUT') { + editFields[i].value = + this.isPlaceholder ? '' : staticLabel.textContent; + } + // Add more tag types here as new createEditable* methods are added. + + editFields[i].setCustomValidity(''); + } + }, + + /** + * Sets the static version of any controls created by createEditable* + * to match the current value of the editable version. Called on commit so + * that there's no flicker of the old value before the model updates. + * @private + */ + updateStaticValues_: function() { + var editFields = this.editFields_; + for (var i = 0; i < editFields.length; i++) { + var staticLabel = editFields[i].staticVersion; + if (!staticLabel) + continue; + + if (editFields[i].tagName == 'INPUT') + staticLabel.textContent = editFields[i].value; + // Add more tag types here as new createEditable* methods are added. + } + }, + + /** + * Called a key is pressed. Handles committing and cancelling edits. + * @param {Event} e The key down event. + * @private + */ + handleKeyDown_: function(e) { + if (!this.editing) + return; + + var endEdit = false; + switch (e.keyIdentifier) { + case 'U+001B': // Esc + this.editCancelled_ = true; + endEdit = true; + break; + case 'Enter': + if (this.currentInputIsValid) + endEdit = true; + break; + } + + if (endEdit) { + // Blurring will trigger the edit to end; see InlineEditableItemList. + this.ownerDocument.activeElement.blur(); + // Make sure that handled keys aren't passed on and double-handled. + // (e.g., esc shouldn't both cancel an edit and close a subpage) + e.stopPropagation(); + } + }, + + /** + * Called when the list item is clicked. If the click target corresponds to + * an editable item, stores that item to focus when edit mode is started. + * @param {Event} e The mouse down event. + * @private + */ + handleMouseDown_: function(e) { + if (!this.editable || this.editing) + return; + + var clickTarget = e.target; + var editFields = this.editFields_; + for (var i = 0; i < editFields.length; i++) { + if (editFields[i] == clickTarget || + editFields[i].staticVersion == clickTarget) { + this.editClickTarget_ = editFields[i]; + return; + } + } + }, + }; + + /** + * Takes care of committing changes to inline editable list items when the + * window loses focus. + */ + function handleWindowBlurs() { + window.addEventListener('blur', function(e) { + var itemAncestor = findAncestor(document.activeElement, function(node) { + return node instanceof InlineEditableItem; + }); + if (itemAncestor); + document.activeElement.blur(); + }); + } + handleWindowBlurs(); + + var InlineEditableItemList = cr.ui.define('list'); + + InlineEditableItemList.prototype = { + __proto__: DeletableItemList.prototype, + + /** + * Focuses the input element of the placeholder if true. + * @type {boolean} + */ + focusPlaceholder: false, + + /** @inheritDoc */ + decorate: function() { + DeletableItemList.prototype.decorate.call(this); + this.setAttribute('inlineeditable', ''); + this.addEventListener('hasElementFocusChange', + this.handleListFocusChange_); + }, + + /** + * Called when the list hierarchy as a whole loses or gains focus; starts + * or ends editing for the lead item if necessary. + * @param {Event} e The change event. + * @private + */ + handleListFocusChange_: function(e) { + var leadItem = this.getListItemByIndex(this.selectionModel.leadIndex); + if (leadItem) { + if (e.newValue) + leadItem.updateEditState(); + else + leadItem.editing = false; + } + }, + + /** + * May be overridden by subclasses to disable focusing the placeholder. + * @return true if the placeholder element should be focused on edit commit. + */ + shouldFocusPlaceholder: function() { + return true; + }, + }; + + // Export + return { + InlineEditableItem: InlineEditableItem, + InlineEditableItemList: InlineEditableItemList, + }; +}); diff --git a/chrome/common/extensions/docs/experimental.savePage.html b/chrome/common/extensions/docs/experimental.savePage.html index 5ace1e4..7a98ddc 100644 --- a/chrome/common/extensions/docs/experimental.savePage.html +++ b/chrome/common/extensions/docs/experimental.savePage.html @@ -426,7 +426,7 @@ permission to use it. <div class="description"> <p class="todo" style="display: none; ">Undocumented.</p> - <p>Saves the content of the tab with given id to MHTML.</p> + <p>Saves the content of the tab with given id as MHTML.</p> <!-- PARAMETERS --> <h4>Parameters</h4> @@ -509,7 +509,7 @@ permission to use it. <dd class="todo" style="display: none; "> Undocumented. </dd> - <dd>The id of the tab to save to MHTML.</dd> + <dd>The id of the tab to save as MHTML.</dd> <dd style="display: none; "> This parameter was added in version <b><span></span></b>. diff --git a/chrome/common/extensions/docs/samples.html b/chrome/common/extensions/docs/samples.html index 8a9e8db0..feca078 100644 --- a/chrome/common/extensions/docs/samples.html +++ b/chrome/common/extensions/docs/samples.html @@ -377,6 +377,7 @@ "8d0a50b57c26bb498be592e871001ffed91541b4": "PAGE ACTION BY CONTENT SHOWS A PAGE ACTION FOR HTML PAGES CONTAINING THE WORD SANDWICH BACKGROUND_PAGE PAGE_ACTION CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.PAGEACTION.SHOW", "80b86ccc6e8520660fa591caa565826f0ed1b12c": "PAGE ACTION BY URL SHOWS A PAGE ACTION FOR URLS WHICH HAVE THE LETTER G IN THEM. BACKGROUND_PAGE PAGE_ACTION TABS CHROME.PAGEACTION.SHOW CHROME.TABS.ONUPDATED", "d74c3c18a1c1dd18b035149105a306f837c8823e": "PAGE BENCHMARKER CHROMIUM PAGE BENCHMARKER. BACKGROUND_PAGE BROWSER_ACTION OPTIONS_PAGE TABS CHROME.BROWSERACTION.ONCLICKED CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETTITLE CHROME.EXTENSION.CONNECT CHROME.EXTENSION.GETBACKGROUNDPAGE CHROME.EXTENSION.GETEXTENSIONTABS CHROME.EXTENSION.GETURL CHROME.EXTENSION.ONCONNECT CHROME.TABS.CREATE CHROME.TABS.EXECUTESCRIPT CHROME.TABS.GET CHROME.TABS.GETALLINWINDOW CHROME.TABS.GETSELECTED CHROME.TABS.REMOVE CHROME.TABS.UPDATE CHROME.WINDOWS.GET CHROME.WINDOWS.GETCURRENT", + "ab4b9e00a673701b355db9eb8f9ebf2c850cd784": "PER-PLUGIN CONTENT SETTINGS CUSTOMIZE YOUR CONTENT SETTING FOR DIFFERENT PLUG-INS. CONTENTSETTINGS OPTIONS_PAGE CHROME.I18N.GETMESSAGE", "e6ae17ab4ccfd7e059c8c01f25760ca5d894c7fd": "PRINT THIS PAGE ADDS A PRINT BUTTON TO THE BROWSER. BACKGROUND_PAGE BROWSER_ACTION TABS CHROME.BROWSERACTION.ONCLICKED CHROME.TABS.UPDATE", "beff6ecd9677dea0a7c648c5042165b48bb66f09": "PROCESS MONITOR ADDS A BROWSER ACTION THAT MONITORS RESOURCE USAGE OF ALL BROWSER PROCESSES. BROWSER_ACTION EXPERIMENTAL POPUP TABS", "3e8e226d87e431296bb110b4f6eb7eec2ca7a826": "PROXY EXTENSION API SAMPLE SET CHROME-SPECIFIC PROXIES; A DEMONSTRATION OF CHROMES PROXY API BACKGROUND_PAGE BROWSER_ACTION POPUP PROXY CHROME.BROWSERACTION.SETBADGEBACKGROUNDCOLOR CHROME.BROWSERACTION.SETBADGETEXT CHROME.BROWSERACTION.SETTITLE CHROME.EXTENSION.ISALLOWEDINCOGNITOACCESS CHROME.EXTENSION.ONREQUEST CHROME.EXTENSION.SENDREQUEST CHROME.I18N.GETMESSAGE CHROME.PROXY.ONPROXYERROR", @@ -3348,6 +3349,95 @@ - <a>Install extension</a> </span> </div> +</div><div class="sample" id="ab4b9e00a673701b355db9eb8f9ebf2c850cd784"> + <img class="icon" src="examples/extensions/plugin_settings/bunny128.png"> + <img class="icon" src="images/sample-default-icon.png" style="display: none; "> + <h2 class="name"> + <a href="#ab4b9e00a673701b355db9eb8f9ebf2c850cd784">Per-plugin content settings</a> + <span style="display: none; ">(packaged app)</span> + </h2> + <p class="metadata features">Uses + <span> + <strong>contentSettings</strong><span style="display: none; ">, </span> + <span> and</span> + </span><span> + <strong>options_page</strong><span style="display: none; ">, </span> + <span style="display: none; "> and</span> + </span> + </p> + <p>Customize your content setting for different plug-ins.</p> + <div class="apicalls"><strong>Calls:</strong> + <ul> + <li> + <code><a href="i18n.html#method-getMessage">chrome.i18n.getMessage</a></code> + </li> + </ul> + </div> + <div class="sourcefiles"><strong>Source files:</strong> + <ul> + <li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/_locales/en/messages.json?content-type=text/plain">_locales/en/messages.json</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/css/plugin_list.css?content-type=text/plain">css/plugin_list.css</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/css/rule_list.css?content-type=text/plain">css/rule_list.css</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/button.css?content-type=text/plain">domui/css/button.css</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/chrome_shared.css?content-type=text/plain">domui/css/chrome_shared.css</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/list.css?content-type=text/plain">domui/css/list.css</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/css/select.css?content-type=text/plain">domui/css/select.css</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr.js?content-type=text/plain">domui/js/cr.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/event_target.js?content-type=text/plain">domui/js/cr/event_target.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui.js?content-type=text/plain">domui/js/cr/ui.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/array_data_model.js?content-type=text/plain">domui/js/cr/ui/array_data_model.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list.js?content-type=text/plain">domui/js/cr/ui/list.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_item.js?content-type=text/plain">domui/js/cr/ui/list_item.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_selection_controller.js?content-type=text/plain">domui/js/cr/ui/list_selection_controller.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_selection_model.js?content-type=text/plain">domui/js/cr/ui/list_selection_model.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list_single_selection_model.js?content-type=text/plain">domui/js/cr/ui/list_single_selection_model.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/util.js?content-type=text/plain">domui/js/util.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/main.js?content-type=text/plain">js/main.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/plugin_list.js?content-type=text/plain">js/plugin_list.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/plugin_settings.js?content-type=text/plain">js/plugin_settings.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/js/rule_list.js?content-type=text/plain">js/rule_list.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/manifest.json?content-type=text/plain">manifest.json</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/options.html?content-type=text/plain">options.html</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/css/list.css?content-type=text/plain">options/css/list.css</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/js/deletable_item_list.js?content-type=text/plain">options/js/deletable_item_list.js</a></code> + </li><li> + <code><a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/options/js/inline_editable_list.js?content-type=text/plain">options/js/inline_editable_list.js</a></code> + </li> + </ul> + </div> + <div> + <a target="_blank" href="http://src.chromium.org/viewvc/chrome/trunk/src/chrome/common/extensions/docs/examples/extensions/plugin_settings/">Browse source</a> + - <a href="examples/extensions/plugin_settings.zip">Download source</a> + <!-- Only show the Install CRX link if a CRX file is provided --> + <span style="display: none; "> + - <a>Install extension</a> + </span> + </div> </div><div class="sample" id="e6ae17ab4ccfd7e059c8c01f25760ca5d894c7fd"> <img class="icon" style="display: none; "> <img class="icon" src="images/sample-default-icon.png"> diff --git a/chrome/common/extensions/docs/samples.json b/chrome/common/extensions/docs/samples.json index dd45316..3771549 100644 --- a/chrome/common/extensions/docs/samples.json +++ b/chrome/common/extensions/docs/samples.json @@ -1845,6 +1845,54 @@ }, { "api_calls": [ + "chrome.i18n.getMessage" + ], + "crx_path": null, + "description": "Customize your content setting for different plug-ins.", + "features": [ + "contentSettings", + "options_page" + ], + "icon": "bunny128.png", + "id": "ab4b9e00a673701b355db9eb8f9ebf2c850cd784", + "name": "Per-plugin content settings", + "packaged_app": false, + "path": "examples\/extensions\/plugin_settings\/", + "protocols": [], + "search_string": "PER-PLUGIN CONTENT SETTINGS CUSTOMIZE YOUR CONTENT SETTING FOR DIFFERENT PLUG-INS. CONTENTSETTINGS OPTIONS_PAGE CHROME.I18N.GETMESSAGE", + "source_files": [ + "_locales\/en\/messages.json", + "css\/plugin_list.css", + "css\/rule_list.css", + "domui\/css\/button.css", + "domui\/css\/chrome_shared.css", + "domui\/css\/list.css", + "domui\/css\/select.css", + "domui\/js\/cr.js", + "domui\/js\/cr\/event_target.js", + "domui\/js\/cr\/ui.js", + "domui\/js\/cr\/ui\/array_data_model.js", + "domui\/js\/cr\/ui\/list.js", + "domui\/js\/cr\/ui\/list_item.js", + "domui\/js\/cr\/ui\/list_selection_controller.js", + "domui\/js\/cr\/ui\/list_selection_model.js", + "domui\/js\/cr\/ui\/list_single_selection_model.js", + "domui\/js\/util.js", + "js\/main.js", + "js\/plugin_list.js", + "js\/plugin_settings.js", + "js\/rule_list.js", + "manifest.json", + "options.html", + "options\/css\/list.css", + "options\/js\/deletable_item_list.js", + "options\/js\/inline_editable_list.js" + ], + "source_hash": "1e2765f821fa09cf0db9cce20ae61d964565986b", + "zip_path": "examples\/extensions\/plugin_settings.zip" + }, + { + "api_calls": [ "chrome.browserAction.onClicked", "chrome.tabs.update" ], |