diff --git a/public/js/components/Sources.js b/public/js/components/Sources.js index 633bb16666..39bedceaeb 100644 --- a/public/js/components/Sources.js +++ b/public/js/components/Sources.js @@ -1,6 +1,7 @@ "use strict"; const React = require("react"); +const { DOM: dom, PropTypes } = React; const { bindActionCreators } = require("redux"); const { connect } = require("react-redux"); const classnames = require("classnames"); @@ -9,126 +10,14 @@ const ManagedTree = React.createFactory(require("./util/ManagedTree")); const { Set } = require("immutable"); const actions = require("../actions"); const { getSelectedSource, getSources } = require("../selectors"); -const { DOM: dom, PropTypes } = React; +const { + createNode, nodeHasChildren, nodeName, + nodeContents, nodePath, createParentMap, + addToTree +} = require("../util/sources-tree.js"); require("./Sources.css"); -function nodeHasChildren(item) { - return item[2] instanceof Array; -} - -function nodeName(item) { - return item[0]; -} - -function nodePath(item) { - return item[1]; -} - -function nodeContents(item) { - return item[2]; -} - -function setNodeContents(item, contents) { - item[2] = contents; -} - -function createNode(name, path, contents) { - return [name, path, contents]; -} - -function createParentMap(tree) { - const map = new WeakMap(); - - function _traverse(subtree) { - if (nodeHasChildren(subtree)) { - for (let child of nodeContents(subtree)) { - map.set(child, subtree); - _traverse(child); - } - } - } - - // Don't link each top-level path to the "root" node because the - // user never sees the root - nodeContents(tree).forEach(_traverse); - return map; -} - -function getURL(source) { - try { - if (!source.get("url")) { - return null; - } - - const url = new URL(source.get("url")); - - // Filter out things like `javascript:` URLs for now. - // Whitelist the protocols because there may be several strange - // ones. - if (url.protocol !== "http:" && url.protocol !== "https:") { - return null; - } - - return url; - } catch (e) { - // If there is a parse error (which may happen with various - // internal script that don't have a correct URL), just ignore it. - return null; - } -} - -function addToTree(tree, source) { - const url = getURL(source); - if (!url) { - return; - } - - const parts = url.pathname.split("/").filter(p => p !== ""); - const isDir = (parts.length === 0 || - parts[parts.length - 1].indexOf(".") === -1); - parts.unshift(url.host); - - let path = ""; - let subtree = tree; - - for (let part of parts) { - const subpaths = nodeContents(subtree); - // We want to sort alphabetically, so find the index where we - // should insert this part. - let idx = subpaths.findIndex(subpath => { - return nodeName(subpath).localeCompare(part) >= 0; - }); - - // The node always acts like one with children, but the code below - // this loop will set the contents of the final node to the source - // object. - const pathItem = createNode(part, path + "/" + part, []); - - if (idx >= 0 && nodeName(subpaths[idx]) === part) { - subtree = subpaths[idx]; - } else { - // Add a new one - const where = idx === -1 ? subpaths.length : idx; - subpaths.splice(where, 0, pathItem); - subtree = subpaths[where]; - } - - // Keep track of the subpaths so we can tag each node with them. - path = path + "/" + part; - } - - // Store the soure in the final created node. - if (isDir) { - setNodeContents( - subtree, - [createNode("(index)", source.get("url"), source)] - ); - } else { - setNodeContents(subtree, source); - } -} - // This is inline because it's much faster. We need to revisit how we // load SVGs, at least for components that render them several times. let Arrow = (props) => { @@ -194,6 +83,31 @@ let SourcesTree = React.createClass({ } }, + renderItem(item, depth, focused, _, expanded, { setExpanded }) { + const arrow = Arrow({ + className: classnames( + "arrow", + { expanded: expanded, + hidden: !nodeHasChildren(item) } + ), + onClick: e => { + e.stopPropagation(); + setExpanded(item, !expanded); + } + }); + + return dom.div( + { className: classnames("node", { focused }), + style: { marginLeft: depth * 15 + "px" }, + onClick: () => this.selectItem(item), + onDoubleClick: e => { + setExpanded(item, !expanded); + } }, + arrow, + nodeName(item) + ); + }, + render() { const { focusedItem, sourceTree, parentMap } = this.state; @@ -212,28 +126,7 @@ let SourcesTree = React.createClass({ itemHeight: 30, autoExpandDepth: 2, onFocus: this.focusItem, - renderItem: (item, depth, focused, _, expanded, { setExpanded }) => { - const arrow = Arrow({ - className: classnames("arrow", - { expanded: expanded, - hidden: !nodeHasChildren(item) }), - onClick: e => { - e.stopPropagation(); - setExpanded(item, !expanded); - } - }); - - return dom.div( - { className: classnames("node", { focused }), - style: { marginLeft: depth * 15 + "px" }, - onClick: () => this.selectItem(item), - onDoubleClick: e => { - setExpanded(item, !expanded); - } }, - arrow, - nodeName(item) - ); - } + renderItem: this.renderItem }); return dom.div({ diff --git a/public/js/main.js b/public/js/main.js index dbde5b86b0..b12ca2802a 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -30,9 +30,7 @@ connectClient(response => { // otherwise, just show the toolbox. if (hasSelectedTab()) { const selectedTab = getSelectedTab(store.getState().tabs.get("tabs")); - debugTab(selectedTab.toJS(), actions).then(() => { - setTimeout(renderToolbox, 0); - }); + debugTab(selectedTab.toJS(), actions).then(renderToolbox); } else { renderToolbox(); } diff --git a/public/js/test/.eslintrc b/public/js/test/.eslintrc new file mode 100644 index 0000000000..e39b7d0071 --- /dev/null +++ b/public/js/test/.eslintrc @@ -0,0 +1,32 @@ +{ + "globals": { + "after": true, + "afterEach": true, + "before": true, + "beforeEach": true, + "context": true, + "describe": true, + "it": true, + "mocha": true, + "setup": true, + "specify": true, + "suite": true, + "suiteSetup": true, + "suiteTeardown": true, + "teardown": true, + "test": true, + "xcontext": true, + "xdescribe": true, + "xit": true, + "xspecify": true, + "assert": true, + "expect": true, + "equal": true, + "ok": true + }, + "rules": { + "camelcase": 0, + "no-unused-vars": [2, {"vars": "all", "args": "none", "varsIgnorePattern": "run_test"}], + "max-nested-callbacks": [2, 4], + } +} diff --git a/public/js/test/sources-tree.js b/public/js/test/sources-tree.js new file mode 100644 index 0000000000..191e846c49 --- /dev/null +++ b/public/js/test/sources-tree.js @@ -0,0 +1,79 @@ +"use strict"; + +const expect = require("expect.js"); +const { Map } = require("immutable"); +const { + createNode, nodeHasChildren, nodeName, + nodeContents, nodePath, addToTree +} = require("../util/sources-tree.js"); + +describe("sources-tree", () => { + it("should provide node API", () => { + const root = createNode("root", "", [createNode("foo", "/foo")]); + expect(nodeName(root)).to.be("root"); + expect(nodeHasChildren(root)).to.be(true); + expect(nodeContents(root).length).to.be(1); + + const child = nodeContents(root)[0]; + expect(nodeName(child)).to.be("foo"); + expect(nodePath(child)).to.be("/foo"); + expect(nodeContents(child)).to.be(null); + expect(nodeHasChildren(child)).to.be(false); + }); + + it("builds a path-based tree", () => { + const source1 = Map({ + url: "http://example.com/foo/source1.js", + actor: "actor1" + }); + const tree = createNode("root", "", []); + + addToTree(tree, source1); + expect(nodeContents(tree).length).to.be(1); + + let base = nodeContents(tree)[0]; + expect(nodeName(base)).to.be("example.com"); + expect(nodeContents(base).length).to.be(1); + + let fooNode = nodeContents(base)[0]; + expect(nodeName(fooNode)).to.be("foo"); + expect(nodeContents(fooNode).length).to.be(1); + + let source1Node = nodeContents(fooNode)[0]; + expect(nodeName(source1Node)).to.be("source1.js"); + }); + + it("alphabetically sorts children", () => { + const source1 = Map({ + url: "http://example.com/source1.js", + actor: "actor1" + }); + const source2 = Map({ + url: "http://example.com/foo/b_source2.js", + actor: "actor2" + }); + const source3 = Map({ + url: "http://example.com/foo/a_source3.js", + actor: "actor3" + }); + const tree = createNode("root", "", []); + + addToTree(tree, source1); + addToTree(tree, source2); + addToTree(tree, source3); + + let base = nodeContents(tree)[0]; + let fooNode = nodeContents(base)[0]; + expect(nodeName(fooNode)).to.be("foo"); + expect(nodeContents(fooNode).length).to.be(2); + + let source1Node = nodeContents(base)[1]; + expect(nodeName(source1Node)).to.be("source1.js"); + + // source2 should be after source1 alphabetically + let source2Node = nodeContents(fooNode)[1]; + let source3Node = nodeContents(fooNode)[0]; + expect(nodeName(source2Node)).to.be("b_source2.js"); + expect(nodeName(source3Node)).to.be("a_source3.js"); + }); +}); diff --git a/public/js/util/sources-tree.js b/public/js/util/sources-tree.js new file mode 100644 index 0000000000..bd83ec138d --- /dev/null +++ b/public/js/util/sources-tree.js @@ -0,0 +1,131 @@ +"use strict"; + +const URL = require("url").parse; + +function nodeHasChildren(item) { + return item[2] instanceof Array; +} + +function nodeName(item) { + return item[0]; +} + +function nodePath(item) { + return item[1]; +} + +function nodeContents(item) { + return item[2]; +} + +function setNodeContents(item, contents) { + item[2] = contents; +} + +function createNode(name, path, contents) { + return [name, path, contents || null]; +} + +function createParentMap(tree) { + const map = new WeakMap(); + + function _traverse(subtree) { + if (nodeHasChildren(subtree)) { + for (let child of nodeContents(subtree)) { + map.set(child, subtree); + _traverse(child); + } + } + } + + // Don't link each top-level path to the "root" node because the + // user never sees the root + nodeContents(tree).forEach(_traverse); + return map; +} + +function getURL(source) { + if (!source.get("url")) { + return null; + } + + let url; + try { + url = new URL(source.get("url")); + } catch (e) { + // If there is a parse error (which may happen with various + // internal script that don't have a correct URL), just ignore it. + return null; + } + + // Filter out things like `javascript:` URLs for now. + // Whitelist the protocols because there may be several strange + // ones. + if (url.protocol !== "http:" && url.protocol !== "https:") { + return null; + } + + return url; +} + +function addToTree(tree, source) { + const url = getURL(source); + if (!url) { + return; + } + + const parts = url.pathname.split("/").filter(p => p !== ""); + const isDir = (parts.length === 0 || + parts[parts.length - 1].indexOf(".") === -1); + parts.unshift(url.host); + + let path = ""; + let subtree = tree; + + for (let part of parts) { + const subpaths = nodeContents(subtree); + // We want to sort alphabetically, so find the index where we + // should insert this part. + let idx = subpaths.findIndex(subpath => { + return nodeName(subpath).localeCompare(part) >= 0; + }); + + // The node always acts like one with children, but the code below + // this loop will set the contents of the final node to the source + // object. + const pathItem = createNode(part, path + "/" + part, []); + + if (idx >= 0 && nodeName(subpaths[idx]) === part) { + subtree = subpaths[idx]; + } else { + // Add a new one + const where = idx === -1 ? subpaths.length : idx; + subpaths.splice(where, 0, pathItem); + subtree = subpaths[where]; + } + + // Keep track of the subpaths so we can tag each node with them. + path = path + "/" + part; + } + + // Store the soure in the final created node. + if (isDir) { + setNodeContents( + subtree, + [createNode("(index)", source.get("url"), source)] + ); + } else { + setNodeContents(subtree, source); + } +} + +module.exports = { + nodeHasChildren, + nodeName, + nodePath, + nodeContents, + setNodeContents, + createNode, + createParentMap, + addToTree +};