mirror of
https://github.com/wassname/talk.git
synced 2026-07-25 13:30:59 +08:00
Use our own UNDO stack!!
This commit is contained in:
@@ -4,15 +4,33 @@ import styles from './RTE.css';
|
||||
import cn from 'classnames';
|
||||
import ContentEditable from 'react-contenteditable';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import { insertNewLine, insertText } from './lib/dom';
|
||||
import {
|
||||
insertNewLine,
|
||||
insertText,
|
||||
getSelectionRange,
|
||||
replaceSelection,
|
||||
cloneNodeAndRange,
|
||||
replaceNodeChildren,
|
||||
} from './lib/dom';
|
||||
import API from './lib/api';
|
||||
import Undo from './lib/undo';
|
||||
import bowser from 'bowser';
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
class RTE extends React.Component {
|
||||
ref = null;
|
||||
api = null;
|
||||
undo = new Undo();
|
||||
buttonsRef = {};
|
||||
|
||||
saveCheckpoint = throttle((html, node, range) => {
|
||||
const args = [html];
|
||||
if (node && range) {
|
||||
args.push(...cloneNodeAndRange(node, range));
|
||||
}
|
||||
this.undo.save(...args);
|
||||
}, 1000);
|
||||
|
||||
createButtonRefHandler(key) {
|
||||
return ref => {
|
||||
if (ref) {
|
||||
@@ -27,12 +45,30 @@ class RTE extends React.Component {
|
||||
Object.keys(this.buttonsRef).map(k => callback(this.buttonsRef[k]));
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.saveCheckpoint(props.value);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.value !== this.ref.htmlEl.innerHTML) {
|
||||
this.undo.clear();
|
||||
this.saveCheckpoint(props.value);
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = () => {
|
||||
this.handleSelectionChange();
|
||||
this.props.onChange({
|
||||
text: this.ref.htmlEl.innerText,
|
||||
html: this.ref.htmlEl.innerHTML,
|
||||
});
|
||||
this.ref.htmlEl.focus();
|
||||
this.saveCheckpoint(
|
||||
this.ref.htmlEl.innerHTML,
|
||||
this.ref.htmlEl,
|
||||
getSelectionRange()
|
||||
);
|
||||
};
|
||||
|
||||
handleRef = ref => (
|
||||
@@ -40,7 +76,6 @@ class RTE extends React.Component {
|
||||
);
|
||||
|
||||
handleSelectionChange = () => {
|
||||
// console.log(window.getSelection().getRangeAt(0));
|
||||
this.forEachButton(b => {
|
||||
b.onSelectionChange && b.onSelectionChange();
|
||||
});
|
||||
@@ -88,27 +123,24 @@ class RTE extends React.Component {
|
||||
setTimeout(() => this.handleSelectionChange());
|
||||
};
|
||||
|
||||
handleKeyDown = () => {
|
||||
handleKeyDown = e => {
|
||||
// IE has issues not firing the onChange event.
|
||||
if (bowser.msie) {
|
||||
setTimeout(this.handleChange);
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyUp = () => {
|
||||
// IE has issues not firing the onChange event.
|
||||
if (bowser.msie) {
|
||||
setTimeout(this.handleChange);
|
||||
// Undo Redo
|
||||
if (e.key === 'z' && e.metaKey) {
|
||||
if (e.shiftKey) {
|
||||
this.handleRedo();
|
||||
} else {
|
||||
this.handleUndo();
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
this.handleSelectionChange();
|
||||
};
|
||||
|
||||
handleKeyPress = e => {
|
||||
this.handleSelectionChange();
|
||||
// IE has issues not firing the onChange event.
|
||||
if (bowser.msie) {
|
||||
setTimeout(this.handleChange);
|
||||
}
|
||||
// Newlines Or Special Enter Behaviors.
|
||||
if (e.key === 'Enter') {
|
||||
if (!e.shiftKey && this.handleSpecialEnter()) {
|
||||
this.handleChange();
|
||||
@@ -124,6 +156,69 @@ class RTE extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyUp = () => {
|
||||
// IE has issues not firing the onChange event.
|
||||
if (bowser.msie) {
|
||||
setTimeout(this.handleChange);
|
||||
}
|
||||
this.handleSelectionChange();
|
||||
};
|
||||
|
||||
restoreCheckpoint(html, node, range) {
|
||||
if (node && range) {
|
||||
// We need to clone it, otherwise we'll mutate
|
||||
// that original one which can still be in the undo stack.
|
||||
const [nodeCloned, rangeCloned] = cloneNodeAndRange(node, range);
|
||||
|
||||
// Remember range values, as `rangeCloned` can changed during
|
||||
// DOM manipulation.
|
||||
const startOffset = rangeCloned.startOffset;
|
||||
const endOffset = rangeCloned.startOffset;
|
||||
|
||||
// Rewrite startContainer if it was pointing to `nodeCloned`.
|
||||
const startContainer =
|
||||
rangeCloned.startContainer === nodeCloned
|
||||
? this.ref.htmlEl
|
||||
: rangeCloned.startContainer;
|
||||
|
||||
// Rewrite endContainer if it was pointing to `nodeCloned`.
|
||||
const endContainer =
|
||||
rangeCloned.endContainer === nodeCloned
|
||||
? this.ref.htmlEl
|
||||
: rangeCloned.endContainer;
|
||||
|
||||
// Replace children with the ones from nodeCloned.
|
||||
replaceNodeChildren(this.ref.htmlEl, nodeCloned);
|
||||
|
||||
// Now setup the selection range.
|
||||
const finalRange = document.createRange();
|
||||
finalRange.setStart(startContainer, startOffset);
|
||||
finalRange.setEnd(endContainer, endOffset);
|
||||
|
||||
// SELECT!
|
||||
replaceSelection(finalRange);
|
||||
} else {
|
||||
this.ref.htmlEl.innerHTML = html;
|
||||
}
|
||||
this.handleChange();
|
||||
}
|
||||
|
||||
handleUndo() {
|
||||
this.saveCheckpoint.flush();
|
||||
if (this.undo.canUndo()) {
|
||||
const [html, node, range] = this.undo.undo();
|
||||
this.restoreCheckpoint(html, node, range);
|
||||
}
|
||||
}
|
||||
|
||||
handleRedo() {
|
||||
this.saveCheckpoint.flush();
|
||||
if (this.undo.canRedo()) {
|
||||
const [html, node, range] = this.undo.redo();
|
||||
this.restoreCheckpoint(html, node, range);
|
||||
}
|
||||
}
|
||||
|
||||
renderButtons() {
|
||||
return this.props.buttons.map(b => {
|
||||
return React.cloneElement(b, {
|
||||
@@ -166,7 +261,6 @@ class RTE extends React.Component {
|
||||
<ContentEditable
|
||||
id={inputId}
|
||||
onMouseUp={this.handleMouseUp}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
onPaste={this.handlePaste}
|
||||
|
||||
@@ -337,8 +337,8 @@ export function getSelectedChildren(ancestor) {
|
||||
export function outdentNode(node, changeSelection) {
|
||||
const parentNode = node.parentNode;
|
||||
const offset = indexOfChildNode(parentNode, node);
|
||||
while (node.childNodes.length) {
|
||||
parentNode.insertBefore(node.childNodes[0], node);
|
||||
while (node.firstChild) {
|
||||
parentNode.insertBefore(node.firstChild, node);
|
||||
}
|
||||
parentNode.removeChild(node);
|
||||
|
||||
@@ -349,3 +349,38 @@ export function outdentNode(node, changeSelection) {
|
||||
replaceSelection(range);
|
||||
}
|
||||
}
|
||||
|
||||
function cloneNodeAndRangeHelper(node, range, rangeCloned) {
|
||||
const nodeCloned = node.cloneNode(false);
|
||||
node.childNodes.forEach(n =>
|
||||
nodeCloned.appendChild(cloneNodeAndRangeHelper(n, range, rangeCloned))
|
||||
);
|
||||
if (range.startContainer === node) {
|
||||
rangeCloned.setStart(nodeCloned, range.startOffset);
|
||||
}
|
||||
if (range.endContainer === node) {
|
||||
rangeCloned.setEnd(nodeCloned, range.endOffset);
|
||||
}
|
||||
return nodeCloned;
|
||||
}
|
||||
|
||||
export function cloneNodeAndRange(node, range) {
|
||||
const rangeCloned = range.cloneRange();
|
||||
const nodeCloned = cloneNodeAndRangeHelper(node, range, rangeCloned);
|
||||
if (
|
||||
rangeCloned.startContainer === range.startContainer ||
|
||||
rangeCloned.endContainer === range.endContainer
|
||||
) {
|
||||
throw new Error('Range not inside node');
|
||||
}
|
||||
return [nodeCloned, rangeCloned];
|
||||
}
|
||||
|
||||
export function replaceNodeChildren(node, node2) {
|
||||
while (node.firstChild) {
|
||||
node.removeChild(node.firstChild);
|
||||
}
|
||||
while (node2.firstChild) {
|
||||
node.appendChild(node2.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
export default class Undo {
|
||||
undoStack = [];
|
||||
redoStack = [];
|
||||
size;
|
||||
|
||||
constructor(size = 100) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.undoStack = [];
|
||||
this.redoStack = [];
|
||||
}
|
||||
|
||||
canUndo() {
|
||||
return this.undoStack.length > 1;
|
||||
}
|
||||
|
||||
canRedo() {
|
||||
return this.redoStack.length;
|
||||
}
|
||||
|
||||
undo() {
|
||||
if (!this.canUndo()) {
|
||||
throw new Error('Nothing to undo');
|
||||
}
|
||||
const cur = this.undoStack.pop();
|
||||
this.redoStack.push(cur);
|
||||
return this.undoStack[this.undoStack.length - 1];
|
||||
}
|
||||
|
||||
redo() {
|
||||
if (!this.canRedo()) {
|
||||
throw new Error('Nothing to redo');
|
||||
}
|
||||
const x = this.redoStack.pop();
|
||||
this.undoStack.push(x);
|
||||
return x;
|
||||
}
|
||||
|
||||
save(x, ...meta) {
|
||||
// Ignore if we already have that saved.
|
||||
if (
|
||||
this.undoStack.length &&
|
||||
this.undoStack[this.undoStack.length - 1][0] === x
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.undoStack.push([x, ...meta]);
|
||||
|
||||
// Adhere to maximum size.
|
||||
if (this.undoStack.length > this.size) {
|
||||
this.undoStack.splice(0, 1);
|
||||
}
|
||||
this.redoStack = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user