9 changed files with 170 additions and 15 deletions
@ -0,0 +1,131 @@
@@ -0,0 +1,131 @@
|
||||
/* |
||||
Since we can't really import react-contenteditable here, I'm borrowing code for this component from here: |
||||
github.com/lovasoa/react-contenteditable/ |
||||
|
||||
and here: |
||||
https://stackoverflow.com/questions/22677931/react-js-onchange-event-for-contenteditable/27255103#27255103
|
||||
|
||||
*/ |
||||
|
||||
import { Component, createRef, createElement } from 'https://unpkg.com/preact?module'; |
||||
|
||||
function replaceCaret(el) { |
||||
// Place the caret at the end of the element
|
||||
const target = document.createTextNode(''); |
||||
el.appendChild(target); |
||||
// do not move caret if element was not focused
|
||||
const isTargetFocused = document.activeElement === el; |
||||
if (target !== null && target.nodeValue !== null && isTargetFocused) { |
||||
var sel = window.getSelection(); |
||||
if (sel !== null) { |
||||
var range = document.createRange(); |
||||
range.setStart(target, target.nodeValue.length); |
||||
range.collapse(true); |
||||
sel.removeAllRanges(); |
||||
sel.addRange(range); |
||||
} |
||||
if (el) el.focus(); |
||||
} |
||||
} |
||||
|
||||
function normalizeHtml(str) { |
||||
return str && str.replace(/ |\u202F|\u00A0/g, ' '); |
||||
} |
||||
|
||||
|
||||
|
||||
export default class ContentEditable extends Component { |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this.el = createRef(); |
||||
|
||||
this.lastHtml = ''; |
||||
|
||||
this.emitChange = this.emitChange.bind(this); |
||||
this.getDOMElement = this.getDOMElement.bind(this); |
||||
} |
||||
|
||||
shouldComponentUpdate(nextProps) { |
||||
const { props } = this; |
||||
const el = this.getDOMElement(); |
||||
|
||||
// We need not rerender if the change of props simply reflects the user's edits.
|
||||
// Rerendering in this case would make the cursor/caret jump
|
||||
|
||||
// Rerender if there is no element yet... (somehow?)
|
||||
if (!el) return true; |
||||
|
||||
// ...or if html really changed... (programmatically, not by user edit)
|
||||
if ( |
||||
normalizeHtml(nextProps.html) !== normalizeHtml(el.innerHTML) |
||||
) { |
||||
return true; |
||||
} |
||||
|
||||
// Handle additional properties
|
||||
return props.disabled !== nextProps.disabled || |
||||
props.tagName !== nextProps.tagName || |
||||
props.className !== nextProps.className || |
||||
props.innerRef !== nextProps.innerRef; |
||||
} |
||||
|
||||
|
||||
|
||||
componentDidUpdate() { |
||||
const el = this.getDOMElement(); |
||||
if (!el) return; |
||||
|
||||
// Perhaps React (whose VDOM gets outdated because we often prevent
|
||||
// rerendering) did not update the DOM. So we update it manually now.
|
||||
if (this.props.html !== el.innerHTML) { |
||||
el.innerHTML = this.props.html; |
||||
} |
||||
this.lastHtml = this.props.html; |
||||
replaceCaret(el); |
||||
} |
||||
|
||||
getDOMElement() { |
||||
return (this.props.innerRef && typeof this.props.innerRef !== 'function' ? this.props.innerRef : this.el).current; |
||||
} |
||||
|
||||
|
||||
emitChange(originalEvt) { |
||||
const el = this.getDOMElement(); |
||||
if (!el) return; |
||||
|
||||
const html = el.innerHTML; |
||||
if (this.props.onChange && html !== this.lastHtml) { |
||||
// Clone event with Object.assign to avoid
|
||||
// "Cannot assign to read only property 'target' of object"
|
||||
const evt = Object.assign({}, originalEvt, { |
||||
target: { |
||||
value: html |
||||
} |
||||
}); |
||||
this.props.onChange(evt); |
||||
} |
||||
this.lastHtml = html; |
||||
} |
||||
|
||||
render(props) { |
||||
const { html, innerRef } = props; |
||||
return createElement( |
||||
'div', |
||||
{ |
||||
...props, |
||||
ref: typeof innerRef === 'function' ? (current) => { |
||||
innerRef(current) |
||||
this.el.current = current |
||||
} : innerRef || this.el, |
||||
onInput: this.emitChange, |
||||
onBlur: this.props.onBlur || this.emitChange, |
||||
onKeyup: this.props.onKeyUp || this.emitChange, |
||||
onKeydown: this.props.onKeyDown || this.emitChange, |
||||
contentEditable: !this.props.disabled, |
||||
dangerouslySetInnerHTML: { __html: html }, |
||||
}, |
||||
this.props.children, |
||||
); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue