切换带有 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" 时,事情变得复杂起来。你必须:
- 从包含
<span class="sC">
的元素中删除最后两个词
- 在跨度后添加单词 "becomes"(来自步骤 1。)并确保它不包含在具有 class姓名
sC
.
- 在文本节点 "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>
我有一个切换按钮,可以更改一些文本。我 运行 遇到的问题是,如果我有 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" 时,事情变得复杂起来。你必须:
- 从包含
<span class="sC">
的元素中删除最后两个词 - 在跨度后添加单词 "becomes"(来自步骤 1。)并确保它不包含在具有 class姓名
sC
. - 在文本节点 "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>