tinymce
Version:
Web based JavaScript HTML WYSIWYG editor control.
238 lines (222 loc) • 8.13 kB
JavaScript
/**
* TinyMCE version 7.9.0 (2025-05-15)
*/
(function () {
'use strict';
var global$2 = tinymce.util.Tools.resolve('tinymce.PluginManager');
var global$1 = tinymce.util.Tools.resolve('tinymce.dom.RangeUtils');
var global = tinymce.util.Tools.resolve('tinymce.util.Tools');
const option = (name) => (editor) => editor.options.get(name);
const register$2 = (editor) => {
const registerOption = editor.options.register;
registerOption('allow_html_in_named_anchor', {
processor: 'boolean',
default: false
});
};
const allowHtmlInNamedAnchor = option('allow_html_in_named_anchor');
const namedAnchorSelector = 'a:not([href])';
const isEmptyString = (str) => !str;
const getIdFromAnchor = (elm) => {
const id = elm.getAttribute('id') || elm.getAttribute('name');
return id || '';
};
const isAnchor = (elm) => elm.nodeName.toLowerCase() === 'a';
const isNamedAnchor = (elm) => isAnchor(elm) && !elm.getAttribute('href') && getIdFromAnchor(elm) !== '';
const isEmptyNamedAnchor = (elm) => isNamedAnchor(elm) && !elm.firstChild;
const removeEmptyNamedAnchorsInSelection = (editor) => {
const dom = editor.dom;
global$1(dom).walk(editor.selection.getRng(), (nodes) => {
global.each(nodes, (node) => {
if (isEmptyNamedAnchor(node)) {
dom.remove(node, false);
}
});
});
};
const isValidId = (id) =>
// Follows HTML4 rules: https://www.w3.org/TR/html401/types.html#type-id
/^[A-Za-z][A-Za-z0-9\-:._]*$/.test(id);
const getNamedAnchor = (editor) => editor.dom.getParent(editor.selection.getStart(), namedAnchorSelector);
const getId = (editor) => {
const anchor = getNamedAnchor(editor);
if (anchor) {
return getIdFromAnchor(anchor);
}
else {
return '';
}
};
const createAnchor = (editor, id) => {
editor.undoManager.transact(() => {
if (!allowHtmlInNamedAnchor(editor)) {
editor.selection.collapse(true);
}
if (editor.selection.isCollapsed()) {
editor.insertContent(editor.dom.createHTML('a', { id }));
}
else {
// Remove any empty named anchors in the selection as they cannot be removed by the formatter since they are cef
removeEmptyNamedAnchorsInSelection(editor);
// Format is set up to truncate any partially selected named anchors so that they are not completely removed
editor.formatter.remove('namedAnchor', undefined, undefined, true);
// Insert new anchor using the formatter - will wrap selected content in anchor
editor.formatter.apply('namedAnchor', { value: id });
// Need to add visual classes to anchors if required
editor.addVisual();
}
});
};
const updateAnchor = (editor, id, anchorElement) => {
anchorElement.removeAttribute('name');
anchorElement.id = id;
editor.addVisual(); // Need to add visual classes to anchors if required
editor.undoManager.add();
};
const insert = (editor, id) => {
const anchor = getNamedAnchor(editor);
if (anchor) {
updateAnchor(editor, id, anchor);
}
else {
createAnchor(editor, id);
}
editor.focus();
};
const insertAnchor = (editor, newId) => {
if (!isValidId(newId)) {
editor.windowManager.alert('ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.');
return false;
}
else {
insert(editor, newId);
return true;
}
};
const open = (editor) => {
const currentId = getId(editor);
editor.windowManager.open({
title: 'Anchor',
size: 'normal',
body: {
type: 'panel',
items: [
{
name: 'id',
type: 'input',
label: 'ID',
placeholder: 'example'
}
]
},
buttons: [
{
type: 'cancel',
name: 'cancel',
text: 'Cancel'
},
{
type: 'submit',
name: 'save',
text: 'Save',
primary: true
}
],
initialData: {
id: currentId
},
onSubmit: (api) => {
if (insertAnchor(editor, api.getData().id)) { // TODO we need a better way to do validation
api.close();
}
}
});
};
const register$1 = (editor) => {
editor.addCommand('mceAnchor', () => {
open(editor);
});
};
// Note: node.firstChild check is for the 'allow_html_in_named_anchor' setting
// Only want to add contenteditable attributes if there is no text within the anchor
const isNamedAnchorNode = (node) => isEmptyString(node.attr('href')) && !isEmptyString(node.attr('id') || node.attr('name'));
const isEmptyNamedAnchorNode = (node) => isNamedAnchorNode(node) && !node.firstChild;
const setContentEditable = (state) => (nodes) => {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (isEmptyNamedAnchorNode(node)) {
node.attr('contenteditable', state);
}
}
};
const setup = (editor) => {
editor.on('PreInit', () => {
editor.parser.addNodeFilter('a', setContentEditable('false'));
editor.serializer.addNodeFilter('a', setContentEditable(null));
});
};
const registerFormats = (editor) => {
editor.formatter.register('namedAnchor', {
inline: 'a',
selector: namedAnchorSelector,
remove: 'all',
split: true,
deep: true,
attributes: {
id: '%value'
},
onmatch: (node, _fmt, _itemName) => {
return isNamedAnchor(node);
}
});
};
const onSetupEditable = (editor) => (api) => {
const nodeChanged = () => {
api.setEnabled(editor.selection.isEditable());
};
editor.on('NodeChange', nodeChanged);
nodeChanged();
return () => {
editor.off('NodeChange', nodeChanged);
};
};
const register = (editor) => {
const onAction = () => editor.execCommand('mceAnchor');
editor.ui.registry.addToggleButton('anchor', {
icon: 'bookmark',
tooltip: 'Anchor',
onAction,
onSetup: (buttonApi) => {
const unbindSelectorChanged = editor.selection.selectorChangedWithUnbind('a:not([href])', buttonApi.setActive).unbind;
const unbindEditableChanged = onSetupEditable(editor)(buttonApi);
return () => {
unbindSelectorChanged();
unbindEditableChanged();
};
}
});
editor.ui.registry.addMenuItem('anchor', {
icon: 'bookmark',
text: 'Anchor...',
onAction,
onSetup: onSetupEditable(editor)
});
};
var Plugin = () => {
global$2.add('anchor', (editor) => {
register$2(editor);
setup(editor);
register$1(editor);
register(editor);
editor.on('PreInit', () => {
registerFormats(editor);
});
});
};
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/
})();