Dewi Morgan
Dewi Morgan

Reputation: 1239

Prevent text in rt tags (furigana) from being edited

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>

This displays the text: text "TEST" with furigana annotation "uftu"

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:

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

Answers (2)

Louys Patrice Bessette
Louys Patrice Bessette

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

Ibrahimtp
Ibrahimtp

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

Related Questions