Add comments

This commit is contained in:
Chi Vinh Le
2018-03-25 00:02:42 +01:00
parent 3bd8d5627b
commit 06ed792005
6 changed files with 98 additions and 22 deletions
@@ -11,6 +11,7 @@ import {
replaceSelection,
cloneNodeAndRange,
replaceNodeChildren,
selectEndOfNode,
} from './lib/dom';
import API from './lib/api';
import Undo from './lib/undo';
@@ -18,11 +19,21 @@ import bowser from 'bowser';
import throttle from 'lodash/throttle';
class RTE extends React.Component {
/// Ref to react-contenteditable
ref = null;
// Our "plugins" api.
api = null;
// Instance of undo stack.
undo = new Undo();
// Refs to the buttons.
buttonsRef = {};
// Should be called on every change to feed
// our Undo stack. We save the innerHTML and if available
// a copy of the contentEditable node and a copy of the range.
saveCheckpoint = throttle((html, node, range) => {
const args = [html];
if (node && range) {
@@ -31,6 +42,12 @@ class RTE extends React.Component {
this.undo.save(...args);
}, 1000);
constructor(props) {
super(props);
this.saveCheckpoint(props.value);
}
// Returns a handler that fills our `buttonsRef`.
createButtonRefHandler(key) {
return ref => {
if (ref) {
@@ -41,16 +58,25 @@ class RTE extends React.Component {
};
}
// Ref to react-contenteditable.
handleRef = ref => (
(this.ref = ref),
(this.api = new API(
this.ref.htmlEl,
this.handleChange,
() => this.undo.canUndo(),
() => this.undo.canRedo(),
this.handleUndo,
this.handleRedo
))
);
forEachButton(callback) {
Object.keys(this.buttonsRef).map(k => callback(this.buttonsRef[k]));
}
constructor(props) {
super(props);
this.saveCheckpoint(props.value);
}
componentWillReceiveProps(props) {
// Clear undo stack if content was set to sth different.
if (props.value !== this.ref.htmlEl.innerHTML) {
this.undo.clear();
this.saveCheckpoint(props.value);
@@ -71,16 +97,17 @@ class RTE extends React.Component {
);
};
handleRef = ref => (
(this.ref = ref), (this.api = new API(this.ref.htmlEl, this.handleChange))
);
handleSelectionChange = () => {
// Let buttons know selection has changeed, so they
// can update.
this.forEachButton(b => {
b.onSelectionChange && b.onSelectionChange();
});
};
// Called when Enter was pressed without shift.
// Traverses from bottom to top and calling
// button handlers and stops when one has handled this event.
handleSpecialEnter = () => {
let handled = false;
const sel = window.getSelection();
@@ -104,6 +131,8 @@ class RTE extends React.Component {
}
};
// We intercept pasting, so that we
// force text/plain content.
handlePaste = e => {
// Get text representation of clipboard
// This works cross browser.
@@ -199,6 +228,7 @@ class RTE extends React.Component {
replaceSelection(finalRange);
} else {
this.ref.htmlEl.innerHTML = html;
selectEndOfNode(this.ref.htmlEl);
}
this.handleChange();
}
@@ -2,27 +2,20 @@ import createToggle from '../factories/createToggle';
import {
findIntersectingTag,
insertNewLineAfterNode,
replaceSelection,
insertNodes,
getSelectedNodesExpanded,
outdentNode,
selectEndOfNode,
} from '../lib/dom';
// TODO: select end of node.
function selectNode(node) {
const range = document.createRange();
const container = node.childNodes.length ? node.childNodes[0] : node;
range.setStart(container, 0);
range.setEnd(container, 0);
replaceSelection(range);
}
function execCommand() {
const bq = findIntersectingTag('BLOCKQUOTE');
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];
@@ -30,11 +23,11 @@ function execCommand() {
selectedNodes.forEach(n => {
node.appendChild(n);
});
selectNode(node);
selectEndOfNode(node);
} else {
node.appendChild(document.createElement('br'));
insertNodes(node);
selectNode(node);
selectEndOfNode(node);
}
}
this.broadcastChange();
@@ -2,6 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import Button from '../components/Button';
/**
* createToggle creates a button that can be active, inactive or disabled
* and reacts on clicks. All callbacks are bound to the API instance.
*/
const createToggle = (
execCommand,
{ onEnter, isActive = () => false, isDisabled = () => false } = {}
@@ -1,9 +1,17 @@
import { selectionIsInside } 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(contentEditable, onChange) {
constructor(contentEditable, onChange, canUndo, canRedo, undo, redo) {
this.contentEditable = contentEditable;
this.broadcastChange = onChange;
this.canUndo = canUndo;
this.canRedo = canRedo;
this.undo = undo;
this.redo = redo;
}
isSelectionInside() {
return selectionIsInside(this.contentEditable);
@@ -384,3 +384,36 @@ export function replaceNodeChildren(node, node2) {
node.appendChild(node2.firstChild);
}
}
export function selectEndOfNode(node) {
for (let i = node.childNodes.length - 1; i >= 0; i--) {
let child = node.childNodes[i];
const s = selectEndOfNode(child);
if (s) {
return true;
}
if (child.tagName === 'BR') {
if (
child.previousSibling &&
child.previousSibling.childName === '#text'
) {
child = child.previousSibling;
} else {
const offset = indexOfChildNode(node, child);
const range = document.createRange();
range.setStart(node, offset);
range.setEnd(node, offset);
replaceSelection(range);
return true;
}
}
if (child.nodeName === '#text') {
const range = document.createRange();
range.setStart(child, child.textContent.length);
range.setEnd(child, child.textContent.length);
replaceSelection(range);
return true;
}
}
return false;
}
@@ -1,4 +1,12 @@
/**
* Simple size limited Undo stack.
*/
export default class Undo {
/**
* undoStack contains all known values.
* The last value of this stack represent
* the most recent change.
*/
undoStack = [];
redoStack = [];
size;