mirror of
https://github.com/wassname/talk.git
synced 2026-07-25 13:30:59 +08:00
Add comments
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user