From a55cee5cc163cbdd1c58fbaef9ae6fb3ef58acf5 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Sun, 25 Mar 2018 17:50:29 +0200 Subject: [PATCH] IE Fixes --- .../client/components/rte/RTE.js | 59 +++++++++---------- .../components/rte/buttons/Blockquote.js | 8 +-- .../client/components/rte/buttons/Bold.js | 29 ++++++++- .../client/components/rte/buttons/Italic.js | 27 ++++++++- .../components/rte/components/Button.css | 2 +- .../client/components/rte/lib/api.js | 44 ++++++++++---- .../client/components/rte/lib/dom.js | 13 +++- 7 files changed, 125 insertions(+), 57 deletions(-) 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 a6da5783a..76557cd02 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/RTE.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/RTE.js @@ -14,7 +14,7 @@ import { selectEndOfNode, isSelectionInside, } from './lib/dom'; -import API from './lib/api'; +import createAPI from './lib/api'; import Undo from './lib/undo'; import bowser from 'bowser'; import throttle from 'lodash/throttle'; @@ -24,7 +24,15 @@ class RTE extends React.Component { ref = null; // Our "plugins" api. - api = null; + api = createAPI( + () => this.ref.htmlEl, + () => this.handleChange(), + () => this.undo.canUndo(), + () => this.undo.canRedo(), + () => this.handleUndo(), + () => this.handleRedo(), + () => this.focused + ); // Instance of undo stack. undo = new Undo(); @@ -36,6 +44,7 @@ class RTE extends React.Component { focus = () => this.ref.htmlEl.focus(); unmounted = false; + focused = false; // Should be called on every change to feed // our Undo stack. We save the innerHTML and if available @@ -65,19 +74,7 @@ class RTE extends React.Component { } // Ref to react-contenteditable. - handleRef = ref => ( - (this.ref = ref), - (this.api = - ref && - new API( - this.ref.htmlEl, - this.handleChange, - () => this.undo.canUndo(), - () => this.undo.canRedo(), - this.handleUndo, - this.handleRedo - )) - ); + handleRef = ref => (this.ref = ref); forEachButton(callback) { Object.keys(this.buttonsRef).map(k => callback(this.buttonsRef[k])); @@ -101,7 +98,6 @@ class RTE extends React.Component { } handleChange = () => { - this.handleSelectionChange(); this.props.onChange({ text: this.ref.htmlEl.innerText, html: this.ref.htmlEl.innerHTML, @@ -148,6 +144,16 @@ class RTE extends React.Component { } }; + handleFocus = () => { + this.focused = true; + }; + + handleBlur = () => { + this.focused = false; + // Sometimes the onselect event doesn't fire on blur. + this.handleSelectionChange(); + }; + // We intercept pasting, so that we // force text/plain content. handlePaste = e => { @@ -165,18 +171,14 @@ class RTE extends React.Component { return false; }; - handleMouseUp = () => { - setTimeout(() => !this.unmounted && this.handleSelectionChange()); - }; - handleKeyDown = e => { // IE has issues not firing the onChange event. if (bowser.msie) { - setTimeout(() => !this.unmounted && this.handleChange); + setTimeout(() => !this.unmounted && this.handleChange()); } - // Undo Redo - if (e.key === 'z' && e.metaKey) { + // Undo Redo 'Z' + if (e.key === 'z' && (e.metaKey || e.ctrlKey)) { if (e.shiftKey) { this.handleRedo(); } else { @@ -202,14 +204,6 @@ class RTE extends React.Component { } }; - handleKeyUp = () => { - // IE has issues not firing the onChange event. - if (bowser.msie) { - setTimeout(() => !this.unmounted && this.handleChange); - } - this.handleSelectionChange(); - }; - restoreCheckpoint(html, node, range) { if (node && range) { // We need to clone it, otherwise we'll mutate @@ -312,6 +306,9 @@ class RTE extends React.Component { onKeyUp={this.handleKeyUp} onPaste={this.handlePaste} onCut={this.handleCut} + onFocus={this.handleFocus} + onBlur={this.handleBlur} + onSelect={this.handleSelectionChange} className={classNames.content} ref={this.handleRef} html={value} 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 ad829c2fd..40addc014 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 @@ -9,7 +9,7 @@ import { } from '../lib/dom'; function execCommand() { - const bq = findIntersecting('BLOCKQUOTE'); + const bq = findIntersecting('BLOCKQUOTE', this.container); if (bq) { outdentNode(bq, true); } else { @@ -34,16 +34,16 @@ function execCommand() { } function isActive() { - return !!findIntersecting('BLOCKQUOTE'); + return this.focused && !!findIntersecting('BLOCKQUOTE', this.container); } -const onEnter = node => { +function onEnter(node) { if (node.tagName !== 'BLOCKQUOTE') { return; } insertNewLineAfterNode(node, true); return true; -}; +} const Blockquote = createToggle(execCommand, { onEnter, isActive }); diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js index 7f6259c77..53bdfe71b 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Bold.js @@ -1,9 +1,32 @@ import createToggle from '../factories/createToggle'; +import { findIntersecting } from '../lib/dom'; -const execCommand = () => document.execCommand('bold'); -const isActive = () => document.queryCommandState('bold'); +const boldTags = ['B', 'STRONG']; -const Bold = createToggle(execCommand, { isActive }); +function execCommand() { + return document.execCommand('bold'); +} + +function isActive() { + return this.focused && document.queryCommandState('bold'); +} +function isDisabled() { + if (!this.focused) { + return false; + } + + // Disable whenever the bold styling came from a different + // tag than those we control. + return !!findIntersecting( + n => + n.nodeName !== '#text' && + window.getComputedStyle(n).getPropertyValue('font-weight') === 'bold' && + !boldTags.includes(n.tagName), + this.container + ); +} + +const Bold = createToggle(execCommand, { isActive, isDisabled }); Bold.defaultProps = { children: 'Bold', diff --git a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js index f7fc6a4f4..64786d5b9 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/buttons/Italic.js @@ -1,9 +1,30 @@ import createToggle from '../factories/createToggle'; +import { findIntersecting } from '../lib/dom'; -const execCommand = () => document.execCommand('italic'); -const isActive = () => document.queryCommandState('italic'); +const italicTags = ['I', 'EM']; -const Italic = createToggle(execCommand, { isActive }); +function execCommand() { + return document.execCommand('italic'); +} +function isActive() { + return this.focused && document.queryCommandState('italic'); +} +function isDisabled() { + if (!this.focused) { + return false; + } + // Disable whenever the italic styling came from a different + // tag than those we control. + return !!findIntersecting( + n => + n.nodeName !== '#text' && + window.getComputedStyle(n).getPropertyValue('font-style') === 'italic' && + !italicTags.includes(n.tagName), + this.container + ); +} + +const Italic = createToggle(execCommand, { isActive, isDisabled }); Italic.defaultProps = { children: 'Italic', diff --git a/plugins/talk-plugin-rich-text/client/components/rte/components/Button.css b/plugins/talk-plugin-rich-text/client/components/rte/components/Button.css index 070906205..39b14fb7e 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/components/Button.css +++ b/plugins/talk-plugin-rich-text/client/components/rte/components/Button.css @@ -39,5 +39,5 @@ .button:disabled{ color: #bbb; cursor: default; - background-color: inherit; + background: none; } diff --git a/plugins/talk-plugin-rich-text/client/components/rte/lib/api.js b/plugins/talk-plugin-rich-text/client/components/rte/lib/api.js index c7ba4d51a..9e018f093 100644 --- a/plugins/talk-plugin-rich-text/client/components/rte/lib/api.js +++ b/plugins/talk-plugin-rich-text/client/components/rte/lib/api.js @@ -1,17 +1,37 @@ +import { isSelectionInside } from './dom'; + /** * An instance of API is passed to all the buttons to * interact with RTE, which servers as a clean abstraction. */ -export default class API { - constructor(container, onChange, canUndo, canRedo, undo, redo) { - this.container = container; - this.broadcastChange = onChange; - this.canUndo = canUndo; - this.canRedo = canRedo; - this.undo = undo; - this.redo = redo; - } - focus() { - this.container.focus(); - } +function createAPI( + getContainer, + broadcastChange, + canUndo, + canRedo, + undo, + redo, + getFocused +) { + return { + broadcastChange, + canUndo, + canRedo, + undo, + redo, + get focused() { + return getFocused(); + }, + get container() { + return getContainer(); + }, + focus() { + this.container.focus(); + }, + isSelectionInside() { + return isSelectionInside(getContainer()); + }, + }; } + +export default createAPI; 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 6c25d6052..dc822bb89 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 @@ -7,6 +7,9 @@ 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)) { @@ -216,6 +219,9 @@ export function getSelectionRange() { // Adds a 'br' marker at the end of the node. function ensureEndMarker(node) { + if (!isBlockElement(node)) { + return; + } if ( !node.lastChild || node.lastChild.tagName !== 'BR' || @@ -465,9 +471,10 @@ export function outdentNode(node, changeSelection) { function cloneNodeAndRangeHelper(node, range, rangeCloned) { const nodeCloned = node.cloneNode(false); - node.childNodes.forEach(n => - nodeCloned.appendChild(cloneNodeAndRangeHelper(n, range, rangeCloned)) - ); + for (let i = 0; i < node.childNodes.length; i++) { + const n = node.childNodes[i]; + nodeCloned.appendChild(cloneNodeAndRangeHelper(n, range, rangeCloned)); + } if (range.startContainer === node) { rangeCloned.setStart(nodeCloned, range.startOffset); }