diff --git a/package.json b/package.json
index b48200a55..3108a4815 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,8 @@
"cli-cursor": "^2.1.0",
"cli-truncate": "^1.1.0",
"is-ci": "^2.0.0",
+ "jsdom": "^15.1.1",
+ "keycode": "^2.2.0",
"lodash.throttle": "^4.1.1",
"log-update": "^3.0.0",
"prop-types": "^15.6.2",
@@ -72,6 +74,7 @@
"eslint-plugin-react": "^7.11.1",
"eslint-plugin-react-hooks": "^1.4.0",
"import-jsx": "^1.3.0",
+ "inspect-process": "^0.5.0",
"ms": "^2.1.1",
"node-pty": "^0.8.1",
"p-queue": "^3.0.0",
@@ -82,7 +85,8 @@
"xo": "^0.24.0"
},
"peerDependencies": {
- "react": ">=16.8.0"
+ "react": ">=16.8.0",
+ "react-dom": "^16.8.6"
},
"babel": {
"plugins": [
diff --git a/src/build-layout.js b/src/build-layout.js
index e9f3eb1cd..c13f1c094 100644
--- a/src/build-layout.js
+++ b/src/build-layout.js
@@ -3,7 +3,7 @@ import applyStyles from './apply-styles';
import measureText from './measure-text';
// Traverse the node tree, create Yoga nodes and assign styles to each Yoga node
-const buildLayout = (node, options) => {
+const buildLayout = (documentHelpers, node, options) => {
const {config, terminalWidth, skipStaticElements} = options;
const yogaNode = Yoga.Node.create(config);
node.yogaNode = yogaNode;
@@ -15,13 +15,15 @@ const buildLayout = (node, options) => {
// `terminalWidth` can be `undefined` if env isn't a TTY
yogaNode.setWidth(terminalWidth || 100);
- if (node.childNodes.length > 0) {
- const childNodes = node.childNodes.filter(childNode => {
+ const childNodes1 = documentHelpers.getChildNodes(node);
+
+ if (childNodes1.length > 0) {
+ const childNodes = childNodes1.filter(childNode => {
return skipStaticElements ? !childNode.unstable__static : true;
});
for (const [index, childNode] of Object.entries(childNodes)) {
- const childYogaNode = buildLayout(childNode, options).yogaNode;
+ const childYogaNode = buildLayout(documentHelpers, childNode, options).yogaNode;
yogaNode.insertChild(childYogaNode, index);
}
}
@@ -33,21 +35,24 @@ const buildLayout = (node, options) => {
applyStyles(yogaNode, style);
// Nodes with only text have a child Yoga node dedicated for that text
- if (node.textContent || node.nodeValue) {
- const {width, height} = measureText(node.textContent || node.nodeValue);
+ const textContent = documentHelpers.getTextContent(node);
+ if (textContent || node.nodeValue) {
+ const {width, height} = measureText(textContent || node.nodeValue);
yogaNode.setWidth(style.width || width);
yogaNode.setHeight(style.height || height);
return node;
}
- if (Array.isArray(node.childNodes) && node.childNodes.length > 0) {
- const childNodes = node.childNodes.filter(childNode => {
+ const childNodes1 = documentHelpers.getChildNodes(node);
+
+ if (Array.isArray(childNodes1) && childNodes1.length > 0) {
+ const childNodes = childNodes1.filter(childNode => {
return skipStaticElements ? !childNode.unstable__static : true;
});
for (const [index, childNode] of Object.entries(childNodes)) {
- const {yogaNode: childYogaNode} = buildLayout(childNode, options);
+ const {yogaNode: childYogaNode} = buildLayout(documentHelpers, childNode, options);
yogaNode.insertChild(childYogaNode, index);
}
}
diff --git a/src/components/App.js b/src/components/App.js
index 6bec457a5..7d7e779f2 100644
--- a/src/components/App.js
+++ b/src/components/App.js
@@ -2,10 +2,83 @@ import readline from 'readline';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import cliCursor from 'cli-cursor';
+import {default as keycode} from 'keycode';
import AppContext from './AppContext';
import StdinContext from './StdinContext';
import StdoutContext from './StdoutContext';
+class DOMKeypressDispatcher extends PureComponent {
+ static propTypes = {
+ stdin: PropTypes.object.isRequired,
+ setRawMode: PropTypes.func.isRequired,
+ document: PropTypes.any,
+ window: PropTypes.any
+ };
+
+ componentDidMount() {
+ if (this.props.document) {
+ const {stdin, setRawMode} = this.props;
+ setRawMode(true);
+ stdin.on('keypress', this.dispatchInput);
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.props.document) {
+ const {stdin, setRawMode} = this.props;
+ stdin.removeListener('keypress', this.dispatchInput);
+ setRawMode(false);
+ }
+ }
+
+ render() {
+ return (null);
+ }
+
+ dispatchInput = (str, key) => {
+ const code = keycode(key.name);
+ const downEvent = new this.props.window.KeyboardEvent('keydown', {
+ key: key.name,
+ charCode: code,
+ ctrlKey: key.ctrl,
+ shiftKey: key.shift,
+ keyCode: code,
+ which: code,
+ bubbles: true,
+ repeat: false,
+ location: 0,
+ isComposing: false
+ });
+ this.props.document.activeElement.dispatchEvent(downEvent);
+ const pressEvent = new this.props.window.KeyboardEvent('keypress', {
+ key: key.name,
+ charCode: code,
+ ctrlKey: key.ctrl,
+ shiftKey: key.shift,
+ keyCode: code,
+ which: code,
+ bubbles: true,
+ repeat: false,
+ location: 0,
+ isComposing: false
+ });
+ this.props.document.activeElement.dispatchEvent(pressEvent);
+ const upEvent = new this.props.window.KeyboardEvent('keyup', {
+ key: key.name,
+ charCode: code,
+ ctrlKey: key.ctrl,
+ shiftKey: key.shift,
+ keyCode: code,
+ which: code,
+ bubbles: true,
+ repeat: false,
+ location: 0,
+ isComposing: false
+ });
+ this.props.document.activeElement.dispatchEvent(upEvent);
+ }
+}
+
// Root component for all Ink apps
// It renders stdin and stdout contexts, so that children can access them if needed
// It also handles Ctrl+C exiting and cursor visibility
@@ -15,7 +88,9 @@ export default class App extends PureComponent {
stdin: PropTypes.object.isRequired,
stdout: PropTypes.object.isRequired,
exitOnCtrlC: PropTypes.bool.isRequired,
- onExit: PropTypes.func.isRequired
+ onExit: PropTypes.func.isRequired,
+ window: PropTypes.object,
+ document: PropTypes.object
};
// Determines if TTY is supported on the provided stdin
@@ -32,6 +107,14 @@ export default class App extends PureComponent {
}
render() {
+ const keyboardEventDispatcher = (this.props.window && this.props.document) ? (
+
+ ) : null;
return (
+ {keyboardEventDispatcher}
{this.props.children}
diff --git a/src/dom.js b/src/dom.js
index eb24e9752..7f59f6b52 100644
--- a/src/dom.js
+++ b/src/dom.js
@@ -1,5 +1,54 @@
// Helper utilities implementing some common DOM methods to simplify reconciliation code
-export const createNode = tagName => ({
+const _documentCreateNode = (document, tagName) => {
+ return document.createElement(tagName);
+};
+
+const _documentAppendChildNode = (node, childNode) => {
+ if (childNode.parentNode) {
+ childNode.parentNode.removeChild(childNode);
+ }
+
+ node.append(childNode);
+}; // Same as `appendChildNode`, but without removing child node from parent node
+
+const _documentAppendStaticNode = (node, childNode) => {
+ node.append(childNode);
+};
+
+const _documentInsertBeforeNode = (node, newChildNode, beforeChildNode) => {
+ if (newChildNode.parentNode) {
+ newChildNode.parentNode.removeChild(newChildNode);
+ }
+
+ node.insertBefore(newChildNode, beforeChildNode);
+};
+
+const _documentRemoveChildNode = (node, removeNode) => {
+ node.removeChild(removeNode);
+};
+
+const _documentSetAttribute = (node, key, value) => {
+ node.setAttribute(key, value);
+};
+
+const _documentCreateTextNode = (document, text) => {
+ return document.createTextNode(text);
+};
+
+const _documentGetChildNodes = node => {
+ return [...node.childNodes];
+};
+
+const _documentGetTextContent = node => {
+ if (node.nodeType === 3) {
+ return node.data;
+ }
+
+ return null;
+};
+
+// Helper utilities implementing some common DOM methods to simplify reconciliation code
+const _createNode = tagName => ({
nodeName: tagName.toUpperCase(),
style: {},
attributes: {},
@@ -7,9 +56,9 @@ export const createNode = tagName => ({
parentNode: null
});
-export const appendChildNode = (node, childNode) => {
+const _appendChildNode = (node, childNode) => {
if (childNode.parentNode) {
- removeChildNode(childNode.parentNode, childNode);
+ _removeChildNode(childNode.parentNode, childNode);
}
childNode.parentNode = node;
@@ -18,13 +67,13 @@ export const appendChildNode = (node, childNode) => {
};
// Same as `appendChildNode`, but without removing child node from parent node
-export const appendStaticNode = (node, childNode) => {
+const _appendStaticNode = (node, childNode) => {
node.childNodes.push(childNode);
};
-export const insertBeforeNode = (node, newChildNode, beforeChildNode) => {
+const _insertBeforeNode = (node, newChildNode, beforeChildNode) => {
if (newChildNode.parentNode) {
- removeChildNode(newChildNode.parentNode, newChildNode);
+ _removeChildNode(newChildNode.parentNode, newChildNode);
}
newChildNode.parentNode = node;
@@ -38,7 +87,7 @@ export const insertBeforeNode = (node, newChildNode, beforeChildNode) => {
node.childNodes.push(newChildNode);
};
-export const removeChildNode = (node, removeNode) => {
+const _removeChildNode = (node, removeNode) => {
removeNode.parentNode = null;
const index = node.childNodes.indexOf(removeNode);
@@ -47,11 +96,47 @@ export const removeChildNode = (node, removeNode) => {
}
};
-export const setAttribute = (node, key, value) => {
+const _setAttribute = (node, key, value) => {
node.attributes[key] = value;
};
-export const createTextNode = text => ({
+const _createTextNode = text => ({
nodeName: '#text',
nodeValue: text
});
+
+const _getChildNodes = node => {
+ return node.childNodes;
+};
+
+const _getTextContent = node => {
+ return node.textContent;
+};
+
+export const createDocumentHelpers = document => {
+ if (document) {
+ return Object.freeze({
+ createNode: tagName => _documentCreateNode(document, tagName),
+ appendChildNode: _documentAppendChildNode,
+ appendStaticNode: _documentAppendStaticNode,
+ insertBeforeNode: _documentInsertBeforeNode,
+ removeChildNode: _documentRemoveChildNode,
+ setAttribute: _documentSetAttribute,
+ createTextNode: text => _documentCreateTextNode(document, text),
+ getChildNodes: _documentGetChildNodes,
+ getTextContent: _documentGetTextContent
+ });
+ }
+
+ return Object.freeze({
+ createNode: _createNode,
+ appendChildNode: _appendChildNode,
+ appendStaticNode: _appendStaticNode,
+ insertBeforeNode: _insertBeforeNode,
+ removeChildNode: _removeChildNode,
+ setAttribute: _setAttribute,
+ createTextNode: _createTextNode,
+ getChildNodes: _getChildNodes,
+ getTextContent: _getTextContent
+ });
+};
diff --git a/src/instance.js b/src/instance.js
index 1733b1cd9..e6c95b147 100644
--- a/src/instance.js
+++ b/src/instance.js
@@ -4,9 +4,9 @@ import autoBind from 'auto-bind';
import logUpdate from 'log-update';
import isCI from 'is-ci';
import signalExit from 'signal-exit';
-import reconciler from './reconciler';
+import {createReconciler} from './reconciler';
import createRenderer from './renderer';
-import {createNode} from './dom';
+import {createDocumentHelpers} from './dom';
import instances from './instances';
import App from './components/App';
@@ -15,10 +15,12 @@ export default class Instance {
autoBind(this);
this.options = options;
+ this.documentHelpers = createDocumentHelpers(options.document);
- this.rootNode = createNode('root');
+ this.rootNode = this.documentHelpers.createNode('root');
this.rootNode.onRender = this.onRender;
this.renderer = createRenderer({
+ documentHelpers: this.documentHelpers,
terminalWidth: options.stdout.columns
});
@@ -38,7 +40,12 @@ export default class Instance {
// so that it's rerendered every time, not just new static parts, like in non-debug mode
this.fullStaticOutput = '';
- this.container = reconciler.createContainer(this.rootNode, false, false);
+ this.reconciler = createReconciler(this.documentHelpers);
+ this.container = this.reconciler.createContainer(this.rootNode, false, false);
+
+ if (options.document) {
+ options.document.body.append(this.rootNode);
+ }
this.exitPromise = new Promise((resolve, reject) => {
this.resolveExitPromise = resolve;
@@ -95,6 +102,8 @@ export default class Instance {
@@ -102,7 +111,7 @@ export default class Instance {
);
- reconciler.updateContainer(tree, this.container);
+ this.reconciler.updateContainer(tree, this.container);
}
unmount(error) {
@@ -122,7 +131,7 @@ export default class Instance {
}
this.isUnmounted = true;
- reconciler.updateContainer(null, this.container);
+ this.reconciler.updateContainer(null, this.container);
instances.delete(this.options.stdout);
if (error instanceof Error) {
diff --git a/src/reconciler.js b/src/reconciler.js
index e094b5554..72cbbb3e1 100644
--- a/src/reconciler.js
+++ b/src/reconciler.js
@@ -3,122 +3,128 @@ import {
unstable_cancelCallback as cancelPassiveEffects
} from 'scheduler';
import ReactReconciler from 'react-reconciler';
-import {
- createNode,
- createTextNode,
- appendChildNode,
- insertBeforeNode,
- removeChildNode,
- setAttribute
-} from './dom';
const NO_CONTEXT = true;
-const hostConfig = {
- schedulePassiveEffects,
- cancelPassiveEffects,
- now: Date.now,
- getRootHostContext: () => NO_CONTEXT,
- prepareForCommit: () => {},
- resetAfterCommit: rootNode => {
- rootNode.onRender();
- },
- getChildHostContext: () => NO_CONTEXT,
- shouldSetTextContent: (type, props) => {
- return typeof props.children === 'string' || typeof props.children === 'number';
- },
- createInstance: (type, newProps) => {
- const node = createNode(type);
+function createHostConfig(documentHelpers) {
+ return {
+ schedulePassiveEffects,
+ cancelPassiveEffects,
+ now: Date.now,
+ getRootHostContext: () => NO_CONTEXT,
+ prepareForCommit: () => {},
+ resetAfterCommit: rootNode => {
+ rootNode.onRender();
+ },
+ getChildHostContext: () => NO_CONTEXT,
+ shouldSetTextContent: (type, props) => {
+ return typeof props.children === 'string' || typeof props.children === 'number';
+ },
+ createInstance: (type, newProps) => {
+ const node = documentHelpers.createNode(type);
- for (const [key, value] of Object.entries(newProps)) {
- if (key === 'children') {
- if (typeof value === 'string' || typeof value === 'number') {
- if (type === 'div') {
- // Text node must be wrapped in another node, so that text can be aligned within container
- const textElement = createNode('div');
- textElement.textContent = String(value);
- appendChildNode(node, textElement);
- }
+ for (const [key, value] of Object.entries(newProps)) {
+ if (key === 'children') {
+ if (typeof value === 'string' || typeof value === 'number') {
+ if (type === 'div') {
+ // Text node must be wrapped in another node, so that text can be aligned within container
+ const textElement = documentHelpers.createNode('div');
+ textElement.textContent = String(value);
+ documentHelpers.appendChildNode(node, textElement);
+ }
- if (type === 'span') {
- node.textContent = String(value);
+ if (type === 'span') {
+ node.textContent = String(value);
+ }
}
+ } else if (key === 'style') {
+ Object.assign(node.style, value);
+ } else if (key === 'unstable__transformChildren') {
+ node.unstable__transformChildren = value; // eslint-disable-line camelcase
+ } else if (key === 'unstable__static') {
+ node.unstable__static = true; // eslint-disable-line camelcase
+ } else {
+ documentHelpers.setAttribute(node, key, value);
}
- } else if (key === 'style') {
- Object.assign(node.style, value);
- } else if (key === 'unstable__transformChildren') {
- node.unstable__transformChildren = value; // eslint-disable-line camelcase
- } else if (key === 'unstable__static') {
- node.unstable__static = true; // eslint-disable-line camelcase
- } else {
- setAttribute(node, key, value);
}
- }
- return node;
- },
- createTextInstance: createTextNode,
- resetTextContent: node => {
- if (node.textContent) {
- node.textContent = '';
- }
+ return node;
+ },
+ createTextInstance: documentHelpers.createTextNode,
+ resetTextContent: node => {
+ if (node.textContent) {
+ node.textContent = '';
+ }
+
+ const childNodes = documentHelpers.getChildNodes(node);
- if (node.childNodes.length > 0) {
- for (const childNode of node.childNodes) {
- childNode.yogaNode.free();
- removeChildNode(node, childNode);
+ if (childNodes.length > 0) {
+ for (const childNode of childNodes) {
+ childNode.yogaNode.free();
+ documentHelpers.removeChildNode(node, childNode);
+ }
}
- }
- },
- getPublicInstance: instance => instance,
- appendInitialChild: appendChildNode,
- appendChild: appendChildNode,
- insertBefore: insertBeforeNode,
- finalizeInitialChildren: () => {},
- supportsMutation: true,
- appendChildToContainer: appendChildNode,
- insertInContainerBefore: insertBeforeNode,
- removeChildFromContainer: removeChildNode,
- prepareUpdate: () => true,
- commitUpdate: (node, updatePayload, type, oldProps, newProps) => {
- for (const [key, value] of Object.entries(newProps)) {
- if (key === 'children') {
- if (typeof value === 'string' || typeof value === 'number') {
- if (type === 'div') {
- // Text node must be wrapped in another node, so that text can be aligned within container
- // If there's no such node, a new one must be created
- if (node.childNodes.length === 0) {
- const textElement = createNode('div');
- textElement.textContent = String(value);
- appendChildNode(node, textElement);
- } else {
- node.childNodes[0].textContent = String(value);
+ },
+ getPublicInstance: instance => instance,
+ appendInitialChild: documentHelpers.appendChildNode,
+ appendChild: documentHelpers.appendChildNode,
+ insertBefore: documentHelpers.insertBeforeNode,
+ finalizeInitialChildren: () => {},
+ supportsMutation: true,
+ appendChildToContainer: documentHelpers.appendChildNode,
+ insertInContainerBefore: documentHelpers.insertBeforeNode,
+ removeChildFromContainer: documentHelpers.removeChildNode,
+ prepareUpdate: () => true,
+ commitUpdate: (node, updatePayload, type, oldProps, newProps) => {
+ for (const [key, value] of Object.entries(newProps)) {
+ if (key === 'children') {
+ if (typeof value === 'string' || typeof value === 'number') {
+ if (type === 'div') {
+ // Text node must be wrapped in another node, so that text can be aligned within container
+ // If there's no such node, a new one must be created
+ if (node.childNodes.length === 0) {
+ const textElement = documentHelpers.createNode('div');
+ textElement.textContent = String(value);
+ documentHelpers.appendChildNode(node, textElement);
+ } else {
+ node.childNodes[0].textContent = String(value);
+ }
}
- }
- if (type === 'span') {
- node.textContent = String(value);
+ if (type === 'span') {
+ node.textContent = String(value);
+ }
}
+ } else if (key === 'style') {
+ Object.assign(node.style, value);
+ } else if (key === 'unstable__transformChildren') {
+ node.unstable__transformChildren = value; // eslint-disable-line camelcase
+ } else if (key === 'unstable__static') {
+ node.unstable__static = true; // eslint-disable-line camelcase
+ } else {
+ documentHelpers.setAttribute(node, key, value);
}
- } else if (key === 'style') {
- Object.assign(node.style, value);
- } else if (key === 'unstable__transformChildren') {
- node.unstable__transformChildren = value; // eslint-disable-line camelcase
- } else if (key === 'unstable__static') {
- node.unstable__static = true; // eslint-disable-line camelcase
+ }
+ },
+ commitTextUpdate: (node, oldText, newText) => {
+ if (node.nodeName === '#text') {
+ node.nodeValue = newText;
} else {
- setAttribute(node, key, value);
+ node.textContent = newText;
}
- }
- },
- commitTextUpdate: (node, oldText, newText) => {
- if (node.nodeName === '#text') {
- node.nodeValue = newText;
- } else {
- node.textContent = newText;
- }
- },
- removeChild: removeChildNode
-};
+ },
+ removeChild: documentHelpers.removeChildNode
+ };
+}
+
+var _cachedReconciler = null; // eslint-disable-line no-var
-export default ReactReconciler(hostConfig); // eslint-disable-line new-cap
+export const createReconciler = documentHelpers => {
+ if (_cachedReconciler) {
+ return _cachedReconciler;
+ }
+
+ // Hopefully documentHelpers is the same...
+ _cachedReconciler = ReactReconciler(createHostConfig(documentHelpers)); // eslint-disable-line new-cap
+ return _cachedReconciler;
+};
diff --git a/src/render-node-to-output.js b/src/render-node-to-output.js
index 89f49f17f..462746118 100644
--- a/src/render-node-to-output.js
+++ b/src/render-node-to-output.js
@@ -2,18 +2,21 @@ import widestLine from 'widest-line';
import wrapText from './wrap-text';
import getMaxWidth from './get-max-width';
-const isAllTextNodes = node => {
+const isAllTextNodes = (documentHelpers, node) => {
if (node.nodeName === '#text') {
return true;
}
if (node.nodeName === 'SPAN') {
- if (node.textContent) {
+ if (documentHelpers.getTextContent(node)) {
return true;
}
- if (Array.isArray(node.childNodes)) {
- return node.childNodes.every(isAllTextNodes);
+ const childNodes = documentHelpers.getChildNodes(node);
+
+ if (Array.isArray(childNodes)) {
+ const fn = node => isAllTextNodes(documentHelpers, node);
+ return childNodes.every(fn);
}
}
@@ -26,10 +29,12 @@ const isAllTextNodes = node => {
//
// Also, this is necessary for libraries like ink-link (https://github.com/sindresorhus/ink-link),
// which need to wrap all children at once, instead of wrapping 3 text nodes separately.
-const squashTextNodes = node => {
+const squashTextNodes = (documentHelpers, node) => {
let text = '';
- for (const childNode of node.childNodes) {
+ const childNodes = documentHelpers.getChildNodes(node);
+
+ for (const childNode of childNodes) {
let nodeText;
if (childNode.nodeName === '#text') {
@@ -37,7 +42,7 @@ const squashTextNodes = node => {
}
if (childNode.nodeName === 'SPAN') {
- nodeText = childNode.textContent || squashTextNodes(childNode);
+ nodeText = documentHelpers.getTextContent(childNode) || squashTextNodes(documentHelpers, childNode);
}
// Since these text nodes are being concatenated, `Output` instance won't be able to
@@ -53,7 +58,7 @@ const squashTextNodes = node => {
};
// After nodes are laid out, render each to output object, which later gets rendered to terminal
-const renderNodeToOutput = (node, output, {offsetX = 0, offsetY = 0, transformers = [], skipStaticElements}) => {
+const renderNodeToOutput = (documentHelpers, node, output, {offsetX = 0, offsetY = 0, transformers = [], skipStaticElements}) => {
if (node.unstable__static && skipStaticElements) {
return;
}
@@ -72,12 +77,12 @@ const renderNodeToOutput = (node, output, {offsetX = 0, offsetY = 0, transformer
}
// Nodes with only text inside
- if (node.textContent) {
- let text = node.textContent;
+ if (documentHelpers.getTextContent(node)) {
+ let text = documentHelpers.getTextContent(node);
// Since text nodes are always wrapped in an additional node, parent node
// is where we should look for attributes
- if (node.parentNode.style.textWrap) {
+ if (node.parentNode && node.parentNode.style && node.parentNode.style.textWrap) {
const currentWidth = widestLine(text);
const maxWidth = getMaxWidth(node.parentNode.yogaNode);
@@ -99,11 +104,14 @@ const renderNodeToOutput = (node, output, {offsetX = 0, offsetY = 0, transformer
}
// Nodes that have other nodes as children
- if (Array.isArray(node.childNodes) && node.childNodes.length > 0) {
+ const childNodes = documentHelpers.getChildNodes(node);
+ if (Array.isArray(childNodes) && childNodes.length > 0) {
const isFlexDirectionRow = node.style.flexDirection === 'row';
- if (isFlexDirectionRow && node.childNodes.every(isAllTextNodes)) {
- let text = squashTextNodes(node);
+ const fn = node => isAllTextNodes(documentHelpers, node);
+
+ if (isFlexDirectionRow && childNodes.every(fn)) {
+ let text = squashTextNodes(documentHelpers, node);
if (node.style.textWrap) {
const currentWidth = widestLine(text);
@@ -120,8 +128,8 @@ const renderNodeToOutput = (node, output, {offsetX = 0, offsetY = 0, transformer
return;
}
- for (const childNode of node.childNodes) {
- renderNodeToOutput(childNode, output, {
+ for (const childNode of childNodes) {
+ renderNodeToOutput(documentHelpers, childNode, output, {
offsetX: x,
offsetY: y,
transformers: newTransformers,
diff --git a/src/renderer.js b/src/renderer.js
index 1b850f120..30a42df9f 100644
--- a/src/renderer.js
+++ b/src/renderer.js
@@ -1,6 +1,5 @@
import Yoga from 'yoga-layout-prebuilt';
import Output from './output';
-import {createNode, appendStaticNode} from './dom';
import buildLayout from './build-layout';
import renderNodeToOutput from './render-node-to-output';
import measureText from './measure-text';
@@ -9,8 +8,9 @@ import getMaxWidth from './get-max-width';
// Since we need to know the width of text container to wrap text, we have to calculate layout twice
// This function is executed after first layout calculation to reassign width and height of text nodes
-const calculateWrappedText = node => {
- if (node.textContent && typeof node.parentNode.style.textWrap === 'string') {
+const calculateWrappedText = (documentHelpers, node) => {
+ const textContent = documentHelpers.getTextContent(node);
+ if (textContent && node.parentNode && node.parentNode.style && typeof node.parentNode.style.textWrap === 'string') {
const {yogaNode} = node;
const parentYogaNode = node.parentNode.yogaNode;
const maxWidth = getMaxWidth(parentYogaNode);
@@ -28,24 +28,30 @@ const calculateWrappedText = node => {
return;
}
- if (Array.isArray(node.childNodes) && node.childNodes.length > 0) {
- for (const childNode of node.childNodes) {
- calculateWrappedText(childNode);
+ const childNodes = documentHelpers.getChildNodes(node);
+
+ if (childNodes && childNodes.length > 0) {
+ for (const childNode of childNodes) {
+ calculateWrappedText(documentHelpers, childNode);
}
}
};
// Since components can be placed anywhere in the tree, this helper finds and returns them
-const getStaticNodes = element => {
+const getStaticNodes = (documentHelpers, element) => {
const staticNodes = [];
- for (const childNode of element.childNodes) {
+ const elementChildNodes = documentHelpers.getChildNodes(element);
+
+ for (const childNode of elementChildNodes) {
if (childNode.unstable__static) {
staticNodes.push(childNode);
}
- if (Array.isArray(childNode.childNodes) && childNode.childNodes.length > 0) {
- staticNodes.push(...getStaticNodes(childNode));
+ const childNodes = documentHelpers.getChildNodes(childNode);
+
+ if (Array.isArray(childNodes) && childNodes.length > 0) {
+ staticNodes.push(...getStaticNodes(documentHelpers, childNode));
}
}
@@ -53,7 +59,7 @@ const getStaticNodes = element => {
};
// Build layout, apply styles, build text output of all nodes and return it
-export default ({terminalWidth}) => {
+export default ({documentHelpers, terminalWidth}) => {
const config = Yoga.Config.create();
// Used to free up memory used by last Yoga node tree
@@ -69,7 +75,7 @@ export default ({terminalWidth}) => {
lastStaticYogaNode.freeRecursive();
}
- const staticElements = getStaticNodes(node);
+ const staticElements = getStaticNodes(documentHelpers, node);
if (staticElements.length > 1) {
if (process.env.NODE_ENV !== 'production') {
console.error('Warning: There can only be one component');
@@ -79,17 +85,17 @@ export default ({terminalWidth}) => {
// component must be built and rendered separately, so that the layout of the other output is unaffected
let staticOutput;
if (staticElements.length === 1) {
- const rootNode = createNode('root');
- appendStaticNode(rootNode, staticElements[0]);
+ const rootNode = documentHelpers.createNode('root');
+ documentHelpers.appendStaticNode(rootNode, staticElements[0]);
- const {yogaNode: staticYogaNode} = buildLayout(rootNode, {
+ const {yogaNode: staticYogaNode} = buildLayout(documentHelpers, rootNode, {
config,
terminalWidth,
skipStaticElements: false
});
staticYogaNode.calculateLayout(Yoga.UNDEFINED, Yoga.UNDEFINED, Yoga.DIRECTION_LTR);
- calculateWrappedText(rootNode);
+ calculateWrappedText(documentHelpers, rootNode);
staticYogaNode.calculateLayout(Yoga.UNDEFINED, Yoga.UNDEFINED, Yoga.DIRECTION_LTR);
// Save current Yoga node tree to free up memory later
@@ -100,17 +106,17 @@ export default ({terminalWidth}) => {
height: staticYogaNode.getComputedHeight()
});
- renderNodeToOutput(rootNode, staticOutput, {skipStaticElements: false});
+ renderNodeToOutput(documentHelpers, rootNode, staticOutput, {skipStaticElements: false});
}
- const {yogaNode} = buildLayout(node, {
+ const {yogaNode} = buildLayout(documentHelpers, node, {
config,
terminalWidth,
skipStaticElements: true
});
yogaNode.calculateLayout(Yoga.UNDEFINED, Yoga.UNDEFINED, Yoga.DIRECTION_LTR);
- calculateWrappedText(node);
+ calculateWrappedText(documentHelpers, node);
yogaNode.calculateLayout(Yoga.UNDEFINED, Yoga.UNDEFINED, Yoga.DIRECTION_LTR);
// Save current node tree to free up memory later
@@ -121,7 +127,7 @@ export default ({terminalWidth}) => {
height: yogaNode.getComputedHeight()
});
- renderNodeToOutput(node, output, {skipStaticElements: true});
+ renderNodeToOutput(documentHelpers, node, output, {skipStaticElements: true});
return {
output: output.get(),