in Chrome,
+ // it removes the div and replaces it with just a
inside the
+ // root. Detach the
; the _ensureBottomLine call will insert a new
+ // block.
+ if (node === self._root && (node = node.firstChild) && node.nodeName === 'BR') {
+ detach(node);
+ }
+ self._ensureBottomLine();
+ self.setRange(range);
+ } catch (error) {
+ didError(error);
+ }
+ },
+
+ // source/keyboard/Backspace.ts
+ Backspace = (self, event, range) => {
+ const root = self._root;
+ self._removeZWS();
+ // Record undo checkpoint.
+ self.saveUndoState(range);
+ // If not collapsed, delete contents
+ if (!range.collapsed) {
+ event.preventDefault();
+ deleteContentsOfRange(range, root);
+ afterDelete(self, range);
+ }
+ // If at beginning of block, merge with previous
+ else if (rangeDoesStartAtBlockBoundary(range, root)) {
+ event.preventDefault();
+ let current = getStartBlockOfRange(range, root);
+ let previous;
+ if (!current) {
+ return;
+ }
+ // In case inline data has somehow got between blocks.
+ fixContainer(current.parentNode, root);
+ // Now get previous block
+ previous = getPreviousBlock(current, root);
+ // Must not be at the very beginning of the text area.
+ if (previous) {
+ // If not editable, just delete whole block.
+ if (!previous.isContentEditable) {
+ detachUneditableNode(previous, root);
+ return;
+ }
+ // Otherwise merge.
+ mergeWithBlock(previous, current, range, root);
+ // If deleted line between containers, merge newly adjacent
+ // containers.
+ current = previous.parentNode;
+ while (current !== root && !current.nextSibling) {
+ current = current.parentNode;
+ }
+ if (current !== root && (current = current.nextSibling)) {
+ mergeContainers(current, root);
+ }
+ self.setSelection(range);
+ }
+ // If at very beginning of text area, allow backspace
+ // to break lists/blockquote.
+ else if (current) {
+ let parent = getClosest(current, root, 'UL,OL,BLOCKQUOTE');
+ if (parent) {
+ return ('BLOCKQUOTE' === parent.nodeName)
+ // Break blockquote
+ ? self.decreaseQuoteLevel(range)
+ // Break list
+ : self.decreaseListLevel(range);
+ }
+ self.setRange(range);
+ }
+ }
+ // Otherwise, leave to browser but check afterwards whether it has
+ // left behind an empty inline tag.
+ else {
+ self.setSelection(range);
+ setTimeout(() => afterDelete(self), 0);
+ }
+ },
+ // source/keyboard/Delete.ts
+ Delete = (self, event, range) => {
+ const root = self._root;
+ let current, next, originalRange,
+ cursorContainer, cursorOffset, nodeAfterCursor;
+ self._removeZWS();
+ // Record undo checkpoint.
+ self.saveUndoState(range);
+ // If not collapsed, delete contents
+ if (!range.collapsed) {
+ event.preventDefault();
+ deleteContentsOfRange(range, root);
+ afterDelete(self, range);
+ }
+ // If at end of block, merge next into this block
+ else if (rangeDoesEndAtBlockBoundary(range, root)) {
+ event.preventDefault();
+ if (current = getStartBlockOfRange(range, root)) {
+ // In case inline data has somehow got between blocks.
+ fixContainer(current.parentNode, root);
+ // Now get next block
+ // Must not be at the very end of the text area.
+ if (next = getNextBlock(current, root)) {
+ // If not editable, just delete whole block.
+ if (!next.isContentEditable) {
+ detachUneditableNode(next, root);
+ return;
+ }
+ // Otherwise merge.
+ mergeWithBlock(current, next, range, root);
+ // If deleted line between containers, merge newly adjacent
+ // containers.
+ next = current.parentNode;
+ while (next !== root && !next.nextSibling) {
+ next = next.parentNode;
+ }
+ if (next !== root && (next = next.nextSibling)) {
+ mergeContainers(next, root);
+ }
+ self.setRange(range);
+ }
+ }
+ }
+ // Otherwise, leave to browser but check afterwards whether it has
+ // left behind an empty inline tag.
+ else {
+ // But first check if the cursor is just before an IMG tag. If so,
+ // delete it ourselves, because the browser won't if it is not
+ // inline.
+ originalRange = range.cloneRange();
+ moveRangeBoundariesUpTree(range, root, root, root);
+ cursorContainer = range.endContainer;
+ cursorOffset = range.endOffset;
+ if (isElement(cursorContainer)) {
+ nodeAfterCursor = cursorContainer.childNodes[cursorOffset];
+ if (nodeAfterCursor?.nodeName === "IMG") {
+ event.preventDefault();
+ detach(nodeAfterCursor);
+ moveRangeBoundariesDownTree(range);
+ afterDelete(self, range);
+ return;
+ }
+ }
+ self.setSelection(originalRange);
+ setTimeout(() => afterDelete(self), 0);
+ }
+ },
// source/keyboard/KeyHandlers.ts
onKey = function(event) {
if (event.defaultPrevented) {
@@ -1372,7 +1616,6 @@ const
if (event[osKey]) { key = ctrlKey + key; }
// if (event.altKey) { key = 'alt-' + key; }
}
-
if (this._keyHandlers[key]) {
this._keyHandlers[key](this, event, range);
// !event.isComposing stops us from blatting Kana-Kanji conversion in Safari
@@ -1410,136 +1653,8 @@ const
}
},
keyHandlers = {
- backspace: (self, event, range) => {
- let root = self._root;
- self._removeZWS();
- // Record undo checkpoint.
- self.saveUndoState(range);
- // If not collapsed, delete contents
- if (!range.collapsed) {
- event.preventDefault();
- deleteContentsOfRange(range, root);
- afterDelete(self, range);
- }
- // If at beginning of block, merge with previous
- else if (rangeDoesStartAtBlockBoundary(range, root)) {
- event.preventDefault();
- let current = getStartBlockOfRange(range, root);
- let previous;
- if (!current) {
- return;
- }
- // In case inline data has somehow got between blocks.
- fixContainer(current.parentNode, root);
- // Now get previous block
- previous = getPreviousBlock(current, root);
- // Must not be at the very beginning of the text area.
- if (previous) {
- // If not editable, just delete whole block.
- if (!previous.isContentEditable) {
- detachUneditableNode(previous, root);
- return;
- }
- // Otherwise merge.
- mergeWithBlock(previous, current, range, root);
- // If deleted line between containers, merge newly adjacent
- // containers.
- current = previous.parentNode;
- while (current !== root && !current.nextSibling) {
- current = current.parentNode;
- }
- if (current !== root && (current = current.nextSibling)) {
- mergeContainers(current, root);
- }
- self.setSelection(range);
- }
- // If at very beginning of text area, allow backspace
- // to break lists/blockquote.
- else if (current) {
- let parent = getClosest(current, root, 'UL,OL,BLOCKQUOTE');
- if (parent) {
- return ('BLOCKQUOTE' === parent.nodeName)
- // Break blockquote
- ? self.decreaseQuoteLevel(range)
- // Break list
- : self.decreaseListLevel(range);
- }
- self.setRange(range);
- }
- }
- // Otherwise, leave to browser but check afterwards whether it has
- // left behind an empty inline tag.
- else {
- self.setSelection(range);
- setTimeout(() => afterDelete(self), 0);
- }
- },
- 'delete': (self, event, range) => {
- let root = self._root;
- let current, next, originalRange,
- cursorContainer, cursorOffset, nodeAfterCursor;
- self._removeZWS();
- // Record undo checkpoint.
- self.saveUndoState(range);
- // If not collapsed, delete contents
- if (!range.collapsed) {
- event.preventDefault();
- deleteContentsOfRange(range, root);
- afterDelete(self, range);
- }
- // If at end of block, merge next into this block
- else if (rangeDoesEndAtBlockBoundary(range, root)) {
- event.preventDefault();
- if (current = getStartBlockOfRange(range, root)) {
- // In case inline data has somehow got between blocks.
- fixContainer(current.parentNode, root);
- // Now get next block
- // Must not be at the very end of the text area.
- if (next = getNextBlock(current, root)) {
- // If not editable, just delete whole block.
- if (!next.isContentEditable) {
- detachUneditableNode(next, root);
- return;
- }
- // Otherwise merge.
- mergeWithBlock(current, next, range, root);
- // If deleted line between containers, merge newly adjacent
- // containers.
- next = current.parentNode;
- while (next !== root && !next.nextSibling) {
- next = next.parentNode;
- }
- if (next !== root && (next = next.nextSibling)) {
- mergeContainers(next, root);
- }
- self.setRange(range);
- }
- }
- }
- // Otherwise, leave to browser but check afterwards whether it has
- // left behind an empty inline tag.
- else {
- // But first check if the cursor is just before an IMG tag. If so,
- // delete it ourselves, because the browser won't if it is not
- // inline.
- originalRange = range.cloneRange();
- moveRangeBoundariesUpTree(range, root, root, root);
- cursorContainer = range.endContainer;
- cursorOffset = range.endOffset;
- if (isElement(cursorContainer)) {
- nodeAfterCursor = cursorContainer.childNodes[cursorOffset];
- if (nodeAfterCursor?.nodeName === 'IMG') {
- event.preventDefault();
- detach(nodeAfterCursor);
- moveRangeBoundariesDownTree(range);
- afterDelete(self, range);
- return;
- }
- }
- self.setSelection(originalRange);
- setTimeout(() => afterDelete(self), 0);
- }
- },
+// "backspace": Backspace,
+// "delete": Delete,
tab: (self, event, range) => {
let root = self._root;
let node, parent;
@@ -1624,8 +1739,6 @@ const
osKey = isMac ? 'metaKey' : 'ctrlKey',
- indexOf = (array, value) => Array.prototype.indexOf.call(array, value),
-
filterAccept = NodeFilter.FILTER_ACCEPT,
/*
typeToBitArray = {
@@ -1647,10 +1760,13 @@ const
isElement = node => node instanceof Element,
isTextNode = node => node instanceof Text,
- createTreeWalker = (root, whatToShow, filter) => doc.createTreeWalker(root, whatToShow, filter ? {
+ createTreeWalker = (root, whatToShow, filter) => {
+ const walker = doc.createTreeWalker(root, whatToShow, filter ? {
acceptNode: node => filter(node) ? filterAccept : NodeFilter.FILTER_SKIP
- } : null
- ),
+ } : null);
+ walker.previousPONode = () => previousPONode(walker, filter);
+ return walker;
+ },
setAttributes = (node, props) => {
props && Object.entries(props).forEach(([k,v]) => {
@@ -1671,66 +1787,6 @@ const
didError = error => console.error(error),
- createRange = (startContainer, startOffset, endContainer, endOffset) => {
- let range = doc.createRange();
- range.setStart(startContainer, startOffset);
- if (endContainer) {
- range.setEnd(endContainer, endOffset);
- } else {
- range.setEnd(startContainer, startOffset);
- }
- return range;
- },
-
- // If you delete the content inside a span with a font styling, Webkit will
- // replace it with a
tag (!). If you delete all the text inside a
- // link in Opera, it won't delete the link. Let's make things consistent. If
- // you delete all text inside an inline tag, remove the inline tag.
- afterDelete = (self, range) => {
- try {
- range = range || self.getSelection();
- let node = range.startContainer,
- parent;
- // Climb the tree from the focus point while we are inside an empty
- // inline element
- if (isTextNode(node)) {
- node = node.parentNode;
- }
- parent = node;
- while (isInline(parent) && (!parent.textContent || parent.textContent === ZWS)) {
- node = parent;
- parent = node.parentNode;
- }
- // If focused in empty inline element
- if (node !== parent) {
- // Move focus to just before empty inline(s)
- range.setStart(parent,
- indexOf(parent.childNodes, node));
- range.collapse(true);
- // Remove empty inline(s)
- node.remove();
- // Fix cursor in block
- if (!isBlock(parent)) {
- parent = getPreviousBlock(parent, self._root);
- }
- fixCursor(parent);
- // Move cursor into text node
- moveRangeBoundariesDownTree(range);
- }
- // If you delete the last character in the sole in Chrome,
- // it removes the div and replaces it with just a
inside the
- // root. Detach the
; the _ensureBottomLine call will insert a new
- // block.
- if (node === self._root && (node = node.firstChild) && node.nodeName === 'BR') {
- detach(node);
- }
- self._ensureBottomLine();
- self.setRange(range);
- } catch (error) {
- didError(error);
- }
- },
-
detachUneditableNode = (node, root) => {
let parent;
while ((parent = node.parentNode)) {
@@ -1757,29 +1813,38 @@ const
}
},
-/*
- // Previous node in post-order.
- previousPONode = walker => {
+ // TreeIterator Previous node in post-order.
+ previousPONode = (walker, filter) => {
let current = walker.currentNode,
root = walker.root,
- nodeType = walker.nodeType, // whatToShow?
- filter = walker.filter,
node;
- while (current) {
- node = current.lastChild;
- while (!node && current && current !== root) {
- node = current.previousSibling;
- if (!node) { current = current.parentNode; }
+ const isAcceptableNode = node => {
+ const nodeType = node.nodeType;
+ const nodeFilterType = nodeType === ELEMENT_NODE ? SHOW_ELEMENT : nodeType === TEXT_NODE ? SHOW_TEXT : 0;
+ return !!(nodeFilterType & walker.whatToShow) && filter(node);
+ }
+ while (true) {
+ if (current === root) {
+ return null;
}
- if (node && (typeToBitArray[node.nodeType] & nodeType) && filter(node)) {
+ node = current.previousSibling;
+ if (node) {
+ while (current = node.lastChild) {
+ node = current;
+ }
+ } else {
+ node = current.parentNode;
+ }
+ if (!node) {
+ return null;
+ }
+ if (isAcceptableNode(node)) {
walker.currentNode = node;
return node;
}
current = node;
}
- return null;
},
-*/
mergeObjects = (base, extras, mayOverride) => {
base = base || {};
@@ -1961,10 +2026,10 @@ keyHandlers[ctrlKey + '['] = changeIndentationLevel('decrease');
keyHandlers[ctrlKey + ']'] = changeIndentationLevel('increase');
keyHandlers[ctrlKey + 'd'] = mapKeyTo('toggleCode');
keyHandlers[ctrlKey + 'y'] = mapKeyTo('redo');
-keyHandlers['redo'] = mapKeyTo('redo');
-keyHandlers[ctrlKey + 'z'] = mapKeyTo('undo');
-keyHandlers['undo'] = mapKeyTo('undo');
+//keyHandlers[ctrlKey + 'z'] = mapKeyTo('undo');
keyHandlers[ctrlKey + 'shift-z'] = mapKeyTo('redo');
+keyHandlers['redo'] = mapKeyTo('redo');
+//keyHandlers['undo'] = mapKeyTo('undo');
class EditStack extends Array
{
@@ -2034,8 +2099,6 @@ class EditStack extends Array
this[undoIndex] = html;
this.index = undoIndex;
this.inUndoState = true;
- } else {
- console.error('EditStack in inUndoState');
}
}
@@ -2100,7 +2163,9 @@ class Squire
this.editStack = new EditStack(this);
this._ignoreChange = false;
- this._mutation = new MutationObserver(()=>this._docWasChanged());
+ this._mutation = new MutationObserver(()=>{
+ this._docWasChanged()
+ });
this._mutation.observe(root, {
childList: true,
attributes: true,
@@ -2148,8 +2213,11 @@ class Squire
this.setHTML('');
}
+ // https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes
_beforeInput(event) {
switch (event.inputType) {
+// case "insertFromPaste":
+// case "insertReplacementText":
case "insertText":
if (isAndroid && event.data && event.data.includes("\n")) {
event.preventDefault();
@@ -2177,7 +2245,7 @@ class Squire
break;
case "historyRedo":
event.preventDefault();
- this.redo();
+// this.redo();
break;
case "formatBold":
case "formaItalic":
@@ -2219,6 +2287,17 @@ class Squire
event.preventDefault();
this.setStyle({fontFamily:event.data});
break;
+/*
+// case "deleteByCut":
+ this.saveUndoState();
+ break;
+*/
+ case "deleteContentBackward":
+ Backspace(this, event, this.getSelection());
+ break;
+ case "deleteContentForward":
+ Delete(this, event, this.getSelection());
+ break;
}
}
@@ -2559,6 +2638,7 @@ class Squire
return this._root.innerHTML;
}
+ // Called by undo() and redo()
_setHTML(html) {
if (html !== undefined) {
const root = this._root;
@@ -3168,6 +3248,7 @@ class Squire
}
block = getStartBlockOfRange(range, root);
if (block && (parent = getClosest(block, root, "PRE"))) {
+ // TODO: also check style="white-space:pre*" ?
moveRangeBoundariesDownTree(range);
node = range.startContainer;
const offset2 = range.startOffset;