@itrocks/contenteditable
Version:
Respectful contenteditable: avoids <div>, respects white-space: pre* styles line breaks
90 lines (89 loc) • 2.95 kB
JavaScript
export class HTMLEditableElement extends HTMLElement {
editable;
constructor(editable) {
super();
this.editable = editable;
}
}
export class ContentEditable {
element;
isActive = false;
keyDownEventListener = (event) => this.onKeyDown(event);
mutationObserver;
activate() {
const element = this.element;
if (!element.hasAttribute('contenteditable')) {
element.setAttribute('contenteditable', '');
}
if (this.isActive)
return;
this.isActive = true;
element.addEventListener('keydown', this.keyDownEventListener);
}
br() {
return getComputedStyle(this.element).whiteSpace.startsWith('pre')
? '\n'
: '<br>';
}
brNode() {
return (this.br() === '\n')
? document.createTextNode('\n')
: document.createElement('br');
}
constructor(element) {
this.element = element;
this.element.editable = this;
this.activate();
this.mutationObserver = new MutationObserver(mutations => mutations.forEach(mutation => {
if ((mutation.type !== 'attributes') || (mutation.attributeName !== 'contenteditable'))
return;
element.hasAttribute('contenteditable')
? this.activate()
: this.deactivate();
}));
this.mutationObserver.observe(element, { attributes: true });
}
deactivate() {
const element = this.element;
if (element.hasAttribute('contenteditable')) {
element.removeAttribute('contenteditable');
}
if (!this.isActive)
return;
this.isActive = false;
const text = element.innerHTML;
if (text.endsWith('<br>') && !text.endsWith(this.br() + '<br>')) {
element.innerHTML = text.slice(0, -4);
}
element.removeEventListener('keydown', this.keyDownEventListener);
}
onKeyDown(event) {
if (event.key !== 'Enter')
return;
event.preventDefault();
const selection = window.getSelection();
if (!selection?.rangeCount)
return;
const brNode = this.brNode();
const range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(brNode);
range.setStartAfter(brNode);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
const br = this.br();
const element = this.element;
const text = element.innerHTML;
if (text.endsWith(br) && !text.endsWith(br + '<br>')) {
element.appendChild(document.createElement('br'));
}
element.dispatchEvent(new Event('input'));
}
value() {
let text = this.element.innerHTML;
return text.endsWith('<br>')
? text.slice(0, -4)
: text;
}
}