diff --git a/browser/components/search/content/searchbar.js b/browser/components/search/content/searchbar.js
index a62ccb21178e..ad28b467744c 100644
--- a/browser/components/search/content/searchbar.js
+++ b/browser/components/search/content/searchbar.js
@@ -9,46 +9,15 @@
// This is loaded into chrome windows with the subscript loader. Wrap in
// a block to prevent accidentally leaking globals onto `window`.
{
-const inheritsMap = {
- ".searchbar-textbox": ["disabled", "disableautocomplete", "searchengine", "src", "newlines"],
- ".searchbar-search-button": ["addengines"],
-};
-
-function inheritAttribute(parent, child, attr) {
- if (!parent.hasAttribute(attr)) {
- child.removeAttribute(attr);
- } else {
- child.setAttribute(attr, parent.getAttribute(attr));
- }
-}
-
/**
* Defines the search bar element.
*/
class MozSearchbar extends MozXULElement {
- static get observedAttributes() {
- let unique = new Set();
- for (let i in inheritsMap) {
- inheritsMap[i].forEach(attr => unique.add(attr));
- }
- return Array.from(unique);
- }
-
- attributeChangedCallback() {
- this.inheritAttributes();
- }
-
- inheritAttributes() {
- if (!this.isConnected) {
- return;
- }
-
- for (let sel in inheritsMap) {
- let node = this.querySelector(sel);
- for (let attr of inheritsMap[sel]) {
- inheritAttribute(this, node, attr);
- }
- }
+ static get inheritedAttributes() {
+ return {
+ ".searchbar-textbox": "disabled,disableautocomplete,searchengine,src,newlines",
+ ".searchbar-search-button": "addengines",
+ };
}
constructor() {
@@ -72,9 +41,9 @@ class MozSearchbar extends MozXULElement {
};
this.content = MozXULElement.parseXULToFragment(`
-
+
-
+
@@ -93,7 +62,7 @@ class MozSearchbar extends MozXULElement {
}
this.appendChild(document.importNode(this.content, true));
- this.inheritAttributes();
+ this.initializeAttributeInheritance();
window.addEventListener("unload", this.destroy);
this._ignoreFocus = false;
diff --git a/toolkit/content/customElements.js b/toolkit/content/customElements.js
index 8f3c3b1f1e9d..d49dc5282922 100644
--- a/toolkit/content/customElements.js
+++ b/toolkit/content/customElements.js
@@ -47,9 +47,120 @@ gXULDOMParser.forceEnableXULXBL();
const MozElements = {};
const MozElementMixin = Base => class MozElement extends Base {
+ /*
+ * A declarative way to wire up attribute inheritance and automatically generate
+ * the `observedAttributes` getter. For example, if you returned:
+ * {
+ * ".foo": "bar,baz=bat"
+ * }
+ *
+ * Then the base class will automatically return ["bar", "bat"] from `observedAttributes`,
+ * and set up an `attributeChangedCallback` to pass those attributes down onto an element
+ * matching the ".foo" selector.
+ *
+ * See the `inheritAttribute` function for more details on the attribute string format.
+ *
+ * @return {Object}
+ */
+ static get inheritedAttributes() {
+ return null;
+ }
+
+ /*
+ * Generate this array based on `inheritedAttributes`, if any. A class is free to override
+ * this if it needs to do something more complex or wants to opt out of this behavior.
+ */
+ static get observedAttributes() {
+ let {inheritedAttributes} = this;
+ if (!inheritedAttributes) {
+ return [];
+ }
+
+ let allAttributes = new Set();
+ for (let sel in inheritedAttributes) {
+ for (let attrName of inheritedAttributes[sel].split(",")) {
+ allAttributes.add(attrName.split("=").pop());
+ }
+ }
+ return [...allAttributes];
+ }
+
+ /*
+ * Provide default lifecycle callback for attribute changes that will inherit attributes
+ * based on the static `inheritedAttributes` Object. This can be overridden by callers.
+ */
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (!this.isConnectedAndReady || oldValue === newValue || !this.inheritedAttributesCache) {
+ return;
+ }
+
+ this.inheritAttributes();
+ }
+
+ /*
+ * After setting content, calling this will cache the elements from selectors in the
+ * static `inheritedAttributes` Object. It'll also do an initial call to `this.inheritAttributes()`,
+ * so in the simple case, this is the only function you need to call.
+ *
+ * This should be called any time the children that are inheriting attributes changes. For instance,
+ * it's common in a connectedCallback to do something like:
+ *
+ * this.textContent = "";
+ * this.append(MozXULElement.parseXULToFragment(``))
+ * this.initializeAttributeInheritance();
+ *
+ */
+ initializeAttributeInheritance() {
+ let {inheritedAttributes} = this.constructor;
+ if (!inheritedAttributes) {
+ return;
+ }
+ this._inheritedAttributesValuesCache = null;
+ this.inheritedAttributesCache = new Map();
+ for (let selector in inheritedAttributes) {
+ let el = this.querySelector(selector);
+ // Skip unmatched selectors in case an element omits some elements in certain cases:
+ if (!el) {
+ continue;
+ }
+ if (this.inheritedAttributesCache.has(el)) {
+ console.error(`Error: duplicate element encountered with ${selector}`);
+ }
+
+ this.inheritedAttributesCache.set(el, inheritedAttributes[selector]);
+ }
+ this.inheritAttributes();
+ }
+
+ /*
+ * Loop through the static `inheritedAttributes` Map and inherit attributes to child elements.
+ *
+ * This usually won't need to be called directly - `this.initializeAttributeInheritance()` and
+ * `this.attributeChangedCallback` will call it for you when appropriate.
+ */
+ inheritAttributes() {
+ let {inheritedAttributes} = this.constructor;
+ if (!inheritedAttributes) {
+ return;
+ }
+
+ if (!this.inheritedAttributesCache) {
+ console.error(`You must call this.initializeAttributeInheritance() for ${this.tagName}`);
+ return;
+ }
+
+ for (let [ el, attrs ] of this.inheritedAttributesCache.entries()) {
+ for (let attr of attrs.split(",")) {
+ this.inheritAttribute(el, attr);
+ }
+ }
+ }
+
/*
* Implements attribute inheritance by a child element. Uses XBL @inherit
- * syntax of |to=from|.
+ * syntax of |to=from|. This can be used directly, but for simple cases
+ * you should use the inheritedAttributes getter and let the base class
+ * handle this for you.
*
* @param {element} child
* A child element that inherits an attribute.
@@ -74,13 +185,13 @@ const MozElementMixin = Base => class MozElement extends Base {
// If our attribute hasn't changed since we last inherited, we don't want to
// propagate it down to the child. This prevents overriding an attribute that's
// been changed on the child (for instance, [checked]).
- if (!this._inheritedAttributesMap) {
- this._inheritedAttributesMap = new WeakMap();
+ if (!this._inheritedAttributesValuesCache) {
+ this._inheritedAttributesValuesCache = new WeakMap();
}
- if (!this._inheritedAttributesMap.has(child)) {
- this._inheritedAttributesMap.set(child, {});
+ if (!this._inheritedAttributesValuesCache.has(child)) {
+ this._inheritedAttributesValuesCache.set(child, {});
}
- let lastInheritedAttributes = this._inheritedAttributesMap.get(child);
+ let lastInheritedAttributes = this._inheritedAttributesValuesCache.get(child);
if ((hasAttr && attrValue === lastInheritedAttributes[attrName]) ||
(!hasAttr && !lastInheritedAttributes.hasOwnProperty(attrName))) {
diff --git a/toolkit/content/tests/chrome/test_custom_element_base.xul b/toolkit/content/tests/chrome/test_custom_element_base.xul
index fa9a37b91d98..753770a65fde 100644
--- a/toolkit/content/tests/chrome/test_custom_element_base.xul
+++ b/toolkit/content/tests/chrome/test_custom_element_base.xul
@@ -16,7 +16,8 @@
-
+
+
@@ -31,7 +32,7 @@
testBaseControlMixin();
testBaseText();
testParseXULToFragment();
- testInherits();
+ testInheritAttributes();
await testCustomInterface();
let htmlWin = await new Promise(resolve => {
@@ -88,8 +89,26 @@
deck.remove();
}
- function testInherits() {
- class InheritsElement extends MozXULElement {
+ function testInheritAttributes() {
+ class InheritsElementDeclarative extends MozXULElement {
+ static get inheritedAttributes() {
+ return {
+ "label": "text=label,foo,boo,bardo=bar",
+ "unmatched": "foo", // Make sure we don't throw on unmatched selectors
+ };
+ }
+
+ connectedCallback() {
+ this.append(MozXULElement.parseXULToFragment(``));
+ this.label = this.querySelector("label");
+ this.initializeAttributeInheritance();
+ }
+ }
+ customElements.define("inherited-element-declarative", InheritsElementDeclarative);
+ let declarativeEl = document.querySelector("inherited-element-declarative");
+ ok(declarativeEl, "declarative inheritance element exists");
+
+ class InheritsElementImperative extends MozXULElement {
static get observedAttributes() {
return [ "label", "foo", "boo", "bar" ];
}
@@ -113,49 +132,49 @@
}
}
- customElements.define("inherited-element", InheritsElement);
- let el = document.querySelector("inherited-element");
- ok(el, "element exists");
+ customElements.define("inherited-element-imperative", InheritsElementImperative);
+ let imperativeEl = document.querySelector("inherited-element-imperative");
+ ok(imperativeEl, "imperative inheritance element exists");
- is(el.label.getAttribute("foo"), "fuagra", "predefined attribute @foo");
- ok(!el.label.hasAttribute("boo"), "predefined attribute @boo");
- ok(!el.label.hasAttribute("bardo"), "predefined attribute @bardo");
- ok(!el.label.textContent, "predefined attribute @label");
+ for (let el of [declarativeEl, imperativeEl]) {
+ info(`Running checks for ${el.tagName}`);
+ is(el.label.getAttribute("foo"), "fuagra", "predefined attribute @foo");
+ ok(!el.label.hasAttribute("boo"), "predefined attribute @boo");
+ ok(!el.label.hasAttribute("bardo"), "predefined attribute @bardo");
+ ok(!el.label.textContent, "predefined attribute @label");
- el.setAttribute("boo", "boo-test");
- is(el.label.getAttribute("boo"), "boo-test",
- "attribute inheritance: boo");
+ el.setAttribute("boo", "boo-test");
+ is(el.label.getAttribute("boo"), "boo-test",
+ "attribute inheritance: boo");
- el.setAttribute("label", "label-test");
- is(el.label.textContent, "label-test",
- "attribute inheritance: text=label attribute change");
+ el.setAttribute("label", "label-test");
+ is(el.label.textContent, "label-test",
+ "attribute inheritance: text=label attribute change");
- el.setAttribute("bar", "bar-test");
- is(el.label.getAttribute("bardo"), "bar-test",
- "attribute inheritance: `=` mapping");
+ el.setAttribute("bar", "bar-test");
+ is(el.label.getAttribute("bardo"), "bar-test",
+ "attribute inheritance: `=` mapping");
- el.label.setAttribute("bardo", "changed-from-child");
- el.inherit();
- is(el.label.getAttribute("bardo"), "changed-from-child",
- "attribute inheritance: doesn't apply when host attr hasn't changed and child attr was changed");
+ el.label.setAttribute("bardo", "changed-from-child");
+ is(el.label.getAttribute("bardo"), "changed-from-child",
+ "attribute inheritance: doesn't apply when host attr hasn't changed and child attr was changed");
- el.label.removeAttribute("bardo");
- el.inherit();
- ok(!el.label.hasAttribute("bardo"),
- "attribute inheritance: doesn't apply when host attr hasn't changed and child attr was removed");
+ el.label.removeAttribute("bardo");
+ ok(!el.label.hasAttribute("bardo"),
+ "attribute inheritance: doesn't apply when host attr hasn't changed and child attr was removed");
- el.setAttribute("bar", "changed-from-host");
- is(el.label.getAttribute("bardo"), "changed-from-host",
- "attribute inheritance: does apply when host attr has changed and child attr was changed");
+ el.setAttribute("bar", "changed-from-host");
+ is(el.label.getAttribute("bardo"), "changed-from-host",
+ "attribute inheritance: does apply when host attr has changed and child attr was changed");
- el.removeAttribute("bar");
- ok(!el.label.hasAttribute("bardo"),
- "attribute inheritance: does apply when host attr has been removed");
-
- el.setAttribute("bar", "changed-from-host-2");
- is(el.label.getAttribute("bardo"), "changed-from-host-2",
- "attribute inheritance: does apply when host attr has changed after being removed");
+ el.removeAttribute("bar");
+ ok(!el.label.hasAttribute("bardo"),
+ "attribute inheritance: does apply when host attr has been removed");
+ el.setAttribute("bar", "changed-from-host-2");
+ is(el.label.getAttribute("bardo"), "changed-from-host-2",
+ "attribute inheritance: does apply when host attr has changed after being removed");
+ }
}
async function testCustomInterface() {
diff --git a/toolkit/content/widgets/autocomplete-richlistitem.js b/toolkit/content/widgets/autocomplete-richlistitem.js
index d77d9e1dd37d..7cd2ae131071 100644
--- a/toolkit/content/widgets/autocomplete-richlistitem.js
+++ b/toolkit/content/widgets/autocomplete-richlistitem.js
@@ -61,56 +61,28 @@ MozElements.MozAutocompleteRichlistitem = class MozAutocompleteRichlistitem exte
this.textContent = "";
this.appendChild(MozXULElement.parseXULToFragment(this._markup));
+ this.initializeAttributeInheritance();
this._boundaryCutoff = null;
this._inOverflow = false;
- this._updateAttributes();
this._adjustAcItem();
}
- static get observedAttributes() {
- return [
- "actiontype",
- "current",
- "selected",
- "image",
- "type",
- ];
- }
-
- get inheritedAttributeMap() {
- if (!this.__inheritedAttributeMap) {
- this.__inheritedAttributeMap = new Map([
- [ this.querySelector(".ac-type-icon"), [ "selected", "current", "type" ] ],
- [ this.querySelector(".ac-site-icon"), [ "src=image", "selected", "type" ] ],
- [ this.querySelector(".ac-title"), [ "selected" ] ],
- [ this.querySelector(".ac-title-text"), [ "selected" ] ],
- [ this.querySelector(".ac-tags"), [ "selected" ] ],
- [ this.querySelector(".ac-tags-text"), [ "selected" ] ],
- [ this.querySelector(".ac-separator"), [ "selected", "actiontype", "type" ] ],
- [ this.querySelector(".ac-url"), [ "selected", "actiontype" ] ],
- [ this.querySelector(".ac-url-text"), [ "selected" ] ],
- [ this.querySelector(".ac-action"), [ "selected", "actiontype" ] ],
- [ this.querySelector(".ac-action-text"), [ "selected" ] ],
- ]);
- }
- return this.__inheritedAttributeMap;
- }
-
- attributeChangedCallback(name, oldValue, newValue) {
- if (this.isConnectedAndReady && oldValue != newValue &&
- this.constructor.observedAttributes.includes(name)) {
- this._updateAttributes();
- }
- }
-
- _updateAttributes() {
- for (let [ el, attrs ] of this.inheritedAttributeMap.entries()) {
- for (let attr of attrs) {
- this.inheritAttribute(el, attr);
- }
- }
+ static get inheritedAttributes() {
+ return {
+ ".ac-type-icon": "selected,current,type",
+ ".ac-site-icon": "src=image,selected,type",
+ ".ac-title": "selected",
+ ".ac-title-text": "selected",
+ ".ac-tags": "selected",
+ ".ac-tags-text": "selected",
+ ".ac-separator": "selected,actiontype,type",
+ ".ac-url": "selected,actiontype",
+ ".ac-url-text": "selected",
+ ".ac-action": "selected,actiontype",
+ ".ac-action-text": "selected",
+ };
}
get _markup() {
@@ -942,31 +914,18 @@ class MozAutocompleteRichlistitemInsecureWarning extends MozElements.MozAutocomp
this.classList.add("forceHandleUnderflow");
}
- static get observedAttributes() {
- return [
- "actiontype",
- "current",
- "selected",
- "image",
- "type",
- ];
- }
-
- get inheritedAttributeMap() {
- if (!this.__inheritedAttributeMap) {
- this.__inheritedAttributeMap = new Map([
- [ this.querySelector(".ac-type-icon"), [ "selected", "current", "type" ] ],
- [ this.querySelector(".ac-site-icon"), [ "src=image", "selected", "type" ] ],
- [ this.querySelector(".ac-title-text"), [ "selected" ] ],
- [ this.querySelector(".ac-tags-text"), [ "selected" ] ],
- [ this.querySelector(".ac-separator"), [ "selected", "actiontype", "type" ] ],
- [ this.querySelector(".ac-url"), [ "selected", "actiontype" ] ],
- [ this.querySelector(".ac-url-text"), [ "selected" ] ],
- [ this.querySelector(".ac-action"), [ "selected", "actiontype" ] ],
- [ this.querySelector(".ac-action-text"), [ "selected" ] ],
- ]);
- }
- return this.__inheritedAttributeMap;
+ static get inheritedAttributes() {
+ return {
+ ".ac-type-icon": "selected,current,type",
+ ".ac-site-icon": "src=image,selected,type",
+ ".ac-title-text": "selected",
+ ".ac-tags-text": "selected",
+ ".ac-separator": "selected,actiontype,type",
+ ".ac-url": "selected,actiontype",
+ ".ac-url-text": "selected",
+ ".ac-action": "selected,actiontype",
+ ".ac-action-text": "selected",
+ };
}
get _markup() {
diff --git a/toolkit/content/widgets/menu.js b/toolkit/content/widgets/menu.js
index dfa8d1f8cc8b..d929c321060f 100644
--- a/toolkit/content/widgets/menu.js
+++ b/toolkit/content/widgets/menu.js
@@ -111,18 +111,13 @@ MozXULElement.implementCustomInterface(MozMenuBase, [Ci.nsIDOMXULContainerElemen
// The element is used for rendering inside of ,
// See SelectParentHelper.jsm.
class MozMenuCaption extends MozMenuBase {
- static get observedAttributes() {
- return [
- "selected",
- "disabled",
- "checked",
- "image",
- "validate",
- "src",
- "label",
- "crop",
- "highlightable",
- ];
+ static get inheritedAttributes() {
+ return {
+ ".menu-iconic-left": "selected,disabled,checked",
+ ".menu-iconic-icon": "src=image,validate,src",
+ ".menu-iconic-text": "value=label,crop,highlightable",
+ ".menu-iconic-highlightable-text": "text=label,crop,highlightable",
+ };
}
_updateAttributes() {
@@ -148,17 +143,13 @@ class MozMenuCaption extends MozMenuBase {
connectedCallback() {
this.textContent = "";
this.appendChild(MozXULElement.parseXULToFragment(`
-