Reputation: 1239
I am trying to create a contenteditable div, in which the user, when editing, does not edit the text within certain HTML tags (or conversely, can ONLY edit the the text within certain tags: either achieves my goal).
For example, say the code is:
<div><ruby>
<rb>T</rb><rt>u</rt>
<rb>E</rb><rt>f</rt>
<rb>S</rb><rt>t</rt>
<rb>T</rb><rt>u</rt>
</ruby></div>
I want to make it so that as they move their cursor with the cursor keys, the cursor moves between the letters delimited with the <rb>
tag, "TEST", as if the letters wrapped in <rt>
; and that, whether they have insert active or not, they can overwrite and delete and insert additional regular characters, but their edits will not affect the <rt>
characters (yes, it's acceptable that if they mess this up, it will look weird, with hanging hiragana with no letters under it, etc: they should ideally be able to come back and add new characters to replace the deleted ones, to fix it up).
In another mode (with shift pressed, or whatever), I hope to make it the opposite: that they can only edit, delete, and move their insertion caret into, the furigana <rt>
characters "uftu", and cannot affect the regular <rb>
characters "TEST".
This gets close:
<div><ruby>
<rb>T</rb><rt contenteditable="false">u</rt>
<rb>E</rb><rt contenteditable="false">f</rt>
<rb>S</rb><rt contenteditable="false">t</rt>
<rb>T</rb><rt contenteditable="false">u</rt>
</ruby></div>
With contenteditable-false for the <rt>
tags (which could be toggled using JS), the caret cannot be moved up into the furigana text.
Unfortunately, there are still issues:
onkeydown... preventDefault()
.<rb>
tag to the left of the next. The only fix for this I can see for this is awful: event handlers to detect cursor movement; some way to find where the cursor moved from and to, and decide where I want it to end up; and tricks from "https://stackoverflow.com/questions/6249095/how-to-set-the-caret-cursor-position-in-a-contenteditable-element-div" to actually move the caret. That all sounds nightmare-kludgey, though, and I hope there's a cleaner way.Perhaps what I really want is just a way to explicitly specify where the valid "insertion caret points" are, and prevent the caret from going anywhere else?
I'd definitely like to avoid external libs (jQuery et al) if at all possible, though if it makes things way simpler because they've already solved this problem, then I'm willing to cave in: no point reinventing this wheel.
Upvotes: 0
Views: 72
Reputation: 33933
In the below solution, a locked
class is toggled on the rt
or rb
elements depending on shift
key being held down or not.
On keydown
, the class toggling occurs and the keyboard arrows are disabled, because it tries to select the text and causes issues.
On keyup
, the class toggling occurs again and, unless it was an arrow key, the "locked" values are restored from an array.
There is an annoying side effect caused by the restoring, where the caret position changes a bit...
But, on my part, I'm stopping here.
You can fix that by looking at getCaretPosition
in this answer and setCaretPosition
in this answer.
const ruby = document.querySelector("ruby");
const editables = ruby.querySelectorAll("rt,rb");
const arrows = ["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown"];
document.addEventListener("keyup", (event) => {
// Toggle the locked classes to rt
if (event.key === "Shift") {
toggleLocked("RT");
return;
}
// Allow keyboard arrows navigation without restoring uselessly
if (arrows.includes(event.key)) {
return;
}
// Restore the "locked values"
restoreLockedValues();
});
document.addEventListener("keydown", (event) => {
// Toggle the locked classes to rb
if (event.key === "Shift") {
toggleLocked("RB");
}
// Disallow keyboard arrows navigation while shift key is pressed
if (event.shiftKey && arrows.includes(event.key)) {
event.preventDefault()
}
});
const toggleLocked = (tag) => {
editables.forEach((el) => el.classList.toggle("locked", el.tagName === tag));
mem = getLockedValue();
console.log(mem);
};
const getLockedValue = () =>
Array.from(document.querySelectorAll(".locked")).map((el) => el.innerText);
const restoreLockedValues = () => {
Array.from(document.querySelectorAll(".locked")).forEach(
(el, index) => (el.innerText = mem[index])
);
};
// An array holding the currently locked values
let mem = getLockedValue();
div {
border: 1px solid black;
width: fit-content;
}
rt {
font-size: 1em;
}
rb {
font-size: 1.4em;
color: red;
}
<div><ruby contenteditable="true">
<rb>T</rb><rt class="locked">u</rt>
<rb>E</rb><rt class="locked">f</rt>
<rb>S</rb><rt class="locked">t</rt>
<rb>T</rb><rt class="locked">u</rt>
</ruby></div>
Upvotes: 1
Reputation: 77
Try this
<div id="editable">
<ruby>
<rb>T</rb><rt class="editable-furigana">u</rt>
<rb>E</rb><rt class="editable-furigana">f</rt>
<rb>S</rb><rt class="editable-furigana">t</rt>
<rb>T</rb><rt class="editabl``e-furigana">u</rt>
</ruby>
</div>
<script> const editableDiv = document.getElementById('editable');
const furiganaTags = editableDiv.getElementsByClassName('editable-furigana');
editableDiv.addEventListener('keydown', function (event) {
const caretPosition = getCaretCharacterOffsetWithin(editableDiv);
const isShiftPressed = event.shiftKey;
if (isShiftPressed) {
// Allow editing only in the furigana tags
for (const tag of furiganaTags) {
tag.setAttribute('contenteditable', 'true');
}
} else {
// Prevent editing in the furigana tags
for (const tag of furiganaTags) {
tag.setAttribute('contenteditable', 'false');
}
}
// Handle backspace key
if (event.key === 'Backspace') {
if (isShiftPressed) {
event.preventDefault();
return;
}
const prevChar = editableDiv.textContent.charAt(caretPosition - 1);
if (prevChar === ' ') {
event.preventDefault();
return;
}
}
// Handle caret movement between <rb> tags
if (!isShiftPressed && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
const currentChar = editableDiv.textContent.charAt(caretPosition);
const nextChar = editableDiv.textContent.charAt(caretPosition + 1);
if (event.key === 'ArrowLeft' && nextChar === ' ') {
setCaretPosition(editableDiv, caretPosition + 1);
} else if (event.key === 'ArrowRight' && currentChar === ' ') {
setCaretPosition(editableDiv, caretPosition - 1);
}
}
});
// Helper function to get the caret position within the div
function getCaretCharacterOffsetWithin(element) {
let caretOffset = 0;
const doc = element.ownerDocument || element.document;
const win = doc.defaultView || doc.parentWindow;
const sel = win.getSelection();
if (sel.rangeCount > 0) {
const range = sel.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(element);
preCaretRange.setEnd(range.endContainer, range.endOffset);
caretOffset = preCaretRange.toString().length;
}
return caretOffset;
}
// Helper function to set the caret position within the div
function setCaretPosition(element, offset) {
const range = document.createRange();
const sel = window.getSelection();
range.setStart(element.firstChild, offset);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
</script>
Upvotes: 1