'Non editable content allowed to delete. How to restrict this?

We have used the TinyMCE editor with "non editable" plugin. we tried to delete the non editable content, it is deleted. How to restrict the delete(delete/backspace) action for non editable content?

Below is my code:

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>

enter image description here enter image description here



Solution 1:[1]

TinyMCE's noneditable plugin is designed to make a block of content non-editiable, but not non-deletable. Rather it treats the entire section of non-editable content as a single character.

To stop content from being deleted with the keyboard, you could use Tiny's event handling structure to look for certain key presses and then interrupt/stop them. Here is a very simple example of how to do that:

http://fiddle.tinymce.com/Mvhaab

You would need to expand this to look at where the cursor is located in the content, and if the result of the keypress would delete something you want to preserve, stop the keypress only in those situations.

Note that this approach will not keep content from being deleted via other methods, such as by removing it as part of a larger selection.

Solution 2:[2]

Wrote an Angular service which so far works alright, possibly needs some tweaking for edge cases. nonDeletableSelectors contains the CSS selectors denoting elements which should be 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, '');
  }

}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Tiny Lincoln
Solution 2