Отмена / повтор не работает при обновлении выделенного текста

#javascript #html #selection

Вопрос:

Я пытался создать несколько кнопок, которые будут добавлять символы уценки при нажатии на них, поэтому я использовал Selection API , чтобы получить выделенный текст и добавить символы, а затем вернуться caret в конечную позицию выделенного текста, и все сделано правильно.

Когда я пытаюсь нажать кнопку отменитьCtrl z, это не возвращается к последнему тексту перед добавлением символов уценки, я знаю, что это потому, что я изменил значение текстового узла.

Есть ли способ сделать это без изменения текста узла и применить отмену, повтор?

 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 amp;amp; 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> 

Ответ №1:

Поскольку вы изменяете содержимое программно, вам придется отменить его программно.

В вашем обработчике нажатия кнопки:

  • зафиксируйте существующее состояние содержимого, прежде чем изменять его
  • создайте функцию, которая возвращает его в это состояние.
  • поместите эту функцию в массив («стек отмены»).
 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...
}
 

Имея это на месте, вы можете прослушивать ctrl-z и вызывать самую последнюю функцию отмены:

 // convenience to determine whether a keyboard event should trigger an undo
const isUndo = ({ctrlKey, metaKey, key}) => key === 'z' amp;amp; (ctrlKey || metaKey);

// keydown event handler for undo
const keydown = (e) => {
  if(isUndo(e) amp;amp; undos.length) {
    e.preventDefault();
    undos.pop()();
  }
}

// listen for keydowns
document.body.addEventListener('keydown', keydown);
 

Есть и некоторые другие соображения, например, должны ли определенные действия пользователя очищать стек отмены, но это основная идея.


Демонстрация с подтверждением концепции

В интересах ясности я заменил ваш код модификации контента, чтобы просто добавлять номер при каждом щелчке.

 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' amp;amp; (ctrlKey || metaKey);

const keydown = (e) => {
    if(isUndo(e) amp;amp; 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> 

Комментарии:

1. я искал что-то, что заставит браузер делать это нормально, не создавая кнопки отмены, повтора. Есть ли что-то, что может сделать это изначально браузером ?

2. Вам не нужно создавать для этого кнопки. Смотрите демонстрацию выше.

3. я знаю, что это будет применено при нажатии Ctrl z , я имею в виду, что есть способ заставить браузер сделать это изначально, не добавляя дополнительный код для отмены, повтора