Reputation: 85
I was trying to create some buttons that will add markdown characters when click on it, so I used Selection API
to get selected text and add characters then back caret
to the end position of selected text and everything is done correctly.
When I try to press on undo Ctrl+z it's not back to the last text before adding the markdown characters I know that this because I changed the text node value.
Is there's a way to do that without effecting the node text and apply undo, redo?
let text = document.getElementById('test'),
btn = document.getElementById('btn')
//function to replace text by index
String.prototype.replaceAt = function(start, end, replacement) {
let text = '';
for (let i = 0; i < this.length; i++) {
if (i >= start && i < end) {
text += ''
if (i === end - 1) {
text += replacement
}
} else {
text += this[i]
}
}
return text
}
function addNewStr(callback) {
var sel = window.getSelection()
try {
var range = sel.getRangeAt(0),
r = document.createRange()
//check if there's text is selected
if (!sel.isCollapsed) {
let startPos = sel.anchorOffset,
endPos = sel.focusOffset,
node = sel.anchorNode,
value = sel.anchorNode.nodeValue,
selectedText = value.substring(startPos, endPos),
parent = node.parentNode
//function to determine if selection start from left to right or right to left
function checkPos(callback) {
if (startPos > endPos) {
return callback(startPos, endPos)
} else {
return callback(endPos, startPos)
}
}
if (typeof callback === 'function') {
//getting the new str from the callback
var replacement = callback(selectedText),
caretIndex;
//apply changes
node.nodeValue = checkPos(function(end, start) {
return node.nodeValue.replaceAt(start, end, replacement)
})
//check if the returned text from the callback is less or bigger than selected text to move caret to the end of selected text
if (replacement.length > selectedText.length) {
caretIndex = checkPos(function(pos) {
return pos + (replacement.length - selectedText.length);
})
} else if (selectedText.length > replacement.length) {
caretIndex = checkPos(function(pos) {
return (pos - selectedText.length) + (replacement.length);
})
}
//back caret to the end of the new position
r.setStart(parent.childNodes[0], caretIndex)
r.collapse(true)
sel.removeAllRanges()
sel.addRange(r)
}
}
} catch (err) {
console.log("Nothing is selected")
}
}
btn.addEventListener("click", function() {
addNewStr(function(str) {
return '__' + str + '__'
})
})
<div contenteditable="true" id="test" placeholder="insertText">
try to select me
</div>
<button id="btn">to strong</button>
Upvotes: 4
Views: 985
Reputation: 27275
Because you're changing the content programmatically, you're going to have to undo it programmatically.
In your button click handler:
const undoStack = [];
function onButtonClick (e) {
// capture the existing state
const textContent = text.innerText;
// create a function to set the div content to its current value
const undoFn = () => text.innerText = textContent;
// push the undo function into the array
undoStack.push(undoFn);
// ...then do whatever the button does...
}
With that in place, you can listen for ctrl-z
and invoke the most recent undo function:
// convenience to determine whether a keyboard event should trigger an undo
const isUndo = ({ctrlKey, metaKey, key}) => key === 'z' && (ctrlKey || metaKey);
// keydown event handler for undo
const keydown = (e) => {
if(isUndo(e) && undos.length) {
e.preventDefault();
undos.pop()();
}
}
// listen for keydowns
document.body.addEventListener('keydown', keydown);
There are some other considerations involved, e.g. whether certain user actions should clear the undo stack, but this is the basic idea.
In the interest of clarity I've replaced your content modification code to just add a number on each click.
const div = document.getElementById('test');
const button = document.querySelector('button');
const undos = [];
button.addEventListener('click', e => {
const text = div.innerText;
undos.push(() => div.innerText = text);
div.innerText += ` ${undos.length} `;
});
const isUndo = ({ctrlKey, metaKey, key}) => key === 'z' && (ctrlKey || metaKey);
const keydown = (e) => {
if(isUndo(e) && undos.length) {
e.preventDefault();
undos.pop()();
}
}
document.body.addEventListener('keydown', keydown);
<div contenteditable="true" id="test" placeholder="insertText">
this is some text
</div>
<button id="btn">to strong</button>
Upvotes: 2