Created
June 29, 2016 15:09
-
-
Save slorber/b84c6c7c2d2c3c6250c53a001b674688 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
var _ = require('lodash'); | |
var $ = require("jquery"); | |
var React = require("react"); | |
var ReactDOM = require("react-dom"); | |
/* | |
This component is technical only. | |
It should have no dependency on stample code! | |
*/ | |
var UncontrolledContentEditable = React.createClass({ | |
propTypes: { | |
// The HTML to set in this content editable | |
html: React.PropTypes.string.isRequired, | |
// What to do on changes (key input,paste...) | |
onChange: React.PropTypes.func.isRequired, | |
// Gives the possibility to "reformat" the dom tree before emitting the change | |
parser: React.PropTypes.func, | |
// Adds a placeholder attribute to the div. | |
// Notice that it requires some CSS to work: | |
// [contenteditable][placeholder]:empty:not(:focus):before { content: attr(placeholder); } | |
placeholder: React.PropTypes.string, | |
// Permits to eventually "lock" the contenteditable. It means that the contenteditable will not be editable anymore | |
// This is useful to change from edition to read-only mode | |
locked: React.PropTypes.bool, | |
// This can be useful to be able to disable the listening of changes | |
// This allow external libraries to be able to manipulate the content without triggering parsing/change callback | |
// This was primarily done to be able to display annotations on text, using AnnotatorJS | |
disableChangeListener: React.PropTypes.bool | |
}, | |
api() { | |
const contentEditable = ReactDOM.findDOMNode(this); | |
const self = this; | |
return { | |
// Insert HTML/node to the last place where the cursor was at when the selection was saved | |
// If no cursor selection was saved, then the html node will be added at the end | |
insertHtml(htmlOrNode) { | |
const node = $(htmlOrNode)[0]; | |
console.debug("insertHtml at range=",self._savedSelectionRange || "END"); | |
restoreSelectionRange(self._savedSelectionRange); | |
self._savedSelectionRange = undefined; | |
insertNodeOverSelection(node,contentEditable); | |
self.emitChange(); | |
self.clearSelectionRanges(); | |
}, | |
// Before displaying an insertHtml/embed element popup, you can call this, so that when you decide | |
// to insertHtml, it will insert the html at the correct place | |
saveSelectionRangeForNextInsertHtml() { | |
if ( self._temporaryLastSelectionRange ) { | |
self._savedSelectionRange = self._temporaryLastSelectionRange; | |
console.debug("Selection range saved: ",self._savedSelectionRange); | |
} | |
} | |
} | |
}, | |
clearSelectionRanges() { | |
this._temporaryLastSelectionRange = undefined; | |
this._savedSelectionRange = undefined; | |
}, | |
shouldComponentUpdate: function(nextProps) { | |
// Special case to avoid cursor jumps | |
// See http://stackoverflow.com/a/27255103/82609 | |
var htmlChanged = nextProps.html !== ReactDOM.findDOMNode(this).innerHTML; | |
// | |
var lockedChanged = this.props.locked !== nextProps.locked; | |
var onChangeChanged = this.props.onChange !== nextProps.onChange; | |
var parserChanged = this.props.parser !== nextProps.parser; | |
var placeholderChanged = this.props.placeholder !== nextProps.placeholder; | |
var shouldUpdate = htmlChanged || lockedChanged || onChangeChanged || parserChanged || placeholderChanged; | |
if ( shouldUpdate ) { | |
// console.debug("UncontrolledContentEditable -> should update ",htmlChanged,lockedChanged,onChangeChanged,parserChanged,placeholderChanged); | |
} | |
return shouldUpdate; | |
}, | |
componentDidUpdate: function() { | |
const node = ReactDOM.findDOMNode(this); | |
const html = this.props.html; | |
// Bypass VDOM diff by React and updates the dom if it is not correct | |
// See http://stackoverflow.com/a/27255103/82609 | |
if ( node.innerHTML !== html ) { | |
node.innerHTML = this.props.html; | |
// If DOM node is still different after that, it's because the browser decided to reformat it! | |
// we emit the reformatting as a change so that we are sync with the store! | |
// (otherwise shouldComponentUpdate may not kick in next time, leading to re-render!) | |
// See https://github.com/lovasoa/react-contenteditable/issues/18 | |
const finalInnerHtml = node.innerHTML; | |
if ( finalInnerHtml !== html ) { | |
this.props.onChange(finalInnerHtml); | |
} | |
} | |
this.clearSelectionRanges(); | |
}, | |
emitChange: function() { | |
if ( this.props.disableChangeListener ) { | |
return; | |
} | |
if ( this.props.parser ) { | |
this.props.parser(ReactDOM.findDOMNode(this)); | |
} | |
var html = ReactDOM.findDOMNode(this).innerHTML; | |
if ( html !== this.lastHtml) { | |
this.props.onChange(html); | |
} | |
this.lastHtml = html; | |
this.clearSelectionRanges(); | |
}, | |
handleBlur(e) { | |
if ( this.props.onBlur ) { | |
this.props.onBlur(e) | |
} | |
// We have to store the selectionRange before the blur event default behavior because otherwise the selection become unavailable | |
// But we store it for a very limited amount of time under which it is allowed to save that range through the API | |
// Used to embed html (img/video) in correct place of the doc | |
this._temporaryLastSelectionRange = getSelectionRange(ReactDOM.findDOMNode(this)); | |
console.debug("OnBlur, selectionRange staged:",this._temporaryLastSelectionRange); | |
if ( this._temporaryLastSelectionRangeTimeout ) clearTimeout(this._temporaryLastSelectionRangeTimeout); | |
this._temporaryLastSelectionRangeTimeout = setTimeout(() => this._temporaryLastSelectionRange = undefined,100); | |
}, | |
render: function() { | |
return <div {...this.props} | |
onBlur={this.handleBlur} | |
onInput={this.emitChange} | |
data-placeholder={this.props.placeholder} | |
contentEditable={this.props.locked ? "false" : "true"} | |
dangerouslySetInnerHTML={{__html: this.props.html}}></div>; | |
} | |
}); | |
module.exports = UncontrolledContentEditable; | |
// See http://stackoverflow.com/a/4826688/82609 | |
function isOrContainsNode(ancestor, descendant) { | |
var node = descendant; | |
while (node) { | |
if (node === ancestor) return true; | |
node = node.parentNode; | |
} | |
return false; | |
} | |
function insertNodeOverSelection(node, containerNode) { | |
var sel, range, html; | |
if (window.getSelection) { | |
sel = window.getSelection(); | |
if (sel.getRangeAt && sel.rangeCount) { | |
range = sel.getRangeAt(0); | |
if (isOrContainsNode(containerNode, range.commonAncestorContainer)) { | |
range.deleteContents(); | |
range.insertNode(node); | |
console.debug("1"); | |
// Move the caret immediately after the inserted node | |
range.setStartAfter(node); | |
range.collapse(true); | |
} else { | |
console.debug("2",containerNode,node); | |
console.debug("2",containerNode.innerHTML); | |
containerNode.appendChild(node); | |
console.debug("2",containerNode.innerHTML); | |
} | |
} else { | |
console.debug("3"); | |
containerNode.appendChild(node); | |
} | |
} | |
// for IE I think | |
else if (document.selection && document.selection.createRange) { | |
range = document.selection.createRange(); | |
if (isOrContainsNode(containerNode, range.parentElement())) { | |
html = (node.nodeType == 3) ? node.data : node.outerHTML; | |
range.pasteHTML(html); | |
} else { | |
containerNode.appendChild(node); | |
} | |
} | |
else { | |
containerNode.appendChild(node); | |
} | |
} | |
function getSelectionRange(containerNode) { | |
if (window.getSelection) { | |
var sel = window.getSelection(); | |
if (sel.getRangeAt && sel.rangeCount) { | |
var range = sel.getRangeAt(0); | |
if ( isOrContainsNode(containerNode, range.commonAncestorContainer) ) { | |
return range.cloneRange(); | |
} | |
} else if (document.selection && document.selection.createRange) { | |
return document.selection.createRange(); | |
} | |
} | |
} | |
function restoreSelectionRange(range) { | |
if ( range ) { | |
if (window.getSelection) { | |
var sel = window.getSelection(); | |
sel.removeAllRanges(); | |
sel.addRange(range); | |
} else if (document.selection && range.select) { | |
range.select(); | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment