不可编辑的内容允许删除。如何限制这个?
Non editable content allowed to delete. How to restrict this?
我们使用了带有“不可编辑”插件的 TinyMCE 编辑器。我们尝试删除不可编辑的内容,它被删除了。如何限制不可编辑内容的delete(delete/backspace)动作?
下面是我的代码:
tinymce.init({
selector: "#myeditablediv",
plugins: "advlist table lists image paste link pagebreak noneditable help",
noneditable_noneditable_class: "mceNonEditable",
menubar: false,
inline: true,
height: 500,
paste_data_images: true,
toolbar_sticky: true,
toolbar:
"bold italic underline | superscript subscript | formatselect | bullist | code pagebreak | link image | COC | table | removeformat | help",
formats: {
editable: {
inline: "span",
styles: { borderBottom: "2px solid gray" },
classes: "mceEditable"
}
},
setup: function (editor) {
editor.ui.registry.addButton("COC", {
text: "<b style='font-size:large;font-weight:bold;'>{CC}</b>",
tooltip: "CopyToClipBoard",
onAction: function (api) {
editor.execCommand("Copy");
}
});
},
toolbar_mode: "floating"
});
.demo-inline {
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.2);
text-align: left;
line-height: 1.3;
background-color: #ffffff;
text-align: left;
vertical-align: top;
padding: 20px 20px 20px 20px;
}
.demo-inline .container {
background-color: #fafafa;
margin: -20px -20px 0 -20px;
padding: 20px;
}
.demo-inline ul,
.demo-inline ol {
padding-left: 20px;
}
.demo-inline ul {
list-style: disc;
}
.demo-inline ol {
list-style: decimal;
}
.demo-inline a {
text-decoration: underline;
}
.demo-inline img {
display: block;
margin-left: auto;
margin-right: auto;
padding: 0px 10px 10px 10px;
}
.demo-inline textarea {
display: none;
}
.demo-inline *[contentEditable="true"]:focus,
.demo-inline *[contentEditable="true"]:hover {
outline: 2px solid #2276d2;
}
#myeditablediv {
margin-top: 20px;
font-family: "Calibri";
font-size: 16px;
line-height: 1.1em;
}
/*Component Editable*/
div.FixedComponent {
text-align: center;
background-color: #d8d8d8;
padding: 10px;
}
div.FixedComponent::before {
content: attr(data-displayname);
}
div[data-prefix]::before {
content: attr(data-prefix);
color: #1f477d !important;
font-weight: bold;
float: left;
display: inline-block;
margin-right: 3px;
}
.componentSuffix::after {
content: " ]";
color: #1f477d !important;
font-weight: bold;
}
div[data-type="content"] {
min-height: 23px;
display: inline;
}
div.ComponentWrapper:focus {
outline: dotted;
}
<script src="https://cdn.tiny.cloud/1/qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc/tinymce/5/tinymce.min.js"></script>
<div class="demo-inline">
<div id="myeditablediv">
Hi tiny
<p class='mceNonEditable'> <b> This is a non editable content</b>
</p>
<p> <span class='mceNonEditable'> <b>This part is non editable</b> </span>
This is a editable content
<span class='mceNonEditable'> <b>This part is non editable</b> </span>
</p>
</div>
</div>
TinyMCE的noneditable
插件是为了制作内容块non-editiable,但是不是non-deletable。相反,它将 non-editable 内容的整个部分视为单个字符。
要阻止内容被键盘删除,您可以使用 Tiny 的事件处理结构来查找某些按键,然后 interrupt/stop 它们。这里有一个非常简单的例子来说明如何做到这一点:
http://fiddle.tinymce.com/Mvhaab
您需要展开它以查看光标在内容中的位置,如果按键的结果会删除您想要保留的内容,请仅在这些情况下停止按键。
请注意,此方法不会阻止通过其他方法删除内容,例如将其作为较大选择的一部分删除。
编写了一个 Angular 服务,目前运行良好,可能需要针对边缘情况进行一些调整。 nonDeletableSelectors
包含 CSS 选择器,表示应为 non-deletable 的元素。 I noticed that there is apparently a TinyMCE bug with non-editable elements though, so the code is more complex than I think it should be.
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class EditorPreventDeleteService {
constructor() { }
public nonDeletableSelectors = ['.mceNonEditable'];
public preventDelete(editor) {
let self = this;
editor.on('keydown', function(event) {
if (self.keyWillDelete(event)) {
let range = editor.selection.getRng(), selection = editor.selection.getSel();
if (!range.collapsed)
return self.checkSelection(editor, event);
else if (event.keyCode == 8)
self.checkBackspace(editor, event, selection);
else if (event.keyCode == 46)
self.checkDelete(editor, event, selection);
}
return true;
});
editor.on('beforeSetContent', event => {
return self.checkSelection(editor, event);
});
editor.on('dragstart', event => {
if (self.checkNode(event.target, true))
self.cancelEvent(event);
});
}
protected checkNode(node, includeChildren = true) {
if (node && node.nodeType !== Node.TEXT_NODE)
for (let nonDeletableSelector of this.nonDeletableSelectors)
if (node.matches(nonDeletableSelector)
|| (includeChildren && node.querySelectorAll(nonDeletableSelector).length > 0))
return true;
return false;
}
protected checkSelection(editor, event) {
const selectedHTMLString = editor.selection.getContent({format : 'html'});
const selectedHTML = new DOMParser().parseFromString(selectedHTMLString, 'text/html').documentElement;
if (this.checkNode(selectedHTML))
return this.cancelEvent(event);
return true;
}
protected checkBackspace(editor, event, selection) {
if (selection.anchorOffset === 0 && this.getPrefixContent(editor, selection).length === 0)
return this.cancelEvent(event);
this.checkCaretDeletion(editor, event, selection, false);
}
protected checkDelete(editor, event, selection) {
this.checkCaretDeletion(editor, event, selection, true);
}
protected checkCaretDeletion(editor, event, selection, forwards = true) { // https://developer.mozilla.org/en-US/docs/Web/API/Selection
let borderingElement = forwards ? selection.anchorNode.nextSibling : selection.anchorNode.previousSibling;
if (selection.anchorNode.nodeType === Node.TEXT_NODE) {
if (this.getTrailingText(selection, forwards, false).length > 0)
return; // not at the border of a text element
} else if (selection.anchorOffset !== (forwards ? selection.anchorNode.childNodes.length : 0)
&& this.trimZeroWidthSpaces(selection.anchorNode.textContent).length > 0
&& this.checkNode(selection.anchorNode.childNodes.item(selection.anchorOffset + (forwards?0:1))))
return this.cancelEvent(event); // not at the border of anchor, anchor not empty, only neighbouring child is deleted
if (this.checkNode(selection.anchorNode) || this.checkNode(borderingElement))
this.cancelEvent(event);
}
protected getPrefixContent(editor, selection) {
let currentRange = editor.selection.getRng(1), tempRange = currentRange.cloneRange();
tempRange.setStartBefore(editor.getBody().childNodes.item(0));
tempRange.setEndBefore(selection.anchorNode);
editor.selection.setRng(tempRange);
let content = editor.selection.getContent({format: 'html'});
editor.selection.setRng(currentRange);
return this.trimZeroWidthSpaces(content.trim());
}
protected getTrailingText(selection, forwards = true, includeSiblings = false) {
let trailer = '', appendTrailer = function(text) { forwards ? trailer += text : trailer = text + trailer; }
if (selection.anchorNode.nodeType === Node.TEXT_NODE) {
let text = selection.anchorNode.textContent;
appendTrailer(forwards ? text.substr(selection.anchorOffset) : text.substr(0, selection.anchorOffset));
} else {
for (let i=selection.anchorOffset ; i>=0 && i<selection.anchorNode.childNodes.length ; i+=(forwards?-1:1))
appendTrailer(selection.anchorNode.childNodes.item(i).textContent);
}
if (includeSiblings) {
let sibling = selection.anchorNode.previousSibling;
while (sibling) {
appendTrailer(sibling.textContent);
sibling = sibling.previousSibling;
}
}
return this.trimZeroWidthSpaces(trailer);
}
protected cancelEvent(event) {
event.preventDefault();
event.stopPropagation();
return false;
}
protected keyWillDelete(evt) {
let c = evt.keyCode;
if (evt.ctrlKey)
return evt.key == 'x' || [8, 46].includes(c);
return [8, 9, 13, 46].includes(c)
|| this.inRange(c, 48, 57)
|| this.inRange(c, 65, 90)
|| this.inRange(c, 96, 111)
|| this.inRange(c, 186, 192)
|| this.inRange(c, 219, 222);
}
protected inRange(val, min, max) {
return val >= min && val <= max;
}
protected trimZeroWidthSpaces(text: string) {
return text.replace(/[\u200B-\u200D\uFEFF]/g, '');
}
}
我们使用了带有“不可编辑”插件的 TinyMCE 编辑器。我们尝试删除不可编辑的内容,它被删除了。如何限制不可编辑内容的delete(delete/backspace)动作?
下面是我的代码:
tinymce.init({
selector: "#myeditablediv",
plugins: "advlist table lists image paste link pagebreak noneditable help",
noneditable_noneditable_class: "mceNonEditable",
menubar: false,
inline: true,
height: 500,
paste_data_images: true,
toolbar_sticky: true,
toolbar:
"bold italic underline | superscript subscript | formatselect | bullist | code pagebreak | link image | COC | table | removeformat | help",
formats: {
editable: {
inline: "span",
styles: { borderBottom: "2px solid gray" },
classes: "mceEditable"
}
},
setup: function (editor) {
editor.ui.registry.addButton("COC", {
text: "<b style='font-size:large;font-weight:bold;'>{CC}</b>",
tooltip: "CopyToClipBoard",
onAction: function (api) {
editor.execCommand("Copy");
}
});
},
toolbar_mode: "floating"
});
.demo-inline {
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.2);
text-align: left;
line-height: 1.3;
background-color: #ffffff;
text-align: left;
vertical-align: top;
padding: 20px 20px 20px 20px;
}
.demo-inline .container {
background-color: #fafafa;
margin: -20px -20px 0 -20px;
padding: 20px;
}
.demo-inline ul,
.demo-inline ol {
padding-left: 20px;
}
.demo-inline ul {
list-style: disc;
}
.demo-inline ol {
list-style: decimal;
}
.demo-inline a {
text-decoration: underline;
}
.demo-inline img {
display: block;
margin-left: auto;
margin-right: auto;
padding: 0px 10px 10px 10px;
}
.demo-inline textarea {
display: none;
}
.demo-inline *[contentEditable="true"]:focus,
.demo-inline *[contentEditable="true"]:hover {
outline: 2px solid #2276d2;
}
#myeditablediv {
margin-top: 20px;
font-family: "Calibri";
font-size: 16px;
line-height: 1.1em;
}
/*Component Editable*/
div.FixedComponent {
text-align: center;
background-color: #d8d8d8;
padding: 10px;
}
div.FixedComponent::before {
content: attr(data-displayname);
}
div[data-prefix]::before {
content: attr(data-prefix);
color: #1f477d !important;
font-weight: bold;
float: left;
display: inline-block;
margin-right: 3px;
}
.componentSuffix::after {
content: " ]";
color: #1f477d !important;
font-weight: bold;
}
div[data-type="content"] {
min-height: 23px;
display: inline;
}
div.ComponentWrapper:focus {
outline: dotted;
}
<script src="https://cdn.tiny.cloud/1/qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc/tinymce/5/tinymce.min.js"></script>
<div class="demo-inline">
<div id="myeditablediv">
Hi tiny
<p class='mceNonEditable'> <b> This is a non editable content</b>
</p>
<p> <span class='mceNonEditable'> <b>This part is non editable</b> </span>
This is a editable content
<span class='mceNonEditable'> <b>This part is non editable</b> </span>
</p>
</div>
</div>
TinyMCE的noneditable
插件是为了制作内容块non-editiable,但是不是non-deletable。相反,它将 non-editable 内容的整个部分视为单个字符。
要阻止内容被键盘删除,您可以使用 Tiny 的事件处理结构来查找某些按键,然后 interrupt/stop 它们。这里有一个非常简单的例子来说明如何做到这一点:
http://fiddle.tinymce.com/Mvhaab
您需要展开它以查看光标在内容中的位置,如果按键的结果会删除您想要保留的内容,请仅在这些情况下停止按键。
请注意,此方法不会阻止通过其他方法删除内容,例如将其作为较大选择的一部分删除。
编写了一个 Angular 服务,目前运行良好,可能需要针对边缘情况进行一些调整。 nonDeletableSelectors
包含 CSS 选择器,表示应为 non-deletable 的元素。 I noticed that there is apparently a TinyMCE bug with non-editable elements though, so the code is more complex than I think it should be.
import {Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class EditorPreventDeleteService {
constructor() { }
public nonDeletableSelectors = ['.mceNonEditable'];
public preventDelete(editor) {
let self = this;
editor.on('keydown', function(event) {
if (self.keyWillDelete(event)) {
let range = editor.selection.getRng(), selection = editor.selection.getSel();
if (!range.collapsed)
return self.checkSelection(editor, event);
else if (event.keyCode == 8)
self.checkBackspace(editor, event, selection);
else if (event.keyCode == 46)
self.checkDelete(editor, event, selection);
}
return true;
});
editor.on('beforeSetContent', event => {
return self.checkSelection(editor, event);
});
editor.on('dragstart', event => {
if (self.checkNode(event.target, true))
self.cancelEvent(event);
});
}
protected checkNode(node, includeChildren = true) {
if (node && node.nodeType !== Node.TEXT_NODE)
for (let nonDeletableSelector of this.nonDeletableSelectors)
if (node.matches(nonDeletableSelector)
|| (includeChildren && node.querySelectorAll(nonDeletableSelector).length > 0))
return true;
return false;
}
protected checkSelection(editor, event) {
const selectedHTMLString = editor.selection.getContent({format : 'html'});
const selectedHTML = new DOMParser().parseFromString(selectedHTMLString, 'text/html').documentElement;
if (this.checkNode(selectedHTML))
return this.cancelEvent(event);
return true;
}
protected checkBackspace(editor, event, selection) {
if (selection.anchorOffset === 0 && this.getPrefixContent(editor, selection).length === 0)
return this.cancelEvent(event);
this.checkCaretDeletion(editor, event, selection, false);
}
protected checkDelete(editor, event, selection) {
this.checkCaretDeletion(editor, event, selection, true);
}
protected checkCaretDeletion(editor, event, selection, forwards = true) { // https://developer.mozilla.org/en-US/docs/Web/API/Selection
let borderingElement = forwards ? selection.anchorNode.nextSibling : selection.anchorNode.previousSibling;
if (selection.anchorNode.nodeType === Node.TEXT_NODE) {
if (this.getTrailingText(selection, forwards, false).length > 0)
return; // not at the border of a text element
} else if (selection.anchorOffset !== (forwards ? selection.anchorNode.childNodes.length : 0)
&& this.trimZeroWidthSpaces(selection.anchorNode.textContent).length > 0
&& this.checkNode(selection.anchorNode.childNodes.item(selection.anchorOffset + (forwards?0:1))))
return this.cancelEvent(event); // not at the border of anchor, anchor not empty, only neighbouring child is deleted
if (this.checkNode(selection.anchorNode) || this.checkNode(borderingElement))
this.cancelEvent(event);
}
protected getPrefixContent(editor, selection) {
let currentRange = editor.selection.getRng(1), tempRange = currentRange.cloneRange();
tempRange.setStartBefore(editor.getBody().childNodes.item(0));
tempRange.setEndBefore(selection.anchorNode);
editor.selection.setRng(tempRange);
let content = editor.selection.getContent({format: 'html'});
editor.selection.setRng(currentRange);
return this.trimZeroWidthSpaces(content.trim());
}
protected getTrailingText(selection, forwards = true, includeSiblings = false) {
let trailer = '', appendTrailer = function(text) { forwards ? trailer += text : trailer = text + trailer; }
if (selection.anchorNode.nodeType === Node.TEXT_NODE) {
let text = selection.anchorNode.textContent;
appendTrailer(forwards ? text.substr(selection.anchorOffset) : text.substr(0, selection.anchorOffset));
} else {
for (let i=selection.anchorOffset ; i>=0 && i<selection.anchorNode.childNodes.length ; i+=(forwards?-1:1))
appendTrailer(selection.anchorNode.childNodes.item(i).textContent);
}
if (includeSiblings) {
let sibling = selection.anchorNode.previousSibling;
while (sibling) {
appendTrailer(sibling.textContent);
sibling = sibling.previousSibling;
}
}
return this.trimZeroWidthSpaces(trailer);
}
protected cancelEvent(event) {
event.preventDefault();
event.stopPropagation();
return false;
}
protected keyWillDelete(evt) {
let c = evt.keyCode;
if (evt.ctrlKey)
return evt.key == 'x' || [8, 46].includes(c);
return [8, 9, 13, 46].includes(c)
|| this.inRange(c, 48, 57)
|| this.inRange(c, 65, 90)
|| this.inRange(c, 96, 111)
|| this.inRange(c, 186, 192)
|| this.inRange(c, 219, 222);
}
protected inRange(val, min, max) {
return val >= min && val <= max;
}
protected trimZeroWidthSpaces(text: string) {
return text.replace(/[\u200B-\u200D\uFEFF]/g, '');
}
}