JSFiddle - React, Tailwind, and code Playground
HTML
<p class="parent" contenteditable="true">Bacon ipsum dolor amet jowl chicken pork loin <span class="move">[move text]</span>tail. Short ribs meatball
<br>bresaola beef boudin hamburger, cow rump swine. Pork belly ribeye leberkas venison
<br>ground <span class="move">[move text]</span>round shoulder pork <span class="move">[move text]</span>chop, strip steak drumstick doner t-bone alcatra pancetta turkey boudin.
<br>Filet mignon pork chop tongue t-bone jowl strip steak. Cow spare ribs bresaola, tail
<br>chicken brisket pastrami shank strip steak ribeye biltong pancetta short
<br>ribs corned beef meatloaf. <span class="move">[move text]</span>Hamburger meatloaf pig strip steak.</p>
CSS
.parent {
background-color: #F7F7F7;
}
.move {
background-color: khaki;
margin: 0 2px;
}
JavaScript
$('p[contenteditable]').on('keydown', function (e) {
// Only handle event if ctrl+left / ctrl+right
if (!e.ctrlKey || (e.which != 37 && e.which != 39)) return;
// and selection is in a span.move
var sel = document.getSelection();
var node = sel.anchorNode;
if (!node || node != sel.focusNode) return;
// Text "[move text]" is in selection
if (node.nodeType == Node.TEXT_NODE) {
node = node.parentNode;
}
if (!node || node.nodeName != 'SPAN' || node.className != 'move') return;
moveSpan(node, e.which == 37); // 37: left, 39: right
e.preventDefault();
e.stopPropagation();
});
function moveSpan (span, toLeft) {
var left = span.previousSibling;
var right = span.nextSibling;
if (!left || left.nodeType != Node.TEXT_NODE || !right || right.nodeType != Node.TEXT_NODE) return;
if (toLeft && !left.length) return;
if (!toLeft && !right.length) return;
if (toLeft) {
right.insertData(0, left.data[left.length - 1]);
left.deleteData(left.length - 1, 1);
} else {
left.appendData(right.data[0]);
right.deleteData(0, 1);
}
}