From 25ef885d38cca089f5cbf1961ded1b8e78344778 Mon Sep 17 00:00:00 2001 From: Dimitris Grammatikogiannis Date: Mon, 29 May 2023 09:09:14 +0200 Subject: [PATCH] [5.0] tinyMCE b/c fixes (#40626) Co-authored-by: Richard Fath Co-authored-by: Brian Teeman --- administrator/components/com_admin/script.php | 97 +++ .../init/exemptions/tinymce.es6.js | 6 +- .../js/plugins/jtemplate/plugin.es5.js | 554 ++++++++++++++++++ .../js/tinymce-builder.es6.js | 2 +- installation/sql/mysql/base.sql | 2 +- installation/sql/postgresql/base.sql | 2 +- .../editors/tinymce/field/tinymcebuilder.php | 12 +- .../editors/tinymce/src/Extension/TinyMCE.php | 45 +- .../tinymce/src/Field/TemplateslistField.php | 17 +- .../tinymce/src/PluginTraits/DisplayTrait.php | 63 +- .../tinymce/src/PluginTraits/KnownButtons.php | 10 +- .../src/PluginTraits/ToolbarPresets.php | 8 +- plugins/editors/tinymce/tinymce.xml | 2 +- 13 files changed, 738 insertions(+), 82 deletions(-) create mode 100644 build/media_source/plg_editors_tinymce/js/plugins/jtemplate/plugin.es5.js diff --git a/administrator/components/com_admin/script.php b/administrator/components/com_admin/script.php index 0a564bbe80def..0132707cc9c40 100644 --- a/administrator/components/com_admin/script.php +++ b/administrator/components/com_admin/script.php @@ -899,6 +899,103 @@ public function postflight($action, $installer) } // Add here code which shall be executed only when updating from an older version than 5.0.0 + if (!$this->migrateTinymceConfiguration()) { + return false; + } + + return true; + } + + /** + * Migrate TinyMCE editor plugin configuration + * + * @return boolean True on success + * + * @since __DEPLOY_VERSION__ + */ + private function migrateTinymceConfiguration(): bool + { + $db = Factory::getDbo(); + + try { + // Get the TinyMCE editor plugin's parameters + $params = $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('editors')) + ->where($db->quoteName('element') . ' = ' . $db->quote('tinymce')) + )->loadResult(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return false; + } + + $params = json_decode($params, true); + + // If there are no toolbars there is nothing to migrate + if (!isset($params['configuration']['toolbars'])) { + return true; + } + + // Each set has its own toolbar configuration + foreach ($params['configuration']['toolbars'] as $setIdx => $toolbarConfig) { + // Migrate menu items if there is a menu + if (isset($toolbarConfig['menu'])) { + /** + * Replace array values with menu item names ("old name" -> "new name"): + * "blockformats" -> "blocks" + * "fontformats" -> "fontfamily" + * "fontsizes" -> "fontsize" + * "formats" -> "styles" + * "template" -> "jtemplate" + */ + $params['configuration']['toolbars'][$setIdx]['menu'] = str_replace( + ['blockformats', 'fontformats', 'fontsizes', 'formats', 'template'], + ['blocks', 'fontfamily', 'fontsize', 'styles', 'jtemplate'], + $toolbarConfig['menu'] + ); + } + + // There could be no toolbar at all, or only toolbar1, or both toolbar1 and toolbar2 + foreach (['toolbar1', 'toolbar2'] as $toolbarIdx) { + // Migrate toolbar buttons if that toolbar exists + if (isset($toolbarConfig[$toolbarIdx])) { + /** + * Replace array values with button names ("old name" -> "new name"): + * "fontselect" -> "fontfamily" + * "fontsizeselect" -> "fontsize" + * "formatselect" -> "blocks" + * "styleselect" -> "styles" + * "template" -> "jtemplate" + */ + $params['configuration']['toolbars'][$setIdx][$toolbarIdx] = str_replace( + ['fontselect', 'fontsizeselect', 'formatselect', 'styleselect', 'template'], + ['fontfamily', 'fontsize', 'blocks', 'styles', 'jtemplate'], + $toolbarConfig[$toolbarIdx] + ); + } + } + } + + $params = json_encode($params); + + $query = $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params)) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('editors')) + ->where($db->quoteName('element') . ' = ' . $db->quote('tinymce')); + + try { + $db->setQuery($query)->execute(); + } catch (Exception $e) { + echo Text::sprintf('JLIB_DATABASE_ERROR_FUNCTION_FAILED', $e->getCode(), $e->getMessage()) . '
'; + + return false; + } return true; } diff --git a/build/build-modules-js/init/exemptions/tinymce.es6.js b/build/build-modules-js/init/exemptions/tinymce.es6.js index bf4d1bff45a12..62b7b28d02860 100644 --- a/build/build-modules-js/init/exemptions/tinymce.es6.js +++ b/build/build-modules-js/init/exemptions/tinymce.es6.js @@ -1,5 +1,5 @@ const { - existsSync, copy, readFile, writeFile, mkdir, + existsSync, copy, readFile, writeFile, mkdir, removeSync, } = require('fs-extra'); const CssNano = require('cssnano'); const Postcss = require('postcss'); @@ -94,4 +94,8 @@ module.exports.tinyMCE = async (packageName, version) => { // Restore our code on the vendor folders await copy(join(RootPath, 'build/media_source/vendor/tinymce/templates'), join(RootPath, 'media/vendor/tinymce/templates'), { preserveTimestamps: true }); + // Drop the template plugin + if (existsSync(join(RootPath, 'media/vendor/tinymce/plugins/template'))) { + removeSync(join(RootPath, 'media/vendor/tinymce/plugins/template')); + } }; diff --git a/build/media_source/plg_editors_tinymce/js/plugins/jtemplate/plugin.es5.js b/build/media_source/plg_editors_tinymce/js/plugins/jtemplate/plugin.es5.js new file mode 100644 index 0000000000000..bfa19e9c33953 --- /dev/null +++ b/build/media_source/plg_editors_tinymce/js/plugins/jtemplate/plugin.es5.js @@ -0,0 +1,554 @@ +/** + * TinyMCE version 6.4.0 (2023-03-15) + */ + +(function () { + 'use strict'; + + var global$3 = tinymce.util.Tools.resolve('tinymce.PluginManager'); + + const hasProto = (v, constructor, predicate) => { + var _a; + if (predicate(v, constructor.prototype)) { + return true; + } else { + return ((_a = v.constructor) === null || _a === void 0 ? void 0 : _a.name) === constructor.name; + } + }; + const typeOf = x => { + const t = typeof x; + if (x === null) { + return 'null'; + } else if (t === 'object' && Array.isArray(x)) { + return 'array'; + } else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) { + return 'string'; + } else { + return t; + } + }; + const isType = type => value => typeOf(value) === type; + const isSimpleType = type => value => typeof value === type; + const isString = isType('string'); + const isObject = isType('object'); + const isArray = isType('array'); + const isNullable = a => a === null || a === undefined; + const isNonNullable = a => !isNullable(a); + const isFunction = isSimpleType('function'); + const isArrayOf = (value, pred) => { + if (isArray(value)) { + for (let i = 0, len = value.length; i < len; ++i) { + if (!pred(value[i])) { + return false; + } + } + return true; + } + return false; + }; + + const constant = value => { + return () => { + return value; + }; + }; + function curry(fn, ...initialArgs) { + return (...restArgs) => { + const all = initialArgs.concat(restArgs); + return fn.apply(null, all); + }; + } + const never = constant(false); + + const escape = text => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + var global$2 = 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('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('jtemplates', { + processor: value => isString(value) || isArrayOf(value, isObject) || 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('template_cdate_classes'); + const getModificationDateClasses = option('template_mdate_classes'); + const getSelectedContentClasses = option('template_selected_content_classes'); + const getPreviewReplaceValues = option('template_preview_replace_values'); + const getTemplateReplaceValues = option('template_replace_values'); + const getTemplates = option('jtemplates'); + const getCdateFormat = option('template_cdate_format'); + const getMdateFormat = option('template_mdate_format'); + const getContentStyle = option('content_style'); + const shouldUseContentCssCors = option('content_css_cors'); + const getBodyClass = option('body_class'); + + const addZeros = (value, len) => { + value = '' + value; + if (value.length < len) { + for (let i = 0; i < len - value.length; i++) { + value = '0' + value; + } + } + return value; + }; + const getDateTime = (editor, fmt, date = new Date()) => { + 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.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; + }; + + class Optional { + constructor(tag, value) { + this.tag = tag; + this.value = value; + } + static some(value) { + return new Optional(true, value); + } + static none() { + return Optional.singletonNone; + } + fold(onNone, onSome) { + if (this.tag) { + return onSome(this.value); + } else { + return onNone(); + } + } + isSome() { + return this.tag; + } + isNone() { + return !this.tag; + } + map(mapper) { + if (this.tag) { + return Optional.some(mapper(this.value)); + } else { + return Optional.none(); + } + } + bind(binder) { + if (this.tag) { + return binder(this.value); + } else { + return Optional.none(); + } + } + exists(predicate) { + return this.tag && predicate(this.value); + } + forall(predicate) { + return !this.tag || predicate(this.value); + } + filter(predicate) { + if (!this.tag || predicate(this.value)) { + return this; + } else { + return Optional.none(); + } + } + getOr(replacement) { + return this.tag ? this.value : replacement; + } + or(replacement) { + return this.tag ? this : replacement; + } + getOrThunk(thunk) { + return this.tag ? this.value : thunk(); + } + orThunk(thunk) { + return this.tag ? this : thunk(); + } + getOrDie(message) { + if (!this.tag) { + throw new Error(message !== null && message !== void 0 ? message : 'Called getOrDie on None'); + } else { + return this.value; + } + } + static from(value) { + return isNonNullable(value) ? Optional.some(value) : Optional.none(); + } + getOrNull() { + return this.tag ? this.value : null; + } + getOrUndefined() { + return this.value; + } + each(worker) { + if (this.tag) { + worker(this.value); + } + } + toArray() { + return this.tag ? [this.value] : []; + } + toString() { + return this.tag ? `some(${ this.value })` : 'none()'; + } + } + Optional.singletonNone = new Optional(false); + + const exists = (xs, pred) => { + for (let i = 0, len = xs.length; i < len; i++) { + const x = xs[i]; + if (pred(x, i)) { + return true; + } + } + return false; + }; + const map = (xs, f) => { + const len = xs.length; + const r = new Array(len); + for (let i = 0; i < len; i++) { + const x = xs[i]; + r[i] = f(x, i); + } + return r; + }; + const findUntil = (xs, pred, until) => { + for (let i = 0, len = xs.length; i < len; i++) { + const x = xs[i]; + if (pred(x, i)) { + return Optional.some(x); + } else if (until(x, i)) { + break; + } + } + return Optional.none(); + }; + const find = (xs, pred) => { + return findUntil(xs, pred, never); + }; + + const hasOwnProperty = Object.hasOwnProperty; + const get = (obj, key) => { + return has(obj, key) ? Optional.from(obj[key]) : Optional.none(); + }; + const has = (obj, key) => hasOwnProperty.call(obj, key); + + var global$1 = tinymce.util.Tools.resolve('tinymce.html.Serializer'); + + const entitiesAttr = { + '"': '"', + '<': '<', + '>': '>', + '&': '&', + '\'': ''' + }; + const htmlEscape = html => html.replace(/["'<>&]/g, match => get(entitiesAttr, match).getOr(match)); + const hasAnyClasses = (dom, n, classes) => exists(classes.split(/\s+/), c => dom.hasClass(n, c)); + const parseAndSerialize = (editor, html) => global$1({ validate: true }, editor.schema).serialize(editor.parser.parse(html, { insert: true })); + + const createTemplateList = (editor, callback) => { + return () => { + const templateList = getTemplates(editor); + if (isFunction(templateList)) { + templateList(callback); + } else if (isString(templateList)) { + fetch(templateList).then(res => { + if (res.ok) { + res.json().then(callback); + } + }); + } else { + callback(templateList); + } + }; + }; + const replaceTemplateValues = (html, templateValues) => { + global$2.each(templateValues, (v, k) => { + if (isFunction(v)) { + v = v(k); + } + html = html.replace(new RegExp('\\{\\$' + escape(k) + '\\}', 'g'), v); + }); + return html; + }; + const replaceVals = (editor, scope) => { + const dom = editor.dom, vl = getTemplateReplaceValues(editor); + global$2.each(dom.select('*', scope), e => { + global$2.each(vl, (v, k) => { + if (dom.hasClass(e, k)) { + if (isFunction(v)) { + v(e); + } + } + }); + }); + }; + const insertTemplate = (editor, _ui, html) => { + const dom = editor.dom; + const sel = editor.selection.getContent(); + html = replaceTemplateValues(html, getTemplateReplaceValues(editor)); + let el = dom.create('div', {}, parseAndSerialize(editor, html)); + const n = dom.select('.mceTmpl', el); + if (n && n.length > 0) { + el = dom.create('div'); + el.appendChild(n[0].cloneNode(true)); + } + global$2.each(dom.select('*', el), n => { + if (hasAnyClasses(dom, n, getCreationDateClasses(editor))) { + n.innerHTML = getDateTime(editor, getCdateFormat(editor)); + } + if (hasAnyClasses(dom, n, getModificationDateClasses(editor))) { + n.innerHTML = getDateTime(editor, getMdateFormat(editor)); + } + if (hasAnyClasses(dom, n, getSelectedContentClasses(editor))) { + n.innerHTML = sel; + } + }); + replaceVals(editor, el); + editor.execCommand('mceInsertContent', false, el.innerHTML); + editor.addVisual(); + }; + + var global = tinymce.util.Tools.resolve('tinymce.Env'); + + const getPreviewContent = (editor, html) => { + var _a; + if (html.indexOf('') === -1) { + let contentCssEntries = ''; + const contentStyle = (_a = getContentStyle(editor)) !== null && _a !== void 0 ? _a : ''; + const cors = shouldUseContentCssCors(editor) ? ' crossorigin="anonymous"' : ''; + global$2.each(editor.contentCSS, url => { + contentCssEntries += ''; + }); + if (contentStyle) { + contentCssEntries += ''; + } + const bodyClass = getBodyClass(editor); + const encode = editor.dom.encode; + const isMetaKeyPressed = global.os.isMacOS() || global.os.isiOS() ? 'e.metaKey' : 'e.ctrlKey && !e.altKey'; + const preventClicksOnLinksScript = ' '; + const directionality = editor.getBody().dir; + const dirAttr = directionality ? ' dir="' + encode(directionality) + '"' : ''; + html = '' + '' + '' + '' + contentCssEntries + preventClicksOnLinksScript + '' + '' + parseAndSerialize(editor, html) + '' + ''; + } + return replaceTemplateValues(html, getPreviewReplaceValues(editor)); + }; + const open = (editor, templateList) => { + const createTemplates = () => { + 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(global$2.map(templateList, (template, index) => { + const isUrlTemplate = t => t.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 => map(templates, t => ({ + text: t.text, + value: t.text + })); + const findTemplate = (templates, templateTitle) => find(templates, t => t.text === templateTitle); + const loadFailedAlert = api => { + editor.windowManager.alert('Could not load the specified template.', () => api.focus('template')); + }; + const getTemplateContent = t => t.value.url.fold(() => Promise.resolve(t.value.content.getOr('')), url => fetch(url).then(res => res.ok ? res.text() : Promise.reject())); + const onChange = (templates, updateDialog) => (api, change) => { + 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 => api => { + 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 => { + const selectBoxItems = createSelectBoxItems(templates); + const buildDialogSpec = (bodyItems, initialData) => ({ + 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, template, previewHtml) => { + const content = getPreviewContent(editor, previewHtml); + const bodyItems = [ + { + type: 'selectbox', + name: 'template', + label: 'Templates', + items: selectBoxItems + }, + { + type: 'htmlpanel', + html: `

${ htmlEscape(template.value.description) }

` + }, + { + 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 = createTemplates(); + optTemplates.each(openDialog); + }; + + const showDialog = editor => templates => { + open(editor, templates); + }; + const register$1 = editor => { + editor.addCommand('mceInsertTemplate', curry(insertTemplate, editor)); + editor.addCommand('mceTemplate', createTemplateList(editor, showDialog(editor))); + }; + + const setup = editor => { + editor.on('PreProcess', o => { + const dom = editor.dom, dateFormat = getMdateFormat(editor); + global$2.each(dom.select('div', o.node), e => { + if (dom.hasClass(e, 'mceTmpl')) { + global$2.each(dom.select('*', e), e => { + if (hasAnyClasses(dom, e, getModificationDateClasses(editor))) { + e.innerHTML = getDateTime(editor, dateFormat); + } + }); + replaceVals(editor, e); + } + }); + }); + }; + + const register = editor => { + const onAction = () => editor.execCommand('mceTemplate'); + editor.ui.registry.addButton('jtemplate', { + icon: 'template', + tooltip: 'Insert template', + onAction + }); + editor.ui.registry.addMenuItem('jtemplate', { + icon: 'template', + text: 'Insert template...', + onAction + }); + }; + + var Plugin = () => { + global$3.add('jtemplate', editor => { + register$2(editor); + register(editor); + register$1(editor); + setup(editor); + }); + }; + + Plugin(); + +})(); diff --git a/build/media_source/plg_editors_tinymce/js/tinymce-builder.es6.js b/build/media_source/plg_editors_tinymce/js/tinymce-builder.es6.js index 7c7a3b3807fb0..cdae08de84272 100644 --- a/build/media_source/plg_editors_tinymce/js/tinymce-builder.es6.js +++ b/build/media_source/plg_editors_tinymce/js/tinymce-builder.es6.js @@ -145,7 +145,7 @@ const tinymce = { 'table-split-cells': '', 'table-top-header': '', table: '', - template: '', + jtemplate: '', 'temporary-placeholder': '', toc: '', translate: '', diff --git a/installation/sql/mysql/base.sql b/installation/sql/mysql/base.sql index 2757cd2a5b39f..9a5d0763c8e84 100644 --- a/installation/sql/mysql/base.sql +++ b/installation/sql/mysql/base.sql @@ -282,7 +282,7 @@ INSERT INTO `#__extensions` (`package_id`, `name`, `type`, `element`, `folder`, (0, 'plg_editors-xtd_readmore', 'plugin', 'readmore', 'editors-xtd', 0, 1, 1, 0, 1, '', '', '', 8, 0), (0, 'plg_editors_codemirror', 'plugin', 'codemirror', 'editors', 0, 1, 1, 0, 1, '', '{"lineNumbers":"1","lineWrapping":"1","matchTags":"1","matchBrackets":"1","marker-gutter":"1","autoCloseTags":"1","autoCloseBrackets":"1","autoFocus":"1","theme":"default","tabmode":"indent"}', '', 1, 0), (0, 'plg_editors_none', 'plugin', 'none', 'editors', 0, 1, 1, 1, 1, '', '', '', 2, 0), -(0, 'plg_editors_tinymce', 'plugin', 'tinymce', 'editors', 0, 1, 1, 0, 1, '', '{"configuration":{"toolbars":{"2":{"toolbar1":["bold","underline","strikethrough","|","undo","redo","|","bullist","numlist","|","pastetext"]},"1":{"menu":["edit","insert","view","format","table","tools"],"toolbar1":["bold","italic","underline","strikethrough","|","alignleft","aligncenter","alignright","alignjustify","|","formatselect","|","bullist","numlist","|","outdent","indent","|","undo","redo","|","link","unlink","anchor","code","|","hr","table","|","subscript","superscript","|","charmap","pastetext","preview"]},"0":{"menu":["edit","insert","view","format","table","tools"],"toolbar1":["bold","italic","underline","strikethrough","|","alignleft","aligncenter","alignright","alignjustify","|","styleselect","|","formatselect","fontselect","fontsizeselect","|","searchreplace","|","bullist","numlist","|","outdent","indent","|","undo","redo","|","link","unlink","anchor","image","|","code","|","forecolor","backcolor","|","fullscreen","|","table","|","subscript","superscript","|","charmap","emoticons","media","hr","ltr","rtl","|","cut","copy","paste","pastetext","|","visualchars","visualblocks","nonbreaking","blockquote","template","|","print","preview","codesample","insertdatetime","removeformat"]}},"setoptions":{"2":{"access":["1"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"0","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""},"1":{"access":["6","2"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"0","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""},"0":{"access":["7","4","8"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"1","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""}}},"sets_amount":3,"html_height":"550","html_width":"750"}', '', 3, 0), +(0, 'plg_editors_tinymce', 'plugin', 'tinymce', 'editors', 0, 1, 1, 0, 1, '', '{"configuration":{"toolbars":{"2":{"toolbar1":["bold","underline","strikethrough","|","undo","redo","|","bullist","numlist","|","pastetext"]},"1":{"menu":["edit","insert","view","format","table","tools"],"toolbar1":["bold","italic","underline","strikethrough","|","alignleft","aligncenter","alignright","alignjustify","|","blocks","|","bullist","numlist","|","outdent","indent","|","undo","redo","|","link","unlink","anchor","code","|","hr","table","|","subscript","superscript","|","charmap","pastetext","preview"]},"0":{"menu":["edit","insert","view","format","table","tools"],"toolbar1":["bold","italic","underline","strikethrough","|","alignleft","aligncenter","alignright","alignjustify","|","styles","|","blocks","fontfamily","fontsize","|","searchreplace","|","bullist","numlist","|","outdent","indent","|","undo","redo","|","link","unlink","anchor","image","|","code","|","forecolor","backcolor","|","fullscreen","|","table","|","subscript","superscript","|","charmap","emoticons","media","hr","ltr","rtl","|","cut","copy","paste","pastetext","|","visualchars","visualblocks","nonbreaking","blockquote","jtemplate","|","print","preview","codesample","insertdatetime","removeformat"]}},"setoptions":{"2":{"access":["1"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"0","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""},"1":{"access":["6","2"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"0","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""},"0":{"access":["7","4","8"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"1","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""}}},"sets_amount":3,"html_height":"550","html_width":"750"}', '', 3, 0), (0, 'plg_extension_finder', 'plugin', 'finder', 'extension', 0, 1, 1, 0, 1, '', '', '', 1, 0), (0, 'plg_extension_joomla', 'plugin', 'joomla', 'extension', 0, 1, 1, 0, 1, '', '', '', 2, 0), (0, 'plg_extension_namespacemap', 'plugin', 'namespacemap', 'extension', 0, 1, 1, 1, 1, '', '{}', '', 3, 0), diff --git a/installation/sql/postgresql/base.sql b/installation/sql/postgresql/base.sql index 3cab39bfccc6e..63e9509eb5887 100644 --- a/installation/sql/postgresql/base.sql +++ b/installation/sql/postgresql/base.sql @@ -288,7 +288,7 @@ INSERT INTO "#__extensions" ("package_id", "name", "type", "element", "folder", (0, 'plg_editors-xtd_readmore', 'plugin', 'readmore', 'editors-xtd', 0, 1, 1, 0, 1, '', '', '', 8, 0), (0, 'plg_editors_codemirror', 'plugin', 'codemirror', 'editors', 0, 1, 1, 0, 1, '', '{"lineNumbers":"1","lineWrapping":"1","matchTags":"1","matchBrackets":"1","marker-gutter":"1","autoCloseTags":"1","autoCloseBrackets":"1","autoFocus":"1","theme":"default","tabmode":"indent"}', '', 1, 0), (0, 'plg_editors_none', 'plugin', 'none', 'editors', 0, 1, 1, 1, 1, '', '', '', 2, 0), -(0, 'plg_editors_tinymce', 'plugin', 'tinymce', 'editors', 0, 1, 1, 0, 1, '', '{"configuration":{"toolbars":{"2":{"toolbar1":["bold","underline","strikethrough","|","undo","redo","|","bullist","numlist","|","pastetext"]},"1":{"menu":["edit","insert","view","format","table","tools"],"toolbar1":["bold","italic","underline","strikethrough","|","alignleft","aligncenter","alignright","alignjustify","|","formatselect","|","bullist","numlist","|","outdent","indent","|","undo","redo","|","link","unlink","anchor","code","|","hr","table","|","subscript","superscript","|","charmap","pastetext","preview"]},"0":{"menu":["edit","insert","view","format","table","tools"],"toolbar1":["bold","italic","underline","strikethrough","|","alignleft","aligncenter","alignright","alignjustify","|","styleselect","|","formatselect","fontselect","fontsizeselect","|","searchreplace","|","bullist","numlist","|","outdent","indent","|","undo","redo","|","link","unlink","anchor","image","|","code","|","forecolor","backcolor","|","fullscreen","|","table","|","subscript","superscript","|","charmap","emoticons","media","hr","ltr","rtl","|","cut","copy","paste","pastetext","|","visualchars","visualblocks","nonbreaking","blockquote","template","|","print","preview","codesample","insertdatetime","removeformat"]}},"setoptions":{"2":{"access":["1"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"0","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""},"1":{"access":["6","2"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"0","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""},"0":{"access":["7","4","8"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"1","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""}}},"sets_amount":3,"html_height":"550","html_width":"750"}', '', 3, 0), +(0, 'plg_editors_tinymce', 'plugin', 'tinymce', 'editors', 0, 1, 1, 0, 1, '', '{"configuration":{"toolbars":{"2":{"toolbar1":["bold","underline","strikethrough","|","undo","redo","|","bullist","numlist","|","pastetext"]},"1":{"menu":["edit","insert","view","format","table","tools"],"toolbar1":["bold","italic","underline","strikethrough","|","alignleft","aligncenter","alignright","alignjustify","|","blocks","|","bullist","numlist","|","outdent","indent","|","undo","redo","|","link","unlink","anchor","code","|","hr","table","|","subscript","superscript","|","charmap","pastetext","preview"]},"0":{"menu":["edit","insert","view","format","table","tools"],"toolbar1":["bold","italic","underline","strikethrough","|","alignleft","aligncenter","alignright","alignjustify","|","styles","|","blocks","fontfamily","fontsize","|","searchreplace","|","bullist","numlist","|","outdent","indent","|","undo","redo","|","link","unlink","anchor","image","|","code","|","forecolor","backcolor","|","fullscreen","|","table","|","subscript","superscript","|","charmap","emoticons","media","hr","ltr","rtl","|","cut","copy","paste","pastetext","|","visualchars","visualblocks","nonbreaking","blockquote","jtemplate","|","print","preview","codesample","insertdatetime","removeformat"]}},"setoptions":{"2":{"access":["1"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"0","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""},"1":{"access":["6","2"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"0","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""},"0":{"access":["7","4","8"],"skin":"0","skin_admin":"0","mobile":"0","drag_drop":"1","path":"","entity_encoding":"raw","lang_mode":"1","text_direction":"ltr","content_css":"1","content_css_custom":"","relative_urls":"1","newlines":"0","use_config_textfilters":"0","invalid_elements":"script,applet,iframe","valid_elements":"","extended_elements":"","resizing":"1","resize_horizontal":"1","element_path":"1","wordcount":"1","image_advtab":"1","advlist":"1","autosave":"1","contextmenu":"1","custom_plugin":"","custom_button":""}}},"sets_amount":3,"html_height":"550","html_width":"750"}', '', 3, 0), (0, 'plg_extension_finder', 'plugin', 'finder', 'extension', 0, 1, 1, 0, 1, '', '', '', 1, 0), (0, 'plg_extension_joomla', 'plugin', 'joomla', 'extension', 0, 1, 1, 0, 1, '', '', '', 2, 0), (0, 'plg_extension_namespacemap', 'plugin', 'namespacemap', 'extension', 0, 1, 1, 1, 1, '', '{}', '', 3, 0), diff --git a/layouts/plugins/editors/tinymce/field/tinymcebuilder.php b/layouts/plugins/editors/tinymce/field/tinymcebuilder.php index d5b354367ac51..f08af22c03ab0 100644 --- a/layouts/plugins/editors/tinymce/field/tinymcebuilder.php +++ b/layouts/plugins/editors/tinymce/field/tinymcebuilder.php @@ -13,6 +13,7 @@ use Joomla\CMS\Document\HtmlDocument; use Joomla\CMS\Factory; use Joomla\CMS\Form\Form; +use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Language\Text; use Joomla\CMS\Layout\FileLayout; @@ -78,10 +79,13 @@ $doc->addScriptOptions( 'plg_editors_tinymce_builder', [ - 'menus' => $menus, - 'buttons' => $buttons, - 'toolbarPreset' => $toolbarPreset, - 'formControl' => $name . '[toolbars]', + 'menus' => $menus, + 'buttons' => $buttons, + 'toolbarPreset' => $toolbarPreset, + 'formControl' => $name . '[toolbars]', + 'external_plugins' => [ + 'jtemplate' => HTMLHelper::_('script', 'plg_editors_tinymce/plugins/jtemplate/plugin.min.js', ['relative' => true, 'version' => 'auto', 'pathOnly' => true]) + ] ] ); diff --git a/plugins/editors/tinymce/src/Extension/TinyMCE.php b/plugins/editors/tinymce/src/Extension/TinyMCE.php index 6db16d0a4573e..469eb7b64ddb5 100644 --- a/plugins/editors/tinymce/src/Extension/TinyMCE.php +++ b/plugins/editors/tinymce/src/Extension/TinyMCE.php @@ -10,7 +10,10 @@ namespace Joomla\Plugin\Editors\TinyMCE\Extension; +use Joomla\CMS\Filesystem\Folder; +use Joomla\CMS\Language\Text; use Joomla\CMS\Plugin\CMSPlugin; +use Joomla\CMS\Session\Session; use Joomla\Database\DatabaseAwareTrait; use Joomla\Plugin\Editors\TinyMCE\PluginTraits\DisplayTrait; @@ -37,13 +40,49 @@ final class TinyMCE extends CMSPlugin protected $autoloadLanguage = true; /** - * Initializes the Editor. + * Returns the templates * * @return void * - * @since 1.5 + * @since __DEPLOY_VERSION__ */ - public function onInit() + public function onAjaxTinymce() { + if (!Session::checkToken('request')) { + echo json_encode([]); + exit(); + } + + $templates = []; + $language = $this->getApplication()->getLanguage(); + $template = $this->getApplication()->input->getPath('template', ''); + + if ('' === $template) { + echo json_encode([]); + exit(); + } + + $filepaths = Folder::exists(JPATH_ROOT . '/templates/' . $template) + ? Folder::files(JPATH_ROOT . '/templates/' . $template, '\.(html|txt)$', false, true) + : []; + + foreach ($filepaths as $filepath) { + $fileinfo = pathinfo($filepath); + $filename = $fileinfo['filename']; + $title_upper = strtoupper($filename); + + if ($filename === 'index') { + continue; + } + + $templates[] = (object) [ + 'title' => $language->hasKey('PLG_TINY_TEMPLATE_' . $title_upper . '_TITLE') ? Text::_('PLG_TINY_TEMPLATE_' . $title_upper . '_TITLE') : $filename, + 'description' => $language->hasKey('PLG_TINY_TEMPLATE_' . $title_upper . '_DESC') ? Text::_('PLG_TINY_TEMPLATE_' . $title_upper . '_DESC') : ' ', + 'content' => file_get_contents($filepath), + ]; + } + + echo json_encode($templates); + exit(); } } diff --git a/plugins/editors/tinymce/src/Field/TemplateslistField.php b/plugins/editors/tinymce/src/Field/TemplateslistField.php index 03d27cd8d9d3c..a56457f4a65f8 100644 --- a/plugins/editors/tinymce/src/Field/TemplateslistField.php +++ b/plugins/editors/tinymce/src/Field/TemplateslistField.php @@ -63,18 +63,13 @@ public function setup(\SimpleXMLElement $element, $value, $group = null) */ public function getOptions() { - $def = new \stdClass(); - $def->value = ''; - $def->text = Text::_('JOPTION_DO_NOT_USE'); - $options = [0 => $def]; - $directories = [JPATH_ROOT . '/templates', JPATH_ROOT . '/media/templates/site']; + $def = new \stdClass(); + $def->value = ''; + $def->text = Text::_('JOPTION_DO_NOT_USE'); + $options = [0 => $def]; + $this->directory = JPATH_ROOT . '/templates'; - foreach ($directories as $directory) { - $this->directory = $directory; - $options = array_merge($options, parent::getOptions()); - } - - return $options; + return array_merge($options, parent::getOptions()); } /** diff --git a/plugins/editors/tinymce/src/PluginTraits/DisplayTrait.php b/plugins/editors/tinymce/src/PluginTraits/DisplayTrait.php index 8f24b0c111271..087be44d1c8e1 100644 --- a/plugins/editors/tinymce/src/PluginTraits/DisplayTrait.php +++ b/plugins/editors/tinymce/src/PluginTraits/DisplayTrait.php @@ -78,6 +78,7 @@ public function onDisplay( $externalPlugins = []; $options = $doc->getScriptOptions('plg_editor_tinymce'); $theme = 'silver'; + $csrf = Session::getFormToken(); // Data object for the layout $textarea = new stdClass(); @@ -304,49 +305,12 @@ public function onDisplay( } } - // Template - $templates = []; - - if (!empty($allButtons['template'])) { - // Do we have a custom content_template_path - $template_path = $levelParams->get('content_template_path'); - $template_path = $template_path ? '/templates/' . $template_path : '/media/vendor/tinymce/templates'; - - $filepaths = Folder::exists(JPATH_ROOT . $template_path) - ? Folder::files(JPATH_ROOT . $template_path, '\.(html|txt)$', false, true) - : []; - - foreach ($filepaths as $filepath) { - $fileinfo = pathinfo($filepath); - $filename = $fileinfo['filename']; - $full_filename = $fileinfo['basename']; - - if ($filename === 'index') { - continue; - } - - $title = $filename; - $title_upper = strtoupper($filename); - $description = ' '; - - if ($language->hasKey('PLG_TINY_TEMPLATE_' . $title_upper . '_TITLE')) { - $title = Text::_('PLG_TINY_TEMPLATE_' . $title_upper . '_TITLE'); - } - - if ($language->hasKey('PLG_TINY_TEMPLATE_' . $title_upper . '_DESC')) { - $description = Text::_('PLG_TINY_TEMPLATE_' . $title_upper . '_DESC'); - } - - $templates[] = [ - 'title' => $title, - 'description' => $description, - 'url' => Uri::root(true) . $template_path . '/' . $full_filename, - ]; - } - } + $jtemplates = !empty($allButtons['jtemplate']) + ? Uri::base(true) . '/index.php?option=com_ajax&plugin=tinymce&group=editors&format=json&format=json&template=' . $levelParams->get('content_template_path') . '&' . $csrf . '=1' + : false; // Check for extra plugins, from the setoptions form - foreach (['wordcount' => 1, 'advlist' => 1, 'autosave' => 1, 'textpattern' => 0] as $pName => $def) { + foreach (['wordcount' => 1, 'advlist' => 1, 'autosave' => 1] as $pName => $def) { if ($levelParams->get($pName, $def)) { $plugins[] = $pName; } @@ -372,7 +336,7 @@ public function onDisplay( Text::script('PLG_TINY_DND_EMPTY_ALT'); $scriptOptions['parentUploadFolder'] = $levelParams->get('path', ''); - $scriptOptions['csrfToken'] = Session::getFormToken(); + $scriptOptions['csrfToken'] = $csrf; $scriptOptions['uploadUri'] = $uploadUrl; // @TODO have a way to select the adapter, similar to $levelParams->get('path', ''); @@ -403,11 +367,6 @@ public function onDisplay( $plugins = array_merge($plugins, explode(strpos($custom_plugin, ',') !== false ? ',' : ' ', $custom_plugin)); } - // Version 6 unload removed plugins - $plugins = array_filter($plugins, function ($plugin) { - return !in_array($plugin, ['hr', 'paste', 'print']); - }); - if ($custom_button) { $toolbar1 = array_merge($toolbar1, explode(strpos($custom_button, ',') !== false ? ',' : ' ', $custom_button)); } @@ -415,6 +374,11 @@ public function onDisplay( // Merge the two toolbars for backwards compatibility $toolbar = array_merge($toolbar1, $toolbar2); + // Should load the templates plugin? + if (in_array('jtemplate', $toolbar)) { + $externalPlugins['jtemplate'] = HTMLHelper::_('script', 'plg_editors_tinymce/plugins/jtemplate/plugin.min.js', ['relative' => true, 'version' => 'auto', 'pathOnly' => true]); + } + // Build the final options set $scriptOptions = array_merge( $scriptOptions, @@ -468,7 +432,7 @@ public function onDisplay( 'width' => $this->params->get('html_width', ''), 'elementpath' => (bool) $levelParams->get('element_path', true), 'resize' => $resizing, - 'templates' => $templates, + 'jtemplates' => $jtemplates, 'external_plugins' => empty($externalPlugins) ? null : $externalPlugins, 'contextmenu' => (bool) $levelParams->get('contextmenu', true) ? null : false, 'toolbar_sticky' => true, @@ -494,14 +458,13 @@ public function onDisplay( if ($levelParams->get('newlines')) { // Break $scriptOptions['force_br_newlines'] = true; - $scriptOptions['forced_root_block'] = ''; } else { // Paragraph $scriptOptions['force_br_newlines'] = false; $scriptOptions['forced_root_block'] = 'p'; } - $scriptOptions['rel_list'] = [ + $scriptOptions['link_rel_list'] = [ ['title' => 'None', 'value' => ''], ['title' => 'Alternate', 'value' => 'alternate'], ['title' => 'Author', 'value' => 'author'], diff --git a/plugins/editors/tinymce/src/PluginTraits/KnownButtons.php b/plugins/editors/tinymce/src/PluginTraits/KnownButtons.php index 3599cd3468b08..92478b6685027 100644 --- a/plugins/editors/tinymce/src/PluginTraits/KnownButtons.php +++ b/plugins/editors/tinymce/src/PluginTraits/KnownButtons.php @@ -74,15 +74,15 @@ public static function getKnownButtons() 'cut' => ['label' => 'Cut'], 'copy' => ['label' => 'Copy'], - 'paste' => ['label' => 'Paste', 'plugin' => 'paste'], - 'pastetext' => ['label' => 'Paste as text', 'plugin' => 'paste'], + 'paste' => ['label' => 'Paste'], + 'pastetext' => ['label' => 'Paste as text'], 'removeformat' => ['label' => 'Clear formatting'], 'language' => ['label' => 'Language'], // Buttons from the plugins 'anchor' => ['label' => 'Anchor', 'plugin' => 'anchor'], - 'hr' => ['label' => 'Horizontal line', 'plugin' => 'hr'], + 'hr' => ['label' => 'Horizontal line'], 'ltr' => ['label' => 'Left to right', 'plugin' => 'directionality'], 'rtl' => ['label' => 'Right to left', 'plugin' => 'directionality'], 'code' => ['label' => 'Source code', 'plugin' => 'code'], @@ -96,10 +96,10 @@ public static function getKnownButtons() 'media' => ['label' => 'Insert/edit video', 'plugin' => 'media'], 'image' => ['label' => 'Insert/edit image', 'plugin' => 'image'], 'pagebreak' => ['label' => 'Page break', 'plugin' => 'pagebreak'], - 'print' => ['label' => 'Print', 'plugin' => 'print'], + 'print' => ['label' => 'Print'], 'preview' => ['label' => 'Preview', 'plugin' => 'preview'], 'fullscreen' => ['label' => 'Fullscreen', 'plugin' => 'fullscreen'], - 'template' => ['label' => 'Insert template', 'plugin' => 'template'], + 'jtemplate' => ['label' => 'Insert template', 'plugin' => 'jtemplate'], 'searchreplace' => ['label' => 'Find and replace', 'plugin' => 'searchreplace'], 'insertdatetime' => ['label' => 'Insert date/time', 'plugin' => 'insertdatetime'], 'help' => ['label' => 'Help', 'plugin' => 'help'], diff --git a/plugins/editors/tinymce/src/PluginTraits/ToolbarPresets.php b/plugins/editors/tinymce/src/PluginTraits/ToolbarPresets.php index 1070d9a506dd2..678411eed1fab 100644 --- a/plugins/editors/tinymce/src/PluginTraits/ToolbarPresets.php +++ b/plugins/editors/tinymce/src/PluginTraits/ToolbarPresets.php @@ -46,7 +46,7 @@ public static function getToolbarPreset() 'toolbar1' => [ 'bold', 'italic', 'underline', 'strikethrough', '|', 'alignleft', 'aligncenter', 'alignright', 'alignjustify', '|', - 'formatselect', '|', + 'blocks', '|', 'bullist', 'numlist', '|', 'outdent', 'indent', '|', 'undo', 'redo', '|', @@ -63,8 +63,8 @@ public static function getToolbarPreset() 'bold', 'italic', 'underline', 'strikethrough', '|', 'alignleft', 'aligncenter', 'alignright', 'alignjustify', '|', 'lineheight', '|', - 'styleselect', '|', - 'formatselect', 'fontfamily', 'fontsizeselect', '|', + 'styles', '|', + 'blocks', 'fontfamily', 'fontsize', '|', 'searchreplace', '|', 'bullist', 'numlist', '|', 'outdent', 'indent', '|', @@ -77,7 +77,7 @@ public static function getToolbarPreset() 'subscript', 'superscript', '|', 'charmap', 'emoticons', 'media', 'hr', 'ltr', 'rtl', '|', 'cut', 'copy', 'paste', 'pastetext', '|', - 'visualchars', 'visualblocks', 'nonbreaking', 'blockquote', 'template', '|', + 'visualchars', 'visualblocks', 'nonbreaking', 'blockquote', 'jtemplate', '|', 'print', 'preview', 'codesample', 'insertdatetime', 'removeformat', 'jxtdbuttons', 'language', ], diff --git a/plugins/editors/tinymce/tinymce.xml b/plugins/editors/tinymce/tinymce.xml index 46915704f1208..bfd23b26ddcad 100644 --- a/plugins/editors/tinymce/tinymce.xml +++ b/plugins/editors/tinymce/tinymce.xml @@ -7,7 +7,7 @@ N/A https://www.tiny.cloud Tiny Technologies, Inc - LGPL + MIT PLG_TINY_XML_DESCRIPTION Joomla\Plugin\Editors\TinyMCE