From 2757791672ba7b34488f1216acab7672fbcc5857 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Sun, 25 Mar 2018 01:20:46 +0100 Subject: [PATCH] Refactor --- .../components/rte/buttons/Blockquote.js | 6 +- .../components/rte/factories/createToggle.js | 2 +- .../client/components/rte/lib/dom.js | 233 +++++++++++++----- 3 files changed, 180 insertions(+), 61 deletions(-) diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js index 7b5b21b95..ad829c2fd 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Blockquote.js @@ -1,6 +1,6 @@ import createToggle from '../factories/createToggle'; import { - findIntersectingTag, + findIntersecting, insertNewLineAfterNode, insertNodes, getSelectedNodesExpanded, @@ -9,7 +9,7 @@ import { } from '../lib/dom'; function execCommand() { - const bq = findIntersectingTag('BLOCKQUOTE'); + const bq = findIntersecting('BLOCKQUOTE'); if (bq) { outdentNode(bq, true); } else { @@ -34,7 +34,7 @@ function execCommand() { } function isActive() { - return !!findIntersectingTag('BLOCKQUOTE'); + return !!findIntersecting('BLOCKQUOTE'); } const onEnter = node => { diff --git a/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js b/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js index c14fc73a1..a12f9f4d9 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/factories/createToggle.js @@ -29,7 +29,7 @@ const createToggle = ( this.props.api.focus(); this.formatToggle(); this.props.api.focus(); - this.syncState(); + setTimeout(this.syncState); }; syncState = () => { diff --git a/plugins/talk-plugin-rich-text/client/components/rte/lib/dom.js b/plugins/talk-plugin-rich-text/client/components/rte/lib/dom.js index 8ce10a31a..69bff32f4 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/lib/dom.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/lib/dom.js @@ -1,37 +1,121 @@ -export function findAncestorWithTag(node, tag) { - do { - if (node.nodeName === tag) { +/** + * Find ancestor with given tag or whith callback returning true. + */ +export function findAncestor(node, tagOrCallback) { + const callback = + typeof tagOrCallback === 'function' + ? tagOrCallback + : n => n.tagName === tagOrCallback; + while (node.parentNode) { + node = node.parentNode; + if (callback(node)) { return node; } - node = node.parentNode; - } while (node); - return false; + } + return null; } -export function findIntersectingTag(tag) { +/** + * Find child with given tag or when callback return true. + */ +export function findChild(node, tagOrCallback) { + const callback = + typeof tagOrCallback === 'function' + ? tagOrCallback + : n => n.tagName === tagOrCallback; + for (let i = 0; i < node.childNodes.length; i++) { + const child = node.childNodes[i]; + if (callback(child)) { + return child; + } + const found = findChild(child, tagOrCallback); + if (found) { + return found; + } + } + return null; +} + +/** + * Find an node intersecting with the selection with given tag or + * with callback returning true. + */ +export function findIntersecting(tagOrCallback) { + const callback = + typeof tagOrCallback === 'function' + ? tagOrCallback + : n => n.tagName === tagOrCallback; + const range = getSelectionRange(); if (!range) { return null; } - const ancestor = findAncestorWithTag(range.startContainer, tag); + if (callback(range.startContainer)) { + return range.startContainer; + } + + const ancestor = findAncestor(range.startContainer, callback); if (ancestor) { return ancestor; } const nodes = getSelectedChildren(range.commonAncestorContainer); for (let i = 0; i < nodes.length; i++) { - if (nodes[i].tagName === tag) { + if (callback(nodes[i])) { return nodes[i]; } - const query = nodes[i].querySelector && nodes[i].querySelector(tag); - if (query) { - return query; + const found = findChild(nodes[i], callback); + if (found) { + return found; } } return null; } +/** + * Same as node.contains but works in IE. + * In addition lookFor can also be a callback. + */ +export function nodeContains(node, lookFor) { + const callback = + typeof lookFor === 'function' ? lookFor : n => n.isSameNode(lookFor); + if (callback(node)) { + return true; + } + return !!findChild(node, callback); +} + +/** + * Returns true if node is not `inline` nor `inline-block`. + */ +export function isBlockElement(node) { + if (node.nodeName === '#text') { + return false; + } + return !window + .getComputedStyle(node) + .getPropertyValue('display') + .startsWith('inline'); +} + +/** + * Find parent that is a block element. + */ +export function findParentBlock(node) { + return findAncestor(node, isBlockElement); +} + +/** + * Find last parent before a block element. + */ +export function lastParentBeforeBlock(node) { + return findAncestor(node, n => !n.parentNode || isBlockElement(n.parentNode)); +} + +/** + * Like `Array.indexOf` but works on `childNodes`. + */ export function indexOfChildNode(parent, child) { for (let i = 0; i < parent.childNodes.length; i++) { if (parent.childNodes[i] === child) { @@ -41,6 +125,10 @@ export function indexOfChildNode(parent, child) { return -1; } +/** + * Same as `document.execCommand('insertText', false, text)` but also + * works for IE. Changes Selection. + */ export function insertText(text) { const selection = window.getSelection(); if (!selection.isCollapsed) { @@ -66,6 +154,10 @@ export function insertText(text) { } } +/** + * Insert nodes to current selection, + * does not change selection. + */ export function insertNodes(...nodes) { const selection = window.getSelection(); if (!selection.isCollapsed) { @@ -93,21 +185,31 @@ export function insertNodes(...nodes) { } } +/** + * Helper to replace current selection with range. + */ export function replaceSelection(range) { const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); } +/** + * Helper to to know if selection is collapsed. + */ export function isSelectionCollapsed() { return window.getSelection().isCollapsed; } +/** + * Helper to get current selection range. + */ export function getSelectionRange() { const selection = window.getSelection(); return selection.rangeCount ? selection.getRangeAt(0) : null; } +// Adds a 'br' marker at the end of the node. function ensureEndMarker(node) { if ( !node.lastChild || @@ -120,18 +222,10 @@ function ensureEndMarker(node) { } } -export function nodeContains(node, lookFor) { - if (node.isSameNode(lookFor)) { - return true; - } - for (let i = 0; i < node.childNodes.length; i++) { - if (nodeContains(node.childNodes[i], lookFor)) { - return true; - } - } - return false; -} - +/** + * Returns true if selection is completely inside + * given node. + */ export function selectionIsInside(node) { const range = getSelectionRange(); return ( @@ -141,6 +235,9 @@ export function selectionIsInside(node) { ); } +/** + * Append a newline to the node. + */ export function appendNewLine(node, changeSelection) { const el = document.createElement('br'); node.appendChild(el); @@ -154,6 +251,11 @@ export function appendNewLine(node, changeSelection) { } } +/** + * Insert new line. This is what happens + * when adding new lines through pressing Enter. + * Deals with browers quirks. + */ export function insertNewLine(changeSelection) { // Insert
node. const el = document.createElement('br'); @@ -180,6 +282,9 @@ export function insertNewLine(changeSelection) { } } +/** + * Inserts a new line after given node. + */ export function insertNewLineAfterNode(node, changeSelection) { if (node.parentNode.lastChild === node) { appendNewLine(node.parentNode, changeSelection); @@ -195,41 +300,20 @@ export function insertNewLineAfterNode(node, changeSelection) { } } -export function getSelectedNode(container, offset) { +/** + * Given a container and a offset, return the selected + * node. Usually to resolve the start or end of a range. + */ +export function getRangeNode(container, offset) { if (container.nodeName === '#text') { return container; } return container.childNodes[offset]; } -export function isBlockElement(node) { - if (node.nodeName === '#text') { - return false; - } - return !window - .getComputedStyle(node) - .getPropertyValue('display') - .startsWith('inline'); -} - -export function findParentBlock(node) { - if (!node.parentNode) { - return null; - } - if (isBlockElement(node.parentNode)) { - return node.parentNode; - } - return findParentBlock(node.parentNode); -} - -export function lastParentBeforeBlock(node) { - let child = node; - while (!isBlockElement(child.parentNode)) { - child = node.parentNode; - } - return child; -} - +/** + * Returns an array of all nodes before `node`. + */ export function getLeftOfNode(node) { let result = []; let leftMost = node; @@ -244,6 +328,9 @@ export function getLeftOfNode(node) { return result; } +/** + * Returns an array of all nodes after `node`. + */ export function getRightOfNode(node) { let result = []; let cur = node; @@ -261,26 +348,40 @@ export function getRightOfNode(node) { return result; } +/** + * Given `node` find the line it belongs too + * and return the whole line as an array. + */ export function getWholeLine(node) { if (isBlockElement(node)) { return [node]; } - const child = lastParentBeforeBlock(node); + const child = isBlockElement(node.parentNode) + ? node + : lastParentBeforeBlock(node); if (child.tagName === 'BR') { return [...getLeftOfNode(child), child]; } return [...getLeftOfNode(child), child, ...getRightOfNode(child)]; } +/** + * Get selected line at the start of the selection. + * Returns an array of nodes. + */ export function getSelectedLine() { const range = getSelectionRange(); if (!range) { return []; } - const start = getSelectedNode(range.startContainer, range.startOffset); + const start = getRangeNode(range.startContainer, range.startOffset); return start ? getWholeLine(start) : []; } +/** + * Finds a commen block ancestor in the selection + * and return "whole" lines as an array of nodes. + */ export function getSelectedNodesExpanded() { const range = getSelectionRange(); if (!range) { @@ -304,6 +405,10 @@ export function getSelectedNodesExpanded() { ]; } +/** + * Returns array of children that intersects with + * the selection. + */ export function getSelectedChildren(ancestor) { const result = []; const range = getSelectionRange(); @@ -314,8 +419,8 @@ export function getSelectedChildren(ancestor) { return result; } - const start = getSelectedNode(range.startContainer, range.startOffset); - const end = getSelectedNode(range.endContainer, range.endOffset); + const start = getRangeNode(range.startContainer, range.startOffset); + const end = getRangeNode(range.endContainer, range.endOffset); let foundStart = false; for (let i = 0; i < ancestor.childNodes.length; i++) { const node = ancestor.childNodes[i]; @@ -334,6 +439,9 @@ export function getSelectedChildren(ancestor) { return result; } +/** + * Removes node and assimilate its children with the parent. + */ export function outdentNode(node, changeSelection) { const parentNode = node.parentNode; const offset = indexOfChildNode(parentNode, node); @@ -364,6 +472,9 @@ function cloneNodeAndRangeHelper(node, range, rangeCloned) { return nodeCloned; } +/** + * Clones node and returns both the cloned node and an equivalent Range. + */ export function cloneNodeAndRange(node, range) { const rangeCloned = range.cloneRange(); const nodeCloned = cloneNodeAndRangeHelper(node, range, rangeCloned); @@ -376,6 +487,9 @@ export function cloneNodeAndRange(node, range) { return [nodeCloned, rangeCloned]; } +/** + * Take children of the second node and replace children of first node. + */ export function replaceNodeChildren(node, node2) { while (node.firstChild) { node.removeChild(node.firstChild); @@ -385,6 +499,11 @@ export function replaceNodeChildren(node, node2) { } } +/** + * Tries to select the end of node. + * Currently looks for
and text nodes to find a suitable + * candidate for a selection. + */ export function selectEndOfNode(node) { for (let i = node.childNodes.length - 1; i >= 0; i--) { let child = node.childNodes[i];