防止rt标签中的文本(振り仮名)被编辑

huangapple go评论53阅读模式
英文:

Prevent text in rt tags (furigana) from being edited

问题

我正在尝试创建一个可编辑的 div,其中用户在编辑时不会编辑特定 HTML 标记内的文本(或者反过来,只能编辑某些标记内的文本:任何方式都可以实现我的目标)。

例如,假设代码如下:

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

这会显示文本:
防止rt标签中的文本(振り仮名)被编辑

我希望使他们在使用光标键移动光标时,光标会在&lt;rb&gt;标记之间的字母之间移动,即"TEST",就像字母包裹在&lt;rt&gt;中一样;而无论他们是否启用插入功能,他们都可以覆盖、删除和插入额外的普通字符,但是他们的编辑不会影响&lt;rt&gt;字符(是的,如果他们搞砸了,看起来会很奇怪,下面没有字母的挂名字母等等:他们理想情况下应该能够回来添加新字符以替换已删除的字符,以修复它)。

在另一种模式下(按住Shift键或其他方式),我希望恰好相反:他们只能编辑、删除并将插入光标移动到挂名字符&lt;rt&gt;"uftu"中,不能影响普通的&lt;rb&gt;字符"TEST"。

这已经很接近:

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

通过为&lt;rt&gt;标记设置contenteditable-false(可以使用JS切换),光标无法移动到挂名文本中。

不幸的是,仍然存在一些问题:

  • 退格键仍然会删除它们。我可能可以通过onkeydown... preventDefault()来修复。
  • 选择它们并按键会删除它们。我可以通过防止它们被选择来修复,也许可以使用相关问题“https://stackoverflow.com/questions/13438391/preventing-text-in-rt-tags-furigana-from-being-selected”的方法。
  • 光标键需要按两次,每个字母之间都要按一次,因为光标从&lt;rb&gt;标记的右侧移动到下一个标记的左侧。我唯一能想到的修复方法是非常糟糕的:事件处理程序来检测光标移动;一种找出光标从哪里移动到哪里的方式,并决定我希望它停在哪里的方法;以及来自“https://stackoverflow.com/questions/6249095/how-to-set-the-caret-cursor-position-in-a-contenteditable-element-div”的技巧,实际上移动光标。但所有这些听起来都很糟糕,我希望有一种更简洁的方法。

也许我真正想要的是一种明确指定有效的“插入光标点”的方法,并防止光标移动到其他任何地方?

我肯定想尽量避免使用外部库(如jQuery等),但如果因为它们已经解决了这个问题而使事情变得更简单,那么我愿意妥协:没有重新发明这个轮子的必要。

英文:

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:

&lt;div&gt;&lt;ruby&gt;
&lt;rb&gt;T&lt;/rb&gt;&lt;rt&gt;u&lt;/rt&gt;
&lt;rb&gt;E&lt;/rb&gt;&lt;rt&gt;f&lt;/rt&gt;
&lt;rb&gt;S&lt;/rb&gt;&lt;rt&gt;t&lt;/rt&gt;
&lt;rb&gt;T&lt;/rb&gt;&lt;rt&gt;u&lt;/rt&gt;
&lt;/ruby&gt;&lt;/div&gt;

This displays the text:
防止rt标签中的文本(振り仮名)被编辑

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 &lt;rb&gt; tag, "TEST", as if the letters wrapped in &lt;rt&gt;; 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 &lt;rt&gt; 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 &lt;rt&gt; characters "uftu", and cannot affect the regular &lt;rb&gt; characters "TEST".

This gets close:

&lt;div&gt;&lt;ruby&gt;
&lt;rb&gt;T&lt;/rb&gt;&lt;rt contenteditable=&quot;false&quot;&gt;u&lt;/rt&gt;
&lt;rb&gt;E&lt;/rb&gt;&lt;rt contenteditable=&quot;false&quot;&gt;f&lt;/rt&gt;
&lt;rb&gt;S&lt;/rb&gt;&lt;rt contenteditable=&quot;false&quot;&gt;t&lt;/rt&gt;
&lt;rb&gt;T&lt;/rb&gt;&lt;rt contenteditable=&quot;false&quot;&gt;u&lt;/rt&gt;
&lt;/ruby&gt;&lt;/div&gt;

With contenteditable-false for the &lt;rt&gt; tags (which could be toggled using JS), the caret cannot be moved up into the furigana text.

Unfortunately, there are still issues:

  • backspace still deletes them. I could maybe fix with onkeydown... preventDefault().
  • selecting them and hitting a key deletes them. I could maybe fix by preventing them from being selected, perhaps using methods from the related question "https://stackoverflow.com/questions/13438391/preventing-text-in-rt-tags-furigana-from-being-selected".
  • the cursor keys need to be pressed a second time between each letter, as the caret moves from being to the right of &lt;rb&gt; 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.

答案1

得分: 1

请尝试以下代码:

<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="editable-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) {
      // 只允许在furigana标签中进行编辑
      for (const tag of furiganaTags) {
        tag.setAttribute('contenteditable', 'true');
      }
    } else {
      // 防止在furigana标签中编辑
      for (const tag of furiganaTags) {
        tag.setAttribute('contenteditable', 'false');
      }
    }

    // 处理退格键
    if (event.key === 'Backspace') {
      if (isShiftPressed) {
        event.preventDefault();
        return;
      }

      const prevChar = editableDiv.textContent.charAt(caretPosition - 1);
      if (prevChar === ' ') {
        event.preventDefault();
        return;
      }
    }

    // 处理caret在<rb>标签之间移动
    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);
      }
    }
  });

  // 获取div中的caret位置的辅助函数
  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;
  }

  // 在div中设置caret位置的辅助函数
  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>
英文:

Try this

  &lt;div id=&quot;editable&quot;&gt;
&lt;ruby&gt;
&lt;rb&gt;T&lt;/rb&gt;&lt;rt class=&quot;editable-furigana&quot;&gt;u&lt;/rt&gt;
&lt;rb&gt;E&lt;/rb&gt;&lt;rt class=&quot;editable-furigana&quot;&gt;f&lt;/rt&gt;
&lt;rb&gt;S&lt;/rb&gt;&lt;rt class=&quot;editable-furigana&quot;&gt;t&lt;/rt&gt;
&lt;rb&gt;T&lt;/rb&gt;&lt;rt class=&quot;editabl``e-furigana&quot;&gt;u&lt;/rt&gt;
&lt;/ruby&gt;
&lt;/div&gt;
&lt;script&gt; const editableDiv = document.getElementById(&#39;editable&#39;);
const furiganaTags = editableDiv.getElementsByClassName(&#39;editable-furigana&#39;);
editableDiv.addEventListener(&#39;keydown&#39;, 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(&#39;contenteditable&#39;, &#39;true&#39;);
}
} else {
// Prevent editing in the furigana tags
for (const tag of furiganaTags) {
tag.setAttribute(&#39;contenteditable&#39;, &#39;false&#39;);
}
}
// Handle backspace key
if (event.key === &#39;Backspace&#39;) {
if (isShiftPressed) {
event.preventDefault();
return;
}
const prevChar = editableDiv.textContent.charAt(caretPosition - 1);
if (prevChar === &#39; &#39;) {
event.preventDefault();
return;
}
}
// Handle caret movement between &lt;rb&gt; tags
if (!isShiftPressed &amp;&amp; (event.key === &#39;ArrowLeft&#39; || event.key === &#39;ArrowRight&#39;)) {
const currentChar = editableDiv.textContent.charAt(caretPosition);
const nextChar = editableDiv.textContent.charAt(caretPosition + 1);
if (event.key === &#39;ArrowLeft&#39; &amp;&amp; nextChar === &#39; &#39;) {
setCaretPosition(editableDiv, caretPosition + 1);
} else if (event.key === &#39;ArrowRight&#39; &amp;&amp; currentChar === &#39; &#39;) {
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 &gt; 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);
}
&lt;/script&gt;

答案2

得分: 1

在下面的解决方案中,根据是否按下“Shift”键,将“locked”类切换到“rt”或“rb”元素上。

在“keydown”事件上,发生类切换,并禁用键盘箭头,因为它尝试选择文本并导致问题。

在“keyup”事件上,再次发生类切换,并且除非是箭头键,否则会从数组中恢复“locked”值。

由于恢复引起的一个讨厌的副作用是光标位置会稍微改变...
但就我而言,我就到此为止了。

您可以通过查看此答案中的“getCaretPosition”(https://stackoverflow.com/a/3976125/2159528)和“setCaretPosition”(https://stackoverflow.com/a/36953852/2159528)来修复这个问题。

const ruby = document.querySelector("ruby");
const editables = ruby.querySelectorAll("rt,rb");
const arrows = ["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown"];

document.addEventListener("keyup", (event) => {
  // 切换rt上的locked类
  if (event.key === "Shift") {
    toggleLocked("RT");
    return;
  }
  // 允许键盘箭头导航而不会无用地恢复
  if (arrows.includes(event.key)) {
    return;
  }
  // 恢复"locked values"
  restoreLockedValues();
});

document.addEventListener("keydown", (event) => {
  // 切换rb上的locked类
  if (event.key === "Shift") {
    toggleLocked("RB");
  }
  // 在按住Shift键时禁止键盘箭头导航
  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])
  );
};

// 一个包含当前锁定值的数组
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>
英文:

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.

<!-- begin snippet: js hide: false console: true babel: false -->

<!-- language: lang-js -->

const ruby = document.querySelector(&quot;ruby&quot;);
const editables = ruby.querySelectorAll(&quot;rt,rb&quot;);
const arrows = [&quot;ArrowUp&quot;, &quot;ArrowLeft&quot;, &quot;ArrowRight&quot;, &quot;ArrowDown&quot;];
document.addEventListener(&quot;keyup&quot;, (event) =&gt; {
// Toggle the locked classes to rt
if (event.key === &quot;Shift&quot;) {
toggleLocked(&quot;RT&quot;);
return;
}
// Allow keyboard arrows navigation without restoring uselessly
if (arrows.includes(event.key)) {
return;
}
// Restore the &quot;locked values&quot;
restoreLockedValues();
});
document.addEventListener(&quot;keydown&quot;, (event) =&gt; {
// Toggle the locked classes to rb
if (event.key === &quot;Shift&quot;) {
toggleLocked(&quot;RB&quot;);
}
// Disallow keyboard arrows navigation while shift key is pressed
if (event.shiftKey &amp;&amp; arrows.includes(event.key)) {
event.preventDefault()
}
});
const toggleLocked = (tag) =&gt; {
editables.forEach((el) =&gt; el.classList.toggle(&quot;locked&quot;, el.tagName === tag));
mem = getLockedValue();
console.log(mem);
};
const getLockedValue = () =&gt;
Array.from(document.querySelectorAll(&quot;.locked&quot;)).map((el) =&gt; el.innerText);
const restoreLockedValues = () =&gt; {
Array.from(document.querySelectorAll(&quot;.locked&quot;)).forEach(
(el, index) =&gt; (el.innerText = mem[index])
);
};
// An array holding the currently locked values
let mem = getLockedValue();

<!-- language: lang-css -->

div {
border: 1px solid black;
width: fit-content;
}
rt {
font-size: 1em;
}
rb {
font-size: 1.4em;
color: red;
}

<!-- language: lang-html -->

&lt;div&gt;&lt;ruby contenteditable=&quot;true&quot;&gt;
&lt;rb&gt;T&lt;/rb&gt;&lt;rt class=&quot;locked&quot;&gt;u&lt;/rt&gt;
&lt;rb&gt;E&lt;/rb&gt;&lt;rt class=&quot;locked&quot;&gt;f&lt;/rt&gt;
&lt;rb&gt;S&lt;/rb&gt;&lt;rt class=&quot;locked&quot;&gt;t&lt;/rt&gt;
&lt;rb&gt;T&lt;/rb&gt;&lt;rt class=&quot;locked&quot;&gt;u&lt;/rt&gt;
&lt;/ruby&gt;&lt;/div&gt;

<!-- end snippet -->

huangapple
  • 本文由 发表于 2023年7月10日 11:15:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/76650485.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定