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,15 @@
<!DOCTYPE html>
<html>
<head>
<title>visualchars Plugin Demo Page</title>
</head>
<body>
<h2>Plugin: visualchars Demo Page</h2>
<div id="ephox-ui">
<textarea name="" id="" cols="30" rows="10" class="tinymce"></textarea>
</div>
<script src="../../../../../js/tinymce/tinymce.js"></script>
<script src="../../../../../scratch/demos/plugins/visualchars/demo.js"></script>
</body>
</html>

View File

@@ -0,0 +1,22 @@
/**
* Demo.js
*
* Released under LGPL License.
* Copyright (c) 1999-2016 Ephox Corp. All rights reserved
*
* License: http://www.tinymce.com/license
* Contributing: http://www.tinymce.com/contributing
*/
declare let tinymce: any;
tinymce.init({
selector: 'textarea.tinymce',
plugins: 'visualchars code',
toolbar: 'visualchars code',
visualchars_default_state: true,
skin_url: '../../../../../js/tinymce/skins/lightgray',
height: 600
});
export {};

View File

@@ -0,0 +1,27 @@
/**
* 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 { Cell } from '@ephox/katamari';
import PluginManager from 'tinymce/core/api/PluginManager';
import Api from './api/Api';
import Commands from './api/Commands';
import Keyboard from './core/Keyboard';
import Bindings from './core/Bindings';
import * as Buttons from './ui/Buttons';
PluginManager.add('visualchars', function (editor) {
const toggleState = Cell(false);
Commands.register(editor, toggleState);
Buttons.register(editor);
Keyboard.setup(editor, toggleState);
Bindings.setup(editor, toggleState);
return Api.get(toggleState);
});
export default function () {}

View File

@@ -0,0 +1,20 @@
/**
* 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 get = function (toggleState) {
const isEnabled = function () {
return toggleState.get();
};
return {
isEnabled
};
};
export default {
get
};

View File

@@ -0,0 +1,18 @@
/**
* 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 Actions from '../core/Actions';
const register = function (editor, toggleState) {
editor.addCommand('mceVisualChars', function () {
Actions.toggleVisualChars(editor, toggleState);
});
};
export default {
register
};

View File

@@ -0,0 +1,14 @@
/**
* 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 fireVisualChars = function (editor, state) {
return editor.fire('VisualChars', { state });
};
export default {
fireVisualChars
};

View File

@@ -0,0 +1,16 @@
/**
* 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 { Editor } from 'tinymce/core/api/Editor';
const isEnabledByDefault = (editor: Editor) => {
return editor.getParam('visualchars_default_state', false);
};
export default {
isEnabledByDefault
};

View File

@@ -0,0 +1,32 @@
/**
* 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 Events from '../api/Events';
import VisualChars from './VisualChars';
const toggleVisualChars = function (editor, toggleState) {
const body = editor.getBody();
const selection = editor.selection;
let bookmark;
toggleState.set(!toggleState.get());
Events.fireVisualChars(editor, toggleState.get());
bookmark = selection.getBookmark();
if (toggleState.get() === true) {
VisualChars.show(editor, body);
} else {
VisualChars.hide(editor, body);
}
selection.moveToBookmark(bookmark);
};
export default {
toggleVisualChars
};

View File

@@ -0,0 +1,23 @@
/**
* 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 { Editor } from 'tinymce/core/api/Editor';
import Settings from '../api/Settings';
import Actions from './Actions';
const setup = (editor: Editor, toggleState) => {
editor.on('init', () => {
// should be false when enabled, so toggling will change it to true
const valueForToggling = !Settings.isEnabledByDefault(editor);
toggleState.set(valueForToggling);
Actions.toggleVisualChars(editor, toggleState);
});
};
export default {
setup
};

View File

@@ -0,0 +1,43 @@
/**
* 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 charMap = {
'\u00a0': 'nbsp',
'\u00ad': 'shy'
};
const charMapToRegExp = function (charMap, global?) {
let key, regExp = '';
for (key in charMap) {
regExp += key;
}
return new RegExp('[' + regExp + ']', global ? 'g' : '');
};
const charMapToSelector = function (charMap) {
let key, selector = '';
for (key in charMap) {
if (selector) {
selector += ',';
}
selector += 'span.mce-' + charMap[key];
}
return selector;
};
export default {
charMap,
regExp: charMapToRegExp(charMap),
regExpGlobal: charMapToRegExp(charMap, true),
selector: charMapToSelector(charMap),
charMapToRegExp,
charMapToSelector
};

View File

@@ -0,0 +1,16 @@
/**
* 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 Data from './Data';
const wrapCharWithSpan = function (value) {
return '<span data-mce-bogus="1" class="mce-' + Data.charMap[value] + '">' + value + '</span>';
};
export default {
wrapCharWithSpan
};

View File

@@ -0,0 +1,27 @@
/**
* 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 Delay from 'tinymce/core/api/util/Delay';
import VisualChars from './VisualChars';
const setup = function (editor, toggleState) {
const debouncedToggle = Delay.debounce(function () {
VisualChars.toggle(editor);
}, 300);
if (editor.settings.forced_root_block !== false) {
editor.on('keydown', function (e) {
if (toggleState.get() === true) {
e.keyCode === 13 ? VisualChars.toggle(editor) : debouncedToggle();
}
});
}
};
export default {
setup
};

View File

@@ -0,0 +1,52 @@
/**
* 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 { Arr } from '@ephox/katamari';
import { Element, Node } from '@ephox/sugar';
import Data from './Data';
import Html from './Html';
const isMatch = function (n) {
return Node.isText(n) &&
Node.value(n) !== undefined &&
Data.regExp.test(Node.value(n));
};
// inlined sugars PredicateFilter.descendants for file size
const filterDescendants = function (scope, predicate) {
let result = [];
const dom = scope.dom();
const children = Arr.map(dom.childNodes, Element.fromDom);
Arr.each(children, function (x) {
if (predicate(x)) {
result = result.concat([ x ]);
}
result = result.concat(filterDescendants(x, predicate));
});
return result;
};
const findParentElm = function (elm, rootElm) {
while (elm.parentNode) {
if (elm.parentNode === rootElm) {
return elm;
}
elm = elm.parentNode;
}
};
const replaceWithSpans = function (html) {
return html.replace(Data.regExpGlobal, Html.wrapCharWithSpan);
};
export default {
isMatch,
filterDescendants,
findParentElm,
replaceWithSpans
};

View File

@@ -0,0 +1,55 @@
/**
* 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 Data from './Data';
import Nodes from './Nodes';
import { Arr } from '@ephox/katamari';
import { Element, Node } from '@ephox/sugar';
const show = function (editor, rootElm) {
let node, div;
const nodeList = Nodes.filterDescendants(Element.fromDom(rootElm), Nodes.isMatch);
Arr.each(nodeList, function (n) {
const withSpans = Nodes.replaceWithSpans(Node.value(n));
div = editor.dom.create('div', null, withSpans);
while ((node = div.lastChild)) {
editor.dom.insertAfter(node, n.dom());
}
editor.dom.remove(n.dom());
});
};
const hide = function (editor, body) {
const nodeList = editor.dom.select(Data.selector, body);
Arr.each(nodeList, function (node) {
editor.dom.remove(node, 1);
});
};
const toggle = function (editor) {
const body = editor.getBody();
const bookmark = editor.selection.getBookmark();
let parentNode = Nodes.findParentElm(editor.selection.getNode(), body);
// if user does select all the parentNode will be undefined
parentNode = parentNode !== undefined ? parentNode : body;
hide(editor, parentNode);
show(editor, parentNode);
editor.selection.moveToBookmark(bookmark);
};
export default {
show,
hide,
toggle
};

View File

@@ -0,0 +1,38 @@
/**
* 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 toggleActiveState = function (editor) {
return function (e) {
const ctrl = e.control;
editor.on('VisualChars', function (e) {
ctrl.active(e.state);
});
};
};
const register = function (editor) {
editor.addButton('visualchars', {
active: false,
title: 'Show invisible characters',
cmd: 'mceVisualChars',
onPostRender: toggleActiveState(editor)
});
editor.addMenuItem('visualchars', {
text: 'Show invisible characters',
cmd: 'mceVisualChars',
onPostRender: toggleActiveState(editor),
selectable: true,
context: 'view',
prependToContext: true
});
};
export {
register
};

View File

@@ -0,0 +1,23 @@
import { RawAssertions } from '@ephox/agar';
import Data from 'tinymce/plugins/visualchars/core/Data';
import { UnitTest } from '@ephox/bedrock';
UnitTest.test('atomic.tinymce.plugins.visualchars.DataTest', function () {
RawAssertions.assertEq(
'should return correct selector',
'span.mce-a,span.mce-b',
Data.charMapToSelector({ a: 'a', b: 'b' })
);
RawAssertions.assertEq(
'should return correct regexp',
'/[ab]/',
Data.charMapToRegExp({ a: 'a', b: 'b' }).toString()
);
RawAssertions.assertEq(
'should return correct global regexp',
'/[ab]/g',
Data.charMapToRegExp({ a: 'a', b: 'b' }, true).toString()
);
});

View File

@@ -0,0 +1,20 @@
import { RawAssertions } from '@ephox/agar';
import Html from 'tinymce/plugins/visualchars/core/Html';
import { UnitTest } from '@ephox/bedrock';
UnitTest.test('atomic.tinymce.plugins.visualchars.HtmlTest', function () {
const nbsp = '\u00a0';
const shy = '\u00AD';
RawAssertions.assertEq(
'should return correct span',
'<span data-mce-bogus="1" class="mce-nbsp">' + nbsp + '</span>',
Html.wrapCharWithSpan(nbsp)
);
RawAssertions.assertEq(
'should return correct span',
'<span data-mce-bogus="1" class="mce-shy">' + shy + '</span>',
Html.wrapCharWithSpan(shy)
);
});

View File

@@ -0,0 +1,50 @@
import { Assertions } from '@ephox/agar';
import { Element } from '@ephox/sugar';
import Nodes from 'tinymce/plugins/visualchars/core/Nodes';
import { UnitTest } from '@ephox/bedrock';
import { document } from '@ephox/dom-globals';
UnitTest.test('atomic.tinymce.plugins.visualchars.NodesTest', function () {
const nbsp = '\u00a0';
const shy = '\u00AD';
const testReplaceWithSpans = function () {
Assertions.assertHtml(
'should return span around shy and nbsp',
'a<span data-mce-bogus="1" class="mce-nbsp">\u00a0</span>b<span data-mce-bogus="1" class="mce-shy">\u00AD</span>',
Nodes.replaceWithSpans('a' + nbsp + 'b' + shy)
);
};
const testFilterDescendants = function () {
const div = document.createElement('div');
div.innerHTML = '<p>a</p>' +
'<p>b' + nbsp + '</p>' +
'<p>c</p>' +
'<p>d' + shy + '</p>';
Assertions.assertEq(
'should return list with nodes with shy or nbsp in it',
2,
Nodes.filterDescendants(Element.fromDom(div), Nodes.isMatch).length
);
};
const testFilterDescendants2 = function () {
const div = document.createElement('div');
div.innerHTML = '<p>a' + nbsp + '</p>' +
'<p>b' + nbsp + '</p>' +
'<p>c' + nbsp + '</p>' +
'<p>d' + shy + '</p>';
Assertions.assertEq(
'should return list with nodes with shy or nbsp in it',
4,
Nodes.filterDescendants(Element.fromDom(div), Nodes.isMatch).length
);
};
testReplaceWithSpans();
testFilterDescendants();
testFilterDescendants2();
});

View File

@@ -0,0 +1,35 @@
import { Keyboard, Keys, Pipeline, Waiter } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyLoader, TinyUi } from '@ephox/mcagar';
import { Element } from '@ephox/sugar';
import Plugin from 'tinymce/plugins/visualchars/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
import { sAssertNbspStruct, sAssertSpanStruct } from '../module/test/Utils';
UnitTest.asynctest('browser.tinymce.plugins.visualchars.DefaultStateTest', (success, failure) => {
Plugin();
Theme();
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyUi = TinyUi(editor);
const tinyApis = TinyApis(editor);
Pipeline.async({}, [
tinyApis.sSetContent('<p>a&nbsp;&nbsp;b</p>'),
// Need to trigger a keydown event to get the visual chars to show after calling set content
Keyboard.sKeydown(Element.fromDom(editor.getDoc()), Keys.space(), { }),
Waiter.sTryUntil('Wait for visual chars to show', tinyApis.sAssertContentStructure(sAssertSpanStruct), 50, 1000),
tinyUi.sClickOnToolbar('click on visualchars button', 'div[aria-label="Show invisible characters"] > button'),
tinyApis.sAssertContentStructure(sAssertNbspStruct),
tinyUi.sClickOnToolbar('click on visualchars button', 'div[aria-label="Show invisible characters"] > button'),
tinyApis.sAssertContentStructure(sAssertSpanStruct)
], onSuccess, onFailure);
}, {
plugins: 'visualchars',
toolbar: 'visualchars',
skin_url: '/project/js/tinymce/skins/lightgray',
visualchars_default_state: true
}, success, failure);
});

View File

@@ -0,0 +1,35 @@
import { Assertions, Pipeline } from '@ephox/agar';
import { UnitTest } from '@ephox/bedrock';
import { TinyApis, TinyLoader, TinyUi } from '@ephox/mcagar';
import Plugin from 'tinymce/plugins/visualchars/Plugin';
import Theme from 'tinymce/themes/modern/Theme';
import { sAssertNbspStruct, sAssertSpanStruct } from '../module/test/Utils';
UnitTest.asynctest('browser.tinymce.plugins.visualchars.PluginTest', (success, failure) => {
Plugin();
Theme();
TinyLoader.setup(function (editor, onSuccess, onFailure) {
const tinyUi = TinyUi(editor);
const tinyApis = TinyApis(editor);
Pipeline.async({}, [
tinyApis.sSetContent('<p>a&nbsp;&nbsp;b</p>'),
Assertions.sAssertEq('assert equal', 0, editor.dom.select('span').length),
tinyUi.sClickOnToolbar('click on visualchars button', 'div[aria-label="Show invisible characters"] > button'),
tinyApis.sAssertContentStructure(sAssertSpanStruct),
tinyUi.sClickOnToolbar('click on visualchars button', 'div[aria-label="Show invisible characters"] > button'),
tinyApis.sAssertContentStructure(sAssertNbspStruct),
tinyUi.sClickOnToolbar('click on visualchars button', 'div[aria-label="Show invisible characters"] > button'),
tinyApis.sAssertContentStructure(sAssertSpanStruct),
tinyUi.sClickOnToolbar('click on visualchars button', 'div[aria-label="Show invisible characters"] > button'),
tinyApis.sAssertContentStructure(sAssertNbspStruct)
], onSuccess, onFailure);
}, {
plugins: 'visualchars',
toolbar: 'visualchars',
skin_url: '/project/js/tinymce/skins/lightgray'
}, success, failure);
});

View File

@@ -0,0 +1,36 @@
import { ApproxStructure } from '@ephox/agar';
const sAssertSpanStruct = ApproxStructure.build(function (s, str) {
return s.element('body', {
children: [
s.element('p', {
children: [
s.text(str.is('a')),
s.element('span', {}),
s.element('span', {}),
s.text(str.is('b'))
]
})
]
});
});
const sAssertNbspStruct = ApproxStructure.build(function (s, str) {
return s.element('body', {
children: [
s.element('p', {
children: [
s.text(str.is('a')),
s.text(str.is('\u00a0')),
s.text(str.is('\u00a0')),
s.text(str.is('b'))
]
})
]
});
});
export {
sAssertNbspStruct,
sAssertSpanStruct
};