TINY-10654: Remove deprecated template plugin (#9415)

This commit is contained in:
ltrouton
2024-02-26 14:14:19 +10:00
committed by GitHub
parent 94acea35f3
commit 8d8dd3de62
41 changed files with 15 additions and 1406 deletions

View File

@ -0,0 +1,6 @@
project: tinymce
kind: Removed
body: Removed deprecated `template` plugin.
time: 2024-02-23T12:25:39.49598509+10:00
custom:
Issue: TINY-10654

View File

@ -15,7 +15,6 @@ import { createPreviewDialog } from './dialogs/PreviewDialog';
import { createTableCellDialog } from './dialogs/TableCellDialog';
import { createTableDialog } from './dialogs/TableDialog';
import { createTableRowDialog } from './dialogs/TableRowDialog';
import { createTemplateDialog } from './dialogs/TemplateDialog';
import { createWordcountDialog } from './dialogs/WordcountDialog';
import { registerDemoContextMenus } from './menus/ContextMenuDemo';
import { registerDemoMenuItems } from './menus/MenuItemDemo';
@ -36,7 +35,6 @@ createPreviewDialog();
createTableCellDialog();
createTableDialog();
createTableRowDialog();
createTemplateDialog();
registerDemoButtons();
registerDemoMenuItems();
registerDemoContextMenus();

View File

@ -1,63 +0,0 @@
import { openDemoDialog } from './DemoDialogHelpers';
export const createTemplateDialog = (): void => {
openDemoDialog(
{
title: 'Insert template',
size: 'large',
body: {
type: 'panel',
items: [
{
name: 'template',
type: 'selectbox',
label: 'Template',
items: [
{
text: 'Some template 1',
value: 'url1.html'
},
{
text: 'Some template 2',
value: 'url2.html'
}
]
},
{
name: 'preview',
type: 'iframe',
label: 'Preview of template',
sandboxed: false
}
]
},
buttons: [
{
type: 'submit',
name: 'ok',
text: 'Ok',
primary: true
},
{
type: 'cancel',
name: 'cancel',
text: 'Cancel'
}
],
initialData: {
template: 'url2.html',
preview: 'some html url'
},
onSubmit: (api) => {
const data = api.getData();
// eslint-disable-next-line no-console
console.log({
template: data.template
});
api.close();
}
}
);
};

View File

@ -1,11 +0,0 @@
import { getDemoRegistry } from '../buttons/DemoRegistry';
export const registerTemplateItems = (): void => {
getDemoRegistry().addButton('template', {
type: 'button',
enabled: true,
onAction: (_buttonApi) => {
// show dialog
}
});
};

View File

@ -14,7 +14,7 @@ let plugins = [
'accordion', 'advlist', 'anchor', 'autolink', 'autoresize', 'autosave', 'charmap', 'code', 'codesample',
'directionality', 'emoticons', 'help', 'fullscreen', 'image', 'importcss', 'insertdatetime',
'link', 'lists', 'media', 'nonbreaking', 'pagebreak', 'preview', 'save', 'searchreplace',
'table', 'template', 'visualblocks', 'visualchars', 'wordcount', 'quickbars'
'table', 'visualblocks', 'visualchars', 'wordcount', 'quickbars'
];
let themes = [

View File

@ -66,7 +66,6 @@ export default (): void => {
cmd('mceSave'),
cmd('SearchReplace'),
cmd('mceSpellcheck'),
cmd('mceInsertTemplate', '{$user}'),
cmd('mceVisualBlocks'),
cmd('mceVisualChars'),
cmd('mceMedia'),
@ -107,7 +106,7 @@ export default (): void => {
plugins: [
'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'code', 'fullscreen', 'insertdatetime', 'media', 'nonbreaking',
'save', 'table', 'directionality', 'emoticons', 'template', 'importcss', 'codesample'
'save', 'table', 'directionality', 'emoticons', 'importcss', 'codesample'
],
toolbar1: 'bold italic',
menubar: false

View File

@ -73,12 +73,6 @@ const settings: RawEditorOptions = {
callback('movie.mp4', { embed: '<p>test</p>' });
}
},
templates: [
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content' },
{ title: 'Some title 2', description: 'Some desc 2', content: '<div class="mceTmpl"><span class="cdate">cdate</span><span class="mdate">mdate</span>My content2</div>' }
],
template_cdate_format: '[CDATE: %m/%d/%Y : %H:%M:%S]',
template_mdate_format: '[MDATE: %m/%d/%Y : %H:%M:%S]',
image_caption: true,
theme: 'silver',
setup: (ed) => {
@ -87,7 +81,7 @@ const settings: RawEditorOptions = {
plugins: [
'autosave', 'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'code', 'fullscreen', 'insertdatetime', 'media', 'nonbreaking',
'save', 'table', 'directionality', 'emoticons', 'template', 'importcss', 'codesample', 'help'
'save', 'table', 'directionality', 'emoticons', 'importcss', 'codesample', 'help'
],
// rtl_ui: true,
add_unload_trigger: false,

View File

@ -7,6 +7,5 @@ export default (): void => {
selector: '#editor',
inline: true,
fixed_toolbar_container: '#toolbar',
plugins: 'template' // lets you check notification positioning
});
};

View File

@ -107,12 +107,6 @@ export default (): void => {
callback('movie.mp4', { embed: '<p>test</p>' });
}
},
templates: [
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content' },
{ title: 'Some title 2', description: 'Some desc 2', content: '<div class="mceTmpl"><span class="cdate">cdate</span><span class="mdate">mdate</span>My content2</div>' }
],
template_cdate_format: '[CDATE: %m/%d/%Y : %H:%M:%S]',
template_mdate_format: '[MDATE: %m/%d/%Y : %H:%M:%S]',
image_caption: true,
theme: 'silver',
setup: (ed) => {
@ -123,7 +117,7 @@ export default (): void => {
plugins: [
'autosave', 'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'code', 'fullscreen', 'insertdatetime', 'media', 'nonbreaking',
'save', 'table', 'directionality', 'emoticons', 'template', 'importcss', 'codesample', 'help', 'accordion'
'save', 'table', 'directionality', 'emoticons', 'importcss', 'codesample', 'help', 'accordion'
],
// rtl_ui: true,
add_unload_trigger: false,

View File

@ -21,14 +21,10 @@ export default (): void => {
{ title: 'None', value: '' },
{ title: 'Some class', value: 'class-name' }
],
templates: [
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content' },
{ title: 'Some title 2', description: 'Some desc 2', content: '<div class="mceTmpl"><span class="cdate">cdate</span><span class="mdate">mdate</span>My content2</div>' }
],
plugins: [
'autosave', 'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'code', 'fullscreen', 'insertdatetime', 'media', 'nonbreaking',
'save', 'table', 'directionality', 'emoticons', 'template', 'codesample', 'help'
'save', 'table', 'directionality', 'emoticons', 'codesample', 'help'
]
};

View File

@ -58,12 +58,6 @@ export default (): void => {
callback('movie.mp4', { source2: 'alt.ogg', poster: 'https://www.google.com/logos/google.jpg' });
}
},
templates: [
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content' },
{ title: 'Some title 2', description: 'Some desc 2', content: '<div class="mceTmpl"><span class="cdate">cdate</span><span class="mdate">mdate</span>My content2</div>' }
],
template_cdate_format: '[CDATE: %m/%d/%Y : %H:%M:%S]',
template_mdate_format: '[MDATE: %m/%d/%Y : %H:%M:%S]',
image_caption: true,
theme: 'silver',
mobile: {
@ -80,7 +74,7 @@ export default (): void => {
plugins: [
'fullscreen', 'help', 'autosave', 'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'code', 'fullscreen', 'insertdatetime', 'media', 'nonbreaking',
'save', 'table', 'directionality', 'emoticons', 'template', 'importcss', 'codesample', 'help'
'save', 'table', 'directionality', 'emoticons', 'importcss', 'codesample', 'help'
],
// rtl_ui: true,
add_unload_trigger: false,

View File

@ -9,15 +9,11 @@ export default (): void => {
selector: 'textarea#editor',
skin_url: '../../../../js/tinymce/skins/ui/oxide',
content_css: '../../../../js/tinymce/skins/content/default/content.css',
templates: [
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content' },
{ title: 'Some title 2', description: 'Some desc 2', content: '<div class="mceTmpl"><span class="cdate">cdate</span><span class="mdate">mdate</span>My content2</div>' }
],
image_caption: true,
plugins: [
'autosave', 'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'code', 'fullscreen', 'insertdatetime', 'media', 'nonbreaking',
'save', 'table', 'directionality', 'emoticons', 'template', 'importcss', 'codesample', 'help'
'save', 'table', 'directionality', 'emoticons', 'importcss', 'codesample', 'help'
],
add_unload_trigger: false,
autosave_ask_before_unload: false,

View File

@ -62,12 +62,6 @@ export default (): void => {
callback('movie.mp4', { source2: 'alt.ogg', poster: 'https://www.google.com/logos/google.jpg' });
}
},
templates: [
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content' },
{ title: 'Some title 2', description: 'Some desc 2', content: '<div class="mceTmpl"><span class="cdate">cdate</span><span class="mdate">mdate</span>My content2</div>' }
],
template_cdate_format: '[CDATE: %m/%d/%Y : %H:%M:%S]',
template_mdate_format: '[MDATE: %m/%d/%Y : %H:%M:%S]',
image_caption: true,
theme: 'silver',
setup: (ed) => {
@ -76,7 +70,7 @@ export default (): void => {
plugins: [
'autosave', 'advlist', 'autolink', 'link', 'image', 'lists', 'charmap', 'preview', 'anchor', 'pagebreak',
'searchreplace', 'wordcount', 'visualblocks', 'visualchars', 'code', 'fullscreen', 'insertdatetime', 'media', 'nonbreaking',
'save', 'table', 'directionality', 'emoticons', 'template', 'importcss', 'codesample', 'help'
'save', 'table', 'directionality', 'emoticons', 'importcss', 'codesample', 'help'
],
// rtl_ui: true,
add_unload_trigger: false,

View File

@ -46,7 +46,6 @@ const urls = Arr.map<PartialPluginUrl, PluginUrl>([
{ key: 'save', name: 'Save' },
{ key: 'searchreplace', name: 'Search and Replace' },
{ key: 'table', name: 'Table' },
{ key: 'template', name: 'Template' },
{ key: 'textcolor', name: 'Text Color' },
{ key: 'visualblocks', name: 'Visual Blocks' },
{ key: 'visualchars', name: 'Visual Characters' },

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Plugin: template Demo Page</title>
</head>
<body>
<h2>Plugin: template Demo Page</h2>
<div id="ephox-ui">
<textarea cols="30" rows="10" class="tinymce"></textarea>
</div>
<script src="../../../../../js/tinymce/tinymce.js"></script>
<script src="../../../../../scratch/demos/plugins/template/demo.js"></script>
</body>
</html>

View File

@ -1,13 +0,0 @@
<!-- This will not be inserted -->
<div class="mceTmpl">
<table width="98%" border="1" cellspacing="0" cellpadding="0">
<tr>
<th scope="col">Header 1</th>
<th scope="col">Header 2</th>
</tr>
<tr>
<td>Cell 1</td>
<td>Cell 2</td>
</tr>
</table>
</div>

View File

@ -1,25 +0,0 @@
import { TinyMCE } from 'tinymce/core/api/PublicApi';
declare let tinymce: TinyMCE;
tinymce.init({
selector: 'textarea.tinymce',
plugins: 'template',
toolbar: 'template',
height: 600,
template_preview_replace_values: {
username: '<em>username here</em>'
},
template_replace_values: {
username: 'Jack',
staffid: '991234'
},
templates: [
{ title: 'Some title 1', description: 'Some desc 1', content: 'My content {$username}' },
{ title: 'Some title 2', description: 'Some desc 2', content: 'My other content' },
{ title: 'Some remote file', description: 'Some desc 3', url: 'development.html' },
{ title: 'Nonexistent remote file', description: 'Some desc 4', url: 'invalid.html' }
]
});
export {};

View File

@ -1,9 +0,0 @@
import Plugin from './Plugin';
Plugin();
/** *****
* DO NOT EXPORT ANYTHING
*
* IF YOU DO ROLLUP WILL LEAVE A GLOBAL ON THE PAGE
*******/

View File

@ -1,15 +0,0 @@
import PluginManager from 'tinymce/core/api/PluginManager';
import * as Commands from './api/Commands';
import * as Options from './api/Options';
import * as FilterContent from './core/FilterContent';
import * as Buttons from './ui/Buttons';
export default (): void => {
PluginManager.add('template', (editor) => {
Options.register(editor);
Buttons.register(editor);
Commands.register(editor);
FilterContent.setup(editor);
});
};

View File

@ -1,20 +0,0 @@
import { Fun } from '@ephox/katamari';
import Editor from 'tinymce/core/api/Editor';
import * as Templates from '../core/Templates';
import { ExternalTemplate } from '../core/Types';
import * as Dialog from '../ui/Dialog';
const showDialog = (editor: Editor) => (templates: ExternalTemplate[]): void => {
Dialog.open(editor, templates);
};
const register = (editor: Editor): void => {
editor.addCommand('mceInsertTemplate', Fun.curry(Templates.insertTemplate, editor));
editor.addCommand('mceTemplate', Templates.createTemplateList(editor, showDialog(editor)));
};
export {
register
};

View File

@ -1,83 +0,0 @@
import { Type } from '@ephox/katamari';
import Editor from 'tinymce/core/api/Editor';
import { EditorOptions } from 'tinymce/core/api/OptionTypes';
import { ExternalTemplate, TemplateValues } from '../core/Types';
type TemplateCallback = (callback: (templates: ExternalTemplate[]) => void) => void;
const option: {
<K extends keyof EditorOptions>(name: K): (editor: Editor) => EditorOptions[K];
<T>(name: string): (editor: Editor) => T;
} = (name: string) => (editor: Editor) =>
editor.options.get(name);
const register = (editor: Editor): void => {
const registerOption = editor.options.register;
registerOption('template_cdate_classes', {
processor: 'string',
default: 'cdate'
});
registerOption('template_mdate_classes', {
processor: 'string',
default: 'mdate'
});
registerOption('template_selected_content_classes', {
processor: 'string',
default: 'selcontent'
});
registerOption('template_preview_replace_values', {
processor: 'object'
});
registerOption('template_replace_values', {
processor: 'object'
});
registerOption('templates', {
processor: (value) => Type.isString(value) || Type.isArrayOf(value, Type.isObject) || Type.isFunction(value),
default: []
});
registerOption('template_cdate_format', {
processor: 'string',
default: editor.translate('%Y-%m-%d')
});
registerOption('template_mdate_format', {
processor: 'string',
default: editor.translate('%Y-%m-%d')
});
};
const getCreationDateClasses = option<string>('template_cdate_classes');
const getModificationDateClasses = option<string>('template_mdate_classes');
const getSelectedContentClasses = option<string>('template_selected_content_classes');
const getPreviewReplaceValues = option<TemplateValues | undefined>('template_preview_replace_values');
const getTemplateReplaceValues = option<TemplateValues | undefined>('template_replace_values');
const getTemplates = option<string | ExternalTemplate[] | TemplateCallback>('templates');
const getCdateFormat = option<string>('template_cdate_format');
const getMdateFormat = option<string>('template_mdate_format');
const getContentStyle = option('content_style');
const shouldUseContentCssCors = option('content_css_cors');
const getBodyClass = option('body_class');
export {
register,
getCreationDateClasses,
getModificationDateClasses,
getSelectedContentClasses,
getPreviewReplaceValues,
getTemplateReplaceValues,
getTemplates,
getCdateFormat,
getMdateFormat,
getBodyClass,
getContentStyle,
shouldUseContentCssCors
};

View File

@ -1,43 +0,0 @@
import Editor from 'tinymce/core/api/Editor';
const addZeros = (value: string | number, len: number): string => {
value = '' + value;
if (value.length < len) {
for (let i = 0; i < (len - value.length); i++) {
value = '0' + value;
}
}
return value;
};
const getDateTime = (editor: Editor, fmt: string, date: Date = new Date()): string => {
const daysShort = 'Sun Mon Tue Wed Thu Fri Sat Sun'.split(' ');
const daysLong = 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday'.split(' ');
const monthsShort = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' ');
const monthsLong = 'January February March April May June July August September October November December'.split(' ');
fmt = fmt.replace('%D', '%m/%d/%Y');
fmt = fmt.replace('%r', '%I:%M:%S %p');
fmt = fmt.replace('%Y', '' + date.getFullYear());
fmt = fmt.replace('%y', '' + (date as any).getYear());
fmt = fmt.replace('%m', addZeros(date.getMonth() + 1, 2));
fmt = fmt.replace('%d', addZeros(date.getDate(), 2));
fmt = fmt.replace('%H', '' + addZeros(date.getHours(), 2));
fmt = fmt.replace('%M', '' + addZeros(date.getMinutes(), 2));
fmt = fmt.replace('%S', '' + addZeros(date.getSeconds(), 2));
fmt = fmt.replace('%I', '' + ((date.getHours() + 11) % 12 + 1));
fmt = fmt.replace('%p', '' + (date.getHours() < 12 ? 'AM' : 'PM'));
fmt = fmt.replace('%B', '' + editor.translate(monthsLong[date.getMonth()]));
fmt = fmt.replace('%b', '' + editor.translate(monthsShort[date.getMonth()]));
fmt = fmt.replace('%A', '' + editor.translate(daysLong[date.getDay()]));
fmt = fmt.replace('%a', '' + editor.translate(daysShort[date.getDay()]));
fmt = fmt.replace('%%', '%');
return fmt;
};
export {
getDateTime
};

View File

@ -1,29 +0,0 @@
import Editor from 'tinymce/core/api/Editor';
import Tools from 'tinymce/core/api/util/Tools';
import * as Options from '../api/Options';
import * as DateTimeHelper from './DateTimeHelper';
import * as Templates from './Templates';
import { hasAnyClasses } from './Utils';
const setup = (editor: Editor): void => {
editor.on('PreProcess', (o) => {
const dom = editor.dom, dateFormat = Options.getMdateFormat(editor);
Tools.each(dom.select('div', o.node), (e) => {
if (dom.hasClass(e, 'mceTmpl')) {
Tools.each(dom.select('*', e), (e) => {
if (hasAnyClasses(dom, e, Options.getModificationDateClasses(editor))) {
e.innerHTML = DateTimeHelper.getDateTime(editor, dateFormat);
}
});
Templates.replaceVals(editor, e);
}
});
});
};
export {
setup
};

View File

@ -1,101 +0,0 @@
import { Regex, Type } from '@ephox/katamari';
import Editor from 'tinymce/core/api/Editor';
import Tools from 'tinymce/core/api/util/Tools';
import * as Options from '../api/Options';
import * as DateTimeHelper from './DateTimeHelper';
import { ExternalTemplate, TemplateValues } from './Types';
import { hasAnyClasses, parseAndSerialize } from './Utils';
const createTemplateList = (editor: Editor, callback: (templates: ExternalTemplate[]) => void) => {
return (): void => {
const templateList = Options.getTemplates(editor);
if (Type.isFunction(templateList)) {
templateList(callback);
} else if (Type.isString(templateList)) {
fetch(templateList)
.then((res) => {
if (res.ok) {
res.json().then(callback);
}
});
} else {
callback(templateList);
}
};
};
const replaceTemplateValues = (html: string, templateValues: TemplateValues | undefined): string => {
Tools.each(templateValues, (v, k) => {
if (Type.isFunction(v)) {
v = v(k);
}
html = html.replace(new RegExp('\\{\\$' + Regex.escape(k) + '\\}', 'g'), v);
});
return html;
};
const replaceVals = (editor: Editor, scope: HTMLElement): void => {
const dom = editor.dom, vl = Options.getTemplateReplaceValues(editor);
Tools.each(dom.select('*', scope), (e) => {
Tools.each(vl, (v, k) => {
if (dom.hasClass(e, k)) {
if (Type.isFunction(v)) {
// TODO: TINY-7792: Investigate as this appears to be a bug as "replaceTemplateValues" above uses
// the same values here and it expects a string and return value so this is not compatible.
v(e as any);
}
}
});
});
};
const insertTemplate = (editor: Editor, _ui: boolean, html: string): void => {
// Note: ui is unused here but is required since this can be called by execCommand
const dom = editor.dom;
const sel = editor.selection.getContent();
html = replaceTemplateValues(html, Options.getTemplateReplaceValues(editor));
let el = dom.create('div', {}, parseAndSerialize(editor, html));
// Find template element within div
const n = dom.select('.mceTmpl', el);
if (n && n.length > 0) {
el = dom.create('div');
el.appendChild(n[0].cloneNode(true));
}
Tools.each(dom.select('*', el), (n) => {
// Replace cdate
if (hasAnyClasses(dom, n, Options.getCreationDateClasses(editor))) {
n.innerHTML = DateTimeHelper.getDateTime(editor, Options.getCdateFormat(editor));
}
// Replace mdate
if (hasAnyClasses(dom, n, Options.getModificationDateClasses(editor))) {
n.innerHTML = DateTimeHelper.getDateTime(editor, Options.getMdateFormat(editor));
}
// Replace selection
if (hasAnyClasses(dom, n, Options.getSelectedContentClasses(editor))) {
n.innerHTML = sel;
}
});
replaceVals(editor, el);
editor.execCommand('mceInsertContent', false, el.innerHTML);
editor.addVisual();
};
export {
createTemplateList,
replaceTemplateValues,
replaceVals,
insertTemplate
};

View File

@ -1,32 +0,0 @@
import { Optional } from '@ephox/katamari';
export interface UrlTemplate {
readonly title: string;
readonly description: string;
readonly url: string;
}
export interface ContentTemplate {
readonly title: string;
readonly description: string;
readonly content: string;
}
export type ExternalTemplate = UrlTemplate | ContentTemplate;
export interface InternalTemplate {
readonly selected: boolean;
readonly text: string;
readonly value: {
readonly url: Optional<string>;
readonly content: Optional<string>;
readonly description: string;
};
}
export interface DialogData {
readonly template: string;
readonly preview: string;
}
export type TemplateValues = Record<string, string | ((name: string) => string)>;

View File

@ -1,30 +0,0 @@
import { Arr, Obj } from '@ephox/katamari';
import DOMUtils from 'tinymce/core/api/dom/DOMUtils';
import Editor from 'tinymce/core/api/Editor';
import HtmlSerializer from 'tinymce/core/api/html/Serializer';
const entitiesAttr: Record<string, string> = {
'"': '&quot;',
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'\'': '&#039;'
};
const htmlEscape = (html: string): string =>
html.replace(/["'<>&]/g, (match) => Obj.get(entitiesAttr, match).getOr(match));
const hasAnyClasses = (dom: DOMUtils, n: Element, classes: string): boolean =>
Arr.exists(classes.split(/\s+/), (c) => dom.hasClass(n, c));
const parseAndSerialize = (editor: Editor, html: string): string =>
HtmlSerializer({ validate: true }, editor.schema).serialize(
editor.parser.parse(html, { insert: true })
);
export {
hasAnyClasses,
htmlEscape,
parseAndSerialize
};

View File

@ -1,37 +0,0 @@
import Editor from 'tinymce/core/api/Editor';
import { Menu, Toolbar } from 'tinymce/core/api/ui/Ui';
const onSetupEditable = (editor: Editor) => (api: Toolbar.ToolbarButtonInstanceApi | Menu.MenuItemInstanceApi): VoidFunction => {
const nodeChanged = () => {
api.setEnabled(editor.selection.isEditable());
};
editor.on('NodeChange', nodeChanged);
nodeChanged();
return () => {
editor.off('NodeChange', nodeChanged);
};
};
const register = (editor: Editor): void => {
const onAction = () => editor.execCommand('mceTemplate');
editor.ui.registry.addButton('template', {
icon: 'template',
tooltip: 'Insert template',
onSetup: onSetupEditable(editor),
onAction
});
editor.ui.registry.addMenuItem('template', {
icon: 'template',
text: 'Insert template...',
onSetup: onSetupEditable(editor),
onAction
});
};
export {
register
};

View File

@ -1,220 +0,0 @@
import { Arr, Optional } from '@ephox/katamari';
import Editor from 'tinymce/core/api/Editor';
import Env from 'tinymce/core/api/Env';
import { Dialog } from 'tinymce/core/api/ui/Ui';
import Tools from 'tinymce/core/api/util/Tools';
import * as Options from '../api/Options';
import * as Templates from '../core/Templates';
import { DialogData, ExternalTemplate, InternalTemplate, UrlTemplate } from '../core/Types';
import * as Utils from '../core/Utils';
type UpdateDialogCallback = (dialogApi: Dialog.DialogInstanceApi<DialogData>, template: InternalTemplate, previewHtml: string) => void;
const getPreviewContent = (editor: Editor, html: string): string => {
let previewHtml = Utils.parseAndSerialize(editor, html);
if (html.indexOf('<html>') === -1) {
let contentCssEntries = '';
const contentStyle = Options.getContentStyle(editor) ?? '';
const cors = Options.shouldUseContentCssCors(editor) ? ' crossorigin="anonymous"' : '';
Tools.each(editor.contentCSS, (url) => {
contentCssEntries += '<link type="text/css" rel="stylesheet" href="' +
editor.documentBaseURI.toAbsolute(url) +
'"' + cors + '>';
});
if (contentStyle) {
contentCssEntries += '<style type="text/css">' + contentStyle + '</style>';
}
const bodyClass = Options.getBodyClass(editor);
const encode = editor.dom.encode;
const isMetaKeyPressed = Env.os.isMacOS() || Env.os.isiOS() ? 'e.metaKey' : 'e.ctrlKey && !e.altKey';
const preventClicksOnLinksScript = (
'<script>' +
'document.addEventListener && document.addEventListener("click", function(e) {' +
'for (var elm = e.target; elm; elm = elm.parentNode) {' +
'if (elm.nodeName === "A" && !(' + isMetaKeyPressed + ')) {' +
'e.preventDefault();' +
'}' +
'}' +
'}, false);' +
'</script> '
);
const directionality = editor.getBody().dir;
const dirAttr = directionality ? ' dir="' + encode(directionality) + '"' : '';
previewHtml = (
'<!DOCTYPE html>' +
'<html>' +
'<head>' +
'<base href="' + encode(editor.documentBaseURI.getURI()) + '">' +
contentCssEntries +
preventClicksOnLinksScript +
'</head>' +
'<body class="' + encode(bodyClass) + '"' + dirAttr + '>' +
previewHtml +
'</body>' +
'</html>'
);
}
return Templates.replaceTemplateValues(previewHtml, Options.getPreviewReplaceValues(editor));
};
const open = (editor: Editor, templateList: ExternalTemplate[]): void => {
const createTemplates = (): Optional<Array<InternalTemplate>> => {
if (!templateList || templateList.length === 0) {
const message = editor.translate('No templates defined.');
editor.notificationManager.open({ text: message, type: 'info' });
return Optional.none();
}
return Optional.from(Tools.map(templateList, (template: ExternalTemplate, index) => {
const isUrlTemplate = (t: ExternalTemplate): t is UrlTemplate => (t as UrlTemplate).url !== undefined;
return {
selected: index === 0,
text: template.title,
value: {
url: isUrlTemplate(template) ? Optional.from(template.url) : Optional.none(),
content: !isUrlTemplate(template) ? Optional.from(template.content) : Optional.none(),
description: template.description
}
};
}));
};
const createSelectBoxItems = (templates: InternalTemplate[]) => Arr.map(templates, (t) => ({
text: t.text,
value: t.text
}));
const findTemplate = (templates: InternalTemplate[], templateTitle: string) => Arr.find(templates, (t) => t.text === templateTitle);
const loadFailedAlert = (api: Dialog.DialogInstanceApi<DialogData>) => {
editor.windowManager.alert('Could not load the specified template.', () => api.focus('template'));
};
const getTemplateContent = (t: InternalTemplate): Promise<string> =>
t.value.url.fold(
() => Promise.resolve(t.value.content.getOr('')),
(url) => fetch(url).then((res) => res.ok ? res.text() : Promise.reject())
);
const onChange = (templates: InternalTemplate[], updateDialog: UpdateDialogCallback) =>
(api: Dialog.DialogInstanceApi<DialogData>, change: { name: string }) => {
if (change.name === 'template') {
const newTemplateTitle = api.getData().template;
findTemplate(templates, newTemplateTitle).each((t) => {
api.block('Loading...');
getTemplateContent(t).then((previewHtml) => {
updateDialog(api, t, previewHtml);
}).catch(() => {
updateDialog(api, t, '');
api.setEnabled('save', false);
loadFailedAlert(api);
});
});
}
};
const onSubmit = (templates: InternalTemplate[]) => (api: Dialog.DialogInstanceApi<DialogData>) => {
const data = api.getData();
findTemplate(templates, data.template).each((t) => {
getTemplateContent(t).then((previewHtml) => {
editor.execCommand('mceInsertTemplate', false, previewHtml);
api.close();
}).catch(() => {
api.setEnabled('save', false);
loadFailedAlert(api);
});
});
};
const openDialog = (templates: InternalTemplate[]) => {
const selectBoxItems = createSelectBoxItems(templates);
const buildDialogSpec = (bodyItems: Dialog.BodyComponentSpec[], initialData: DialogData): Dialog.DialogSpec<DialogData> => ({
title: 'Insert Template',
size: 'large',
body: {
type: 'panel',
items: bodyItems
},
initialData,
buttons: [
{
type: 'cancel',
name: 'cancel',
text: 'Cancel'
},
{
type: 'submit',
name: 'save',
text: 'Save',
primary: true
}
],
onSubmit: onSubmit(templates),
onChange: onChange(templates, updateDialog)
});
const updateDialog = (dialogApi: Dialog.DialogInstanceApi<DialogData>, template: InternalTemplate, previewHtml: string) => {
const content = getPreviewContent(editor, previewHtml);
const bodyItems: Dialog.BodyComponentSpec[] = [
{
type: 'listbox',
name: 'template',
label: 'Templates',
items: selectBoxItems
},
{
type: 'htmlpanel',
html: `<p aria-live="polite">${Utils.htmlEscape(template.value.description)}</p>`
},
{
label: 'Preview',
type: 'iframe',
name: 'preview',
sandboxed: false,
transparent: false
}
];
const initialData = {
template: template.text,
preview: content
};
dialogApi.unblock();
dialogApi.redial(buildDialogSpec(bodyItems, initialData));
dialogApi.focus('template');
};
const dialogApi = editor.windowManager.open(buildDialogSpec([], { template: '', preview: '' }));
dialogApi.block('Loading...');
getTemplateContent(templates[0]).then((previewHtml) => {
updateDialog(dialogApi, templates[0], previewHtml);
}).catch(() => {
updateDialog(dialogApi, templates[0], '');
dialogApi.setEnabled('save', false);
loadFailedAlert(dialogApi);
});
};
const optTemplates: Optional<InternalTemplate[]> = createTemplates();
optTemplates.each(openDialog);
};
export {
open,
getPreviewContent
};

View File

@ -1 +0,0 @@
<p><em>this is external</em></p>

View File

@ -1,140 +0,0 @@
import { afterEach, describe, it } from '@ephox/bedrock-client';
import { TinyAssertions, TinyHooks, TinySelections } from '@ephox/wrap-mcagar';
import Editor from 'tinymce/core/api/Editor';
import Plugin from 'tinymce/plugins/template/Plugin';
import { pInsertTemplate } from '../module/InsertTemplate';
import { Settings } from '../module/Settings';
describe('browser.tinymce.plugins.template.DatesTest', () => {
const hook = TinyHooks.bddSetupLight<Editor>({
plugins: 'template',
toolbar: 'template',
base_url: '/project/tinymce/js/tinymce'
}, [ Plugin ]);
const { addSettings, cleanupSettings } = Settings(hook);
afterEach(() => {
const editor = hook.editor();
cleanupSettings();
editor.setContent('');
});
it('TBA: Test cdate in snippet with default class', async () => {
const editor = hook.editor();
addSettings({
templates: [{ title: 'a', description: 'b', content: '<p class="cdate">x</p>' }],
template_cdate_format: 'fake date',
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor, '<p class="cdate">fake date</p>');
});
it('TBA: Test cdate in snippet with custom class', async () => {
const editor = hook.editor();
addSettings({
template_cdate_classes: 'customCdateClass',
templates: [{ title: 'a', description: 'b', content: '<p class="customCdateClass">x</p>' }],
template_cdate_format: 'fake date'
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor,
'<p class="customCdateClass">fake date</p>'
);
});
it('TBA: Test mdate updates with each serialization', async () => {
const editor = hook.editor();
addSettings({
template_mdate_format: 'fake modified date',
template_cdate_format: 'fake created date',
templates: [{ title: 'a', description: 'b', content: '<div class="mceTmpl"><p class="mdate"></p><p class="cdate"></p></div>' }]
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor, [
'<div class="mceTmpl">',
'<p class="mdate">fake modified date</p>',
'<p class="cdate">fake created date</p>',
'</div>'
].join('\n'));
addSettings({ template_mdate_format: 'changed modified date' });
TinyAssertions.assertContent(editor, [
'<div class="mceTmpl">',
'<p class="mdate">changed modified date</p>',
'<p class="cdate">fake created date</p>',
'</div>'
].join('\n'));
});
it('TBA: Test mdate updates with each serialization with custom class', async () => {
const editor = hook.editor();
addSettings({
template_mdate_classes: 'modified',
template_mdate_format: 'fake modified date',
template_cdate_format: 'fake created date',
templates: [{ title: 'a', description: 'b', content: '<div class="mceTmpl"><p class="modified"></p><p class="cdate"></p></div>' }]
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor, [
'<div class="mceTmpl">',
'<p class="modified">fake modified date</p>',
'<p class="cdate">fake created date</p>',
'</div>'
].join('\n'));
addSettings({ template_mdate_format: 'changed modified date' });
TinyAssertions.assertContent(editor, [
'<div class="mceTmpl">',
'<p class="modified">changed modified date</p>',
'<p class="cdate">fake created date</p>',
'</div>'
].join('\n'));
});
it('TBA: Multiple replacement classes provided via options', async () => {
const editor = hook.editor();
addSettings({
templates: [{ title: 'a', description: 'b', content: '<div class="mceTmpl"><p class="cdate1">x</p><p class="mdate2">y</p></div>' }],
template_cdate_classes: 'cdate1 cdate2',
template_cdate_format: 'fake created date',
template_mdate_classes: 'mdate1 mdate2',
template_mdate_format: 'fake modified date',
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor, [
'<div class="mceTmpl">',
'<p class="cdate1">fake created date</p>',
'<p class="mdate2">fake modified date</p>',
'</div>'
].join('\n'));
TinySelections.setCursor(editor, [ 0, 1, 0 ], 'fake modified date'.length);
editor.insertContent('<p class="mdate1">inserted modified date</p>');
addSettings({ template_mdate_format: 'changed modified date' });
TinyAssertions.assertContent(editor, [
'<div class="mceTmpl">',
'<p class="cdate1">fake created date</p>',
'<p class="mdate2">changed modified date</p>',
'<p class="mdate1">changed modified date</p>',
'</div>'
].join('\n'));
});
it('TINY-7433: replacement classes with regex like names', async () => {
const editor = hook.editor();
addSettings({
templates: [{ title: 'a', description: 'b', content: '<div class="mceTmpl"><p class="custom+cdate">x</p><p class="custom+mdate">y</p></div>' }],
template_cdate_classes: 'custom+cdate',
template_cdate_format: 'fake created date',
template_mdate_classes: 'custom+mdate',
template_mdate_format: 'fake modified date',
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor, [
'<div class="mceTmpl">',
'<p class="custom+cdate">fake created date</p>',
'<p class="custom+mdate">fake modified date</p>',
'</div>'
].join('\n'));
});
});

View File

@ -1,119 +0,0 @@
import { afterEach, describe, it } from '@ephox/bedrock-client';
import { TinyHooks } from '@ephox/wrap-mcagar';
import { assert } from 'chai';
import Editor from 'tinymce/core/api/Editor';
import Env from 'tinymce/core/api/Env';
import Plugin from 'tinymce/plugins/template/Plugin';
import { getPreviewContent } from 'tinymce/plugins/template/ui/Dialog';
import { Settings } from '../module/Settings';
const metaKey = Env.os.isMacOS() || Env.os.isiOS() ? 'e.metaKey' : 'e.ctrlKey && !e.altKey';
const host = document.location.host;
const noCorsNoStyle = '<!DOCTYPE html><html><head>' +
`<base href="http://${host}/">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/ui/oxide/content.min.css">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/content/default/content.css">` +
'<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A" && !(' +
metaKey +
')) {e.preventDefault();}}}, false);</script> ' +
'</head><body class=""></body></html>';
const corsNoStyle = '<!DOCTYPE html><html><head>' +
`<base href="http://${host}/">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/ui/oxide/content.min.css" crossorigin="anonymous">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/content/default/content.css" crossorigin="anonymous">` +
'<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A" && !(' +
metaKey +
')) {e.preventDefault();}}}, false);</script> </head><body class=""></body></html>';
const noCorsStyle = '<!DOCTYPE html><html><head>' +
`<base href="http://${host}/">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/ui/oxide/content.min.css">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/content/default/content.css">` +
'<style type="text/css">This is the style inserted into the document</style>' +
'<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A" && !(' +
metaKey +
')) {e.preventDefault();}}}, false);</script> ' +
'</head><body class=""></body></html>';
const corsStyle = '<!DOCTYPE html><html><head>' +
`<base href="http://${host}/">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/ui/oxide/content.min.css" crossorigin="anonymous">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/content/default/content.css" crossorigin="anonymous">` +
'<style type="text/css">This is the style inserted into the document</style>' +
'<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A" && !(' +
metaKey +
')) {e.preventDefault();}}}, false);</script> ' +
'</head><body class=""></body></html>';
const corsStyleAndContent = '<!DOCTYPE html><html><head>' +
`<base href="http://${host}/">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/ui/oxide/content.min.css" crossorigin="anonymous">` +
`<link type="text/css" rel="stylesheet" href="http://${host}/project/tinymce/js/tinymce/skins/content/default/content.css" crossorigin="anonymous">` +
'<style type="text/css">This is the style inserted into the document</style>' +
'<script>document.addEventListener && document.addEventListener("click", function(e) {for (var elm = e.target; elm; elm = elm.parentNode) {if (elm.nodeName === "A" && !(' +
metaKey +
')) {e.preventDefault();}}}, false);</script> ' +
'</head>' +
'<body class=""><p>Custom content which was provided</p></body></html>';
// TODO TINY-10480: Investigate flaky tests
describe.skip('browser.tinymce.plugins.template.Dialog.getPreviewContent', () => {
const hook = TinyHooks.bddSetupLight<Editor>({
plugins: 'template',
base_url: '/project/tinymce/js/tinymce'
}, [ Plugin ]);
const checkPreview = (expected: string, html: string = '') => {
const editor = hook.editor();
assert.equal(getPreviewContent(editor, html), expected);
};
const { addSettings, cleanupSettings } = Settings(hook);
afterEach(() => {
cleanupSettings();
});
it('TINY-6115: Dialog.getPreviewContent: No CORS or content style, no previous HTML', () => {
checkPreview(noCorsNoStyle);
});
it('TINY-6115: Dialog.getPreviewContent: CORS but no content style, no previous HTML', () => {
addSettings({ content_css_cors: true });
checkPreview(corsNoStyle);
});
it('TINY-6115: Dialog.getPreviewcontent: No CORS but content style, no previous HTML', () => {
addSettings({ content_style: 'This is the style inserted into the document' });
checkPreview(noCorsStyle);
});
it('TINY-6115: Dialog.getPreviewContent: No CORS but content style, no previous HTML', () => {
addSettings({
content_css_cors: true,
content_style: 'This is the style inserted into the document'
});
checkPreview(corsStyle);
});
it('TINY-6115: Dialog.getPreviewContent: with provided content', () => {
addSettings({
content_css_cors: true,
content_style: 'This is the style inserted into the document'
});
checkPreview(corsStyleAndContent, 'Custom content which was provided');
});
it('TINY-6115: Dialog.getPreviewContent: with provided html', () => {
addSettings({
content_css_cors: true,
content_style: 'This is the style inserted into the document'
});
// TINY-9867: Preview content is parsed to minimise visual discrepancy with inserted content
checkPreview('<p>Custom content here</p>', '<html>Custom content here');
});
});

View File

@ -1,36 +0,0 @@
import { UiFinder, Waiter } from '@ephox/agar';
import { describe, it } from '@ephox/bedrock-client';
import { SugarBody } from '@ephox/sugar';
import { TinyAssertions, TinyHooks, TinyUiActions } from '@ephox/wrap-mcagar';
import Editor from 'tinymce/core/api/Editor';
import Plugin from 'tinymce/plugins/template/Plugin';
const dialogSelector = 'div.tox-dialog';
const alertDialogSelector = 'div.tox-dialog.tox-alert-dialog';
const toolbarButtonSelector = '[role="toolbar"] button[aria-label="Insert template"]';
describe('browser.tinymce.plugins.template.InvalidUrlTest', () => {
const hook = TinyHooks.bddSetupLight<Editor>({
plugins: 'template',
toolbar: 'template',
base_url: '/project/tinymce/js/tinymce'
}, [ Plugin ]);
it('TBA: Test loading in snippet from file that does not exist', async () => {
const editor = hook.editor();
editor.setContent('');
editor.options.set('templates', [{ title: 'invalid', description: 'b', url: '/custom/404' }, { title: 'a', description: 'a', content: '<strong>c</strong>' }]);
TinyUiActions.clickOnToolbar(editor, toolbarButtonSelector);
await TinyUiActions.pWaitForDialog(editor);
await TinyUiActions.pWaitForPopup(editor, alertDialogSelector);
// Click on Save button (should be disabled)
TinyUiActions.clickOnUi(editor, 'button.tox-button:contains(OK)');
await Waiter.pTryUntil('Alert dialog should close', () => UiFinder.notExists(SugarBody.body(), alertDialogSelector));
TinyUiActions.submitDialog(editor);
await Waiter.pTryUntil('Dialog should not close', () => UiFinder.exists(SugarBody.body(), dialogSelector));
TinyUiActions.cancelDialog(editor);
await Waiter.pTryUntil('Dialog should close', () => UiFinder.notExists(SugarBody.body(), dialogSelector));
TinyAssertions.assertContent(editor, '');
});
});

View File

@ -1,40 +0,0 @@
import { Keys, UiFinder } from '@ephox/agar';
import { describe, it } from '@ephox/bedrock-client';
import { SugarBody } from '@ephox/sugar';
import { TinyHooks, TinySelections, TinyState, TinyUiActions } from '@ephox/wrap-mcagar';
import Editor from 'tinymce/core/api/Editor';
import Plugin from 'tinymce/plugins/template/Plugin';
describe('browser.tinymce.plugins.template.NoneditableRootTest', () => {
const hook = TinyHooks.bddSetup<Editor>({
plugins: 'template',
toolbar: 'template',
base_url: '/project/tinymce/js/tinymce'
}, [ Plugin ], true);
it('TINY-9669: Disable template button on noneditable content', () => {
TinyState.withNoneditableRootEditor(hook.editor(), (editor) => {
editor.setContent('<div>Noneditable content</div><div contenteditable="true">Editable content</div>');
TinySelections.setSelection(editor, [ 0, 0 ], 0, [ 0, 0 ], 2);
UiFinder.exists(SugarBody.body(), '[aria-label="Insert template"][aria-disabled="true"]');
TinySelections.setSelection(editor, [ 1, 0 ], 0, [ 1, 0 ], 2);
UiFinder.exists(SugarBody.body(), '[aria-label="Insert template"][aria-disabled="false"]');
});
});
it('TINY-9669: Disable template menuitem on noneditable content', async () => {
await TinyState.withNoneditableRootEditorAsync(hook.editor(), async (editor) => {
editor.setContent('<div>Noneditable content</div><div contenteditable="true">Editable content</div>');
TinySelections.setSelection(editor, [ 0, 0 ], 0, [ 0, 0 ], 2);
TinyUiActions.clickOnMenu(editor, 'button:contains("Insert")');
await TinyUiActions.pWaitForUi(editor, '[role="menuitem"][aria-label="Insert template..."][aria-disabled="true"]');
TinyUiActions.keystroke(editor, Keys.escape());
TinySelections.setSelection(editor, [ 1, 0 ], 0, [ 1, 0 ], 2);
TinyUiActions.clickOnMenu(editor, 'button:contains("Insert")');
await TinyUiActions.pWaitForUi(editor, '[role="menuitem"][aria-label="Insert template..."][aria-disabled="false"]');
TinyUiActions.keystroke(editor, Keys.escape());
});
});
});

View File

@ -1,48 +0,0 @@
import { afterEach, describe, it } from '@ephox/bedrock-client';
import { TinyAssertions, TinyHooks, TinySelections } from '@ephox/wrap-mcagar';
import Editor from 'tinymce/core/api/Editor';
import Plugin from 'tinymce/plugins/template/Plugin';
import { pInsertTemplate } from '../module/InsertTemplate';
import { Settings } from '../module/Settings';
describe('browser.tinymce.plugins.template.SelectedContentTest', () => {
const hook = TinyHooks.bddSetupLight<Editor>({
plugins: 'template',
toolbar: 'template',
base_url: '/project/tinymce/js/tinymce'
}, [ Plugin ]);
const { addSettings, cleanupSettings } = Settings(hook);
afterEach(() => {
const editor = hook.editor();
cleanupSettings();
editor.setContent('');
});
it('TBA: Test selected content replacement with default class', async () => {
const editor = hook.editor();
editor.setContent('Text');
TinySelections.setSelection(editor, [ 0, 0 ], 0, [ 0, 0 ], 4);
addSettings({
templates: [{ title: 'a', description: 'b', content: '<h1 class="selcontent">This will be replaced</h1>' }],
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor, '<h1 class="selcontent">Text</h1>');
});
it('TBA: Test selected content replacement with custom class', async () => {
const editor = hook.editor();
editor.setContent('Text');
TinySelections.setSelection(editor, [ 0, 0 ], 0, [ 0, 0 ], 4);
addSettings({
template_selected_content_classes: 'customSelected',
templates: [{ title: 'a', description: 'b', content: '<h1 class="customSelected">This will be replaced/h1>' }],
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor, '<h1 class="customSelected">Text</h1>'
);
});
});

View File

@ -1,151 +0,0 @@
import { UiFinder, Waiter } from '@ephox/agar';
import { afterEach, beforeEach, context, describe, it } from '@ephox/bedrock-client';
import { Cell } from '@ephox/katamari';
import { SugarElement } from '@ephox/sugar';
import { TinyAssertions, TinyHooks } from '@ephox/wrap-mcagar';
import { assert } from 'chai';
import Editor from 'tinymce/core/api/Editor';
import Plugin from 'tinymce/plugins/template/Plugin';
import { pInsertTemplate, pPreviewTemplate } from '../module/InsertTemplate';
import { Settings } from '../module/Settings';
describe('browser.tinymce.plugins.template.TemplateSanityTest', () => {
const hook = TinyHooks.bddSetupLight<Editor>({
plugins: 'template',
toolbar: 'template',
base_url: '/project/tinymce/js/tinymce'
}, [ Plugin ]);
const { addSettings, cleanupSettings } = Settings(hook);
beforeEach(() => {
const editor = hook.editor();
editor.setContent('');
});
afterEach(() => {
cleanupSettings();
});
it('TBA: Test basic template insertion', async () => {
const editor = hook.editor();
addSettings({
templates: [{ title: 'a', description: 'b', content: '<strong>c</strong>' }],
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor, '<p><strong>c</strong></p>');
});
it('TBA: Test basic content replacement', async () => {
const editor = hook.editor();
addSettings({
template_replace_values: { name: 'Tester', email: 'test@test.com' },
templates: [{ title: 'a', description: 'b', content: '<p>{$name} {$email}</p>' }]
});
await pInsertTemplate(editor);
TinyAssertions.assertContent(editor, '<p>Tester test@test.com</p>');
});
it('TBA: Test loading in snippet from other file', async () => {
const editor = hook.editor();
addSettings({
templates: [{ title: 'a', description: '<strong>b</strong>', url: '/project/tinymce/src/plugins/template/test/html/test_template.html' }]
});
await pInsertTemplate(editor, (dialogEl) => {
UiFinder.exists(dialogEl, 'p:contains("<strong>b</strong>")');
});
TinyAssertions.assertContent(editor, '<p><em>this is external</em></p>');
});
it('TBA: Test command', () => {
const editor = hook.editor();
addSettings({
template_replace_values: { name: 'Tester', email: 'test@test.com' },
});
editor.execCommand('mceInsertTemplate', false, '<p>{$name}</p>');
TinyAssertions.assertContent(editor, '<p>Tester</p>');
});
it('TINY-7433: Replace template values with regex like keys', () => {
const editor = hook.editor();
addSettings({
template_replace_values: { 'first+name': 'Tester', 'email': 'test@test.com' },
});
editor.execCommand('mceInsertTemplate', false, '<p>{$first+name}</p>');
TinyAssertions.assertContent(editor, '<p>Tester</p>');
});
context('Previewing unparsed content', () => {
const unparsedHtml = '<img src="error" onerror="throw new Error();">';
const unparsedPreviewHtmlSelector = 'p > img[src="error"][onerror="throw new Error();"]';
const parsedPreviewHtmlSelector = 'p > img[src="error"][data-mce-src="error"]';
const pPreviewAndAssertNoUnparsedContent = async (editor: Editor): Promise<void> => {
const assertNoUnparsedContent = (dialogEl: SugarElement<Node>): void => {
UiFinder.findIn<HTMLIFrameElement>(dialogEl, 'iframe').fold(
() => assert.fail('Preview iframe not found'),
(iframe) => {
const iframeDoc = iframe.dom.contentDocument;
const iframeBody = SugarElement.fromDom(iframeDoc?.body as Node);
UiFinder.exists(iframeBody, parsedPreviewHtmlSelector);
UiFinder.notExists(iframeBody, unparsedPreviewHtmlSelector);
}
);
};
try {
await pPreviewTemplate(editor, assertNoUnparsedContent);
} catch {
assert.fail('Unparsed html read');
}
};
it('TINY-9244: Parsed html should be shown when previewing template', async () => {
const editor = hook.editor();
addSettings({
templates: [{ title: 'a', description: 'b', content: unparsedHtml }],
});
await pPreviewAndAssertNoUnparsedContent(editor);
});
it('TINY-9867: Parsed html should be shown when previewing template containing <html> tags', async () => {
const editor = hook.editor();
addSettings({
templates: [{ title: 'a', description: 'b', content: `<html>${unparsedHtml}</html>` }],
});
await pPreviewAndAssertNoUnparsedContent(editor);
});
});
context('Inserting unparsed content', () => {
const unparsedHtml = '<img src="error" onerror="window.document.unparsedHtmlFn();">';
const assertFnDoesNotReadUnParsedHtmlInDom = async (editor: Editor, fn: (unparsedHtml: string) => void | Promise<void>): Promise<void> => {
const isUnParsedHtmlRead = Cell(false);
(editor.getDoc() as any).unparsedHtmlFn = () => {
isUnParsedHtmlRead.set(true);
};
await fn(unparsedHtml);
// wait for any unparsed html to be read and error to be thrown if it is
await Waiter.pWait(1);
assert.isFalse(isUnParsedHtmlRead.get(), 'Unparsed html read');
(editor.getDoc() as any).unparsedHtmlFn = null;
};
it('TINY-9244: Unparsed html should not be read when inserting template via command', async () => {
const editor = hook.editor();
await assertFnDoesNotReadUnParsedHtmlInDom(editor, (unparsedHtml) => {
editor.execCommand('mceInsertTemplate', false, unparsedHtml);
});
});
it('TINY-9244: Unparsed html should not be read when inserting template via dialog', async () => {
const editor = hook.editor();
addSettings({ templates: [{ title: 'a', description: 'b', content: unparsedHtml }] });
await assertFnDoesNotReadUnParsedHtmlInDom(editor, async () => {
await pInsertTemplate(editor);
});
});
});
});

View File

@ -1,45 +0,0 @@
import { UiFinder, Waiter } from '@ephox/agar';
import { Type } from '@ephox/katamari';
import { SugarBody, SugarElement } from '@ephox/sugar';
import { TinyUiActions } from '@ephox/wrap-mcagar';
import Editor from 'tinymce/core/api/Editor';
const toolbarButtonSelector = '[role="toolbar"] button[aria-label="Insert template"]';
const dialogSelector = 'div.tox-dialog';
const waitUntilIframeLoaded = async (dialogEl: SugarElement<Node>): Promise<void> => {
await UiFinder.pWaitForState<HTMLIFrameElement>('iframe is loaded', dialogEl, 'iframe', (elm) => {
// fallback for pre-IE 8 using contentWindow.document
const iframeDoc = elm.dom.contentDocument || elm.dom.contentWindow?.document;
return Type.isNonNullable(iframeDoc?.body.firstChild);
});
};
const pUseTemplateDialog = async (editor: Editor, submit: boolean, assertFn?: (elm: SugarElement<Node>) => void): Promise<void> => {
TinyUiActions.clickOnToolbar(editor, toolbarButtonSelector);
const dialogEl = await TinyUiActions.pWaitForDialog(editor);
if (Type.isFunction(assertFn)) {
await waitUntilIframeLoaded(dialogEl);
assertFn(dialogEl);
}
if (submit) {
TinyUiActions.submitDialog(editor);
} else {
TinyUiActions.closeDialog(editor);
}
await Waiter.pTryUntil('Dialog should close', () => UiFinder.notExists(SugarBody.body(), dialogSelector));
};
const pInsertTemplate = async (editor: Editor, assertFn?: (elm: SugarElement<Node>) => void): Promise<void> => {
await pUseTemplateDialog(editor, true, assertFn);
};
const pPreviewTemplate = async (editor: Editor, assertFn?: (elm: SugarElement<Node>) => void): Promise<void> => {
await pUseTemplateDialog(editor, false, assertFn);
};
export {
pInsertTemplate,
pPreviewTemplate
};

View File

@ -1,31 +0,0 @@
import { Obj } from '@ephox/katamari';
import { TinyHooks } from '@ephox/wrap-mcagar';
import Editor from 'tinymce/core/api/Editor';
interface Settings {
readonly addSettings: (config: Record<string, any>) => void;
readonly cleanupSettings: () => void;
}
const Settings = (hook: TinyHooks.Hook<Editor>): Settings => {
let settings = new Set<string>();
const addSettings = (config: Record<string, any>) => {
const editor = hook.editor();
Obj.each(config, (val, key) => {
editor.options.set(key, val);
settings.add(key);
});
};
const cleanupSettings = () => {
const editor = hook.editor();
settings.forEach((key) => editor.options.unset(key));
settings = new Set<string>();
};
return { addSettings, cleanupSettings };
};
export { Settings };

View File

@ -23,7 +23,7 @@ const defaultMenus: Record<string, MenuSpec> = {
file: { title: 'File', items: 'newdocument restoredraft | preview | export print | deleteallconversations' },
edit: { title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall | searchreplace' },
view: { title: 'View', items: 'code revisionhistory | visualaid visualchars visualblocks | spellchecker | preview fullscreen | showcomments' },
insert: { title: 'Insert', items: 'image link media addcomment pageembed template inserttemplate codesample inserttable accordion | charmap emoticons hr | pagebreak nonbreaking anchor tableofcontents footnotes | mergetags | insertdatetime' },
insert: { title: 'Insert', items: 'image link media addcomment pageembed inserttemplate codesample inserttable accordion | charmap emoticons hr | pagebreak nonbreaking anchor tableofcontents footnotes | mergetags | insertdatetime' },
format: { title: 'Format', items: 'bold italic underline strikethrough superscript subscript codeformat | styles blocks fontfamily fontsize align lineheight | forecolor backcolor | language | removeformat' },
tools: { title: 'Tools', items: 'aidialog aishortcuts | spellchecker spellcheckerlanguage | autocorrect capitalization | a11ycheck code typography wordcount addtemplate' },
table: { title: 'Table', items: 'inserttable | cell row column | advtablesort | tableprops deletetable' },

View File

@ -40,7 +40,6 @@
"tinymce/plugins/save/*": ["src/plugins/save/main/ts/*"],
"tinymce/plugins/searchreplace/*": ["src/plugins/searchreplace/main/ts/*"],
"tinymce/plugins/table/*": ["src/plugins/table/main/ts/*"],
"tinymce/plugins/template/*": ["src/plugins/template/main/ts/*"],
"tinymce/plugins/visualblocks/*": ["src/plugins/visualblocks/main/ts/*"],
"tinymce/plugins/visualchars/*": ["src/plugins/visualchars/main/ts/*"],
"tinymce/plugins/wordcount/*": ["src/plugins/wordcount/main/ts/*"],

View File

@ -41,7 +41,6 @@
"tinymce/plugins/save/*": ["modules/tinymce/src/plugins/save/main/ts/*"],
"tinymce/plugins/searchreplace/*": ["modules/tinymce/src/plugins/searchreplace/main/ts/*"],
"tinymce/plugins/table/*": ["modules/tinymce/src/plugins/table/main/ts/*"],
"tinymce/plugins/template/*": ["modules/tinymce/src/plugins/template/main/ts/*"],
"tinymce/plugins/visualblocks/*": ["modules/tinymce/src/plugins/visualblocks/main/ts/*"],
"tinymce/plugins/visualchars/*": ["modules/tinymce/src/plugins/visualchars/main/ts/*"],
"tinymce/plugins/wordcount/*": ["modules/tinymce/src/plugins/wordcount/main/ts/*"],

View File

@ -63,7 +63,6 @@
"tinymce/plugins/save/*": ["modules/tinymce/src/plugins/save/main/ts/*"],
"tinymce/plugins/searchreplace/*": ["modules/tinymce/src/plugins/searchreplace/main/ts/*"],
"tinymce/plugins/table/*": ["modules/tinymce/src/plugins/table/main/ts/*"],
"tinymce/plugins/template/*": ["modules/tinymce/src/plugins/template/main/ts/*"],
"tinymce/plugins/visualblocks/*": ["modules/tinymce/src/plugins/visualblocks/main/ts/*"],
"tinymce/plugins/visualchars/*": ["modules/tinymce/src/plugins/visualchars/main/ts/*"],
"tinymce/plugins/wordcount/*": ["modules/tinymce/src/plugins/wordcount/main/ts/*"],