diff --git a/plugins/talk-plugin-rich-text/client/components/rte/RTE.js b/plugins/talk-plugin-rich-text/client/components/rte/RTE.js index 403d1ed02..655ebfc1d 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/RTE.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/RTE.js @@ -13,6 +13,7 @@ import { replaceNodeChildren, selectEndOfNode, isSelectionInside, + traverse, } from './lib/dom'; import createAPI from './lib/api'; import Undo from './lib/undo'; @@ -98,6 +99,13 @@ class RTE extends React.Component { } handleChange = () => { + // TODO: don't rely on this hack. + // It removes all `style` attr that + // remaining execCommand still add. + traverse(this.ref.htmlEl, n => { + n.removeAttribute && n.removeAttribute('style'); + }); + this.props.onChange({ text: this.ref.htmlEl.innerText, html: this.ref.htmlEl.innerHTML, @@ -116,7 +124,6 @@ class RTE extends React.Component { this.forEachButton(b => { b.onSelectionChange && b.onSelectionChange(); }); - console.log('sel', getSelectionRange()); }; // Called when Enter was pressed without shift. @@ -218,6 +225,16 @@ class RTE extends React.Component { } }; + handleKeyPress = e => { + const character = String.fromCharCode(e.charCode); + if (character) { + insertText(character); + this.handleChange(); + e.preventDefault(); + return false; + } + }; + restoreCheckpoint(html, node, range) { if (node && range) { // We need to clone it, otherwise we'll mutate @@ -317,6 +334,7 @@ class RTE extends React.Component { id={inputId} onMouseUp={this.handleMouseUp} onKeyDown={this.handleKeyDown} + onKeyPress={this.handleKeyPress} onKeyUp={this.handleKeyUp} onPaste={this.handlePaste} onCut={this.handleCut} 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 c3b6bfc5a..d8a409b44 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 @@ -6,7 +6,7 @@ import { getSelectedNodesExpanded, outdentNode, selectEndOfNode, - endWithNewLine, + indentNodes, } from '../lib/dom'; function execCommand() { @@ -14,19 +14,13 @@ function execCommand() { if (bq) { outdentNode(bq, true); } else { - const node = document.createElement('blockquote'); - // Expanded selection means we always select whole lines. const selectedNodes = getSelectedNodesExpanded(); if (selectedNodes.length) { - const firstNode = selectedNodes[0]; - firstNode.parentNode.insertBefore(node, firstNode); - selectedNodes.forEach(n => { - node.appendChild(n); - }); - endWithNewLine(node); + const node = indentNodes(selectedNodes, 'blockquote'); selectEndOfNode(node); } else { + const node = document.createElement('blockquote'); node.appendChild(document.createElement('br')); insertNodes(node); selectEndOfNode(node); 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 df78ac37c..66ac35662 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,3 +1,41 @@ +/** + * Traverse DOM tree until callback returns anything. + */ +export function traverse(node, callback) { + let result; + for (let i = 0; i < node.childNodes.length; i++) { + const child = node.childNodes[i]; + result = callback(child); + if (result === undefined) { + result = traverse(child, callback); + } + if (result !== undefined) { + return result; + } + } +} + +/** + * Traverse DOM tree backwards until callback returns anything. + * If hits limitTo returns null. + */ +export function traverseUp(node, callback, limitTo) { + let result; + if (node.isSameNode(limitTo)) { + return null; + } + while (node.parentNode) { + node = node.parentNode; + result = callback(node); + if (result !== undefined) { + return result; + } + if (limitTo && node.isSameNode(limitTo)) { + return null; + } + } +} + /** * Find ancestor with given tag or whith callback returning true. * If `limitTo` is passed, the search is limited to this container. @@ -7,19 +45,17 @@ export function findAncestor(node, tagOrCallback, limitTo) { typeof tagOrCallback === 'function' ? tagOrCallback : n => n.tagName === tagOrCallback; - if (node.isSameNode(limitTo)) { - return null; - } - while (node.parentNode) { - node = node.parentNode; - if (callback(node)) { - return node; - } - if (limitTo && node.isSameNode(limitTo)) { - return null; - } - } - return null; + return ( + traverseUp( + node, + n => { + if (callback(n)) { + return n; + } + }, + limitTo + ) || null + ); } /** @@ -30,17 +66,13 @@ export function findChild(node, tagOrCallback) { 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; + return ( + traverse(node, n => { + if (callback(n)) { + return n; + } + }) || null + ); } /** @@ -220,19 +252,13 @@ export function getSelectionRange() { return selection.rangeCount ? selection.getRangeAt(0) : null; } -// Adds a 'br' marker at the end of the node. -export function endWithNewLine(node) { +// Adds a bogus 'br' at the end of the node if not existant. +export function addBogusBR(node) { if (!isBlockElement(node)) { return; } - if ( - !node.lastChild || - node.lastChild.tagName !== 'BR' || - node.lastChild.className !== 'coral-rte-end-marker' - ) { - const br = document.createElement('br'); - br.className = 'coral-rte-end-marker'; - node.appendChild(br); + if (!node.lastChild || !isBogusBR(node.lastChild)) { + node.appendChild(document.createElement('br')); } } @@ -275,6 +301,17 @@ export function insertNewLine(changeSelection) { const el = document.createElement('br'); insertNodes(el); + // If we are adding to the end of the node, we also need + // to add a bogus br. + if (!el.nextSibling) { + el.parentNode.appendChild(document.createElement('br')); + } + + // Adding directly before a block element needs also a bogus br. + if (el.nextSibling && isBlockElement(el.nextSibling)) { + el.parentNode.insertBefore(document.createElement('br'), el.nextSibling); + } + // Calculate next selection. const range = document.createRange(); if (el.nextSibling) { @@ -282,10 +319,6 @@ export function insertNewLine(changeSelection) { range.setStart(el.parentNode, offset); range.setEnd(el.parentNode, offset); } else { - // We need to add a
marker at the end, because we can't - // select the last
. - endWithNewLine(el.parentNode); - const offset = el.parentNode.childNodes.length - 1; range.setStart(el.parentNode, offset); range.setEnd(el.parentNode, offset); @@ -342,6 +375,14 @@ export function getLeftOfNode(node) { return result; } +export function isBogusBR(node) { + return ( + (!node.previousSibling || !isBlockElement(node.previousSibling)) && + node.tagName === 'BR' && + (!node.nextSibling || isBlockElement(node.previousSibling)) + ); +} + /** * Returns an array of all nodes after `node`. */ @@ -356,7 +397,11 @@ export function getRightOfNode(node) { cur = cur.nextSibling; result.push(cur); } - if (cur.nextSibling && cur.nextSibling.tagName === 'BR') { + if ( + cur.nextSibling && + cur.nextSibling.tagName === 'BR' && + !isBogusBR(cur.nextSibling) + ) { result.push(cur.nextSibling); } return result; @@ -457,11 +502,28 @@ export function getSelectedChildren(ancestor) { * Removes node and assimilate its children with the parent. */ export function outdentNode(node, changeSelection) { + /// Remove bogus br + if (node.lastChild && node.lastChild.tagName === 'BR') { + node.removeChild(node.lastChild); + } + + // A new line to substitute the missing block element. + const needExtraLine = + node.nextSibling && + !isBlockElement(node.nextSibling) && + !isBlockElement(node.lastChild); + const parentNode = node.parentNode; const offset = indexOfChildNode(parentNode, node); + while (node.firstChild) { parentNode.insertBefore(node.firstChild, node); } + + if (needExtraLine) { + parentNode.insertBefore(document.createElement('BR'), node); + } + parentNode.removeChild(node); if (changeSelection) { @@ -472,6 +534,36 @@ export function outdentNode(node, changeSelection) { } } +/** + * Indent children. + */ +export function indentNodes(nodes, tagName) { + const node = document.createElement(tagName); + + // Remove bogus BR if the blockquote is the last element. + // Otherwise there will be an unwanted empty line. + const lastNode = nodes[nodes.length - 1]; + if ( + lastNode.nextSibling === lastNode.parentNode.lastChild && + isBogusBR(lastNode.parentNode.lastChild) + ) { + lastNode.parentNode.removeChild(lastNode.parentNode.lastChild); + } + + const firstNode = nodes[0]; + // Remove previous br as it is not needed + if (firstNode.previousSibling && firstNode.previousSibling.tagName === 'BR') { + firstNode.parentNode.removeChild(firstNode.previousSibling); + } + + // Finally indent. + firstNode.parentNode.insertBefore(node, firstNode); + nodes.forEach(n => { + node.appendChild(n); + }); + return node; +} + function cloneNodeAndRangeHelper(node, range, rangeCloned) { const nodeCloned = node.cloneNode(false); for (let i = 0; i < node.childNodes.length; i++) {