diff --git a/administrator/components/com_media/resources/scripts/components/browser/items/item.es6.js b/administrator/components/com_media/resources/scripts/components/browser/items/item.es6.js index b52bbd233180e..85975fee55e97 100644 --- a/administrator/components/com_media/resources/scripts/components/browser/items/item.es6.js +++ b/administrator/components/com_media/resources/scripts/components/browser/items/item.es6.js @@ -94,6 +94,8 @@ export default { thumb: this.item.thumb, fileType: this.item.mime_type ? this.item.mime_type : false, extension: this.item.extension ? this.item.extension : false, + width: this.item.width ? this.item.width : 0, + height: this.item.height ? this.item.height : 0, }, }, ), diff --git a/administrator/components/com_media/resources/scripts/components/browser/items/row.vue b/administrator/components/com_media/resources/scripts/components/browser/items/row.vue index b7165fa0548f6..2870763f8584d 100644 --- a/administrator/components/com_media/resources/scripts/components/browser/items/row.vue +++ b/administrator/components/com_media/resources/scripts/components/browser/items/row.vue @@ -104,6 +104,8 @@ export default { if (this.item.type === 'file') { data.path = this.item.path; data.thumb = this.item.thumb ? this.item.thumb : false; + data.width = this.item.width ? this.item.width : 0; + data.height = this.item.height ? this.item.height : 0; const ev = new CustomEvent('onMediaFileSelected', { bubbles: true, diff --git a/administrator/language/en-GB/joomla.ini b/administrator/language/en-GB/joomla.ini index 06aa152e0d29a..91642dd0a9bef 100644 --- a/administrator/language/en-GB/joomla.ini +++ b/administrator/language/en-GB/joomla.ini @@ -241,6 +241,8 @@ JFIELD_LOGOUT_REDIRECT_PAGE_DESC="Select or create the page the user will be red JFIELD_LOGOUT_REDIRECT_PAGE_LABEL="Logout Redirection Page" JFIELD_LOGOUT_REDIRECT_URL_DESC="If a URL is entered here, users will be redirected to it after logout.
The URL must be internal (eg: index.php?Itemid=999)." JFIELD_LOGOUT_REDIRECT_URL_LABEL="Logout Redirect" +JFIELD_MEDIA_ALT_LABEL="Alt Text" +JFIELD_MEDIA_LAZY_LABEL="Image will be lazyloaded" ; Do not translate the text between the {} JFIELD_META_DESCRIPTION_COUNTER="{remaining} characters remaining of {maxlength} characters." JFIELD_META_DESCRIPTION_DESC="An optional paragraph to be used as the description of the page in the HTML output. This will generally display in the results of search engines." diff --git a/administrator/language/en-GB/plg_editors_tinymce.ini b/administrator/language/en-GB/plg_editors_tinymce.ini index 3ab4bfae8a35c..ff7512c18296b 100644 --- a/administrator/language/en-GB/plg_editors_tinymce.ini +++ b/administrator/language/en-GB/plg_editors_tinymce.ini @@ -8,6 +8,9 @@ PLG_TINY_BUILDER="TinyMCE Configuration Builder" PLG_TINY_BUTTON_TOGGLE_EDITOR="Toggle Editor" PLG_TINY_CONFIG_TEXTFILTER_ACL_LABEL="Use Joomla Text Filter" PLG_TINY_CORE_BUTTONS="CMS Content" +PLG_TINY_DND_ADDITIONALDATA="Additional Data" +PLG_TINY_DND_ALTTEXT="Alt Text" +PLG_TINY_DND_LAZYLOADED="Image will be lazyloaded" PLG_TINY_ERR_CUSTOMCSSFILENOTPRESENT="The file name %s was entered in the TinyMCE Custom CSS field. This file could not be found in the default template folder. No styles are available." PLG_TINY_ERR_EDITORCSSFILENOTPRESENT="Could not find the file 'editor.css' in the template or templates/system folder. No styles are available." PLG_TINY_ERR_UNSUPPORTEDBROWSER="Drag and drop image upload is not available for your browser. Please consider using a fully HTML5 compatible browser." diff --git a/build/media_source/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js b/build/media_source/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js index 73bd140e69a15..6f434591622ef 100644 --- a/build/media_source/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js +++ b/build/media_source/plg_editors_tinymce/js/plugins/dragdrop/plugin.es6.js @@ -1,10 +1,11 @@ /* eslint-disable no-undef */ tinymce.PluginManager.add('jdragndrop', (editor) => { + let responseData; // Reset the drop area border tinyMCE.DOM.bind(document, 'dragleave', (e) => { e.stopPropagation(); e.preventDefault(); - tinyMCE.activeEditor.contentAreaContainer.style.borderWidth = '1px 0 0'; + editor.contentAreaContainer.style.borderWidth = '1px 0 0'; return false; }); @@ -26,12 +27,12 @@ tinymce.PluginManager.add('jdragndrop', (editor) => { }); function uploadFile(name, content) { - const url = `${tinyMCE.activeEditor.settings.uploadUri}&path=${tinyMCE.activeEditor.settings.comMediaAdapter}`; + const url = `${editor.settings.uploadUri}&path=${editor.settings.comMediaAdapter}`; const data = { - [tinyMCE.activeEditor.settings.csrfToken]: '1', + [editor.settings.csrfToken]: '1', name, content, - parent: tinyMCE.activeEditor.settings.parentUploadFolder, + parent: editor.settings.parentUploadFolder, }; Joomla.request({ @@ -49,18 +50,68 @@ tinymce.PluginManager.add('jdragndrop', (editor) => { } if (response.data && response.data.path) { + responseData = response.data; let urlPath; // For local adapters use relative paths - if (/local-/.test(response.data.adapter)) { + if (/local-/.test(responseData.adapter)) { const { rootFull } = Joomla.getOptions('system.paths'); urlPath = `${response.data.thumb_path.split(rootFull)[1]}`; - } else if (response.data.thumb_path) { + } else if (responseData.thumb_path) { // Absolute path for different domain - urlPath = response.data.thumb_path; + urlPath = responseData.thumb_path; } - tinyMCE.activeEditor.execCommand('mceInsertContent', false, ``); + const dialogClose = (api) => { + const dialogData = api.getData(); + const altValue = dialogData.altText ? ` alt="${dialogData.altText}"` : ''; + const lazyValue = dialogData.isLazy ? ' loading="lazy"' : ''; + const width = dialogData.isLazy ? ` width="${responseData.width}"` : ''; + const height = dialogData.isLazy ? ` height="${responseData.height}"` : ''; + editor.execCommand('mceInsertContent', false, ``); + }; + + editor.windowManager.open({ + title: Joomla.Text._('PLG_TINY_DND_ADDITIONALDATA'), + body: { + type: 'panel', + items: [ + { + type: 'input', + name: 'altText', + label: Joomla.Text._('PLG_TINY_DND_ALTTEXT'), + }, + { + type: 'checkbox', + name: 'isLazy', + label: Joomla.Text._('PLG_TINY_DND_LAZYLOADED'), + }, + ], + }, + buttons: [ + { + type: 'cancel', + text: 'Cancel', + }, + { + type: 'submit', + name: 'submitButton', + text: 'Save', + primary: true, + }, + ], + initialData: { + altText: '', + isLazy: true, + }, + onSubmit(api) { + dialogClose(api); + api.close(); + }, + onCancel(api) { + dialogClose(api); + }, + }); } }, onError: (xhr) => { diff --git a/build/media_source/system/joomla.asset.json b/build/media_source/system/joomla.asset.json index f78516af1d90b..7ada4f2bc66af 100644 --- a/build/media_source/system/joomla.asset.json +++ b/build/media_source/system/joomla.asset.json @@ -210,6 +210,12 @@ "uri": "system/fields/joomla-field-media.min.css", "webcomponent": true }, + { + "name": "webcomponent.image-select", + "type": "script", + "uri": "system/fields/joomla-image-select.min.js", + "webcomponent": true + }, { "name": "webcomponent.field-media", "type": "script", diff --git a/build/media_source/system/js/fields/joomla-field-media.w-c.es6.js b/build/media_source/system/js/fields/joomla-field-media.w-c.es6.js index cc2a33054cff8..a9ac24d0a9c1b 100644 --- a/build/media_source/system/js/fields/joomla-field-media.w-c.es6.js +++ b/build/media_source/system/js/fields/joomla-field-media.w-c.es6.js @@ -1,87 +1,10 @@ ((customElements, Joomla) => { + 'use strict'; + if (!Joomla) { throw new Error('Joomla API is not properly initiated'); } - Joomla.selectedFile = {}; - - window.document.addEventListener('onMediaFileSelected', (e) => { - Joomla.selectedFile = e.detail; - }); - - const execTransform = (resp, editor, fieldClass) => { - if (resp.success === true) { - if (resp.data[0].url) { - if (/local-/.test(resp.data[0].adapter)) { - const { rootFull } = Joomla.getOptions('system.paths'); - - // eslint-disable-next-line prefer-destructuring - Joomla.selectedFile.url = resp.data[0].url.split(rootFull)[1]; - if (resp.data[0].thumb_path) { - Joomla.selectedFile.thumb = resp.data[0].thumb_path; - } else { - Joomla.selectedFile.thumb = false; - } - } else if (resp.data[0].thumb_path) { - Joomla.selectedFile.thumb = resp.data[0].thumb_path; - } - } else { - Joomla.selectedFile.url = false; - } - - const isElement = (o) => ( - typeof HTMLElement === 'object' ? o instanceof HTMLElement - : o && typeof o === 'object' && o !== null && o.nodeType === 1 && typeof o.nodeName === 'string' - ); - - if (Joomla.selectedFile.url) { - if (!isElement(editor) && (typeof editor !== 'object')) { - Joomla.editors.instances[editor].replaceSelection(``); - } else if (!isElement(editor) && (typeof editor === 'object' && editor.id)) { - window.parent.Joomla.editors.instances[editor.id].replaceSelection(``); - } else { - editor.value = Joomla.selectedFile.url; - fieldClass.updatePreview(); - } - } - } - }; - - /** - * Create and dispatch onMediaFileSelected Event - * - * @param {object} data The data for the detail - * - * @returns {void} - */ - Joomla.getImage = (data, editor, fieldClass) => new Promise((resolve, reject) => { - if (!data || (typeof data === 'object' && (!data.path || data.path === ''))) { - Joomla.selectedFile = {}; - resolve({ - resp: { - success: false, - }, - }); - return; - } - - const apiBaseUrl = `${Joomla.getOptions('system.paths').baseFull}index.php?option=com_media&format=json`; - - Joomla.request({ - url: `${apiBaseUrl}&task=api.files&url=true&path=${data.path}&${Joomla.getOptions('csrf.token')}=1&format=json`, - method: 'GET', - perform: true, - headers: { 'Content-Type': 'application/json' }, - onSuccess: (response) => { - const resp = JSON.parse(response); - resolve(execTransform(resp, editor, fieldClass)); - }, - onError: (err) => { - reject(err); - }, - }); - }); - class JoomlaFieldMedia extends HTMLElement { constructor() { super(); @@ -204,20 +127,20 @@ show() { this.modalElement.open(); - Joomla.selectedFile = {}; + Joomla.selectedMediaFile = {}; this.buttonSaveSelectedElement.addEventListener('click', this.onSelected); } modalClose() { - Joomla.getImage(Joomla.selectedFile, this.inputElement, this) + Joomla.getImage(Joomla.selectedMediaFile, this.inputElement, this) .then(() => { Joomla.Modal.getCurrent().close(); - Joomla.selectedFile = {}; + Joomla.selectedMediaFile = {}; }) .catch(() => { Joomla.Modal.getCurrent().close(); - Joomla.selectedFile = {}; + Joomla.selectedMediaFile = {}; Joomla.renderMessages({ error: [Joomla.Text._('JLIB_APPLICATION_ERROR_SERVER')], }); @@ -248,15 +171,14 @@ this.previewElement.innerHTML = ''; const imgPreview = new Image(); - switch (this.type) { - case 'image': + const mediaType = { + image() { imgPreview.src = /http/.test(value) ? value : Joomla.getOptions('system.paths').rootFull + value; imgPreview.setAttribute('alt', ''); - break; - default: - // imgPreview.src = dummy image path; - break; - } + }, + }; + + mediaType[this.type](); this.previewElement.style.width = this.previewWidth; this.previewElement.appendChild(imgPreview); diff --git a/build/media_source/system/js/fields/joomla-image-select.w-c.es6.js b/build/media_source/system/js/fields/joomla-image-select.w-c.es6.js new file mode 100644 index 0000000000000..fe300904c064d --- /dev/null +++ b/build/media_source/system/js/fields/joomla-image-select.w-c.es6.js @@ -0,0 +1,270 @@ +((customElements, Joomla) => { + 'use strict'; + + if (!Joomla) { + throw new Error('Joomla API is not properly initiated'); + } + + /** + * An object holding all the information of the selected image in media manager + * eg: + * { + * extension: "png" + * fileType: "image/png" + * height: 44 + * path: "local-0:/powered_by.png" + * thumb: undefined + * width: 294 + * } + */ + Joomla.selectedMediaFile = {}; + + /** + * Event Listener that updates the Joomla.selectedMediaFile + * to the selected file in the media manager + */ + window.document.addEventListener('onMediaFileSelected', (e) => { + Joomla.selectedMediaFile = e.detail; + + const currentModal = Joomla.Modal.getCurrent(); + const container = currentModal.querySelector('.modal-body'); + + // No extra attributes (lazy, alt) for fields + if (container.closest('joomla-field-media')) { + return; + } + + const optionsEl = container.querySelector('joomla-field-mediamore'); + if (optionsEl) { + optionsEl.parentNode.removeChild(optionsEl); + } + + if (Joomla.selectedMediaFile.path) { + container.insertAdjacentHTML('afterbegin', ``); + } + }); + + /** + * Method to check if passed param is HTMLElement + * + * @param o {string|HTMLElement} Element to be checked + * + * @returns {boolean} + */ + const isElement = (o) => ( + typeof HTMLElement === 'object' ? o instanceof HTMLElement + : o && typeof o === 'object' && o.nodeType === 1 && typeof o.nodeName === 'string' + ); + + /** + * Method to safely append parameters to a URL string + * + * @param url {string} The URL + * @param key {string} The key of the parameter + * @param value {string} The value of the parameter + * + * @returns {string} + */ + const appendParam = (url, key, value) => { + const newKey = encodeURIComponent(key); + const newValue = encodeURIComponent(value); + const r = new RegExp(`(&|\\?)${key}=[^&]*`); + let s = url; + const param = `${newKey}=${newValue}`; + + s = s.replace(r, `$1${param}`); + + if (!RegExp.$1 && s.includes('?')) { + return `${s}&${param}`; + } + + if (!RegExp.$1 && !s.includes('?')) { + return `${s}?${param}`; + } + + return s; + }; + + /** + * Method to append the image in an editor or a field + * + * @param resp + * @param editor + * @param fieldClass + */ + const execTransform = (resp, editor, fieldClass) => { + if (resp.success === true) { + if (resp.data[0].url) { + if (/local-/.test(resp.data[0].adapter)) { + const { rootFull } = Joomla.getOptions('system.paths'); + + // eslint-disable-next-line prefer-destructuring + Joomla.selectedMediaFile.url = resp.data[0].url.split(rootFull)[1]; + if (resp.data[0].thumb_path) { + Joomla.selectedMediaFile.thumb = resp.data[0].thumb_path; + } else { + Joomla.selectedMediaFile.thumb = false; + } + } else if (resp.data[0].thumb_path) { + Joomla.selectedMediaFile.thumb = resp.data[0].thumb_path; + } + } else { + Joomla.selectedMediaFile.url = false; + } + + if (Joomla.selectedMediaFile.url) { + let isLazy = ''; + let alt = ''; + + if (!isElement(editor)) { + const currentModal = fieldClass.closest('.modal-content'); + const attribs = currentModal.querySelector('joomla-field-mediamore'); + if (attribs) { + alt = attribs.getAttribute('alt-value') ? ` alt="${attribs.getAttribute('alt-value')}"` : ''; + if (attribs.getAttribute('is-lazy') === 'true') { + isLazy = ` loading="lazy" width="${Joomla.selectedMediaFile.width}" height="${Joomla.selectedMediaFile.height}"`; + } + attribs.parentNode.removeChild(attribs); + } + + Joomla.editors.instances[editor].replaceSelection(``); + } else { + const val = appendParam(Joomla.selectedMediaFile.url, 'joomla_image_width', Joomla.selectedMediaFile.width); + editor.value = appendParam(val, 'joomla_image_height', Joomla.selectedMediaFile.height); + fieldClass.updatePreview(); + } + } + } + }; + + /** + * Method that resolves the real url for the selected image + * + * @param data {object} The data for the detail + * @param editor {string|object} The data for the detail + * @param fieldClass {HTMLElement} The fieldClass for the detail + * + * @returns {void} + */ + Joomla.getImage = (data, editor, fieldClass) => new Promise((resolve, reject) => { + if (!data || (typeof data === 'object' && (!data.path || data.path === ''))) { + Joomla.selectedMediaFile = {}; + resolve({ + resp: { + success: false, + }, + }); + return; + } + + const apiBaseUrl = `${Joomla.getOptions('system.paths').rootFull}administrator/index.php?option=com_media&format=json`; + + Joomla.request({ + url: `${apiBaseUrl}&task=api.files&url=true&path=${data.path}&${Joomla.getOptions('csrf.token')}=1&format=json`, + method: 'GET', + perform: true, + headers: { 'Content-Type': 'application/json' }, + onSuccess: (response) => { + const resp = JSON.parse(response); + resolve(execTransform(resp, editor, fieldClass)); + }, + onError: (err) => { + reject(err); + }, + }); + }); + + /** + * A sipmle Custom Element for adding alt text and controlling + * the lazy loading on a selected image + * + * Will be rendered only for editor content images + * Attributes: + * - parent-id: the id of the parent media field {string} + * - lazy-label: The text for the checkbox label {string} + * - alt-label: The text for the alt label {string} + * - is-lazy: The value for the lazyloading (calculated, defaults to 'true') {string} + * - alt-value: The value for the alt text (calculated, defaults to '') {string} + */ + class JoomlaFieldMediaOptions extends HTMLElement { + constructor() { + super(); + + this.adjustedHeight = false; + this.lazyInputFn = this.lazyInputFn.bind(this); + this.altInputFn = this.altInputFn.bind(this); + this.adjustHeight = this.adjustHeight.bind(this); + } + + get parentId() { return this.getAttribute('parent-id'); } + + get lazytext() { return this.getAttribute('lazy-label'); } + + get alttext() { return this.getAttribute('alt-label'); } + + connectedCallback() { + this.innerHTML = ` +
+
+
+
+ +
+ +
+
+
+
+ + +
+
+
`; + + // Add event listeners + this.lazyInput = this.querySelector(`#${this.parentId}-lazy`); + this.lazyInput.addEventListener('change', this.lazyInputFn); + this.altInput = this.querySelector(`#${this.parentId}-alt`); + this.altInput.addEventListener('input', this.altInputFn); + + // Set initial values + this.setAttribute('is-lazy', !!this.lazyInput.checked); + this.setAttribute('alt-value', ''); + + // Reduce iframe height + if (!this.adjustedHeight) { + requestAnimationFrame(this.adjustHeight); + } + } + + disconnectedCallback() { + this.lazyInput.removeEventListener('click', this.lazyInputFn); + if (this.enableAltField) { + this.altInput.removeEventListener('click', this.altInputFn); + } + + this.innerHTML = ''; + } + + lazyInputFn(e) { + this.setAttribute('is-lazy', !!e.target.checked); + } + + altInputFn(e) { + this.setAttribute('alt-value', e.target.value.replace(/"/g, '"')); + } + + adjustHeight() { + const that = this; + const parentEl = this.parentNode; + const nextEl = this.nextElementSibling; + requestAnimationFrame(() => { + const height = `${parentEl.getBoundingClientRect().height - (that.getBoundingClientRect().height + 40)}`; + nextEl.style.height = `${height}px`; + that.adjustedHeight = true; + }); + } + } + + customElements.define('joomla-field-mediamore', JoomlaFieldMediaOptions); +})(customElements, Joomla); diff --git a/language/en-GB/joomla.ini b/language/en-GB/joomla.ini index 98cbf8927ed52..a807a853a8eed 100644 --- a/language/en-GB/joomla.ini +++ b/language/en-GB/joomla.ini @@ -182,6 +182,8 @@ JFIELD_COLOR_VALUE="Colour with hexadecimal value of" JFIELD_FIELDS_CATEGORY_DESC="Select the category that this field is assigned to." JFIELD_LANGUAGE_DESC="Assign a language to this article." JFIELD_LANGUAGE_LABEL="Language" +JFIELD_MEDIA_ALT_LABEL="Alt Text" +JFIELD_MEDIA_LAZY_LABEL="Image will be lazyloaded" ; Do not translate the text between the {} JFIELD_META_DESCRIPTION_COUNTER="{remaining} characters remaining of {maxlength} characters." JFIELD_META_DESCRIPTION_DESC="Metadata description." diff --git a/layouts/joomla/content/full_image.php b/layouts/joomla/content/full_image.php index 6bc95b309004a..f4f08b6f87ce6 100644 --- a/layouts/joomla/content/full_image.php +++ b/layouts/joomla/content/full_image.php @@ -9,18 +9,36 @@ defined('_JEXEC') or die; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\Utilities\ArrayHelper; + $params = $displayData->params; $images = json_decode($displayData->images); + +if (empty($images->image_fulltext)) +{ + return; +} + +$imgclass = empty($images->float_fulltext) ? $params->get('float_fulltext') : $images->float_fulltext; +$extraAttr = ''; +$img = HTMLHelper::cleanImageURL($images->image_fulltext); +$alt = empty($images->image_fulltext_alt) && empty($images->image_fulltext_alt_empty) ? '' : 'alt="' . htmlspecialchars($images->image_fulltext_alt, ENT_COMPAT, 'UTF-8') . '"'; + +// Set lazyloading only for images which have width and height attributes +if ((isset($img->attributes['width']) && (int) $img->attributes['width'] > 0) +&& (isset($img->attributes['height']) && (int) $img->attributes['height'] > 0)) +{ + $extraAttr = ArrayHelper::toString($img->attributes) . ' loading="lazy"'; +} ?> -image_fulltext)) : ?> - float_fulltext) ? $params->get('float_fulltext') : $images->float_fulltext; ?> - image_fulltext_alt) && empty($images->image_fulltext_alt_empty) ? '' : 'alt="' . htmlspecialchars($images->image_fulltext_alt, ENT_COMPAT, 'UTF-8') . '"'; ?> -
- - itemprop="image"/> - image_fulltext_caption) && $images->image_fulltext_caption !== '') : ?> -
image_fulltext_caption, ENT_COMPAT, 'UTF-8'); ?>
- -
- +
+ alt + itemprop="image" + + /> + image_fulltext_caption !== '') : ?> +
image_fulltext_caption, ENT_COMPAT, 'UTF-8'); ?>
+ +
diff --git a/layouts/joomla/content/intro_image.php b/layouts/joomla/content/intro_image.php index be5e685022a89..f4b36a0721c91 100644 --- a/layouts/joomla/content/intro_image.php +++ b/layouts/joomla/content/intro_image.php @@ -9,34 +9,48 @@ defined('_JEXEC') or die; +use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\Router\Route; use Joomla\Component\Content\Site\Helper\RouteHelper; +use Joomla\Utilities\ArrayHelper; $params = $displayData->params; $images = json_decode($displayData->images); +if (empty($images->image_intro)) +{ + return; +} + +$imgclass = empty($images->float_intro) ? $params->get('float_intro') : $images->float_intro; +$extraAttr = ''; +$img = HTMLHelper::cleanImageURL($images->image_intro); +$alt = empty($images->image_intro_alt) && empty($images->image_intro_alt_empty) ? '' : 'alt="'. htmlspecialchars($images->image_intro_alt, ENT_COMPAT, 'UTF-8') .'"'; + +// Set lazyloading only for images which have width and height attributes +if ((isset($img->attributes['width']) && (int) $img->attributes['width'] > 0) +&& (isset($img->attributes['height']) && (int) $img->attributes['height'] > 0)) +{ + $extraAttr = ArrayHelper::toString($img->attributes) . ' loading="lazy"'; +} ?> -image_intro)) : ?> - float_intro) ? $params->get('float_intro') : $images->float_intro; ?> - image_intro_alt) && empty($images->image_intro_alt_empty) ? '' : 'alt="'. htmlspecialchars($images->image_intro_alt, ENT_COMPAT, 'UTF-8') .'"'; ?> -
- get('link_intro_image') && ($params->get('access-view') || $params->get('show_noauth', '0') == '1')) : ?> - + itemprop="thumbnailUrl" - /> - - - + /> + + + itemprop="thumbnailUrl" - > - - image_intro_caption) && $images->image_intro_caption !== '') : ?> -
image_intro_caption, ENT_COMPAT, 'UTF-8'); ?>
- -
- + + /> + + image_intro_caption) && $images->image_intro_caption !== '') : ?> +
image_intro_caption, ENT_COMPAT, 'UTF-8'); ?>
+ + diff --git a/layouts/joomla/editors/buttons/modal.php b/layouts/joomla/editors/buttons/modal.php index b1edb21f233b0..070a6c9e909e1 100644 --- a/layouts/joomla/editors/buttons/modal.php +++ b/layouts/joomla/editors/buttons/modal.php @@ -32,8 +32,8 @@ if (is_array($button->get('options')) && isset($options['confirmText']) && isset($options['confirmCallback'])) { - $confirm = ''; + $confirm = ''; } if (null !== $button->get('id')) @@ -57,7 +57,7 @@ 'width' => array_key_exists('width', $options) ? $options['width'] : '800px', 'bodyHeight' => array_key_exists('bodyHeight', $options) ? $options['bodyHeight'] : '70', 'modalWidth' => array_key_exists('modalWidth', $options) ? $options['modalWidth'] : '80', - 'footer' => $confirm . '' ) ); diff --git a/layouts/joomla/form/field/media.php b/layouts/joomla/form/field/media.php index 5d568295383f0..3533e03792ccc 100644 --- a/layouts/joomla/form/field/media.php +++ b/layouts/joomla/form/field/media.php @@ -108,9 +108,15 @@ // Correctly route the url to ensure it's correctly using sef modes and subfolders $url = Route::_($url); +$wam = Factory::getDocument()->getWebAssetManager(); -Factory::getDocument()->getWebAssetManager() - ->useStyle('webcomponent.field-media') +$wam->useScript('webcomponent.image-select'); + +Text::script('JFIELD_MEDIA_LAZY_LABEL'); +Text::script('JFIELD_MEDIA_ALT_LABEL'); +Text::script('JFIELD_MEDIA_CONFIRM_TEXT'); + +$wam->useStyle('webcomponent.field-media') ->useScript('webcomponent.field-media'); Text::script('JLIB_APPLICATION_ERROR_SERVER'); diff --git a/libraries/src/HTML/HTMLHelper.php b/libraries/src/HTML/HTMLHelper.php index 9f305cf8a2d71..59f863b34591c 100644 --- a/libraries/src/HTML/HTMLHelper.php +++ b/libraries/src/HTML/HTMLHelper.php @@ -654,6 +654,72 @@ protected static function includeRelativeFiles($folder, $file, $relative, $detec return $includes; } + /** + * Gets a URL, cleans the Joomla specific params and returns an object + * + * @param string $url The relative or absolute URL to use for the src attribute. + * + * @return object + * @example { + * url: 'string', + * width: integer, + * height: integer, + * } + * + * @since __DEPLOY_VERSION__ + */ + public static function cleanImageURL($url) + { + $obj = new \stdClass; + + $obj->attributes = [ + 'width' => 0, + 'height' => 0, + ]; + + if (!strpos($url, '?')) + { + $obj->url = $url; + + return $obj; + } + + $pieces = explode('?', $url); + + parse_str($pieces[1], $urlParams); + + if (isset($urlParams['joomla_image_height']) && $urlParams['joomla_image_height'] !== 'null') + { + if ((int) $urlParams['joomla_image_height'] > 0) + { + $obj->attributes['height'] = $urlParams['joomla_image_height']; + } + else + { + unset($obj->attributes['height']); + } + + unset($urlParams['joomla_image_height']); + } + + if (isset($urlParams['joomla_image_width']) && $urlParams['joomla_image_width'] !== 'null') + { + if ((int) $urlParams['joomla_image_width'] > 0) + { + $obj->attributes['width'] = $urlParams['joomla_image_width']; + } + else + { + unset($obj->attributes['width']); + } + + unset($urlParams['joomla_image_width']); + } + + $obj->url = $pieces[0] . (count($urlParams) ? '?' . http_build_query($urlParams) : ''); + + return $obj; + } /** * Write a `` element diff --git a/plugins/editors-xtd/image/image.php b/plugins/editors-xtd/image/image.php index 15217bcaa81c3..0bf4562b04659 100644 --- a/plugins/editors-xtd/image/image.php +++ b/plugins/editors-xtd/image/image.php @@ -62,6 +62,11 @@ public function onDisplay($name, $asset, $author) || (count($user->getAuthorisedCategories($extension, 'core.edit')) > 0) || (count($user->getAuthorisedCategories($extension, 'core.edit.own')) > 0 && $author === $user->id)) { + $app->getDocument()->getWebAssetManager()->useScript('webcomponent.image-select'); + + Text::script('JFIELD_MEDIA_LAZY_LABEL'); + Text::script('JFIELD_MEDIA_ALT_LABEL'); + $app->getDocument()->getWebAssetManager()->useScript('webcomponent.field-media'); $link = 'index.php?option=com_media&view=media&tmpl=component&e_name=' . $name . '&asset=' . $asset . '&author=' . $author; @@ -80,7 +85,7 @@ public function onDisplay($name, $asset, $author) 'bodyHeight' => '70', 'modalWidth' => '80', 'tinyPath' => $link, - 'confirmCallback' => 'Joomla.getImage(Joomla.selectedFile, \'' . $name . '\')', + 'confirmCallback' => 'Joomla.getImage(Joomla.selectedMediaFile, \'' . $name . '\', this)', 'confirmText' => Text::_('PLG_IMAGE_BUTTON_INSERT') ]; diff --git a/plugins/editors/tinymce/tinymce.php b/plugins/editors/tinymce/tinymce.php index f88c9e4482893..cbe96113649e7 100644 --- a/plugins/editors/tinymce/tinymce.php +++ b/plugins/editors/tinymce/tinymce.php @@ -523,6 +523,9 @@ public function onDisplay( Text::script('PLG_TINY_ERR_UNSUPPORTEDBROWSER'); Text::script('JERROR'); + Text::script('PLG_TINY_DND_ADDITIONALDATA'); + Text::script('PLG_TINY_DND_ALTTEXT'); + Text::script('PLG_TINY_DND_LAZYLOADED'); $scriptOptions['parentUploadFolder'] = $levelParams->get('path', ''); $scriptOptions['csrfToken'] = Session::getFormToken(); diff --git a/plugins/fields/imagelist/tmpl/imagelist.php b/plugins/fields/imagelist/tmpl/imagelist.php index 3a83967ecf92e..89c69708ad239 100644 --- a/plugins/fields/imagelist/tmpl/imagelist.php +++ b/plugins/fields/imagelist/tmpl/imagelist.php @@ -8,6 +8,8 @@ */ defined('_JEXEC') or die; +use Joomla\CMS\Image\Image; + if ($field->value == '') { return; @@ -31,18 +33,28 @@ continue; } + $imageFilePath = htmlentities($path, ENT_COMPAT, 'UTF-8', true); + if ($fieldParams->get('directory', '/') !== '/') { - $buffer .= sprintf('', + $imageInfo = Image::getImageFileProperties('images/' . $fieldParams->get('directory') . '/' . $imageFilePath); + + $buffer .= sprintf('', + $imageInfo->width, + $imageInfo->height, $fieldParams->get('directory'), - htmlentities($path, ENT_COMPAT, 'UTF-8', true), + $imageFilePath, $class ); } else { - $buffer .= sprintf('', - htmlentities($path, ENT_COMPAT, 'UTF-8', true), + $imageInfo = Image::getImageFileProperties('images/' . $imageFilePath); + + $buffer .= sprintf('', + $imageInfo->width, + $imageInfo->height, + $imageFilePath, $class ); } diff --git a/plugins/fields/media/tmpl/media.php b/plugins/fields/media/tmpl/media.php index 619d4278e4614..ed88369317066 100644 --- a/plugins/fields/media/tmpl/media.php +++ b/plugins/fields/media/tmpl/media.php @@ -8,6 +8,8 @@ */ defined('_JEXEC') or die; +use Joomla\CMS\HTML\HTMLHelper; + if ($field->value == '') { return; @@ -26,13 +28,16 @@ if ($value) { - $path = $value['imagefile']; - $alt = empty($value['alt_text']) && empty($value['alt_empty']) ? '' : ' alt="' . htmlspecialchars($value['alt_text'], ENT_COMPAT, 'UTF-8') . '"'; + $img = HTMLHelper::cleanImageURL($value['imagefile']); + $imgUrl = htmlentities($img->url, ENT_COMPAT, 'UTF-8', true); + $alt = empty($value['alt_text']) && empty($value['alt_empty']) ? '' : ' alt="' . htmlspecialchars($value['alt_text'], ENT_COMPAT, 'UTF-8') . '"'; - if (file_exists($path)) + if (file_exists($img->url)) { - $buffer .= sprintf('', - htmlentities($path, ENT_COMPAT, 'UTF-8', true), + $buffer .= sprintf('', + $img->attributes['width'], + $img->attributes['height'], + $imgUrl, $class, $alt );