430 lines
12 KiB
JavaScript
430 lines
12 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
"use strict";
|
|
|
|
const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js");
|
|
|
|
const PANE_APPEARANCE_DELAY = 50;
|
|
|
|
var namedTimeoutsStore = new Map();
|
|
|
|
/**
|
|
* Helper for draining a rapid succession of events and invoking a callback
|
|
* once everything settles down.
|
|
*
|
|
* @param string id
|
|
* A string identifier for the named timeout.
|
|
* @param number wait
|
|
* The amount of milliseconds to wait after no more events are fired.
|
|
* @param function callback
|
|
* Invoked when no more events are fired after the specified time.
|
|
*/
|
|
const setNamedTimeout = function setNamedTimeout(id, wait, callback) {
|
|
clearNamedTimeout(id);
|
|
|
|
namedTimeoutsStore.set(
|
|
id,
|
|
setTimeout(() => namedTimeoutsStore.delete(id) && callback(), wait)
|
|
);
|
|
};
|
|
exports.setNamedTimeout = setNamedTimeout;
|
|
|
|
/**
|
|
* Clears a named timeout.
|
|
* @see setNamedTimeout
|
|
*
|
|
* @param string id
|
|
* A string identifier for the named timeout.
|
|
*/
|
|
const clearNamedTimeout = function clearNamedTimeout(id) {
|
|
if (!namedTimeoutsStore) {
|
|
return;
|
|
}
|
|
clearTimeout(namedTimeoutsStore.get(id));
|
|
namedTimeoutsStore.delete(id);
|
|
};
|
|
exports.clearNamedTimeout = clearNamedTimeout;
|
|
|
|
/**
|
|
* Helpers for creating and messaging between UI components.
|
|
*/
|
|
exports.ViewHelpers = {
|
|
/**
|
|
* Convenience method, dispatching a custom event.
|
|
*
|
|
* @param Node target
|
|
* A custom target element to dispatch the event from.
|
|
* @param string type
|
|
* The name of the event.
|
|
* @param any detail
|
|
* The data passed when initializing the event.
|
|
* @return boolean
|
|
* True if the event was cancelled or a registered handler
|
|
* called preventDefault.
|
|
*/
|
|
dispatchEvent(target, type, detail) {
|
|
if (!(target instanceof Node)) {
|
|
// Event cancelled.
|
|
return true;
|
|
}
|
|
const document = target.ownerDocument || target;
|
|
const dispatcher = target.ownerDocument ? target : document.documentElement;
|
|
|
|
const event = document.createEvent("CustomEvent");
|
|
event.initCustomEvent(type, true, true, detail);
|
|
return dispatcher.dispatchEvent(event);
|
|
},
|
|
|
|
/**
|
|
* Helper delegating some of the DOM attribute methods of a node to a widget.
|
|
*
|
|
* @param object widget
|
|
* The widget to assign the methods to.
|
|
* @param Node node
|
|
* A node to delegate the methods to.
|
|
*/
|
|
delegateWidgetAttributeMethods(widget, node) {
|
|
widget.getAttribute = widget.getAttribute || node.getAttribute.bind(node);
|
|
widget.setAttribute = widget.setAttribute || node.setAttribute.bind(node);
|
|
widget.removeAttribute =
|
|
widget.removeAttribute || node.removeAttribute.bind(node);
|
|
},
|
|
|
|
/**
|
|
* Helper delegating some of the DOM event methods of a node to a widget.
|
|
*
|
|
* @param object widget
|
|
* The widget to assign the methods to.
|
|
* @param Node node
|
|
* A node to delegate the methods to.
|
|
*/
|
|
delegateWidgetEventMethods(widget, node) {
|
|
widget.addEventListener =
|
|
widget.addEventListener || node.addEventListener.bind(node);
|
|
widget.removeEventListener =
|
|
widget.removeEventListener || node.removeEventListener.bind(node);
|
|
},
|
|
|
|
/**
|
|
* Checks if the specified object looks like it's been decorated by an
|
|
* event emitter.
|
|
*
|
|
* @return boolean
|
|
* True if it looks, walks and quacks like an event emitter.
|
|
*/
|
|
isEventEmitter(object) {
|
|
return object?.on && object?.off && object?.once && object?.emit;
|
|
},
|
|
|
|
/**
|
|
* Checks if the specified object is an instance of a DOM node.
|
|
*
|
|
* @return boolean
|
|
* True if it's a node, false otherwise.
|
|
*/
|
|
isNode(object) {
|
|
return (
|
|
object instanceof Node ||
|
|
object instanceof Element ||
|
|
Cu.getClassName(object) == "DocumentFragment"
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Prevents event propagation when navigation keys are pressed.
|
|
*
|
|
* @param Event e
|
|
* The event to be prevented.
|
|
*/
|
|
preventScrolling(e) {
|
|
switch (e.keyCode) {
|
|
case KeyCodes.DOM_VK_UP:
|
|
case KeyCodes.DOM_VK_DOWN:
|
|
case KeyCodes.DOM_VK_LEFT:
|
|
case KeyCodes.DOM_VK_RIGHT:
|
|
case KeyCodes.DOM_VK_PAGE_UP:
|
|
case KeyCodes.DOM_VK_PAGE_DOWN:
|
|
case KeyCodes.DOM_VK_HOME:
|
|
case KeyCodes.DOM_VK_END:
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if the enter key or space was pressed
|
|
*
|
|
* @param event event
|
|
* The event triggered by a keydown or keypress on an element
|
|
*/
|
|
isSpaceOrReturn(event) {
|
|
return (
|
|
event.keyCode === KeyCodes.DOM_VK_SPACE ||
|
|
event.keyCode === KeyCodes.DOM_VK_RETURN
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Sets a toggled pane hidden or visible. The pane can either be displayed on
|
|
* the side (right or left depending on the locale) or at the bottom.
|
|
*
|
|
* @param object flags
|
|
* An object containing some of the following properties:
|
|
* - visible: true if the pane should be shown, false to hide
|
|
* - animated: true to display an animation on toggle
|
|
* - delayed: true to wait a few cycles before toggle
|
|
* - callback: a function to invoke when the toggle finishes
|
|
* @param Node pane
|
|
* The element representing the pane to toggle.
|
|
*/
|
|
togglePane(flags, pane) {
|
|
// Make sure a pane is actually available first.
|
|
if (!pane) {
|
|
return;
|
|
}
|
|
|
|
// Hiding is always handled via margins, not the hidden attribute.
|
|
pane.removeAttribute("hidden");
|
|
|
|
// Add a class to the pane to handle min-widths, margins and animations.
|
|
pane.classList.add("generic-toggled-pane");
|
|
|
|
// Avoid toggles in the middle of animation.
|
|
if (pane.hasAttribute("animated")) {
|
|
return;
|
|
}
|
|
|
|
// Avoid useless toggles.
|
|
if (flags.visible == !pane.classList.contains("pane-collapsed")) {
|
|
if (flags.callback) {
|
|
flags.callback();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// The "animated" attributes enables animated toggles (slide in-out).
|
|
if (flags.animated) {
|
|
pane.setAttribute("animated", "");
|
|
} else {
|
|
pane.removeAttribute("animated");
|
|
}
|
|
|
|
// Computes and sets the pane margins in order to hide or show it.
|
|
const doToggle = () => {
|
|
// Negative margins are applied to "right" and "left" to support RTL and
|
|
// LTR directions, as well as to "bottom" to support vertical layouts.
|
|
// Unnecessary negative margins are forced to 0 via CSS in widgets.css.
|
|
if (flags.visible) {
|
|
pane.style.marginLeft = "0";
|
|
pane.style.marginRight = "0";
|
|
pane.style.marginBottom = "0";
|
|
pane.classList.remove("pane-collapsed");
|
|
} else {
|
|
const width = Math.floor(pane.getAttribute("width")) + 1;
|
|
const height = Math.floor(pane.getAttribute("height")) + 1;
|
|
pane.style.marginLeft = -width + "px";
|
|
pane.style.marginRight = -width + "px";
|
|
pane.style.marginBottom = -height + "px";
|
|
}
|
|
|
|
// Wait for the animation to end before calling afterToggle()
|
|
if (flags.animated) {
|
|
const options = {
|
|
useCapture: false,
|
|
once: true,
|
|
};
|
|
|
|
pane.addEventListener(
|
|
"transitionend",
|
|
() => {
|
|
// Prevent unwanted transitions: if the panel is hidden and the layout
|
|
// changes margins will be updated and the panel will pop out.
|
|
pane.removeAttribute("animated");
|
|
|
|
if (!flags.visible) {
|
|
pane.classList.add("pane-collapsed");
|
|
}
|
|
if (flags.callback) {
|
|
flags.callback();
|
|
}
|
|
},
|
|
options
|
|
);
|
|
} else {
|
|
if (!flags.visible) {
|
|
pane.classList.add("pane-collapsed");
|
|
}
|
|
|
|
// Invoke the callback immediately since there's no transition.
|
|
if (flags.callback) {
|
|
flags.callback();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Sometimes it's useful delaying the toggle a few ticks to ensure
|
|
// a smoother slide in-out animation.
|
|
if (flags.delayed) {
|
|
pane.ownerDocument.defaultView.setTimeout(
|
|
doToggle,
|
|
PANE_APPEARANCE_DELAY
|
|
);
|
|
} else {
|
|
doToggle();
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* A generic Item is used to describe children present in a Widget.
|
|
*
|
|
* This is basically a very thin wrapper around a Node, with a few
|
|
* characteristics, like a `value` and an `attachment`.
|
|
*
|
|
* The characteristics are optional, and their meaning is entirely up to you.
|
|
* - The `value` should be a string, passed as an argument.
|
|
* - The `attachment` is any kind of primitive or object, passed as an argument.
|
|
*
|
|
* Iterable via "for (let childItem of parentItem) { }".
|
|
*
|
|
* @param object ownerView
|
|
* The owner view creating this item.
|
|
* @param Node element
|
|
* A prebuilt node to be wrapped.
|
|
* @param string value
|
|
* A string identifying the node.
|
|
* @param any attachment
|
|
* Some attached primitive/object.
|
|
*/
|
|
function Item(ownerView, element, value, attachment) {
|
|
this.ownerView = ownerView;
|
|
this.attachment = attachment;
|
|
this._value = value + "";
|
|
this._prebuiltNode = element;
|
|
this._itemsByElement = new Map();
|
|
}
|
|
|
|
Item.prototype = {
|
|
get value() {
|
|
return this._value;
|
|
},
|
|
get target() {
|
|
return this._target;
|
|
},
|
|
get prebuiltNode() {
|
|
return this._prebuiltNode;
|
|
},
|
|
|
|
/**
|
|
* Immediately appends a child item to this item.
|
|
*
|
|
* @param Node element
|
|
* A Node representing the child element to append.
|
|
* @param object options [optional]
|
|
* Additional options or flags supported by this operation:
|
|
* - attachment: some attached primitive/object for the item
|
|
* - attributes: a batch of attributes set to the displayed element
|
|
* - finalize: function invoked when the child item is removed
|
|
* @return Item
|
|
* The item associated with the displayed element.
|
|
*/
|
|
append(element, options = {}) {
|
|
const item = new Item(this, element, "", options.attachment);
|
|
|
|
// Entangle the item with the newly inserted child node.
|
|
// Make sure this is done with the value returned by appendChild(),
|
|
// to avoid storing a potential DocumentFragment.
|
|
this._entangleItem(item, this._target.appendChild(element));
|
|
|
|
// Handle any additional options after entangling the item.
|
|
if (options.attributes) {
|
|
options.attributes.forEach(e => item._target.setAttribute(e[0], e[1]));
|
|
}
|
|
if (options.finalize) {
|
|
item.finalize = options.finalize;
|
|
}
|
|
|
|
// Return the item associated with the displayed element.
|
|
return item;
|
|
},
|
|
|
|
/**
|
|
* Immediately removes the specified child item from this item.
|
|
*
|
|
* @param Item item
|
|
* The item associated with the element to remove.
|
|
*/
|
|
remove(item) {
|
|
if (!item) {
|
|
return;
|
|
}
|
|
this._target.removeChild(item._target);
|
|
this._untangleItem(item);
|
|
},
|
|
|
|
/**
|
|
* Entangles an item (model) with a displayed node element (view).
|
|
*
|
|
* @param Item item
|
|
* The item describing a target element.
|
|
* @param Node element
|
|
* The element displaying the item.
|
|
*/
|
|
_entangleItem(item, element) {
|
|
this._itemsByElement.set(element, item);
|
|
item._target = element;
|
|
},
|
|
|
|
/**
|
|
* Untangles an item (model) from a displayed node element (view).
|
|
*
|
|
* @param Item item
|
|
* The item describing a target element.
|
|
*/
|
|
_untangleItem(item) {
|
|
if (item.finalize) {
|
|
item.finalize(item);
|
|
}
|
|
for (const childItem of item) {
|
|
item.remove(childItem);
|
|
}
|
|
|
|
this._unlinkItem(item);
|
|
item._target = null;
|
|
},
|
|
|
|
/**
|
|
* Deletes an item from the its parent's storage maps.
|
|
*
|
|
* @param Item item
|
|
* The item describing a target element.
|
|
*/
|
|
_unlinkItem(item) {
|
|
this._itemsByElement.delete(item._target);
|
|
},
|
|
|
|
/**
|
|
* Returns a string representing the object.
|
|
* Avoid using `toString` to avoid accidental JSONification.
|
|
* @return string
|
|
*/
|
|
stringify() {
|
|
return JSON.stringify(
|
|
{
|
|
value: this._value,
|
|
target: this._target + "",
|
|
prebuiltNode: this._prebuiltNode + "",
|
|
attachment: this.attachment,
|
|
},
|
|
null,
|
|
2
|
|
);
|
|
},
|
|
|
|
_value: "",
|
|
_target: null,
|
|
_prebuiltNode: null,
|
|
finalize: null,
|
|
attachment: null,
|
|
};
|