This commit is contained in:
Chi Vinh Le
2018-03-25 17:50:29 +02:00
parent 69e194f648
commit a55cee5cc1
7 changed files with 125 additions and 57 deletions
@@ -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}
@@ -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 });
@@ -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',
@@ -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',
@@ -39,5 +39,5 @@
.button:disabled{
color: #bbb;
cursor: default;
background-color: inherit;
background: none;
}
@@ -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;
@@ -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);
}