【问题标题】:Toggle a specific Text with JavaScript not working as expected使用 JavaScript 切换特定文本未按预期工作
【发布时间】:2018-12-17 15:32:57
【问题描述】:

我有一个切换按钮,可以更改一些文本。我遇到的问题是,如果我有 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。谢谢你!

【问题讨论】:

  • 如果有人选择 Sc-span 中的字符以及没有 Sc-span 的字符,您打算如何解决?会是这样吗:bbrrb (** bb ** rr ** b **)?

标签: javascript html css editor toggle


【解决方案1】:

问题在于这个document.querySelector('span.sC')。在所有情况下,它将选择带有 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>

【讨论】:

  • 这很接近,但仍然有点问题。如果您选择 2,如果您尝试关闭样式,则会出错。我会玩,看看我能用它做什么,谢谢。我想我需要在一个数组中获取所有跨度,并且只影响选定的跨度。只是不太确定该怎么做..
【解决方案2】:

由于您需要处理所有边缘情况,因此如果没有库,这不是那么容易实现的。我将首先介绍一些边缘情况,然后给你一个如何实现它们的例子。

简单案例

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

“没有人想要太复杂的代码,因为它变得难以管理。”

考虑一个用户选择了“没人想要”,然后按下了切换小型大写字母按钮。您最终应该得到如下所示的内容(粗体部分代表小型大写字母中的文本):

“没有人想要太复杂的代码,因为它变得难以管理。”

这是一个简单的案例。只需将片段“Nobody want”包装在&lt;span&gt; 中,并为其指定sC 类。所有其他尚未进入小型股的细分市场也是如此。这里没什么太难的。

边缘案例 1

但是假设您处于以下状态(再次,粗体段代表小型大写字母中的文本):

“没有人想要太复杂的代码因为它变得难以管理。”

当用户选择并切换单词“变成”时,事情变得复杂了。你必须:

  1. 从包含&lt;span class="sC"&gt;的元素中删除最后两个单词
  2. 在跨度之后添加单词“成为”(从步骤 1 开始。)并确保它不包含在类名称为 sC 的 &lt;span&gt; 中。
  3. 在文本节点 "becomes"(在步骤 2 中插入)之后的新 &lt;span class="sC"&gt; 元素中添加单词 "unmanageable"。

边缘情况 2

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

“没有人想要太复杂代码因为它变得难以管理。”

当有人选择并切换片段“想要太复杂的代码,因为”时会发生什么?可以说:让这个片段中的每一个字符

  • small-caps : 当它不是时
  • 恢复正常:当它目前是小型股时

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

边缘情况 N

假设你从一个嵌套列表开始

  • 一个
    • B
    • C
  • D

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

迈向解决方案的第一步

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

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>

【讨论】:

  • 这是一个很好的例子。我会玩一点。非常感谢
  • 这有一个很好的解释,而且效果很好 感谢您花时间研究这个例子。干得好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-01-13
  • 2021-12-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-10
  • 2016-03-28
相关资源
最近更新 更多