summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/components/splitter
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/components/splitter')
-rw-r--r--devtools/client/shared/components/splitter/Draggable.js106
-rw-r--r--devtools/client/shared/components/splitter/GridElementResizer.css32
-rw-r--r--devtools/client/shared/components/splitter/GridElementWidthResizer.js138
-rw-r--r--devtools/client/shared/components/splitter/SplitBox.css93
-rw-r--r--devtools/client/shared/components/splitter/SplitBox.js351
-rw-r--r--devtools/client/shared/components/splitter/moz.build11
6 files changed, 731 insertions, 0 deletions
diff --git a/devtools/client/shared/components/splitter/Draggable.js b/devtools/client/shared/components/splitter/Draggable.js
new file mode 100644
index 0000000000..3d18e49c34
--- /dev/null
+++ b/devtools/client/shared/components/splitter/Draggable.js
@@ -0,0 +1,106 @@
+/* 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 {
+ createRef,
+ Component,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+class Draggable extends Component {
+ static get propTypes() {
+ return {
+ onMove: PropTypes.func.isRequired,
+ onDoubleClick: PropTypes.func,
+ onStart: PropTypes.func,
+ onStop: PropTypes.func,
+ style: PropTypes.object,
+ title: PropTypes.string,
+ className: PropTypes.string,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.draggableEl = createRef();
+
+ this.startDragging = this.startDragging.bind(this);
+ this.stopDragging = this.stopDragging.bind(this);
+ this.onDoubleClick = this.onDoubleClick.bind(this);
+ this.onMove = this.onMove.bind(this);
+
+ this.mouseX = 0;
+ this.mouseY = 0;
+ }
+ startDragging(ev) {
+ const xDiff = Math.abs(this.mouseX - ev.clientX);
+ const yDiff = Math.abs(this.mouseY - ev.clientY);
+
+ // This allows for double-click.
+ if (this.props.onDoubleClick && xDiff + yDiff <= 1) {
+ return;
+ }
+ this.mouseX = ev.clientX;
+ this.mouseY = ev.clientY;
+
+ if (this.isDragging) {
+ return;
+ }
+ this.isDragging = true;
+ ev.preventDefault();
+
+ this.draggableEl.current.addEventListener("mousemove", this.onMove);
+ this.draggableEl.current.setPointerCapture(ev.pointerId);
+
+ this.props.onStart && this.props.onStart();
+ }
+
+ onDoubleClick() {
+ if (this.props.onDoubleClick) {
+ this.props.onDoubleClick();
+ }
+ }
+
+ onMove(ev) {
+ if (!this.isDragging) {
+ return;
+ }
+
+ ev.preventDefault();
+ // Use viewport coordinates so, moving mouse over iframes
+ // doesn't mangle (relative) coordinates.
+ this.props.onMove(ev.clientX, ev.clientY);
+ }
+
+ stopDragging(ev) {
+ if (!this.isDragging) {
+ return;
+ }
+ this.isDragging = false;
+ ev.preventDefault();
+
+ this.draggableEl.current.removeEventListener("mousemove", this.onMove);
+ this.draggableEl.current.releasePointerCapture(ev.pointerId);
+ this.props.onStop && this.props.onStop();
+ }
+
+ render() {
+ return dom.div({
+ ref: this.draggableEl,
+ role: "presentation",
+ style: this.props.style,
+ title: this.props.title,
+ className: this.props.className,
+ onMouseDown: this.startDragging,
+ onMouseUp: this.stopDragging,
+ onDoubleClick: this.onDoubleClick,
+ });
+ }
+}
+
+module.exports = Draggable;
diff --git a/devtools/client/shared/components/splitter/GridElementResizer.css b/devtools/client/shared/components/splitter/GridElementResizer.css
new file mode 100644
index 0000000000..dfa69592e9
--- /dev/null
+++ b/devtools/client/shared/components/splitter/GridElementResizer.css
@@ -0,0 +1,32 @@
+/* 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/. */
+
+.grid-element-width-resizer {
+ /* The space we'll have on each side of the "splitter border" */
+ --inline-inset: 3px;
+ /* We use the --inline-inset value that we multiply by 2 and add 1px to center the splitter */
+ width: calc(1px + (2 * var(--inline-inset)));
+ position: relative;
+ cursor: ew-resize;
+ z-index: 10;
+}
+
+.grid-element-width-resizer.start {
+ justify-self: start;
+ inset-inline-start: calc(-1 * var(--inline-inset));
+}
+
+.grid-element-width-resizer.end {
+ justify-self: end;
+ inset-inline-start: var(--inline-inset);
+}
+
+.dragging,
+.dragging * {
+ /* When resizing, we keep the "resize" cursor on every element we might hover */
+ cursor: ew-resize !important;
+ /* This prevents to trigger some :hover style and is better for performance
+ * when resizing */
+ pointer-events: none !important;
+}
diff --git a/devtools/client/shared/components/splitter/GridElementWidthResizer.js b/devtools/client/shared/components/splitter/GridElementWidthResizer.js
new file mode 100644
index 0000000000..c6ab6f3e14
--- /dev/null
+++ b/devtools/client/shared/components/splitter/GridElementWidthResizer.js
@@ -0,0 +1,138 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const Draggable = createFactory(
+ require("resource://devtools/client/shared/components/splitter/Draggable.js")
+);
+
+class GridElementWidthResizer extends Component {
+ static get propTypes() {
+ return {
+ getControlledElementNode: PropTypes.func.isRequired,
+ enabled: PropTypes.bool,
+ position: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ onResizeEnd: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.onStartMove = this.onStartMove.bind(this);
+ this.onStopMove = this.onStopMove.bind(this);
+ this.onMove = this.onMove.bind(this);
+ this.state = {
+ dragging: false,
+ isRTLElement: false,
+ defaultCursor: null,
+ defaultWidth: null,
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.enabled === true && this.props.enabled === false) {
+ this.onStopMove();
+ const controlledElementNode = this.props.getControlledElementNode();
+ controlledElementNode.style.width = this.state.defaultWidth;
+ }
+ }
+
+ // Dragging Events
+
+ /**
+ * Set 'resizing' cursor on entire document during splitter dragging.
+ * This avoids cursor-flickering that happens when the mouse leaves
+ * the splitter bar area (happens frequently).
+ */
+ onStartMove() {
+ const controlledElementNode = this.props.getControlledElementNode();
+ if (!controlledElementNode) {
+ return;
+ }
+
+ const doc = controlledElementNode.ownerDocument;
+ const defaultCursor = doc.documentElement.style.cursor;
+ const defaultWidth = doc.documentElement.style.width;
+ doc.documentElement.style.cursor = "ew-resize";
+ doc.firstElementChild.classList.add("dragging");
+
+ this.setState({
+ dragging: true,
+ isRTLElement:
+ controlledElementNode.ownerDocument.defaultView.getComputedStyle(
+ controlledElementNode
+ ).direction === "rtl",
+ defaultCursor,
+ defaultWidth,
+ });
+ }
+
+ onStopMove() {
+ const controlledElementNode = this.props.getControlledElementNode();
+ if (!this.state.dragging || !controlledElementNode) {
+ return;
+ }
+ const doc = controlledElementNode.ownerDocument;
+ doc.documentElement.style.cursor = this.state.defaultCursor;
+ doc.firstElementChild.classList.remove("dragging");
+
+ this.setState({
+ dragging: false,
+ });
+
+ if (this.props.onResizeEnd) {
+ const { width } = controlledElementNode.getBoundingClientRect();
+ this.props.onResizeEnd(width);
+ }
+ }
+
+ /**
+ * Adjust size of the controlled panel.
+ */
+ onMove(x) {
+ const controlledElementNode = this.props.getControlledElementNode();
+ if (!this.state.dragging || !controlledElementNode) {
+ return;
+ }
+ const nodeBounds = controlledElementNode.getBoundingClientRect();
+ const { isRTLElement } = this.state;
+ const { position } = this.props;
+
+ const size =
+ (isRTLElement && position === "end") ||
+ (!isRTLElement && position === "start")
+ ? nodeBounds.width + (nodeBounds.left - x)
+ : x - nodeBounds.left;
+
+ controlledElementNode.style.width = `${size}px`;
+ }
+
+ render() {
+ if (!this.props.enabled) {
+ return null;
+ }
+
+ const classNames = ["grid-element-width-resizer", this.props.position];
+ if (this.props.className) {
+ classNames.push(this.props.className);
+ }
+
+ return Draggable({
+ className: classNames.join(" "),
+ onStart: this.onStartMove,
+ onStop: this.onStopMove,
+ onMove: this.onMove,
+ });
+ }
+}
+
+module.exports = GridElementWidthResizer;
diff --git a/devtools/client/shared/components/splitter/SplitBox.css b/devtools/client/shared/components/splitter/SplitBox.css
new file mode 100644
index 0000000000..6028619e7b
--- /dev/null
+++ b/devtools/client/shared/components/splitter/SplitBox.css
@@ -0,0 +1,93 @@
+/* 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/. */
+
+.split-box {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ height: 100%;
+ width: 100%;
+}
+
+.split-box.vert {
+ flex-direction: row;
+}
+
+.split-box.horz {
+ flex-direction: column;
+}
+
+.split-box > .uncontrolled {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ overflow: auto;
+}
+
+.split-box > .controlled {
+ display: flex;
+ overflow: auto;
+}
+
+.split-box > .splitter {
+ background-image: none;
+ border: 0;
+ border-style: solid;
+ border-color: transparent;
+ background-color: var(--theme-splitter-color);
+ background-clip: content-box;
+ position: relative;
+
+ box-sizing: border-box;
+
+ /* Positive z-index positions the splitter on top of its siblings and makes
+ it clickable on both sides. */
+ z-index: 1;
+}
+
+.split-box.vert > .splitter {
+ min-width: var(--devtools-vertical-splitter-min-width);
+
+ border-inline-start-width: var(--devtools-splitter-inline-start-width);
+ border-inline-end-width: var(--devtools-splitter-inline-end-width);
+
+ margin-inline-start: calc(-1 * var(--devtools-splitter-inline-start-width) - 1px);
+ margin-inline-end: calc(-1 * var(--devtools-splitter-inline-end-width));
+
+ cursor: ew-resize;
+}
+
+.split-box.horz > .splitter {
+ /* Emphasize the horizontal splitter width and color */
+ min-height: var(--devtools-emphasized-horizontal-splitter-min-height);
+
+ background-color: var(--theme-emphasized-splitter-color);
+
+ border-top-width: var(--devtools-splitter-top-width);
+ border-bottom-width: var(--devtools-splitter-bottom-width);
+
+ margin-top: calc(-1 * var(--devtools-splitter-top-width) - 1px);
+ margin-bottom: calc(-1 * var(--devtools-splitter-bottom-width));
+
+ cursor: ns-resize;
+}
+
+/* Emphasized splitter has the hover style. */
+.split-box.horz > .splitter:hover {
+ background-color: var(--theme-emphasized-splitter-color-hover);
+}
+
+.split-box.disabled {
+ pointer-events: none;
+}
+
+/**
+ * Make sure splitter panels are not processing any mouse
+ * events. This is good for performance during splitter
+ * bar dragging.
+ */
+.split-box.dragging > .controlled,
+.split-box.dragging > .uncontrolled {
+ pointer-events: none;
+}
diff --git a/devtools/client/shared/components/splitter/SplitBox.js b/devtools/client/shared/components/splitter/SplitBox.js
new file mode 100644
index 0000000000..2bf0fdb74d
--- /dev/null
+++ b/devtools/client/shared/components/splitter/SplitBox.js
@@ -0,0 +1,351 @@
+/* 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 {
+ Component,
+ createFactory,
+} = require("resource://devtools/client/shared/vendor/react.js");
+const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
+const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
+
+const Draggable = createFactory(
+ require("resource://devtools/client/shared/components/splitter/Draggable.js")
+);
+
+/**
+ * This component represents a Splitter. The splitter supports vertical
+ * as well as horizontal mode.
+ */
+class SplitBox extends Component {
+ static get propTypes() {
+ return {
+ // Custom class name. You can use more names separated by a space.
+ className: PropTypes.string,
+ // Initial size of controlled panel.
+ initialSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Initial width of controlled panel.
+ initialWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Initial height of controlled panel.
+ initialHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Left/top panel
+ startPanel: PropTypes.any,
+ // Left/top panel collapse state.
+ startPanelCollapsed: PropTypes.bool,
+ // Min panel size.
+ minSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Max panel size.
+ maxSize: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ // Right/bottom panel
+ endPanel: PropTypes.any,
+ // Right/bottom panel collapse state.
+ endPanelCollapsed: PropTypes.bool,
+ // True if the right/bottom panel should be controlled.
+ endPanelControl: PropTypes.bool,
+ // Size of the splitter handle bar.
+ splitterSize: PropTypes.number,
+ // True if the splitter bar is vertical (default is vertical).
+ vert: PropTypes.bool,
+ // Style object.
+ style: PropTypes.object,
+ // Call when controlled panel was resized.
+ onControlledPanelResized: PropTypes.func,
+ // Optional callback when splitbox resize stops
+ onResizeEnd: PropTypes.func,
+ // Retrieve DOM reference to the start panel element
+ onSelectContainerElement: PropTypes.any,
+ };
+ }
+
+ static get defaultProps() {
+ return {
+ splitterSize: 5,
+ vert: true,
+ endPanelControl: false,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+
+ /**
+ * The state stores whether or not the end panel should be controlled, the current
+ * orientation (vertical or horizontal), the splitter size, and the current size
+ * (width/height). All these values can change during the component's life time.
+ */
+ this.state = {
+ // True if the right/bottom panel should be controlled.
+ endPanelControl: props.endPanelControl,
+ // True if the splitter bar is vertical (default is vertical).
+ vert: props.vert,
+ // Size of the splitter handle bar.
+ splitterSize: props.splitterSize,
+ // Width of controlled panel.
+ width: props.initialWidth || props.initialSize,
+ // Height of controlled panel.
+ height: props.initialHeight || props.initialSize,
+ };
+
+ this.onStartMove = this.onStartMove.bind(this);
+ this.onStopMove = this.onStopMove.bind(this);
+ this.onMove = this.onMove.bind(this);
+ }
+
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
+ UNSAFE_componentWillReceiveProps(nextProps) {
+ const { endPanelControl, splitterSize, vert } = nextProps;
+
+ if (endPanelControl != this.props.endPanelControl) {
+ this.setState({ endPanelControl });
+ }
+
+ if (splitterSize != this.props.splitterSize) {
+ this.setState({ splitterSize });
+ }
+
+ if (vert !== this.props.vert) {
+ this.setState({ vert });
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ return (
+ nextState.width != this.state.width ||
+ nextState.endPanelControl != this.props.endPanelControl ||
+ nextState.height != this.state.height ||
+ nextState.vert != this.state.vert ||
+ nextState.splitterSize != this.state.splitterSize ||
+ nextProps.startPanel != this.props.startPanel ||
+ nextProps.endPanel != this.props.endPanel ||
+ nextProps.minSize != this.props.minSize ||
+ nextProps.maxSize != this.props.maxSize
+ );
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (
+ this.props.onControlledPanelResized &&
+ (prevState.width !== this.state.width ||
+ prevState.height !== this.state.height)
+ ) {
+ this.props.onControlledPanelResized(this.state.width, this.state.height);
+ }
+ }
+
+ // Dragging Events
+
+ /**
+ * Set 'resizing' cursor on entire document during splitter dragging.
+ * This avoids cursor-flickering that happens when the mouse leaves
+ * the splitter bar area (happens frequently).
+ */
+ onStartMove() {
+ const doc = this.splitBox.ownerDocument;
+ const defaultCursor = doc.documentElement.style.cursor;
+ doc.documentElement.style.cursor = this.state.vert
+ ? "ew-resize"
+ : "ns-resize";
+
+ this.splitBox.classList.add("dragging");
+
+ this.setState({
+ defaultCursor,
+ });
+ }
+
+ onStopMove() {
+ const doc = this.splitBox.ownerDocument;
+ doc.documentElement.style.cursor = this.state.defaultCursor;
+
+ this.splitBox.classList.remove("dragging");
+
+ if (this.props.onResizeEnd) {
+ this.props.onResizeEnd(
+ this.state.vert ? this.state.width : this.state.height
+ );
+ }
+ }
+
+ /**
+ * Adjust size of the controlled panel. Depending on the current
+ * orientation we either remember the width or height of
+ * the splitter box.
+ */
+ onMove(x, y) {
+ const nodeBounds = this.splitBox.getBoundingClientRect();
+
+ let size;
+ let { endPanelControl, vert } = this.state;
+
+ if (vert) {
+ // Use the document owning the SplitBox to detect rtl. The global document might be
+ // the one bound to the toolbox shared BrowserRequire, which is irrelevant here.
+ const doc = this.splitBox.ownerDocument;
+
+ // Switch the control flag in case of RTL. Note that RTL
+ // has impact on vertical splitter only.
+ if (doc.dir === "rtl") {
+ endPanelControl = !endPanelControl;
+ }
+
+ size = endPanelControl
+ ? nodeBounds.left + nodeBounds.width - x
+ : x - nodeBounds.left;
+
+ this.setState({
+ width: this.getConstrainedSizeInPx(size, nodeBounds.width),
+ });
+ } else {
+ size = endPanelControl
+ ? nodeBounds.top + nodeBounds.height - y
+ : y - nodeBounds.top;
+
+ this.setState({
+ height: this.getConstrainedSizeInPx(size, nodeBounds.height),
+ });
+ }
+ }
+
+ /**
+ * Calculates the constrained size taking into account the minimum width or
+ * height passed via this.props.minSize.
+ *
+ * @param {Number} requestedSize
+ * The requested size
+ * @param {Number} splitBoxWidthOrHeight
+ * The width or height of the splitBox
+ *
+ * @return {Number}
+ * The constrained size
+ */
+ getConstrainedSizeInPx(requestedSize, splitBoxWidthOrHeight) {
+ let minSize = this.props.minSize + "";
+
+ if (minSize.endsWith("%")) {
+ minSize = (parseFloat(minSize) / 100) * splitBoxWidthOrHeight;
+ } else if (minSize.endsWith("px")) {
+ minSize = parseFloat(minSize);
+ }
+ return Math.max(requestedSize, minSize);
+ }
+
+ // Rendering
+
+ // eslint-disable-next-line complexity
+ render() {
+ const { endPanelControl, splitterSize, vert } = this.state;
+ const {
+ startPanel,
+ startPanelCollapsed,
+ endPanel,
+ endPanelCollapsed,
+ minSize,
+ maxSize,
+ onSelectContainerElement,
+ } = this.props;
+
+ const style = Object.assign(
+ {
+ // Set the size of the controlled panel (height or width depending on the
+ // current state). This can be used to help with styling of dependent
+ // panels.
+ "--split-box-controlled-panel-size": `${
+ vert ? this.state.width : this.state.height
+ }`,
+ },
+ this.props.style
+ );
+
+ // Calculate class names list.
+ let classNames = ["split-box"];
+ classNames.push(vert ? "vert" : "horz");
+ if (this.props.className) {
+ classNames = classNames.concat(this.props.className.split(" "));
+ }
+
+ let leftPanelStyle;
+ let rightPanelStyle;
+
+ // Set proper size for panels depending on the current state.
+ if (vert) {
+ leftPanelStyle = {
+ maxWidth: endPanelControl ? null : maxSize,
+ minWidth: endPanelControl ? null : minSize,
+ width: endPanelControl ? null : this.state.width,
+ };
+ rightPanelStyle = {
+ maxWidth: endPanelControl ? maxSize : null,
+ minWidth: endPanelControl ? minSize : null,
+ width: endPanelControl ? this.state.width : null,
+ };
+ } else {
+ leftPanelStyle = {
+ maxHeight: endPanelControl ? null : maxSize,
+ minHeight: endPanelControl ? null : minSize,
+ height: endPanelControl ? null : this.state.height,
+ };
+ rightPanelStyle = {
+ maxHeight: endPanelControl ? maxSize : null,
+ minHeight: endPanelControl ? minSize : null,
+ height: endPanelControl ? this.state.height : null,
+ };
+ }
+
+ // Calculate splitter size
+ const splitterStyle = {
+ flex: "0 0 " + splitterSize + "px",
+ };
+
+ return dom.div(
+ {
+ className: classNames.join(" "),
+ ref: div => {
+ this.splitBox = div;
+ },
+ style,
+ },
+ startPanel && !startPanelCollapsed
+ ? dom.div(
+ {
+ className: endPanelControl ? "uncontrolled" : "controlled",
+ style: leftPanelStyle,
+ role: "presentation",
+ ref: div => {
+ this.startPanelContainer = div;
+ if (onSelectContainerElement) {
+ onSelectContainerElement(div);
+ }
+ },
+ },
+ startPanel
+ )
+ : null,
+ splitterSize > 0
+ ? Draggable({
+ className: "splitter",
+ style: splitterStyle,
+ onStart: this.onStartMove,
+ onStop: this.onStopMove,
+ onMove: this.onMove,
+ })
+ : null,
+ endPanel && !endPanelCollapsed
+ ? dom.div(
+ {
+ className: endPanelControl ? "controlled" : "uncontrolled",
+ style: rightPanelStyle,
+ role: "presentation",
+ ref: div => {
+ this.endPanelContainer = div;
+ },
+ },
+ endPanel
+ )
+ : null
+ );
+ }
+}
+
+module.exports = SplitBox;
diff --git a/devtools/client/shared/components/splitter/moz.build b/devtools/client/shared/components/splitter/moz.build
new file mode 100644
index 0000000000..4abe762b34
--- /dev/null
+++ b/devtools/client/shared/components/splitter/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ "Draggable.js",
+ "GridElementWidthResizer.js",
+ "SplitBox.js",
+)