This commit is contained in:
2025-10-22 15:39:40 +08:00
commit b0b510fac1
2720 changed files with 415933 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Plugin: noneditable Demo Page</title>
</head>
<body>
<h2>Plugin: noneditable Demo Page</h2>
<div id="ephox-ui">
<button class="clicky">clicky</button>
<button class="clicky2">clicky2</button>
<button class="boldy">boldy</button>
<div style="height: 200px"></div>
<div class="tinymce"><div contenteditable="false">
CEF DIV
</div></div>
<textarea class="tinymce"></textarea>
</div>
<script src="../../../../../js/tinymce/tinymce.js"></script>
<script src="../../../../../scratch/demos/plugins/noneditable/demo.js"></script>
</body>
</html>

View File

@@ -0,0 +1,44 @@
/**
* Demo.js
*
* Released under LGPL License.
* Copyright (c) 1999-2017 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
import { document } from '@ephox/dom-globals';
declare let tinymce: any;
const button = document.querySelector('button.clicky');
button.addEventListener('click', function () {
tinymce.activeEditor.insertContent(content);
});
const content = '<span class="mceNonEditable">[NONEDITABLE]</span>';
const button2 = document.querySelector('button.boldy');
button2.addEventListener('click', function () {
tinymce.activeEditor.execCommand('bold');
});
tinymce.init({
selector: 'div.tinymce',
theme: 'modern',
inline: true,
skin_url: '../../../../../js/tinymce/skins/lightgray',
plugins: 'noneditable code',
toolbar: 'code',
height: 600
});
tinymce.init({
selector: 'textarea.tinymce',
theme: 'modern',
skin_url: '../../../../../js/tinymce/skins/lightgray',
plugins: 'noneditable code',
toolbar: 'code',
height: 600
});
export {};

View File

@@ -0,0 +1,15 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import PluginManager from 'tinymce/core/api/PluginManager';
import FilterContent from './core/FilterContent';
PluginManager.add('noneditable', function (editor) {
FilterContent.setup(editor);
});
export default function () { }

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
const getNonEditableClass = function (editor) {
return editor.getParam('noneditable_noneditable_class', 'mceNonEditable');
};
const getEditableClass = function (editor) {
return editor.getParam('noneditable_editable_class', 'mceEditable');
};
const getNonEditableRegExps = function (editor) {
const nonEditableRegExps = editor.getParam('noneditable_regexp', []);
if (nonEditableRegExps && nonEditableRegExps.constructor === RegExp) {
return [nonEditableRegExps];
} else {
return nonEditableRegExps;
}
};
export default {
getNonEditableClass,
getEditableClass,
getNonEditableRegExps
};

View File

@@ -0,0 +1,116 @@
/**
* Copyright (c) Tiny Technologies, Inc. All rights reserved.
* Licensed under the LGPL or a commercial license.
* For LGPL see License.txt in the project root for license information.
* For commercial licenses see https://www.tiny.cloud/
*/
import Tools from 'tinymce/core/api/util/Tools';
import Settings from '../api/Settings';
const hasClass = function (checkClassName) {
return function (node) {
return (' ' + node.attr('class') + ' ').indexOf(checkClassName) !== -1;
};
};
const replaceMatchWithSpan = function (editor, content, cls) {
return function (match) {
const args = arguments, index = args[args.length - 2];
const prevChar = index > 0 ? content.charAt(index - 1) : '';
// Is value inside an attribute then don't replace
if (prevChar === '"') {
return match;
}
// Is value inside a contentEditable='false' tag
if (prevChar === '>') {
const findStartTagIndex = content.lastIndexOf('<', index);
if (findStartTagIndex !== -1) {
const tagHtml = content.substring(findStartTagIndex, index);
if (tagHtml.indexOf('contenteditable="false"') !== -1) {
return match;
}
}
}
return (
'<span class="' + cls + '" data-mce-content="' + editor.dom.encode(args[0]) + '">' +
editor.dom.encode(typeof args[1] === 'string' ? args[1] : args[0]) + '</span>'
);
};
};
const convertRegExpsToNonEditable = function (editor, nonEditableRegExps, e) {
let i = nonEditableRegExps.length, content = e.content;
// Don't replace the variables when raw is used for example on undo/redo
if (e.format === 'raw') {
return;
}
while (i--) {
content = content.replace(nonEditableRegExps[i], replaceMatchWithSpan(editor, content, Settings.getNonEditableClass(editor)));
}
e.content = content;
};
const setup = function (editor) {
let editClass, nonEditClass;
const contentEditableAttrName = 'contenteditable';
editClass = ' ' + Tools.trim(Settings.getEditableClass(editor)) + ' ';
nonEditClass = ' ' + Tools.trim(Settings.getNonEditableClass(editor)) + ' ';
const hasEditClass = hasClass(editClass);
const hasNonEditClass = hasClass(nonEditClass);
const nonEditableRegExps = Settings.getNonEditableRegExps(editor);
editor.on('PreInit', function () {
if (nonEditableRegExps.length > 0) {
editor.on('BeforeSetContent', function (e) {
convertRegExpsToNonEditable(editor, nonEditableRegExps, e);
});
}
editor.parser.addAttributeFilter('class', function (nodes) {
let i = nodes.length, node;
while (i--) {
node = nodes[i];
if (hasEditClass(node)) {
node.attr(contentEditableAttrName, 'true');
} else if (hasNonEditClass(node)) {
node.attr(contentEditableAttrName, 'false');
}
}
});
editor.serializer.addAttributeFilter(contentEditableAttrName, function (nodes) {
let i = nodes.length, node;
while (i--) {
node = nodes[i];
if (!hasEditClass(node) && !hasNonEditClass(node)) {
continue;
}
if (nonEditableRegExps.length > 0 && node.attr('data-mce-content')) {
node.name = '#text';
node.type = 3;
node.raw = true;
node.value = node.attr('data-mce-content');
} else {
node.attr(contentEditableAttrName, null);
}
}
});
});
};
export default {
setup
};

View File

@@ -0,0 +1,50 @@
import { Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { LegacyUnit, TinyLoader } from '@ephox/mcagar';
import Plugin from 'tinymce/plugins/noneditable/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
UnitTest.asynctest('browser.tinymce.plugins.noneditable.NonEditablePluginTest', function () {
const success = arguments[arguments.length - 2];
const failure = arguments[arguments.length - 1];
const suite = LegacyUnit.createSuite();
Plugin();
Theme();
suite.test('noneditable class', function (editor) {
editor.setContent('<p><span class="mceNonEditable">abc</span></p>');
LegacyUnit.equal(editor.dom.select('span')[0].contentEditable, 'false');
});
suite.test('editable class', function (editor) {
editor.setContent('<p><span class="mceEditable">abc</span></p>');
LegacyUnit.equal(editor.dom.select('span')[0].contentEditable, 'true');
});
suite.test('noneditable regexp', function (editor) {
editor.setContent('<p>{test1}{test2}</p>');
LegacyUnit.equal(editor.dom.select('span').length, 2);
LegacyUnit.equal(editor.dom.select('span')[0].contentEditable, 'false');
LegacyUnit.equal(editor.dom.select('span')[1].contentEditable, 'false');
LegacyUnit.equal(editor.getContent(), '<p>{test1}{test2}</p>');
});
suite.test('noneditable regexp inside cE=false', function (editor) {
editor.setContent('<span contenteditable="false">{test1}</span>');
LegacyUnit.equal(editor.dom.select('span').length, 1);
});
TinyLoader.setup(function (editor, onSuccess, onFailure) {
Pipeline.async({}, suite.toSteps(editor), onSuccess, onFailure);
}, {
add_unload_trigger: false,
indent: false,
noneditable_regexp: [/\{[^\}]+\}/g],
plugins: 'noneditable',
entities: 'raw',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});