Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
70
gaia/distros/spark/apps/customizer/js/elements/fxos-code-editor.js
Executable file
70
gaia/distros/spark/apps/customizer/js/elements/fxos-code-editor.js
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
define(["exports"], function (exports) {
|
||||
"use strict";
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
var template = "<style scoped>\n .container {\n position: relative;\n width: 100%;\n height: 100%;\n }\n .line-numbers {\n background: #95368c;\n color: #fff;\n font-family: Consolas,Monaco,\"Andale Mono\",monospace;\n font-size: 14px;\n line-height: 1.2em;\n position: absolute;\n padding: 0 2px;\n top: 0;\n left: 0;\n width: 28px;\n height: 100%;\n overflow: hidden;\n }\n textarea {\n background: #000;\n border: none;\n color: #fff;\n font-family: Consolas,Monaco,\"Andale Mono\",monospace;\n font-size: 14px;\n line-height: 1.2em;\n position: absolute;\n top: 0;\n left: 32px;\n width: calc(100% - 32px);\n height: 100%;\n margin: 0;\n padding: 0;\n -moz-user-select: text !important;\n }\n .line-numbers,\n textarea {\n font-family: Consolas,Monaco,\"Andale Mono\",monospace;\n font-size: 14px;\n line-height: 1.2em;\n }\n</style>\n<div class=\"container\">\n <div class=\"line-numbers\">\n <div>1</div>\n </div>\n <textarea autocomplete=\"off\"\n autocorrect=\"off\"\n autocapitalize=\"off\"\n inputmode=\"verbatim\"\n x-inputmode=\"verbatim\"\n spellcheck=\"false\"\n wrap=\"off\"></textarea>\n</div>";
|
||||
|
||||
proto.createdCallback = function () {
|
||||
var _this = this;
|
||||
var value = this.innerHTML;
|
||||
|
||||
this.shadow = this.createShadowRoot();
|
||||
this.shadow.innerHTML = template;
|
||||
|
||||
this.lineNumbers = this.shadow.querySelector(".line-numbers");
|
||||
|
||||
this.textarea = this.shadow.querySelector("textarea");
|
||||
this.textarea.value = value;
|
||||
|
||||
this.textarea.addEventListener("keyup", function () {
|
||||
_this.dispatchEvent(new CustomEvent("change"));
|
||||
updateLineNumbers(_this);
|
||||
});
|
||||
|
||||
this.textarea.addEventListener("scroll", function () {
|
||||
_this.lineNumbers.scrollTop = _this.textarea.scrollTop;
|
||||
});
|
||||
};
|
||||
|
||||
Object.defineProperty(proto, "value", {
|
||||
get: function () {
|
||||
return this.textarea.value;
|
||||
},
|
||||
|
||||
set: function (value) {
|
||||
this.textarea.value = value;
|
||||
updateLineNumbers(this);
|
||||
}
|
||||
});
|
||||
|
||||
function updateLineNumbers(element) {
|
||||
var html = "";
|
||||
|
||||
if (element.value.length < 5000) {
|
||||
var lines = element.value.split("\n").length;
|
||||
if (lines === element.lineNumbers.childElementCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 1; i <= lines; i++) {
|
||||
html += "<div>" + i + "</div>";
|
||||
}
|
||||
}
|
||||
|
||||
element.lineNumbers.innerHTML = html;
|
||||
element.lineNumbers.scrollTop = element.textarea.scrollTop;
|
||||
}
|
||||
|
||||
try {
|
||||
document.registerElement("fxos-code-editor", { prototype: proto });
|
||||
} catch (e) {
|
||||
if (e.name !== "NotSupportedError") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
})(window);
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
define(["exports"], function (exports) {
|
||||
"use strict";
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
var shadowHTML = "<style scoped>\n.overlay {\n display: none;\n position: absolute;\n box-sizing: border-box;\n pointer-events: none;\n z-index: 9999999; /* above the app, but below the other customizer elements */\n background-color: #00caf2;\n border: 2px dotted #fff;\n outline: 1px solid #00caf2;\n opacity: 0.75;\n}\n.label {\n background-color: rgba(0, 0, 0, 0.5);\n border-bottom-right-radius: 2px;\n color: #fff;\n font-family: 'FiraSans';\n font-size: 10px;\n line-height: 1.2em;\n text-overflow: ellipsis;\n white-space: nowrap;\n display: inline-block;\n position: absolute;\n padding: 0 2px;\n top: 0;\n left: 0;\n max-width: 100%;\n overflow: hidden;\n}\n</style>\n<div class=\"overlay\">\n <div class=\"label\"></div>\n</div>";
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
proto.createdCallback = function () {
|
||||
this.shadow = this.createShadowRoot();
|
||||
this.shadow.innerHTML = shadowHTML;
|
||||
this.overlay = this.shadow.querySelector(".overlay");
|
||||
this.label = this.shadow.querySelector(".label");
|
||||
};
|
||||
|
||||
proto.highlight = function (element) {
|
||||
// Figure out where the element is
|
||||
var rect = element && element.getBoundingClientRect();
|
||||
|
||||
// If the element has zero size, hide the highlight
|
||||
if (!rect || (rect.width === 0 && rect.height === 0)) {
|
||||
this.overlay.style.display = "none";
|
||||
} else {
|
||||
// Otherwise, highlight the element.
|
||||
// Note that we use add the document scroll offsets to the element
|
||||
// coordinates to get document coordinates instead of screen coordinates.
|
||||
this.overlay.style.left = (rect.left + window.pageXOffset) + "px";
|
||||
this.overlay.style.top = (rect.top + window.pageYOffset) + "px";
|
||||
this.overlay.style.width = rect.width + "px";
|
||||
this.overlay.style.height = rect.height + "px";
|
||||
this.overlay.style.display = "block";
|
||||
|
||||
// Set the label to properly identify the element.
|
||||
this.label.textContent = element.tagName;
|
||||
if (element.id) {
|
||||
this.label.textContent += "#" + element.id;
|
||||
}
|
||||
|
||||
// And try to move the element so that it is on screen
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
proto.hide = function () {
|
||||
this.overlay.style.display = "none";
|
||||
};
|
||||
|
||||
try {
|
||||
document.registerElement("fxos-customizer-highlighter", { prototype: proto });
|
||||
} catch (e) {
|
||||
if (e.name !== "NotSupportedError") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
})(window);
|
||||
});
|
||||
192
gaia/distros/spark/apps/customizer/js/elements/fxos-customizer.js
Executable file
192
gaia/distros/spark/apps/customizer/js/elements/fxos-customizer.js
Executable file
|
|
@ -0,0 +1,192 @@
|
|||
define(["exports"], function (exports) {
|
||||
"use strict";
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
var template = "<style scoped>\n[data-icon]:before {\n font-family: \"gaia-icons\";\n content: attr(data-icon);\n display: inline-block;\n font-weight: 500;\n font-style: normal;\n text-decoration: inherit;\n text-transform: none;\n text-rendering: optimizeLegibility;\n font-size: 30px;\n}\n[data-customizer-icon]:before {\n font-family: \"customizer-icons\";\n content: attr(data-customizer-icon);\n display: inline-block;\n font-weight: 500;\n font-style: normal;\n text-decoration: inherit;\n text-transform: none;\n text-rendering: optimizeLegibility;\n font-size: 30px;\n}\ngaia-dom-tree {\n width: 100%;\n height: calc(100% - 46px);\n}\n.pin {\n position: absolute;\n top: 0;\n right: 0;\n margin: 1rem !important;\n opacity: 1;\n transition: opacity 0.5s ease-in-out;\n}\n.pin.scrolling {\n pointer-events: none;\n opacity: 0;\n}\n</style>\n<gaia-button circular class=\"pin\" data-action=\"settings\">\n <i data-icon=\"settings\"></i>\n</gaia-button>\n<gaia-dom-tree></gaia-dom-tree>\n<gaia-toolbar>\n <button data-customizer-icon=\"edit\" data-action=\"edit\" disabled></button>\n <button data-customizer-icon=\"copy\" data-action=\"copyOrMove\" disabled></button>\n <button data-icon=\"add\" data-action=\"append\" disabled></button>\n <button data-customizer-icon=\"source\" data-action=\"viewSource\" disabled></button>\n <button data-icon=\"delete\" data-action=\"remove\" disabled></button>\n</gaia-toolbar>";
|
||||
|
||||
proto.createdCallback = function () {
|
||||
this.shadow = this.createShadowRoot();
|
||||
this.shadow.innerHTML = template;
|
||||
|
||||
this.settingsButton = this.shadow.querySelector("[data-action=\"settings\"]");
|
||||
this.gaiaDomTree = this.shadow.querySelector("gaia-dom-tree");
|
||||
this.gaiaToolbar = this.shadow.querySelector("gaia-toolbar");
|
||||
|
||||
this.settingsButton.addEventListener("click", this._handleMenuAction.bind(this));
|
||||
this.gaiaDomTree.addEventListener("click", this._handleSelected.bind(this));
|
||||
this.gaiaToolbar.addEventListener("click", this._handleAction.bind(this));
|
||||
|
||||
this._watchScrolling();
|
||||
|
||||
this.gaiaDomTree.addEventListener("contextmenu", function (evt) {
|
||||
evt.stopPropagation();
|
||||
});
|
||||
|
||||
this._rootNodeClickHandler = this._handleClick.bind(this);
|
||||
};
|
||||
|
||||
proto.setRootNode = function (rootNode) {
|
||||
// If we already have a root node defined, disconnect from it first
|
||||
if (this._root) {
|
||||
this.unwatchChanges();
|
||||
this.gaiaDomTree.setRoot(null);
|
||||
this.gaiaDomTree.render();
|
||||
this._root.removeEventListener("click", this._rootNodeClickHandler);
|
||||
this._root = null;
|
||||
}
|
||||
|
||||
if (!rootNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up the new root node
|
||||
this._root = rootNode;
|
||||
rootNode.addEventListener("click", this._rootNodeClickHandler);
|
||||
this.gaiaDomTree.setRoot(rootNode);
|
||||
this.gaiaDomTree.render();
|
||||
this.watchChanges();
|
||||
|
||||
// Pre-expand the root node
|
||||
var rootTreeNode = this.gaiaDomTree.treeMap.get(rootNode);
|
||||
if (rootTreeNode) {
|
||||
this.gaiaDomTree.expandNode(rootTreeNode);
|
||||
}
|
||||
|
||||
var head = rootNode.querySelector("head");
|
||||
var body = rootNode.querySelector("body");
|
||||
if (!head || !body) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-expand the <head> node
|
||||
var headTreeNode = this.gaiaDomTree.treeMap.get(head);
|
||||
if (headTreeNode) {
|
||||
this.gaiaDomTree.expandNode(headTreeNode);
|
||||
}
|
||||
|
||||
// Pre-expand the <body> node
|
||||
var bodyTreeNode = this.gaiaDomTree.treeMap.get(body);
|
||||
if (bodyTreeNode) {
|
||||
this.gaiaDomTree.expandNode(bodyTreeNode);
|
||||
}
|
||||
};
|
||||
|
||||
proto._watchScrolling = function () {
|
||||
var _this = this;
|
||||
this.gaiaDomTree.shadowRoot.addEventListener("scroll", function (evt) {
|
||||
if (_this._scrollTimeout) {
|
||||
clearTimeout(_this._scrollTimeout);
|
||||
}
|
||||
|
||||
_this._scrollTimeout = setTimeout(function () {
|
||||
_this.settingsButton.classList.remove("scrolling");
|
||||
}, 500);
|
||||
|
||||
_this.settingsButton.classList.add("scrolling");
|
||||
}, true);
|
||||
};
|
||||
|
||||
proto._shadowContains = function (el) {
|
||||
var customizerRootView = document.body.querySelector(".fxos-customizer-main-view");
|
||||
|
||||
if (!el || el == document.documentElement) {
|
||||
return false;
|
||||
} else if (el == customizerRootView) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this._shadowContains(el.parentNode || el.host);
|
||||
};
|
||||
|
||||
proto.watchChanges = function () {
|
||||
var _this2 = this;
|
||||
var OBSERVER_CONFIG = {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
subtree: true
|
||||
};
|
||||
|
||||
this._observer = new MutationObserver(function (mutations) {
|
||||
// Only re-render if a mutation occurred in the app itself, and is
|
||||
// outside of the customizer addon. This depends on the customizer
|
||||
// root element having the class "fxos-customizer-main-view"
|
||||
for (var i = mutations.length - 1; i >= 0; i--) {
|
||||
if (!_this2._shadowContains(mutations[i].target)) {
|
||||
var selectedNode = _this2.gaiaDomTree.selectedNode;
|
||||
_this2.gaiaDomTree.render();
|
||||
_this2.select(selectedNode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._observer.observe(this._root, OBSERVER_CONFIG);
|
||||
};
|
||||
|
||||
proto.unwatchChanges = function () {
|
||||
this._observer.disconnect();
|
||||
this._observer = null;
|
||||
};
|
||||
|
||||
proto.select = function (node) {
|
||||
this.gaiaDomTree.select(node);
|
||||
};
|
||||
|
||||
proto._handleMenuAction = function (e) {
|
||||
var action = e.target.dataset.action;
|
||||
if (action) {
|
||||
this.dispatchEvent(new CustomEvent("menu", {
|
||||
detail: action
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
proto._handleSelected = function (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
var selectedNode = this.gaiaDomTree.selectedNode;
|
||||
if (!selectedNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selected = this._selected = (selectedNode.nodeType === Node.TEXT_NODE) ? selectedNode.parentNode : selectedNode;
|
||||
|
||||
[].forEach.call(this.gaiaToolbar.querySelectorAll("button"), function (button) {
|
||||
button.disabled = !selected;
|
||||
});
|
||||
|
||||
this.gaiaToolbar.querySelector("[data-action=\"viewSource\"]").disabled = (selected.tagName !== "SCRIPT" || !selected.hasAttribute("src")) && (selected.tagName !== "LINK" || !selected.hasAttribute("href"));
|
||||
|
||||
this.dispatchEvent(new CustomEvent("selected", {
|
||||
detail: this._selected
|
||||
}));
|
||||
};
|
||||
|
||||
proto._handleAction = function (e) {
|
||||
this.dispatchEvent(new CustomEvent("action:" + e.target.dataset.action, {
|
||||
detail: this._selected
|
||||
}));
|
||||
};
|
||||
|
||||
proto._handleClick = function (e) {
|
||||
if (e.target === this.gaiaDomTree) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.select(e.target);
|
||||
};
|
||||
|
||||
try {
|
||||
document.registerElement("fxos-customizer", { prototype: proto });
|
||||
} catch (e) {
|
||||
if (e.name !== "NotSupportedError") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
})(window);
|
||||
});
|
||||
422
gaia/distros/spark/apps/customizer/js/elements/fxos-inspector.js
Executable file
422
gaia/distros/spark/apps/customizer/js/elements/fxos-inspector.js
Executable file
|
|
@ -0,0 +1,422 @@
|
|||
define(["exports"], function (exports) {
|
||||
"use strict";
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
|
||||
var HTML_ESCAPE_CHARS = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"\"": """,
|
||||
"'": "'",
|
||||
"/": "/"
|
||||
};
|
||||
|
||||
var CONSTANT_PROPERTY_REGEX = /^[A-Z_]+$/;
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
var template = "<style scoped>\n [data-customizer-icon]:before {\n font-family: \"customizer-icons\";\n content: attr(data-customizer-icon);\n display: inline-block;\n font-weight: 500;\n font-style: normal;\n text-decoration: inherit;\n text-transform: none;\n text-rendering: optimizeLegibility;\n font-size: 30px;\n }\n [flex] {\n display: flex;\n justify-content: space-between;\n padding: 0 5px;\n }\n [flex] > * {\n flex: 1 1 auto;\n margin-right: 5px;\n margin-left: 5px;\n }\n #container {\n box-sizing: border-box;\n position: relative;\n width: 100%;\n height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n }\n gaia-list a {\n position: relative;\n text-decoration: none;\n }\n gaia-list [hidden] {\n display: none;\n }\n gaia-list h3 {\n font-size: 18px;\n max-width: calc(100% - 2em);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n gaia-list [data-customizer-icon] {\n position: absolute;\n padding: 1.6em 1em;\n top: 0;\n right: 0;\n }\n gaia-list .value {\n color: #00aac5;\n }\n fxos-code-editor {\n display: block;\n width: 100%;\n height: calc(100% - 100px);\n }\n</style>\n<div id=\"container\"></div>\n<gaia-dialog-prompt id=\"new-attribute-name\">\n Enter new attribute name\n</gaia-dialog-prompt>";
|
||||
|
||||
proto.createdCallback = function () {
|
||||
var _this = this;
|
||||
this.shadow = this.createShadowRoot();
|
||||
this.shadow.innerHTML = template;
|
||||
|
||||
this.container = this.shadow.querySelector("#container");
|
||||
this.newAttributeName = this.shadow.querySelector("#new-attribute-name");
|
||||
|
||||
this.keyPath = [];
|
||||
|
||||
// Handle all clicks within this component.
|
||||
this.container.addEventListener("click", function (evt) {
|
||||
// Handle case where <a href> was clicked.
|
||||
var target = evt.target.closest("a");
|
||||
if (target) {
|
||||
evt.preventDefault();
|
||||
|
||||
// Handle case where <i data-action="remove"> was clicked.
|
||||
if (evt.target.dataset.action === "remove") {
|
||||
_this._handleRemoveAttributeClick(target);
|
||||
}
|
||||
|
||||
// Otherwise, treat as regular list item click.
|
||||
else {
|
||||
_this._handleListItemClick(target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle case where <gaia-button> was clicked.
|
||||
target = evt.target.closest("gaia-button");
|
||||
if (target) {
|
||||
evt.preventDefault();
|
||||
_this._handleButtonClick(target);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Filter the list of properties 500ms after a keypress
|
||||
// in the <gaia-text-input> search box.
|
||||
this.container.addEventListener("keyup", function (evt) {
|
||||
var textInput = evt.target.closest("gaia-text-input");
|
||||
if (!textInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(_this._searchTimeout);
|
||||
_this._searchTimeout = setTimeout(function () {
|
||||
var value = textInput.value.toLowerCase();
|
||||
var items = [].slice.apply(_this.container.querySelectorAll("gaia-list a[data-search]"));
|
||||
|
||||
// Toggle each list items' [hidden] attribute if the search text
|
||||
// isn't blank and the items' [data-search] value contains the
|
||||
// current search text.
|
||||
items.forEach(function (i) {
|
||||
return i.hidden = value && i.dataset.search.indexOf(value) === -1;
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
|
||||
// Automatically set focus to the input box when the
|
||||
// <gaia-dialog-prompt> is opened.
|
||||
this.newAttributeName.addEventListener("opened", function () {
|
||||
_this.newAttributeName.els.input.focus();
|
||||
});
|
||||
|
||||
// Reset the <gaia-dialog-prompt> value when closed.
|
||||
this.newAttributeName.addEventListener("closed", function () {
|
||||
_this.newAttributeName.els.input.value = "";
|
||||
});
|
||||
|
||||
// Create a new attribute when the <gaia-dialog-prompt>
|
||||
// is submitted.
|
||||
this.newAttributeName.els.submit.addEventListener("click", function () {
|
||||
try {
|
||||
var name = _this.newAttributeName.els.input.value;
|
||||
var attribute = document.createAttribute(name);
|
||||
|
||||
_this.target.setNamedItem(attribute);
|
||||
|
||||
_this.dispatchEvent(new CustomEvent("createattribute", {
|
||||
detail: JSON.stringify({
|
||||
keyPath: _this.keyPath,
|
||||
expression: "['" + _this.keyPath.join("']['") + "']",
|
||||
name: name
|
||||
})
|
||||
}));
|
||||
} catch (e) {
|
||||
window.alert("Invalid attribute name");
|
||||
return;
|
||||
}
|
||||
|
||||
_this.render();
|
||||
});
|
||||
};
|
||||
|
||||
proto.render = function () {
|
||||
var _this2 = this;
|
||||
clearTimeout(this._searchTimeout);
|
||||
|
||||
this.target = this.rootTarget;
|
||||
this.keyPath.forEach(function (key) {
|
||||
return _this2.target = _this2.target[key];
|
||||
});
|
||||
|
||||
this.container.innerHTML = renderTargetPage(this.target, this.keyPath.length === 0).innerHTML;
|
||||
this.container.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
proto.setRootTarget = function (rootTarget) {
|
||||
this.rootTarget = rootTarget;
|
||||
this.target = this.rootTarget;
|
||||
|
||||
this.keyPath = [];
|
||||
this.render();
|
||||
};
|
||||
|
||||
proto._handleButtonClick = function (target) {
|
||||
var _this3 = this;
|
||||
switch (target.dataset.action) {
|
||||
case "cancel":
|
||||
this.keyPath.pop();
|
||||
break;
|
||||
case "save":
|
||||
(function () {
|
||||
var value = _this3.container.querySelector("fxos-code-editor").value;
|
||||
var target = _this3.rootTarget;
|
||||
|
||||
_this3.keyPath.forEach(function (key, index) {
|
||||
if (index < _this3.keyPath.length - 1) {
|
||||
target = target[key];
|
||||
return;
|
||||
}
|
||||
|
||||
target[key] = value;
|
||||
});
|
||||
|
||||
_this3.dispatchEvent(new CustomEvent("save", {
|
||||
detail: JSON.stringify({
|
||||
keyPath: _this3.keyPath,
|
||||
expression: "['" + _this3.keyPath.join("']['") + "']",
|
||||
value: value
|
||||
})
|
||||
}));
|
||||
|
||||
_this3.keyPath.pop();
|
||||
})();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.render();
|
||||
};
|
||||
|
||||
proto._handleRemoveAttributeClick = function (target) {
|
||||
var name = target.getAttribute("href");
|
||||
this.target.removeNamedItem(name);
|
||||
|
||||
this.dispatchEvent(new CustomEvent("removeattribute", {
|
||||
detail: JSON.stringify({
|
||||
keyPath: this.keyPath,
|
||||
expression: "['" + this.keyPath.join("']['") + "']",
|
||||
name: name
|
||||
})
|
||||
}));
|
||||
|
||||
this.render();
|
||||
};
|
||||
|
||||
proto._handleListItemClick = function (target) {
|
||||
// If the link clicked has an `href` pointing
|
||||
// to "../", go back to the previous level in
|
||||
// the key path.
|
||||
var href = target.getAttribute("href");
|
||||
if (href === "../") {
|
||||
this.keyPath.pop();
|
||||
}
|
||||
|
||||
// Show "Create New Attribute" prompt.
|
||||
else if (href === "#add-attribute") {
|
||||
this.newAttributeName.open();
|
||||
}
|
||||
|
||||
// Otherwise, push the next part of the key path.
|
||||
else {
|
||||
this.keyPath.push(href);
|
||||
}
|
||||
|
||||
this.render();
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function for determining the type of
|
||||
* page to render for the current target (editor
|
||||
* or list).
|
||||
*/
|
||||
function renderTargetPage(target, isRoot) {
|
||||
// If the target is not an object, assume that
|
||||
// it is editable and render the editor page.
|
||||
if (typeof target !== "object") {
|
||||
return renderTargetEditor(target);
|
||||
}
|
||||
|
||||
// Otherwise, the target is an enumerable object
|
||||
// and we can render the list page.
|
||||
return renderTargetList(target, isRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for rendering the editor page.
|
||||
*/
|
||||
function renderTargetEditor(target) {
|
||||
var page = document.createElement("section");
|
||||
page.innerHTML = "<fxos-code-editor>" + target + "</fxos-code-editor>\n<section flex>\n <gaia-button data-action=\"cancel\">Cancel</gaia-button>\n <gaia-button data-action=\"save\">Save</gaia-button>\n</section>";
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for rendering the list page.
|
||||
*/
|
||||
function renderTargetList(target, isRoot) {
|
||||
var list = document.createElement("gaia-list");
|
||||
|
||||
if (!isRoot) {
|
||||
list.appendChild(renderBackItem());
|
||||
}
|
||||
|
||||
if (target instanceof window.NamedNodeMap) {
|
||||
list.appendChild(renderAddAttributeItem());
|
||||
}
|
||||
|
||||
getSortedProperties(target).forEach(function (property) {
|
||||
list.appendChild(renderTargetListItem(target, property));
|
||||
});
|
||||
|
||||
var page = document.createElement("section");
|
||||
page.innerHTML = "<section flex>\n <gaia-text-input type=\"search\" placeholder=\"Search Properties\"></gaia-text-input>\n</section>\n" + list.outerHTML;
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for rendering a list item.
|
||||
*/
|
||||
function renderTargetListItem(target, property) {
|
||||
var value = target[property];
|
||||
if (value instanceof window.Attr) {
|
||||
value = "'" + escapeHTML(value.value) + "'";
|
||||
} else if (typeof value === "string") {
|
||||
value = "'" + escapeHTML(value) + "'";
|
||||
}
|
||||
|
||||
var a = document.createElement("a");
|
||||
a.href = property;
|
||||
a.dataset.search = property.toLowerCase();
|
||||
a.innerHTML = "<h3>" + property + " = <span class=\"value\">" + value + "</span></h3>";
|
||||
|
||||
// Append "X" button to remove attributes.
|
||||
if (target instanceof window.NamedNodeMap) {
|
||||
a.innerHTML += "<i data-customizer-icon=\"remove\" data-action=\"remove\"></i>";
|
||||
}
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for rendering the "Up One Level [..]" list item.
|
||||
*/
|
||||
function renderBackItem() {
|
||||
var a = document.createElement("a");
|
||||
a.href = "../";
|
||||
a.innerHTML = "<h3>Up One Level [..]</h3>";
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for rendering the "Create New Attribute"
|
||||
* list item.
|
||||
*/
|
||||
function renderAddAttributeItem() {
|
||||
var a = document.createElement("a");
|
||||
a.href = "#add-attribute";
|
||||
a.innerHTML = "<h3>Create New Attribute</h3>\n<i data-customizer-icon=\"add\"></i>";
|
||||
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for enumerating and sorting all direct
|
||||
* properties for the specified target object.
|
||||
* This also handles an `attributes` object as a
|
||||
* special case and returns a sorted array of
|
||||
* attribute names instead of an array-indexed
|
||||
* list of `Attr` objects.
|
||||
*/
|
||||
function getSortedProperties(target) {
|
||||
var properties = [];
|
||||
|
||||
// If the `target` is a `NamedNodeMap` (attributes),
|
||||
// enumerate the attributes and push their names
|
||||
// instead of treating it as properties.
|
||||
if (target instanceof window.NamedNodeMap) {
|
||||
for (var _iterator = target[Symbol.iterator](), _step; !(_step = _iterator.next()).done;) {
|
||||
var attr = _step.value;
|
||||
properties.push(attr.name);
|
||||
}
|
||||
|
||||
properties = properties.sort();
|
||||
}
|
||||
|
||||
// Otherwise, emumerate the properties as usual.
|
||||
else {
|
||||
for (var property in target) {
|
||||
// Omit invalid properties.
|
||||
if (!isValidProperty(target, property)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Omit constants.
|
||||
if (CONSTANT_PROPERTY_REGEX.test(property)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Omit native functions unless the target object
|
||||
// directly contains them (e.g. `Element.prototype`).
|
||||
if (!target.hasOwnProperty(property) && isNativeFunction(target[property])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
properties.push(property + "");
|
||||
}
|
||||
|
||||
// Explicitly add the `nodeValue`, `textContent` and
|
||||
// `value` properties for empty attributes so their
|
||||
// values can be set.
|
||||
if (target instanceof window.Attr && !target.value) {
|
||||
properties.push("nodeValue");
|
||||
properties.push("textContent");
|
||||
properties.push("value");
|
||||
}
|
||||
|
||||
properties = properties.sort();
|
||||
|
||||
// Append `__proto__` property to end of array after
|
||||
// sorting (if the object has one).
|
||||
if (target.__proto__) {
|
||||
properties.push("__proto__");
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes HTML strings so that <, > and quotation
|
||||
* characters are properly rendered in the list items.
|
||||
*/
|
||||
function escapeHTML(html) {
|
||||
return html.replace(/[&<>"'\/]/g, function (s) {
|
||||
return HTML_ESCAPE_CHARS[s];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the specified property is valid for
|
||||
* the target object.
|
||||
* NOTE: The `try`/`catch` is necessary to catch
|
||||
* exceptions for properties that cannot be directly
|
||||
* accessed.
|
||||
*/
|
||||
function isValidProperty(target, property) {
|
||||
try {
|
||||
return !!target[property];
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a function is native. Native functions
|
||||
* are filtered from the list items unless they are direct
|
||||
* members of the current target object. This is to
|
||||
* provide consistency with object/property inspection on
|
||||
* desktop DevTools.
|
||||
*/
|
||||
function isNativeFunction(value) {
|
||||
return typeof value === "function" && !! ~value.toString().indexOf("[native code]");
|
||||
}
|
||||
|
||||
try {
|
||||
document.registerElement("fxos-inspector", { prototype: proto });
|
||||
} catch (e) {
|
||||
if (e.name !== "NotSupportedError") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
})(window);
|
||||
});
|
||||
Loading…
Reference in a new issue