diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js index 984081bd6..6973db8c8 100644 --- a/resources/js/wysiwyg/config.js +++ b/resources/js/wysiwyg/config.js @@ -13,6 +13,7 @@ import {getPlugin as getImagemanagerPlugin} from './plugins-imagemanager'; import {getPlugin as getAboutPlugin} from './plugins-about'; import {getPlugin as getDetailsPlugin} from './plugins-details'; import {getPlugin as getTasklistPlugin} from './plugins-tasklist'; +import {handleEmbedAlignmentChanges} from './fixes'; const styleFormats = [ {title: 'Large Header', format: 'h2', preview: 'color: blue;'}, @@ -35,9 +36,9 @@ const styleFormats = [ ]; const formats = { - alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-left'}, - aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-center'}, - alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img', classes: 'align-right'}, + alignleft: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-left'}, + aligncenter: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-center'}, + alignright: {selector: 'p,h1,h2,h3,h4,h5,h6,td,th,div,ul,ol,li,table,img,iframe,video,span', classes: 'align-right'}, calloutsuccess: {block: 'p', exact: true, attributes: {class: 'callout success'}}, calloutinfo: {block: 'p', exact: true, attributes: {class: 'callout info'}}, calloutwarning: {block: 'p', exact: true, attributes: {class: 'callout warning'}}, @@ -177,6 +178,8 @@ function getSetupCallback(options) { setupFilters(editor); }); + handleEmbedAlignmentChanges(editor); + // Custom handler hook window.$events.emitPublic(options.containerElement, 'editor-tinymce::setup', {editor}); diff --git a/resources/js/wysiwyg/fixes.js b/resources/js/wysiwyg/fixes.js new file mode 100644 index 000000000..46984b45e --- /dev/null +++ b/resources/js/wysiwyg/fixes.js @@ -0,0 +1,55 @@ +/** + * Handle alignment for embed (iframe/video) content. + * TinyMCE built-in handling doesn't work well for these when classes are used for + * alignment, since the editor wraps these elements in a non-editable preview span + * which looses tracking and setting of alignment options. + * Here we manually manage these properties and formatting events, by effectively + * syncing the alignment classes to the parent preview span. + * @param {Editor} editor + */ +export function handleEmbedAlignmentChanges(editor) { + function updateClassesForPreview(previewElem) { + const mediaTarget = previewElem.querySelector('iframe, video'); + if (!mediaTarget) { + return; + } + + const alignmentClasses = [...mediaTarget.classList.values()].filter(c => c.startsWith('align-')); + const previewAlignClasses = [...previewElem.classList.values()].filter(c => c.startsWith('align-')); + previewElem.classList.remove(...previewAlignClasses); + previewElem.classList.add(...alignmentClasses); + } + + editor.on('SetContent', () => { + const previewElems = editor.dom.select('span.mce-preview-object'); + for (const previewElem of previewElems) { + updateClassesForPreview(previewElem); + } + }); + + editor.on('FormatApply', event => { + const isAlignment = event.format.startsWith('align'); + if (!event.node || !event.node.matches('.mce-preview-object')) { + return; + } + + const realTarget = event.node.querySelector('iframe, video'); + if (isAlignment && realTarget) { + const className = (editor.formatter.get(event.format)[0]?.classes || [])[0]; + const toAdd = !realTarget.classList.contains(className); + + const wrapperClasses = (event.node.getAttribute('data-mce-p-class') || '').split(' '); + const wrapperClassesFiltered = wrapperClasses.filter(c => !c.startsWith('align-')); + if (toAdd) { + wrapperClassesFiltered.push(className); + } + + const classesToApply = wrapperClassesFiltered.join(' '); + event.node.setAttribute('data-mce-p-class', classesToApply); + + realTarget.setAttribute('class', classesToApply); + editor.formatter.apply(event.format, {}, realTarget); + updateClassesForPreview(event.node); + } + }); +} diff --git a/resources/sass/_content.scss b/resources/sass/_content.scss index 10a2cd983..bde52bb77 100644 --- a/resources/sass/_content.scss +++ b/resources/sass/_content.scss @@ -11,24 +11,24 @@ .align-left { text-align: left; } - img.align-left, table.align-left { + img.align-left, table.align-left, iframe.align-left, video.align-left { float: left !important; margin: $-xs $-m $-m 0; } .align-right { text-align: right !important; } - img.align-right, table.align-right { + img.align-right, table.align-right, iframe.align-right, video.align-right { float: right !important; margin: $-xs 0 $-xs $-s; } .align-center { text-align: center; } - img.align-center { + img.align-center, video.align-center, iframe.align-center { display: block; } - img.align-center, table.align-center { + img.align-center, table.align-center, iframe.align-center, video.align-center { margin-left: auto; margin-right: auto; } diff --git a/resources/sass/_tinymce.scss b/resources/sass/_tinymce.scss index 7450f6016..8e036fc46 100644 --- a/resources/sass/_tinymce.scss +++ b/resources/sass/_tinymce.scss @@ -72,6 +72,30 @@ body.page-content.mce-content-body { overflow: hidden; } +// Allow alignment to be reflected in media embed wrappers +.page-content.mce-content-body .mce-preview-object.align-right { + float: right !important; + margin: $-xs 0 $-xs $-s; +} + +.page-content.mce-content-body .mce-preview-object.align-left { + float: left !important; + margin: $-xs $-m $-m 0; +} + +.page-content.mce-content-body .mce-preview-object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +.page-content.mce-content-body .mce-preview-object iframe, +.page-content.mce-content-body .mce-preview-object video { + display: block; + margin: 0 !important; + float: none !important; +} + /** * Dark Mode Overrides */