切换带有 JavaScript 的特定文本未按预期工作

Toggle a specific Text with JavaScript not working as expected

我有一个切换按钮,可以更改一些文本。我 运行 遇到的问题是,如果我有 2 个单词并且我想更改其中一个的文本,它会更改一个,但是当我将其关闭时,样式将从两个跨度而不是所选文本的跨度中删除。

如何从选定的特定文本中删除跨度并将跨度保留在其他文本上?

function headuppercase(e) {
  tags('span', 'sC');
}

function tags(tag, clas) {
  var ele = document.createElement(tag);
  ele.classList.add(clas);
  wrap(ele);
}

function wrap(tags) {
  var el = document.querySelector('span.sC');
  sel = window.getSelection();
  if (!el) {
    if (sel.rangeCount && sel.getRangeAt) {
      range = sel.getRangeAt(0);
    }
    document.designMode = "on";
    if (range) {
      sel.removeAllRanges();
      sel.addRange(range);
    }
    range.surroundContents(tags);
  } else {
    var parent = el.parentNode;
    while (el.firstChild) parent.insertBefore(el.firstChild, el);
    parent.removeChild(el);
  }
  document.designMode = "off";
}
.ourbutton {
  padding: 5px;
  float: left;
  font-variant: small-caps;
}

.container {
  width: 200px;
  height: 300px;
  float: left;
}

.spanA {
  width: 100px;
  height: 80px;
  max-width: 200px;
  max-height: 300px;
  float: left;
  border: thin blue solid;
}

.sC {
  font-variant: small-caps;
}
<button class="ourbutton" type="button" onclick="headuppercase();">Tt</button>
<div class="container">
  <span class="spanA" contenteditable="true"></span>
</div>

否 jQuery 请。谢谢你!

问题出在这个 document.querySelector('span.sC') 上。在所有情况下,它都会 select 第一个 sC 跨度,这不好,因为你必须处理当前的跨度。

这里有一个修复思路:

function headuppercase(e) {
  tags('span', 'sC');
}

function tags(tag, clas) {
  var ele = document.createElement(tag);
  ele.classList.add(clas);
  wrap(ele);
}

function wrap(tags) {
  sel = window.getSelection();
    if (sel.rangeCount && sel.getRangeAt) {
      range = sel.getRangeAt(0);
    }
    document.designMode = "on";
    if (range) {
      sel.removeAllRanges();
      sel.addRange(range);
    }
    range.surroundContents(tags);
    if(tags.querySelector('.sC')) {
      tags.classList.remove('sC');
      tags.innerHTML=tags.querySelector('.sC').innerHTML;
    }
  document.designMode = "off";
}
.ourbutton {
  padding: 5px;
  float: left;
  font-variant: small-caps;
}

.container {
  width: 200px;
  height: 300px;
  float: left;
}

.spanA {
  width: 100px;
  height: 80px;
  max-width: 200px;
  max-height: 300px;
  float: left;
  border: thin blue solid;
}

.sC {
  font-variant: small-caps;
}
<button class="ourbutton" type="button" onclick="headuppercase();">Tt</button>
<div class="container">
  <span class="spanA" contenteditable="true"></span>
</div>

由于您需要处理所有边缘情况,所以如果没有库,这就不太容易实现。我将首先介绍一些边缘情况,然后举例说明如何实现它们。

简单案例

假设我们在文本节点中有以下字符串

"Nobody wants too complex code because it becomes unmanageable."

假设用户选择了单词 "Nobody wants" 并按下了小型大写字母切换按钮。您最终应该得到如下所示的内容(其中粗体部分代表小型大写字母的文本):

"Nobody wants too complex code because it becomes unmanageable."

这是一个简单的案例。只需将段“Nobody wants”包裹在 <span> 中,然后给它 sC class。所有其他尚未进入小型股的细分市场也是如此。这里没什么难的。

边缘案例 1

但是假设您处于以下状态(同样,粗体部分代表小型大写字母的文本):

"Nobody wants too complex code because it becomes unmanageable."

当用户选择并切换单词 "becomes" 时,事情变得复杂起来。你必须:

  1. 从包含 <span class="sC"> 的元素中删除最后两个词
  2. 在跨度后添加单词 "becomes"(来自步骤 1。)并确保它不包含在具有 class姓名sC.
  3. 在文本节点 "becomes"[=82= 之后的新 <span class="sC"> 元素内添加单词 "unmanageable" ](在第 2 步中插入。)

边缘情况 2

或者假设您处于以下状态(同样,粗体部分代表小型大写字母的文本):

"Nobody wants too complex code because it becomes unmanageable."

当有人选择并切换段 "wants too complex code because" 时会发生什么?可以说:让这个段中的每个字符

  • small-caps : 当它不是
  • 恢复正常 : 当前为小盘时

很容易看出您将再次需要大量拆分现有跨度元素创建新文本节点,等等

边缘情况 N

假设您从嵌套列表开始

  • A
    • B
    • C
  • D

并且用户一次选择了最后两个项目。然后需要将每一项分别包装成一个span。

迈向解决方案的第一步

虽然在您的问题中不清楚应如何处理所有边缘情况,但这是迈向解决方案的第一步。

const allCapsClass = 'sC'
function toggleCase() {
 const selection = window.getSelection()
  if (selection && selection.rangeCount>0) {
   const spans = wrapInNonNestedSpanners(selection.getRangeAt(0))
    for (const span of spans) {
     const classes = span.classList
     const action = classes.contains(allCapsClass) ? 'remove' : 'add'
      classes[action](allCapsClass)
    }
  }
}


const spannerClassName = "non-nested-spanner"
const spannerQuerySelector = `.${spannerClassName}`

function wrapInNonNestedSpanners(range) {
 const containingSpanner = getContainingSpanner(range)
 const result = []
 if (containingSpanner != null) { // Edge case 1
   const endRange = document.createRange() // contents of the span after range
   endRange.selectNode(containingSpanner)
   endRange.setStart(range.endContainer, range.endOffset)
   const endContents = endRange.cloneContents()
   const wrappedSelectionContents = containingSpanner.cloneNode(false)
   wrappedSelectionContents.appendChild(range.cloneContents())
   endRange.deleteContents()
   range.deleteContents()
   const parent = containingSpanner.parentNode
   const next = containingSpanner.nextSibling
   parent.insertBefore(wrappedSelectionContents, next)
   result.push(wrappedSelectionContents)
   if (!isEmptySpanner(endContents.childNodes[0])) parent.insertBefore(endContents, next)
   if (isEmptySpanner(containingSpanner)) parent.removeChild(containingSpanner)
   const newSelection = document.createRange()
   newSelection.selectNode(wrappedSelectionContents)
   window.getSelection().removeAllRanges()
   window.getSelection().addRange(newSelection)
 } else { // Edge case 2
  const contents = range.extractContents()
  const spanners = contents.querySelectorAll(spannerQuerySelector)
  let endRange = document.createRange() // range before the span
  for (let index = spanners.length-1; index>=0; index--) {
   const spanner = spanners[index]
   endRange.selectNodeContents(contents)
   endRange.setStartAfter(spanner)
   if (!endRange.collapsed) {
    const wrappedEndContents = createSpannerWrapping(endRange.extractContents())
    range.insertNode(wrappedEndContents)
    result.unshift(wrappedEndContents)
   }
   range.insertNode(spanner)
   result.unshift(spanner)
  }
  const rest = createSpannerWrapping(contents)
  if (!isEmptySpanner(rest)) {
   range.insertNode(rest)
   result.unshift(rest)
  }
 }
 return result
}

function getContainingSpanner(range) {
 let cursor = range.commonAncestorContainer
 if (cursor.classList == undefined) cursor = cursor.parentElement
 while (cursor.parentElement != null) {
  if (cursor.classList.contains(spannerClassName)) return cursor
  cursor = cursor.parentElement
 }
 return null
}

function createSpannerWrapping(childNode) {
 const spanner = document.createElement('span')
 spanner.classList.add(spannerClassName)
 spanner.appendChild(childNode)
 return spanner
}

function isEmptySpanner(spanner) {
 if (spanner.childNodes.length == 0) return true
 else if (spanner.childNodes.length == 1) {
  const node = spanner.childNodes[0]
  return node instanceof Text && node.length == 0
 }
 return false
}
.sC {
  font-variant: small-caps;
}
<section contenteditable>
  Hello world this is some text
</section>
<button onclick="toggleCase()">Toggle case</button>