From f91f771979e11f5cdb740d395afba08eea05eec9 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sun, 31 May 2026 21:43:21 +0200 Subject: [PATCH 1/5] add refresh handler for auth tokens fix typescript errors --- .../UIJettyHttpHandlerExtension.java | 2 + .../ui/http/auth/RefreshTokenHandler.java | 74 +++++++++++++++++++ .../cms/modules/ui/utils/AuthUtil.java | 16 ++-- .../manager/actions/media/edit-focal-point.js | 4 + .../manager/actions/page/translations.js | 2 +- .../src/main/resources/manager/index.html | 3 +- .../resources/manager/js/manager-inject.js | 1 + .../src/main/resources/manager/js/manager.js | 13 ++++ .../manager/js/modules/filebrowser.d.ts | 2 +- .../manager/js/modules/form/field.checkbox.js | 7 +- .../manager/js/modules/form/field.color.js | 3 + .../manager/js/modules/form/field.date.js | 3 + .../manager/js/modules/form/field.datetime.js | 3 + .../manager/js/modules/form/field.easymde.js | 3 + .../manager/js/modules/form/field.list.js | 18 +++-- .../manager/js/modules/form/field.mail.js | 3 + .../manager/js/modules/form/field.markdown.js | 14 +++- .../manager/js/modules/form/field.media.js | 15 +++- .../manager/js/modules/form/field.number.js | 6 +- .../manager/js/modules/form/field.radio.js | 6 +- .../manager/js/modules/form/field.range.js | 6 +- .../js/modules/form/field.reference.js | 12 ++- .../manager/js/modules/form/field.select.js | 4 +- .../manager/js/modules/form/field.text.js | 3 + .../manager/js/modules/form/field.textarea.js | 4 + .../manager/js/modules/form/forms.js | 8 +- .../manager/js/modules/form/utils.d.ts | 4 +- .../manager/js/modules/localization.d.ts | 2 +- .../js/modules/manager/media.inject.js | 4 +- .../js/modules/manager/toolbar.inject.js | 16 ++-- .../manager/js/modules/preview.history.d.ts | 2 +- .../manager/js/modules/preview.utils.d.ts | 2 +- .../resources/manager/js/modules/rpc/rpc.js | 3 +- .../resources/manager/js/modules/state.js | 1 + .../manager/js/modules/ui-state.d.ts | 4 +- .../resources/manager/public/manager-login.js | 1 + .../ts/dist/actions/media/edit-focal-point.js | 4 + .../main/ts/dist/actions/page/translations.js | 2 +- .../src/main/ts/dist/js/manager-inject.js | 1 + .../ui-module/src/main/ts/dist/js/manager.js | 13 ++++ .../main/ts/dist/js/modules/filebrowser.d.ts | 2 +- .../ts/dist/js/modules/form/field.checkbox.js | 7 +- .../ts/dist/js/modules/form/field.color.js | 3 + .../ts/dist/js/modules/form/field.date.js | 3 + .../ts/dist/js/modules/form/field.datetime.js | 3 + .../ts/dist/js/modules/form/field.easymde.js | 3 + .../ts/dist/js/modules/form/field.list.js | 18 +++-- .../ts/dist/js/modules/form/field.mail.js | 3 + .../ts/dist/js/modules/form/field.markdown.js | 14 +++- .../ts/dist/js/modules/form/field.media.js | 15 +++- .../ts/dist/js/modules/form/field.number.js | 6 +- .../ts/dist/js/modules/form/field.radio.js | 6 +- .../ts/dist/js/modules/form/field.range.js | 6 +- .../dist/js/modules/form/field.reference.js | 12 ++- .../ts/dist/js/modules/form/field.select.js | 4 +- .../ts/dist/js/modules/form/field.text.js | 3 + .../ts/dist/js/modules/form/field.textarea.js | 4 + .../src/main/ts/dist/js/modules/form/forms.js | 8 +- .../main/ts/dist/js/modules/form/utils.d.ts | 4 +- .../main/ts/dist/js/modules/localization.d.ts | 2 +- .../dist/js/modules/manager/media.inject.js | 4 +- .../dist/js/modules/manager/toolbar.inject.js | 16 ++-- .../ts/dist/js/modules/preview.history.d.ts | 2 +- .../ts/dist/js/modules/preview.utils.d.ts | 2 +- .../src/main/ts/dist/js/modules/rpc/rpc.js | 3 +- .../src/main/ts/dist/js/modules/state.js | 1 + .../src/main/ts/dist/js/modules/ui-state.d.ts | 4 +- .../src/main/ts/dist/public/manager-login.js | 1 + modules/ui-module/src/main/ts/globals.d.ts | 1 + .../ts/src/actions/media/edit-focal-point.ts | 17 +++-- .../main/ts/src/actions/media/select-media.ts | 8 +- .../src/actions/page/section-set-published.ts | 2 +- .../main/ts/src/actions/page/translations.ts | 12 +-- .../src/main/ts/src/actions/reload-preview.ts | 2 +- .../ui-module/src/main/ts/src/js/manager.js | 14 ++++ .../ts/src/js/modules/form/field.checkbox.ts | 13 ++-- .../ts/src/js/modules/form/field.color.ts | 7 +- .../main/ts/src/js/modules/form/field.date.ts | 8 +- .../ts/src/js/modules/form/field.datetime.ts | 8 +- .../ts/src/js/modules/form/field.easymde.ts | 16 +++- .../main/ts/src/js/modules/form/field.list.ts | 28 ++++--- .../main/ts/src/js/modules/form/field.mail.ts | 7 +- .../ts/src/js/modules/form/field.markdown.ts | 26 ++++--- .../ts/src/js/modules/form/field.media.ts | 37 +++++++--- .../ts/src/js/modules/form/field.number.ts | 8 +- .../ts/src/js/modules/form/field.radio.ts | 10 ++- .../ts/src/js/modules/form/field.range.ts | 10 ++- .../ts/src/js/modules/form/field.reference.ts | 18 ++++- .../ts/src/js/modules/form/field.select.ts | 6 +- .../main/ts/src/js/modules/form/field.text.ts | 7 +- .../ts/src/js/modules/form/field.textarea.ts | 8 +- .../src/main/ts/src/js/modules/form/forms.ts | 17 +++-- .../src/main/ts/src/js/modules/form/utils.ts | 2 +- .../manager/manager.message.handlers.ts | 2 +- .../ts/src/js/modules/manager/media.inject.ts | 10 +-- .../src/js/modules/manager/toolbar.inject.ts | 16 ++-- .../src/main/ts/src/js/modules/rpc/rpc.ts | 3 +- 97 files changed, 576 insertions(+), 200 deletions(-) create mode 100644 modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java index 820b15747..18c1114fa 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/UIJettyHttpHandlerExtension.java @@ -43,6 +43,7 @@ import com.condation.cms.modules.ui.http.auth.CSRFHandler; import com.condation.cms.modules.ui.http.auth.LoginResourceHandler; import com.condation.cms.modules.ui.http.auth.LogoutHandler; +import com.condation.cms.modules.ui.http.auth.RefreshTokenHandler; import com.condation.cms.modules.ui.http.auth.UIAuthHandler; import com.condation.cms.modules.ui.http.auth.UIAuthRedirectHandler; import com.condation.cms.modules.ui.services.RemoteMethodService; @@ -135,6 +136,7 @@ public Mapping getMapping() { try { + mapping.add(PathSpec.from("/manager/refresh"), new RefreshTokenHandler(getContext(), getRequestContext())); mapping.add(PathSpec.from("/manager/login"), new LoginResourceHandler(getContext(), getRequestContext())); //mapping.add(PathSpec.from("/manager/login.action"), new LoginHandler(getContext(), getRequestContext(), failedLoginsCounter)); mapping.add(PathSpec.from("/manager/login.action"), new AjaxLoginHandler(getContext(), getRequestContext(), failedLoginsCounter, logins)); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java new file mode 100644 index 000000000..7f7e1b8f6 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java @@ -0,0 +1,74 @@ +package com.condation.cms.modules.ui.http.auth; + +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import com.condation.cms.api.configuration.configs.ServerConfiguration; +import com.condation.cms.api.feature.features.ConfigurationFeature; +import com.condation.cms.api.module.SiteModuleContext; +import com.condation.cms.api.request.RequestContext; +import com.condation.cms.modules.ui.http.JettyHandler; +import com.condation.cms.modules.ui.utils.AuthUtil; +import com.condation.cms.modules.ui.utils.TokenUtils; +import com.condation.cms.modules.ui.utils.json.UIGsonProvider; +import java.time.Duration; +import java.util.Map; +import org.eclipse.jetty.io.Content; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; +import org.eclipse.jetty.util.Callback; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Handler für den expliziten Refresh-Endpunkt. + * + * Die Route /manager/refresh verwendet hier das Refresh-Cookie, um bei Bedarf + * neue Auth- und Refresh-Tokens zu setzen. + */ +@RequiredArgsConstructor +@Slf4j +public class RefreshTokenHandler extends JettyHandler { + + private final SiteModuleContext moduleContext; + private final RequestContext requestContext; + + @Override + public boolean handle(Request request, Response response, Callback callback) throws Exception { + if (!request.getMethod().equalsIgnoreCase("POST")) { + return false; + } + + boolean refreshed = AuthUtil.refreshTokens(request, response, moduleContext, requestContext); + if (refreshed) { + var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + response.setStatus(200); + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(Map.of( + "status", "ok", + "previewToken", TokenUtils.createToken(getUsername(request, moduleContext, requestContext), secret, Duration.ofHours(1), Duration.ofDays(7)) + )), callback); + } else { + response.setStatus(401); + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(Map.of("status", "error", "reason", "unauthorized")), callback); + } + + return true; + } +} diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/AuthUtil.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/AuthUtil.java index e71a60a29..de35fc8e7 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/AuthUtil.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/AuthUtil.java @@ -65,19 +65,21 @@ private static boolean tryRefresh(Request request, Response response, SiteModule var payload = TokenUtils.getPayload(token, secret); if (payload.isPresent()) { - if (refreshTokenCache.contains(token)) { - refreshTokenCache.invalidate(token); + refreshTokenCache.invalidate(token); // best-effort invalidation; refresh should still work after restart if token is valid - Optional userOpt = moduleContext.get(InjectorFeature.class).injector().getInstance(UserService.class).byUsername(Realm.of("manager-users"), payload.get().username()); - if (userOpt.isPresent()) { - updateCookies(userOpt.get(), response, requestContext, moduleContext); - return true; - } + Optional userOpt = moduleContext.get(InjectorFeature.class).injector().getInstance(UserService.class).byUsername(Realm.of("manager-users"), payload.get().username()); + if (userOpt.isPresent()) { + updateCookies(userOpt.get(), response, requestContext, moduleContext); + return true; } } return false; } + public static boolean refreshTokens(Request request, Response response, SiteModuleContext moduleContext, RequestContext requestContext) { + return tryRefresh(request, response, moduleContext, requestContext); + } + public static boolean checkAuthTokens(Request request, Response response, SiteModuleContext moduleContext, RequestContext requestContext) { var authCookie = CookieUtil.getCookie(request, UIConstants.COOKIE_CMS_TOKEN); diff --git a/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.js b/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.js index 6c4a63928..a917e7b54 100644 --- a/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.js +++ b/modules/ui-module/src/main/resources/manager/actions/media/edit-focal-point.js @@ -66,6 +66,10 @@ export async function runAction(params) { const wrapper = document.getElementById("cmsFocalWrapper"); const image = document.getElementById("cms-image"); const point = document.getElementById("cmsFocalPoint"); + if (wrapper === null || image === null || point === null) { + console.error("One or more required elements not found"); + return; + } if (image.complete) { setFocalPoint(image, point, focalX, focalY); } diff --git a/modules/ui-module/src/main/resources/manager/actions/page/translations.js b/modules/ui-module/src/main/resources/manager/actions/page/translations.js index 7745258f6..f026bca19 100644 --- a/modules/ui-module/src/main/resources/manager/actions/page/translations.js +++ b/modules/ui-module/src/main/resources/manager/actions/page/translations.js @@ -40,7 +40,7 @@ export async function runAction(params) { onOk: async (event) => { }, onShow: async (modalElement) => { - modalElement.querySelectorAll('button[data-action]').forEach(button => { + modalElement.querySelectorAll('button[data-action]').forEach((button) => { button.addEventListener('click', async (e) => { const action = e.currentTarget.getAttribute('data-action'); const siteId = e.currentTarget.getAttribute('data-id'); diff --git a/modules/ui-module/src/main/resources/manager/index.html b/modules/ui-module/src/main/resources/manager/index.html index 878d7ac6d..76e512206 100644 --- a/modules/ui-module/src/main/resources/manager/index.html +++ b/modules/ui-module/src/main/resources/manager/index.html @@ -94,7 +94,8 @@ baseUrl: '{{ managerBaseURL }}', contextPath: '{{ contextPath }}', siteId: '{{ siteId }}', - previewUrl: "{{ links.createUrl('/?preview=manager') | raw }}" + previewUrl: "{{ links.createUrl('/?preview=manager') | raw }}", + refreshUrl: "{{ links.createUrl('/manager/refresh') | raw }}" } diff --git a/modules/ui-module/src/main/resources/manager/js/manager-inject.js b/modules/ui-module/src/main/resources/manager/js/manager-inject.js index 75083adbc..2b11d2b1d 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-inject.js +++ b/modules/ui-module/src/main/resources/manager/js/manager-inject.js @@ -1,3 +1,4 @@ +"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/resources/manager/js/manager.js b/modules/ui-module/src/main/resources/manager/js/manager.js index 9987b664d..800a0a030 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager.js +++ b/modules/ui-module/src/main/resources/manager/js/manager.js @@ -29,7 +29,20 @@ import { setCSRFToken } from '@cms/modules/utils.js'; frameMessenger.on('load', (payload) => { EventBus.emit("preview:loaded", {}); }); +function heartbeat() { + fetch(window.manager.refreshUrl, { + method: "POST", + credentials: "include" + }) + .then(res => res.json()) + .then(data => { + window.manager.previewToken = data.previewToken; + }); +} document.addEventListener("DOMContentLoaded", function () { + setInterval(() => { + heartbeat(); + }, 10 * 1000); //PreviewHistory.init("/"); //updateStateButton(); activatePreviewOverlay(); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts index 0c529bf7f..0869c773e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts @@ -20,6 +20,6 @@ */ export function openFileBrowser(optionsParam: any): Promise; export namespace state { - let options: any; + let options: null; let currentFolder: string; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js index 16f4e92f7..6bbe7a57b 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.checkbox.js @@ -24,7 +24,7 @@ const createCheckboxField = (options, value = []) => { const key = options.key || ""; const name = options.name || id; const title = options.title || ""; - const choices = options.options.choices || []; + const choices = options.options?.choices || []; const selectedValues = new Set(value); const checkboxes = choices.map((choice, idx) => { const inputId = `${id}-${idx}`; @@ -46,7 +46,10 @@ const createCheckboxField = (options, value = []) => { `; }; const getData = (context) => { - const data = {}; + var data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='checkbox']").forEach(container => { const name = container.querySelector("input[type='checkbox']").name; const checkedBoxes = container.querySelectorAll("input[type='checkbox']:checked"); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.js index 234163587..6cfba7fd7 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.color.js @@ -33,6 +33,9 @@ const createColorField = (options, value = '#000000') => { }; const getColorData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='color'] input").forEach((el) => { data[el.name] = { type: 'color', diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.js index 31a16dedd..4a090c7e8 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.date.js @@ -41,6 +41,9 @@ const createDateField = (options, value = '') => { }; const getDateData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='date'] input").forEach((el) => { const value = getUTCDateFromInput(el); // "2025-05-31" data[el.name] = { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.js index bff91c58e..8df816d5e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.datetime.js @@ -41,6 +41,9 @@ const createDateTimeField = (options, value = '') => { }; const getDateTimeData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='datetime'] input").forEach((el) => { const value = getUTCDateTimeFromInput(el); // "2025-05-31T15:30" data[el.name] = { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.js index 7b547663f..12e86fe6e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.easymde.js @@ -34,6 +34,9 @@ const createMarkdownField = (options, value = '') => { }; const getData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } markdownEditors.forEach(({ input, editor }) => { data[input.name] = { type: "easymde", diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.js index b520c9714..b7b60256e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.list.js @@ -80,7 +80,7 @@ const handleAddItem = (e, container, context) => { `; listGroup.insertAdjacentHTML("beforeend", itemMarkup); - var itemElement = listGroup.querySelector(`[data-cms-form-field-item="${itemId}"]`); + const itemElement = listGroup.querySelector(`[data-cms-form-field-item="${itemId}"]`); if (itemElement) { itemElement.addEventListener('dblclick', (e) => handleDoubleClick(e, context)); const removeBtn = itemElement.querySelector('.remove-btn'); @@ -99,7 +99,7 @@ const getItemForm = async (el) => { const getContentResponse = await getContent({ uri: contentNode.result.uri }); - var selected = pageTemplates.filter(pageTemplate => pageTemplate.template === getContentResponse?.result?.meta?.template); + var selected = pageTemplates.filter((pageTemplate) => pageTemplate.template === getContentResponse?.result?.meta?.template); const listContainer = el.closest("[data-cms-form-field-type='list']"); const fieldName = listContainer?.getAttribute('name'); var itemForm = []; @@ -108,7 +108,7 @@ const getItemForm = async (el) => { } if (!itemForm || itemForm.length === 0) { let itemTypes = (await getListItemTypes({})).result; - var selectedItemType = itemTypes.filter(itemType => itemType.name === fieldName); + var selectedItemType = itemTypes.filter((itemType) => itemType.name === fieldName); itemForm = (selectedItemType.length === 1) ? selectedItemType[0].data?.form.fields : []; } return itemForm; @@ -136,16 +136,22 @@ const handleDoubleClick = async (event, context) => { el.setAttribute('data-cms-form-field-item-data', JSON.stringify(updateData)); const listContainer = el.closest("[data-cms-form-field-type='list']"); const nameField = listContainer?.getAttribute('data-name-field') || 'name'; - el.querySelector('.object-name').textContent = updateData[nameField]; + const objectNameEl = el.querySelector('.object-name'); + if (!objectNameEl) + return; + objectNameEl.textContent = updateData[nameField] || ""; } }); } }; const getData = (context) => { var data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='list']").forEach((el) => { let value = []; - el.querySelectorAll("[data-cms-form-field-item]").forEach(itemEl => { + el.querySelectorAll("[data-cms-form-field-item]").forEach((itemEl) => { const itemData = itemEl.getAttribute('data-cms-form-field-item-data'); if (itemData) { value.push(JSON.parse(itemData)); @@ -162,7 +168,7 @@ const getData = (context) => { return data; }; const init = (context) => { - context.formElement.querySelectorAll("[data-cms-form-field-type='list']").forEach(listContainer => { + context.formElement?.querySelectorAll("[data-cms-form-field-type='list']").forEach(listContainer => { listContainer.querySelectorAll("[data-cms-form-field-item]").forEach(field => { field.addEventListener('dblclick', (e) => handleDoubleClick(e, context)); // Remove-Button-Listener setzen diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.js index 1ca07c934..cb4651b92 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.mail.js @@ -34,6 +34,9 @@ const createEmailField = (options, value = '') => { }; const getData = (context) => { var data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='mail'] input").forEach((el) => { let value = el.value; data[el.name] = { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.js index 0d134d310..8475a02c8 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.markdown.js @@ -38,7 +38,11 @@ const createMarkdownField = (options, value = '') => { }; const getData = (context) => { const data = {}; - const editorInputs = context.formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); + const formElement = context.formElement; + if (!formElement) { + return data; + } + const editorInputs = formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); editorInputs.forEach((input) => { const editor = input.cherryEditor; if (editor && editor.getMarkdown) { @@ -58,8 +62,12 @@ const getData = (context) => { return data; }; const init = async (context) => { + const formElement = context.formElement; + if (!formElement) { + return; + } const cmsTagsMenu = await buildCmsTagsMenu(); - const editorInputs = context.formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); + const editorInputs = formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); editorInputs.forEach((input) => { const containerId = input.dataset.cherryId; const initialValue = decodeURIComponent(input.dataset.initialValue || ""); @@ -113,7 +121,7 @@ const getEditorFromEvent = (event) => { const buildCmsTagsMenu = async () => { const response = await getTagNames({}); const tagNames = response.result || []; - const submenuConfig = tagNames.map(tag => ({ + const submenuConfig = tagNames.map((tag) => ({ name: tag.charAt(0).toUpperCase() + tag.slice(1), value: tag, noIcon: true, diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.js index be29625a7..653d4ab9e 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.media.js @@ -56,6 +56,9 @@ const createMediaField = (options, value = '') => { }; const getData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='media']").forEach(wrapper => { const input = wrapper.querySelector(".cms-media-input-value"); if (input) { @@ -68,6 +71,9 @@ const getData = (context) => { return data; }; const init = (context) => { + if (!context.formElement) { + return; + } context.formElement.querySelectorAll("[data-cms-form-field-type='media']").forEach(wrapper => { const dropZone = wrapper.querySelector(".cms-drop-zone"); const input = wrapper.querySelector(".cms-media-input"); @@ -101,7 +107,14 @@ const init = (context) => { //dropZone.addEventListener("click", () => input.click()); // Handle file selection input.addEventListener("change", (e) => { - const file = e.target.files[0]; + if (e.target === null) { + return; + } + var inputElement = e.target; + if (inputElement.files == null) { + return; + } + const file = inputElement.files[0]; if (file) { preview.src = URL.createObjectURL(file); handleUpload(wrapper, file); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.js index 7111e6fed..10f481286 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.number.js @@ -37,7 +37,11 @@ const createNumberField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='number'] input").forEach((el) => { + const formElement = context.formElement; + if (!formElement) { + return data; + } + formElement.querySelectorAll("[data-cms-form-field-type='number'] input").forEach((el) => { const value = el.value; data[el.name] = { type: 'number', diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.js index a3efe8928..9412f5aa5 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.radio.js @@ -47,7 +47,11 @@ const createRadioField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='radio']").forEach(container => { + const formElement = context.formElement; + if (!formElement) { + return data; + } + formElement.querySelectorAll("[data-cms-form-field-type='radio']").forEach(container => { const name = container.querySelector("input[type='radio']").name; const checked = container.querySelector("input[type='radio']:checked"); if (checked) { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.js index 152ed99e4..19485a951 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.range.js @@ -38,7 +38,11 @@ const createRangeField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='range'] input").forEach((el) => { + const formElement = context.formElement; + if (!formElement) { + return data; + } + formElement.querySelectorAll("[data-cms-form-field-type='range'] input").forEach((el) => { data[el.name] = { type: 'range', value: parseFloat(el.value) diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.js index 8282b9387..958b62884 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.reference.js @@ -41,7 +41,11 @@ const createReferenceField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='reference'] input").forEach((el) => { + const formElement = context.formElement; + if (!formElement) { + return data; + } + formElement.querySelectorAll("[data-cms-form-field-type='reference'] input").forEach((el) => { let value = el.value; data[el.name] = { type: 'reference', @@ -51,7 +55,11 @@ const getData = (context) => { return data; }; const init = (context) => { - context.formElement.querySelectorAll("[data-cms-form-field-type='reference']").forEach(wrapper => { + const formElement = context.formElement; + if (!formElement) { + return; + } + formElement.querySelectorAll("[data-cms-form-field-type='reference']").forEach(wrapper => { const fileManager = wrapper.querySelector(".cms-reference-button"); if (!fileManager) return; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.js index 7ed94d20d..9da65446f 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.select.js @@ -41,8 +41,8 @@ const createSelectField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement - .querySelectorAll("[data-cms-form-field-type='select'] select") + context.formElement?. + querySelectorAll("[data-cms-form-field-type='select'] select") .forEach((el) => { let value = el.value; // optional: type-konvertierung, aber fallback ist immer der echte Wert diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.js index 63b8c3576..0c01a7971 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.text.js @@ -34,6 +34,9 @@ const createTextField = (options, value = '') => { }; const getData = (context) => { var data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='text'] input").forEach((el) => { let value = el.value; data[el.name] = { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.js b/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.js index 34581dc5d..a26c4bb31 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/field.textarea.js @@ -34,6 +34,10 @@ const createTextAreaField = (options, value = '') => { }; const getData = (context) => { var data = {}; + if (context.formElement === null) { + console.error('Form element not found.'); + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='text'] textarea").forEach((el) => { let value = el.value; data[el.name] = { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js index eee7c6d5b..ea091c80c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/forms.js @@ -45,7 +45,7 @@ const createForm = (options) => { formElement: null, fields: fields }; - const fieldHtml = fields.map(field => { + const fieldHtml = fields.map((field) => { const val = values[field.name] || ''; switch (field.type) { case 'email': @@ -103,6 +103,10 @@ const createForm = (options) => { } container.innerHTML = html; context.formElement = container.querySelector('form'); + if (!context.formElement) { + console.error('Form element not found.'); + return; + } context.formElement.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName.toLowerCase() !== 'textarea') { e.preventDefault(); @@ -111,7 +115,7 @@ const createForm = (options) => { context.formElement.addEventListener('submit', (e) => { e.preventDefault(); e.stopPropagation(); - context.formElement.classList.add('was-validated'); + context.formElement?.classList.add('was-validated'); }); CodeField.init(context); MarkdownField.init(context); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts index 7d3d89dcb..23eeb2258 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts @@ -20,7 +20,7 @@ */ declare const createID: () => string; declare const utcToLocalDateTimeInputValue: (utcString: string) => string; -declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string; +declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string | null; declare function utcToLocalDateInputValue(utcString: string): string; -declare function getUTCDateFromInput(inputElement: HTMLInputElement): string; +declare function getUTCDateFromInput(inputElement: HTMLInputElement): string | null; export { createID, utcToLocalDateTimeInputValue, getUTCDateTimeFromInput, utcToLocalDateInputValue, getUTCDateFromInput }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts index b1e393873..e91051759 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts @@ -21,7 +21,7 @@ export function localizeUi(): Promise; export namespace i18n { let _locale: any; - let _cache: any; + let _cache: null; /** * Loads and merges remote localizations with defaults. */ diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.js b/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.js index 193502b61..3b2e62255 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.js @@ -84,7 +84,7 @@ export const initContentMediaToolbar = (img) => { return; } var toolbar = img.closest('[data-cms-toolbar]'); - var parentToolbarDef = JSON.parse(toolbar.dataset.cmsToolbar); + var parentToolbarDef = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); if (!parentToolbarDef) { return; } @@ -103,7 +103,7 @@ export const initMediaToolbar = (img) => { if (!isSameDomainImage(img)) { return; } - var toolbarDefinition = JSON.parse(img.dataset.cmsMediaToolbar); + var toolbarDefinition = JSON.parse(img.dataset.cmsMediaToolbar || '{}'); initToolbar(img, toolbarDefinition); }; export const initToolbar = (img, toolbarDefinition) => { diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js index 580cdf532..392198a9a 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/toolbar.inject.js @@ -22,7 +22,7 @@ import frameMessenger from "@cms/modules/frameMessenger.js"; import { EDIT_ATTRIBUTES_ICON, EDIT_PAGE_ICON, SECTION_ADD_ICON, SECTION_DELETE_ICON, SECTION_SORT_ICON, SECTION_UNPUBLISHED_ICON } from "@cms/modules/manager/toolbar-icons"; const addSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'add-sectionEntry', payload: { @@ -33,7 +33,7 @@ const addSection = (event) => { }; const deleteSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'delete-sectionEntry', payload: { @@ -44,7 +44,7 @@ const deleteSection = (event) => { }; const setPublishForSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var action = event.currentTarget.getAttribute('data-cms-action'); var command = { type: 'section-set-published', @@ -57,7 +57,7 @@ const setPublishForSection = (event) => { }; const orderSections = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'edit-sections', payload: { @@ -68,7 +68,7 @@ const orderSections = (event) => { }; const editContent = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'edit', payload: { @@ -83,7 +83,7 @@ const editContent = (event) => { }; const editAttributes = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'edit', payload: { @@ -116,7 +116,7 @@ const editAttributes = (event) => { frameMessenger.send(window.parent, command); }; export const initToolbar = (container) => { - var toolbarDefinition = JSON.parse(container.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(container.dataset.cmsToolbar || '{}'); if (!toolbarDefinition.actions) { return; } @@ -143,7 +143,7 @@ export const initToolbar = (container) => { toolbar.classList.remove('visible'); } }); - toolbarDefinition.actions.forEach(action => { + toolbarDefinition.actions.forEach((action) => { if (action === "editContent") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'edit'); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts index 520ce2957..6cfeb16ad 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts @@ -22,6 +22,6 @@ export namespace PreviewHistory { export { init }; export { navigatePreview }; } -declare function init(defaultUrl?: any): void; +declare function init(defaultUrl?: null): void; declare function navigatePreview(url: any, usePush?: boolean): void; export {}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts index e7c2a28ba..20c850ee6 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts @@ -23,4 +23,4 @@ export function deActivatePreviewOverlay(): void; export function getPreviewUrl(): any; export function reloadPreview(): void; export function loadPreview(url: any): void; -export function getPreviewFrame(): HTMLElement; +export function getPreviewFrame(): HTMLElement | null; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js index d6c1aa58d..9974e2a6d 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js @@ -28,11 +28,12 @@ const executeRemoteMethodCall = async (method, parameters) => { method: method, parameters: parameters }; + const csrfToken = getCSRFToken(); var response = await fetch(window.manager.baseUrl + "/rpc", { method: "POST", headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': getCSRFToken() + ...(csrfToken && { 'X-CSRF-Token': csrfToken }) }, body: JSON.stringify(data) }); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/state.js b/modules/ui-module/src/main/resources/manager/js/modules/state.js index 7274c0b66..a0988236f 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/state.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/state.js @@ -1,3 +1,4 @@ +"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts index 65018962d..30e36cd4d 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts @@ -20,11 +20,11 @@ */ export namespace UIStateManager { function setTabState(key: any, value: any): void; - function getTabState(key: any, defaultValue?: any): any; + function getTabState(key: any, defaultValue?: null): any; function setLocale(locale: any): void; function getLocale(): any; function removeTabState(key: any): void; function setAuthToken(token: any): void; - function getAuthToken(): string; + function getAuthToken(): string | null; function clearAuthToken(): void; } diff --git a/modules/ui-module/src/main/resources/manager/public/manager-login.js b/modules/ui-module/src/main/resources/manager/public/manager-login.js index 6f21eee99..24aa077ae 100644 --- a/modules/ui-module/src/main/resources/manager/public/manager-login.js +++ b/modules/ui-module/src/main/resources/manager/public/manager-login.js @@ -1,3 +1,4 @@ +"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/dist/actions/media/edit-focal-point.js b/modules/ui-module/src/main/ts/dist/actions/media/edit-focal-point.js index 6c4a63928..a917e7b54 100644 --- a/modules/ui-module/src/main/ts/dist/actions/media/edit-focal-point.js +++ b/modules/ui-module/src/main/ts/dist/actions/media/edit-focal-point.js @@ -66,6 +66,10 @@ export async function runAction(params) { const wrapper = document.getElementById("cmsFocalWrapper"); const image = document.getElementById("cms-image"); const point = document.getElementById("cmsFocalPoint"); + if (wrapper === null || image === null || point === null) { + console.error("One or more required elements not found"); + return; + } if (image.complete) { setFocalPoint(image, point, focalX, focalY); } diff --git a/modules/ui-module/src/main/ts/dist/actions/page/translations.js b/modules/ui-module/src/main/ts/dist/actions/page/translations.js index 7745258f6..f026bca19 100644 --- a/modules/ui-module/src/main/ts/dist/actions/page/translations.js +++ b/modules/ui-module/src/main/ts/dist/actions/page/translations.js @@ -40,7 +40,7 @@ export async function runAction(params) { onOk: async (event) => { }, onShow: async (modalElement) => { - modalElement.querySelectorAll('button[data-action]').forEach(button => { + modalElement.querySelectorAll('button[data-action]').forEach((button) => { button.addEventListener('click', async (e) => { const action = e.currentTarget.getAttribute('data-action'); const siteId = e.currentTarget.getAttribute('data-id'); diff --git a/modules/ui-module/src/main/ts/dist/js/manager-inject.js b/modules/ui-module/src/main/ts/dist/js/manager-inject.js index 75083adbc..2b11d2b1d 100644 --- a/modules/ui-module/src/main/ts/dist/js/manager-inject.js +++ b/modules/ui-module/src/main/ts/dist/js/manager-inject.js @@ -1,3 +1,4 @@ +"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/dist/js/manager.js b/modules/ui-module/src/main/ts/dist/js/manager.js index 9987b664d..800a0a030 100644 --- a/modules/ui-module/src/main/ts/dist/js/manager.js +++ b/modules/ui-module/src/main/ts/dist/js/manager.js @@ -29,7 +29,20 @@ import { setCSRFToken } from '@cms/modules/utils.js'; frameMessenger.on('load', (payload) => { EventBus.emit("preview:loaded", {}); }); +function heartbeat() { + fetch(window.manager.refreshUrl, { + method: "POST", + credentials: "include" + }) + .then(res => res.json()) + .then(data => { + window.manager.previewToken = data.previewToken; + }); +} document.addEventListener("DOMContentLoaded", function () { + setInterval(() => { + heartbeat(); + }, 10 * 1000); //PreviewHistory.init("/"); //updateStateButton(); activatePreviewOverlay(); diff --git a/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts index 34d9f9cb9..e10842a8d 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts @@ -1,5 +1,5 @@ export function openFileBrowser(optionsParam: any): Promise; export namespace state { - let options: any; + let options: null; let currentFolder: string; } diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.checkbox.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.checkbox.js index e6997083d..3efa4721d 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.checkbox.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.checkbox.js @@ -4,7 +4,7 @@ const createCheckboxField = (options, value = []) => { const key = options.key || ""; const name = options.name || id; const title = options.title || ""; - const choices = options.options.choices || []; + const choices = options.options?.choices || []; const selectedValues = new Set(value); const checkboxes = choices.map((choice, idx) => { const inputId = `${id}-${idx}`; @@ -26,7 +26,10 @@ const createCheckboxField = (options, value = []) => { `; }; const getData = (context) => { - const data = {}; + var data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='checkbox']").forEach(container => { const name = container.querySelector("input[type='checkbox']").name; const checkedBoxes = container.querySelectorAll("input[type='checkbox']:checked"); diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.color.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.color.js index 234163587..6cfba7fd7 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.color.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.color.js @@ -33,6 +33,9 @@ const createColorField = (options, value = '#000000') => { }; const getColorData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='color'] input").forEach((el) => { data[el.name] = { type: 'color', diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.date.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.date.js index 31a16dedd..4a090c7e8 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.date.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.date.js @@ -41,6 +41,9 @@ const createDateField = (options, value = '') => { }; const getDateData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='date'] input").forEach((el) => { const value = getUTCDateFromInput(el); // "2025-05-31" data[el.name] = { diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.datetime.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.datetime.js index bff91c58e..8df816d5e 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.datetime.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.datetime.js @@ -41,6 +41,9 @@ const createDateTimeField = (options, value = '') => { }; const getDateTimeData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='datetime'] input").forEach((el) => { const value = getUTCDateTimeFromInput(el); // "2025-05-31T15:30" data[el.name] = { diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.easymde.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.easymde.js index 7b547663f..12e86fe6e 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.easymde.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.easymde.js @@ -34,6 +34,9 @@ const createMarkdownField = (options, value = '') => { }; const getData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } markdownEditors.forEach(({ input, editor }) => { data[input.name] = { type: "easymde", diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.list.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.list.js index b520c9714..b7b60256e 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.list.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.list.js @@ -80,7 +80,7 @@ const handleAddItem = (e, container, context) => { `; listGroup.insertAdjacentHTML("beforeend", itemMarkup); - var itemElement = listGroup.querySelector(`[data-cms-form-field-item="${itemId}"]`); + const itemElement = listGroup.querySelector(`[data-cms-form-field-item="${itemId}"]`); if (itemElement) { itemElement.addEventListener('dblclick', (e) => handleDoubleClick(e, context)); const removeBtn = itemElement.querySelector('.remove-btn'); @@ -99,7 +99,7 @@ const getItemForm = async (el) => { const getContentResponse = await getContent({ uri: contentNode.result.uri }); - var selected = pageTemplates.filter(pageTemplate => pageTemplate.template === getContentResponse?.result?.meta?.template); + var selected = pageTemplates.filter((pageTemplate) => pageTemplate.template === getContentResponse?.result?.meta?.template); const listContainer = el.closest("[data-cms-form-field-type='list']"); const fieldName = listContainer?.getAttribute('name'); var itemForm = []; @@ -108,7 +108,7 @@ const getItemForm = async (el) => { } if (!itemForm || itemForm.length === 0) { let itemTypes = (await getListItemTypes({})).result; - var selectedItemType = itemTypes.filter(itemType => itemType.name === fieldName); + var selectedItemType = itemTypes.filter((itemType) => itemType.name === fieldName); itemForm = (selectedItemType.length === 1) ? selectedItemType[0].data?.form.fields : []; } return itemForm; @@ -136,16 +136,22 @@ const handleDoubleClick = async (event, context) => { el.setAttribute('data-cms-form-field-item-data', JSON.stringify(updateData)); const listContainer = el.closest("[data-cms-form-field-type='list']"); const nameField = listContainer?.getAttribute('data-name-field') || 'name'; - el.querySelector('.object-name').textContent = updateData[nameField]; + const objectNameEl = el.querySelector('.object-name'); + if (!objectNameEl) + return; + objectNameEl.textContent = updateData[nameField] || ""; } }); } }; const getData = (context) => { var data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='list']").forEach((el) => { let value = []; - el.querySelectorAll("[data-cms-form-field-item]").forEach(itemEl => { + el.querySelectorAll("[data-cms-form-field-item]").forEach((itemEl) => { const itemData = itemEl.getAttribute('data-cms-form-field-item-data'); if (itemData) { value.push(JSON.parse(itemData)); @@ -162,7 +168,7 @@ const getData = (context) => { return data; }; const init = (context) => { - context.formElement.querySelectorAll("[data-cms-form-field-type='list']").forEach(listContainer => { + context.formElement?.querySelectorAll("[data-cms-form-field-type='list']").forEach(listContainer => { listContainer.querySelectorAll("[data-cms-form-field-item]").forEach(field => { field.addEventListener('dblclick', (e) => handleDoubleClick(e, context)); // Remove-Button-Listener setzen diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.mail.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.mail.js index 1ca07c934..cb4651b92 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.mail.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.mail.js @@ -34,6 +34,9 @@ const createEmailField = (options, value = '') => { }; const getData = (context) => { var data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='mail'] input").forEach((el) => { let value = el.value; data[el.name] = { diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.markdown.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.markdown.js index 0d134d310..8475a02c8 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.markdown.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.markdown.js @@ -38,7 +38,11 @@ const createMarkdownField = (options, value = '') => { }; const getData = (context) => { const data = {}; - const editorInputs = context.formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); + const formElement = context.formElement; + if (!formElement) { + return data; + } + const editorInputs = formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); editorInputs.forEach((input) => { const editor = input.cherryEditor; if (editor && editor.getMarkdown) { @@ -58,8 +62,12 @@ const getData = (context) => { return data; }; const init = async (context) => { + const formElement = context.formElement; + if (!formElement) { + return; + } const cmsTagsMenu = await buildCmsTagsMenu(); - const editorInputs = context.formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); + const editorInputs = formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); editorInputs.forEach((input) => { const containerId = input.dataset.cherryId; const initialValue = decodeURIComponent(input.dataset.initialValue || ""); @@ -113,7 +121,7 @@ const getEditorFromEvent = (event) => { const buildCmsTagsMenu = async () => { const response = await getTagNames({}); const tagNames = response.result || []; - const submenuConfig = tagNames.map(tag => ({ + const submenuConfig = tagNames.map((tag) => ({ name: tag.charAt(0).toUpperCase() + tag.slice(1), value: tag, noIcon: true, diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.media.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.media.js index be29625a7..653d4ab9e 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.media.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.media.js @@ -56,6 +56,9 @@ const createMediaField = (options, value = '') => { }; const getData = (context) => { const data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='media']").forEach(wrapper => { const input = wrapper.querySelector(".cms-media-input-value"); if (input) { @@ -68,6 +71,9 @@ const getData = (context) => { return data; }; const init = (context) => { + if (!context.formElement) { + return; + } context.formElement.querySelectorAll("[data-cms-form-field-type='media']").forEach(wrapper => { const dropZone = wrapper.querySelector(".cms-drop-zone"); const input = wrapper.querySelector(".cms-media-input"); @@ -101,7 +107,14 @@ const init = (context) => { //dropZone.addEventListener("click", () => input.click()); // Handle file selection input.addEventListener("change", (e) => { - const file = e.target.files[0]; + if (e.target === null) { + return; + } + var inputElement = e.target; + if (inputElement.files == null) { + return; + } + const file = inputElement.files[0]; if (file) { preview.src = URL.createObjectURL(file); handleUpload(wrapper, file); diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.number.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.number.js index 7111e6fed..10f481286 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.number.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.number.js @@ -37,7 +37,11 @@ const createNumberField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='number'] input").forEach((el) => { + const formElement = context.formElement; + if (!formElement) { + return data; + } + formElement.querySelectorAll("[data-cms-form-field-type='number'] input").forEach((el) => { const value = el.value; data[el.name] = { type: 'number', diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.radio.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.radio.js index a3efe8928..9412f5aa5 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.radio.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.radio.js @@ -47,7 +47,11 @@ const createRadioField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='radio']").forEach(container => { + const formElement = context.formElement; + if (!formElement) { + return data; + } + formElement.querySelectorAll("[data-cms-form-field-type='radio']").forEach(container => { const name = container.querySelector("input[type='radio']").name; const checked = container.querySelector("input[type='radio']:checked"); if (checked) { diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.range.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.range.js index 152ed99e4..19485a951 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.range.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.range.js @@ -38,7 +38,11 @@ const createRangeField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='range'] input").forEach((el) => { + const formElement = context.formElement; + if (!formElement) { + return data; + } + formElement.querySelectorAll("[data-cms-form-field-type='range'] input").forEach((el) => { data[el.name] = { type: 'range', value: parseFloat(el.value) diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.reference.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.reference.js index 8282b9387..958b62884 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.reference.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.reference.js @@ -41,7 +41,11 @@ const createReferenceField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='reference'] input").forEach((el) => { + const formElement = context.formElement; + if (!formElement) { + return data; + } + formElement.querySelectorAll("[data-cms-form-field-type='reference'] input").forEach((el) => { let value = el.value; data[el.name] = { type: 'reference', @@ -51,7 +55,11 @@ const getData = (context) => { return data; }; const init = (context) => { - context.formElement.querySelectorAll("[data-cms-form-field-type='reference']").forEach(wrapper => { + const formElement = context.formElement; + if (!formElement) { + return; + } + formElement.querySelectorAll("[data-cms-form-field-type='reference']").forEach(wrapper => { const fileManager = wrapper.querySelector(".cms-reference-button"); if (!fileManager) return; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.select.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.select.js index 7ed94d20d..9da65446f 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.select.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.select.js @@ -41,8 +41,8 @@ const createSelectField = (options, value = '') => { }; const getData = (context) => { const data = {}; - context.formElement - .querySelectorAll("[data-cms-form-field-type='select'] select") + context.formElement?. + querySelectorAll("[data-cms-form-field-type='select'] select") .forEach((el) => { let value = el.value; // optional: type-konvertierung, aber fallback ist immer der echte Wert diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.text.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.text.js index 63b8c3576..0c01a7971 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.text.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.text.js @@ -34,6 +34,9 @@ const createTextField = (options, value = '') => { }; const getData = (context) => { var data = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='text'] input").forEach((el) => { let value = el.value; data[el.name] = { diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/field.textarea.js b/modules/ui-module/src/main/ts/dist/js/modules/form/field.textarea.js index 34581dc5d..a26c4bb31 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/field.textarea.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/field.textarea.js @@ -34,6 +34,10 @@ const createTextAreaField = (options, value = '') => { }; const getData = (context) => { var data = {}; + if (context.formElement === null) { + console.error('Form element not found.'); + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='text'] textarea").forEach((el) => { let value = el.value; data[el.name] = { diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/forms.js b/modules/ui-module/src/main/ts/dist/js/modules/form/forms.js index eee7c6d5b..ea091c80c 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/forms.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/forms.js @@ -45,7 +45,7 @@ const createForm = (options) => { formElement: null, fields: fields }; - const fieldHtml = fields.map(field => { + const fieldHtml = fields.map((field) => { const val = values[field.name] || ''; switch (field.type) { case 'email': @@ -103,6 +103,10 @@ const createForm = (options) => { } container.innerHTML = html; context.formElement = container.querySelector('form'); + if (!context.formElement) { + console.error('Form element not found.'); + return; + } context.formElement.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.tagName.toLowerCase() !== 'textarea') { e.preventDefault(); @@ -111,7 +115,7 @@ const createForm = (options) => { context.formElement.addEventListener('submit', (e) => { e.preventDefault(); e.stopPropagation(); - context.formElement.classList.add('was-validated'); + context.formElement?.classList.add('was-validated'); }); CodeField.init(context); MarkdownField.init(context); diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts index 79c944de7..525b147b7 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts @@ -1,6 +1,6 @@ declare const createID: () => string; declare const utcToLocalDateTimeInputValue: (utcString: string) => string; -declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string; +declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string | null; declare function utcToLocalDateInputValue(utcString: string): string; -declare function getUTCDateFromInput(inputElement: HTMLInputElement): string; +declare function getUTCDateFromInput(inputElement: HTMLInputElement): string | null; export { createID, utcToLocalDateTimeInputValue, getUTCDateTimeFromInput, utcToLocalDateInputValue, getUTCDateFromInput }; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts index 0b22efabb..9eeed5a15 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts @@ -1,7 +1,7 @@ export function localizeUi(): Promise; export namespace i18n { let _locale: any; - let _cache: any; + let _cache: null; /** * Loads and merges remote localizations with defaults. */ diff --git a/modules/ui-module/src/main/ts/dist/js/modules/manager/media.inject.js b/modules/ui-module/src/main/ts/dist/js/modules/manager/media.inject.js index 193502b61..3b2e62255 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/manager/media.inject.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/manager/media.inject.js @@ -84,7 +84,7 @@ export const initContentMediaToolbar = (img) => { return; } var toolbar = img.closest('[data-cms-toolbar]'); - var parentToolbarDef = JSON.parse(toolbar.dataset.cmsToolbar); + var parentToolbarDef = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); if (!parentToolbarDef) { return; } @@ -103,7 +103,7 @@ export const initMediaToolbar = (img) => { if (!isSameDomainImage(img)) { return; } - var toolbarDefinition = JSON.parse(img.dataset.cmsMediaToolbar); + var toolbarDefinition = JSON.parse(img.dataset.cmsMediaToolbar || '{}'); initToolbar(img, toolbarDefinition); }; export const initToolbar = (img, toolbarDefinition) => { diff --git a/modules/ui-module/src/main/ts/dist/js/modules/manager/toolbar.inject.js b/modules/ui-module/src/main/ts/dist/js/modules/manager/toolbar.inject.js index 580cdf532..392198a9a 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/manager/toolbar.inject.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/manager/toolbar.inject.js @@ -22,7 +22,7 @@ import frameMessenger from "@cms/modules/frameMessenger.js"; import { EDIT_ATTRIBUTES_ICON, EDIT_PAGE_ICON, SECTION_ADD_ICON, SECTION_DELETE_ICON, SECTION_SORT_ICON, SECTION_UNPUBLISHED_ICON } from "@cms/modules/manager/toolbar-icons"; const addSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'add-sectionEntry', payload: { @@ -33,7 +33,7 @@ const addSection = (event) => { }; const deleteSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'delete-sectionEntry', payload: { @@ -44,7 +44,7 @@ const deleteSection = (event) => { }; const setPublishForSection = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var action = event.currentTarget.getAttribute('data-cms-action'); var command = { type: 'section-set-published', @@ -57,7 +57,7 @@ const setPublishForSection = (event) => { }; const orderSections = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'edit-sections', payload: { @@ -68,7 +68,7 @@ const orderSections = (event) => { }; const editContent = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'edit', payload: { @@ -83,7 +83,7 @@ const editContent = (event) => { }; const editAttributes = (event) => { var toolbar = event.target.closest('[data-cms-toolbar]'); - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command = { type: 'edit', payload: { @@ -116,7 +116,7 @@ const editAttributes = (event) => { frameMessenger.send(window.parent, command); }; export const initToolbar = (container) => { - var toolbarDefinition = JSON.parse(container.dataset.cmsToolbar); + var toolbarDefinition = JSON.parse(container.dataset.cmsToolbar || '{}'); if (!toolbarDefinition.actions) { return; } @@ -143,7 +143,7 @@ export const initToolbar = (container) => { toolbar.classList.remove('visible'); } }); - toolbarDefinition.actions.forEach(action => { + toolbarDefinition.actions.forEach((action) => { if (action === "editContent") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'edit'); diff --git a/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts index 02a1aa102..19f953414 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts @@ -2,6 +2,6 @@ export namespace PreviewHistory { export { init }; export { navigatePreview }; } -declare function init(defaultUrl?: any): void; +declare function init(defaultUrl?: null): void; declare function navigatePreview(url: any, usePush?: boolean): void; export {}; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts index f2f167f34..24beb8ecc 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts @@ -3,4 +3,4 @@ export function deActivatePreviewOverlay(): void; export function getPreviewUrl(): any; export function reloadPreview(): void; export function loadPreview(url: any): void; -export function getPreviewFrame(): HTMLElement; +export function getPreviewFrame(): HTMLElement | null; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.js b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.js index d6c1aa58d..9974e2a6d 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.js @@ -28,11 +28,12 @@ const executeRemoteMethodCall = async (method, parameters) => { method: method, parameters: parameters }; + const csrfToken = getCSRFToken(); var response = await fetch(window.manager.baseUrl + "/rpc", { method: "POST", headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': getCSRFToken() + ...(csrfToken && { 'X-CSRF-Token': csrfToken }) }, body: JSON.stringify(data) }); diff --git a/modules/ui-module/src/main/ts/dist/js/modules/state.js b/modules/ui-module/src/main/ts/dist/js/modules/state.js index 7274c0b66..a0988236f 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/state.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/state.js @@ -1,3 +1,4 @@ +"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts index cf3faa9ae..d064b5d45 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts @@ -1,10 +1,10 @@ export namespace UIStateManager { function setTabState(key: any, value: any): void; - function getTabState(key: any, defaultValue?: any): any; + function getTabState(key: any, defaultValue?: null): any; function setLocale(locale: any): void; function getLocale(): any; function removeTabState(key: any): void; function setAuthToken(token: any): void; - function getAuthToken(): string; + function getAuthToken(): string | null; function clearAuthToken(): void; } diff --git a/modules/ui-module/src/main/ts/dist/public/manager-login.js b/modules/ui-module/src/main/ts/dist/public/manager-login.js index 6f21eee99..24aa077ae 100644 --- a/modules/ui-module/src/main/ts/dist/public/manager-login.js +++ b/modules/ui-module/src/main/ts/dist/public/manager-login.js @@ -1,3 +1,4 @@ +"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/globals.d.ts b/modules/ui-module/src/main/ts/globals.d.ts index 714f0a99e..e74d43604 100644 --- a/modules/ui-module/src/main/ts/globals.d.ts +++ b/modules/ui-module/src/main/ts/globals.d.ts @@ -12,6 +12,7 @@ declare global { contextPath: string, siteId: string, previewUrl: string, + refreshUrl: string, }, EasyMDE : any, Cherry: any diff --git a/modules/ui-module/src/main/ts/src/actions/media/edit-focal-point.ts b/modules/ui-module/src/main/ts/src/actions/media/edit-focal-point.ts index 7c8b57cc7..4bbdef763 100644 --- a/modules/ui-module/src/main/ts/src/actions/media/edit-focal-point.ts +++ b/modules/ui-module/src/main/ts/src/actions/media/edit-focal-point.ts @@ -25,7 +25,7 @@ import { reloadPreview } from "@cms/modules/preview.utils.js"; import { getMediaMetaData, setMediaMetaData } from "@cms/modules/rpc/rpc-media.js"; import { showToast } from "@cms/modules/toast.js"; -export async function runAction(params) { +export async function runAction(params : any) { var uri = params.options.uri || null; var mediaUrl = removeFormatParamFromUrl(uri); @@ -46,8 +46,8 @@ export async function runAction(params) { openModal({ title: i18n.t("media.focal.title", "Edit focal point"), body: template, - onCancel: (event) => { }, - onOk: async (event) => { + onCancel: (event : any) => { }, + onOk: async (event : any) => { var setMetaResponse = await setMediaMetaData({ image: mediaUrl, meta: { @@ -70,10 +70,15 @@ export async function runAction(params) { reloadPreview(); }, onShow: () => { - const wrapper: HTMLElement = document.getElementById("cmsFocalWrapper"); - const image: HTMLImageElement = document.getElementById("cms-image") as HTMLImageElement; - const point: HTMLElement = document.getElementById("cmsFocalPoint"); + const wrapper: HTMLElement | null = document.getElementById("cmsFocalWrapper"); + const image: HTMLImageElement | null = document.getElementById("cms-image") as HTMLImageElement; + const point: HTMLElement | null = document.getElementById("cmsFocalPoint"); + if (wrapper === null || image === null || point === null) { + console.error("One or more required elements not found"); + return; + } + if (image.complete) { setFocalPoint(image, point, focalX, focalY); } else { diff --git a/modules/ui-module/src/main/ts/src/actions/media/select-media.ts b/modules/ui-module/src/main/ts/src/actions/media/select-media.ts index 2bb2bb4dd..08cbb96b2 100644 --- a/modules/ui-module/src/main/ts/src/actions/media/select-media.ts +++ b/modules/ui-module/src/main/ts/src/actions/media/select-media.ts @@ -25,10 +25,10 @@ import { getPreviewUrl, reloadPreview } from "@cms/modules/preview.utils.js"; import { getContentNode, setMeta } from "@cms/modules/rpc/rpc-content.js"; import { showToast } from "@cms/modules/toast.js"; -export async function runAction(params) { +export async function runAction(params : any) { - var uri = null + var uri : any = null if (params.options.uri) { uri = params.options.uri } else { @@ -40,7 +40,7 @@ export async function runAction(params) { openFileBrowser({ type: "assets", - filter : (file) => { + filter : (file: any) => { return file.media || file.directory; }, onSelect: async (file: any) => { @@ -52,7 +52,7 @@ export async function runAction(params) { selectedFile = file.uri.substring(1); // Remove leading slash if present } - var updateData = {} + var updateData : any = {} updateData[params.options.metaElement] = { type: 'media', value: selectedFile diff --git a/modules/ui-module/src/main/ts/src/actions/page/section-set-published.ts b/modules/ui-module/src/main/ts/src/actions/page/section-set-published.ts index 62ef9c5ee..570856ff1 100644 --- a/modules/ui-module/src/main/ts/src/actions/page/section-set-published.ts +++ b/modules/ui-module/src/main/ts/src/actions/page/section-set-published.ts @@ -22,7 +22,7 @@ import { reloadPreview } from "@cms/modules/preview.utils"; import { setMeta } from "@cms/modules/rpc/rpc-content"; -export async function runAction(params) { +export async function runAction(params : any) { var request = { uri : params.sectionUri, diff --git a/modules/ui-module/src/main/ts/src/actions/page/translations.ts b/modules/ui-module/src/main/ts/src/actions/page/translations.ts index f86790dd4..5a374dd2d 100644 --- a/modules/ui-module/src/main/ts/src/actions/page/translations.ts +++ b/modules/ui-module/src/main/ts/src/actions/page/translations.ts @@ -40,13 +40,13 @@ export async function runAction(params: any) { openModal({ title: 'Manage Translations', body: modelContent, - onCancel: (event) => { }, - onOk: async (event) => { + onCancel: (event : any) => { }, + onOk: async (event : any) => { }, - onShow: async (modalElement) => { + onShow: async (modalElement : any) => { - modalElement.querySelectorAll('button[data-action]').forEach(button => { - button.addEventListener('click', async (e) => { + modalElement.querySelectorAll('button[data-action]').forEach((button : HTMLElement) => { + button.addEventListener('click', async (e : any) => { const action = (e.currentTarget as HTMLElement).getAttribute('data-action'); const siteId = (e.currentTarget as HTMLElement).getAttribute('data-id'); const lang = (e.currentTarget as HTMLElement).getAttribute('data-lang'); @@ -55,7 +55,7 @@ export async function runAction(params: any) { openFileBrowser({ siteId: siteId || '', type: 'content', - onSelect: async (file) => { + onSelect: async (file : any) => { console.log('Selected translation file:', file); if (file && file.uri) { var selectedFile = file.uri; // Use the file's URI diff --git a/modules/ui-module/src/main/ts/src/actions/reload-preview.ts b/modules/ui-module/src/main/ts/src/actions/reload-preview.ts index f87a6c05e..cda1908cf 100644 --- a/modules/ui-module/src/main/ts/src/actions/reload-preview.ts +++ b/modules/ui-module/src/main/ts/src/actions/reload-preview.ts @@ -21,6 +21,6 @@ import { reloadPreview } from "@cms/modules/preview.utils.js"; -export async function runAction(params) { +export async function runAction(params : any) { reloadPreview(); } diff --git a/modules/ui-module/src/main/ts/src/js/manager.js b/modules/ui-module/src/main/ts/src/js/manager.js index 00ae31760..1372de511 100644 --- a/modules/ui-module/src/main/ts/src/js/manager.js +++ b/modules/ui-module/src/main/ts/src/js/manager.js @@ -34,9 +34,23 @@ frameMessenger.on('load', (payload) => { EventBus.emit("preview:loaded", {}); }); +function heartbeat() { + fetch(window.manager.refreshUrl, { + method: "POST", + credentials: "include" + }) + .then(res => res.json()) + .then(data => { + window.manager.previewToken = data.previewToken; + }); +} document.addEventListener("DOMContentLoaded", function () { + setInterval(() => { + heartbeat(); + }, 10 * 1000); + //PreviewHistory.init("/"); //updateStateButton(); diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.checkbox.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.checkbox.ts index 580b8df72..40924e31d 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.checkbox.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.checkbox.ts @@ -31,13 +31,13 @@ export interface CheckboxOptions extends FieldOptions{ }; } -const createCheckboxField = (options : CheckboxOptions, value = []) => { +const createCheckboxField = (options : CheckboxOptions, value: string[] = []) => { const id = createID(); const key = options.key || ""; const name = options.name || id; const title = options.title || ""; - const choices = options.options.choices || []; - const selectedValues = new Set(value); + const choices = options.options?.choices || []; + const selectedValues = new Set(value); const checkboxes = choices.map((choice, idx) => { const inputId = `${id}-${idx}`; @@ -61,11 +61,14 @@ const createCheckboxField = (options : CheckboxOptions, value = []) => { }; const getData = (context : FormContext) => { - const data = {}; + var data : any = {}; + if (!context.formElement) { + return data; + } context.formElement.querySelectorAll("[data-cms-form-field-type='checkbox']").forEach(container => { const name = (container.querySelector("input[type='checkbox']") as HTMLInputElement).name; const checkedBoxes = container.querySelectorAll("input[type='checkbox']:checked"); - const values = Array.from(checkedBoxes).map((el : HTMLInputElement) => el.value); + const values = Array.from(checkedBoxes).map((el : any) => el.value); data[name] = { type: 'checkbox', value: values diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.color.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.color.ts index 42c4b673e..fc25659ac 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.color.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.color.ts @@ -39,8 +39,11 @@ const createColorField = (options: ColorFieldOptions, value = '#000000') => { }; const getColorData = (context : FormContext) => { - const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='color'] input").forEach((el: HTMLInputElement ) => { + const data : any = {}; + if (!context.formElement) { + return data; + } + context.formElement.querySelectorAll("[data-cms-form-field-type='color'] input").forEach((el: any ) => { data[el.name] = { type: 'color', value: el.value diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.date.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.date.ts index edd4e7172..9991dd43d 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.date.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.date.ts @@ -51,9 +51,11 @@ const createDateField = (options: DateFieldOptions, value : any = '') => { const getDateData = (context : FormContext) => { - const data = {}; - - context.formElement.querySelectorAll("[data-cms-form-field-type='date'] input").forEach((el: HTMLInputElement) => { + const data : any = {}; + if (!context.formElement) { + return data; + } + context.formElement.querySelectorAll("[data-cms-form-field-type='date'] input").forEach((el: any) => { const value = getUTCDateFromInput(el); // "2025-05-31" data[el.name] = { type: "date", diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.datetime.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.datetime.ts index fdea9b84c..7fad13082 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.datetime.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.datetime.ts @@ -51,9 +51,11 @@ const createDateTimeField = (options: DateTimeFieldOptions, value : any = '') => const getDateTimeData = (context : FormContext) => { - const data = {}; - - context.formElement.querySelectorAll("[data-cms-form-field-type='datetime'] input").forEach((el: HTMLInputElement) => { + const data : any = {}; + if (!context.formElement) { + return data; + } + context.formElement.querySelectorAll("[data-cms-form-field-type='datetime'] input").forEach((el: any) => { const value = getUTCDateTimeFromInput(el); // "2025-05-31T15:30" data[el.name] = { type: 'datetime', diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.easymde.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.easymde.ts index 2a08cf5ab..9c803e474 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.easymde.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.easymde.ts @@ -22,7 +22,12 @@ import { createID } from "@cms/modules/form/utils.js"; import { i18n } from "@cms/modules/localization.js" import { FieldOptions, FormContext, FormField } from "@cms/modules/form/forms.js"; -let markdownEditors = []; +interface MarkdownEditorEntry { + input: HTMLTextAreaElement; + editor: any; +} + +let markdownEditors: MarkdownEditorEntry[] = []; export interface EasyMDEFieldOptions extends FieldOptions { } @@ -40,8 +45,11 @@ const createMarkdownField = (options : EasyMDEFieldOptions, value : string = '') }; const getData = (context : FormContext) => { - const data = {}; - markdownEditors.forEach(({ input, editor }) => { + const data : any = {}; + if (!context.formElement) { + return data; + } + markdownEditors.forEach(({ input , editor }) => { data[input.name] = { type: "easymde", value: editor.value() @@ -54,7 +62,7 @@ const init = (context : FormContext) => { markdownEditors = []; const editorInputs = document.querySelectorAll('[data-cms-form-field-type="easymde"] textarea'); - editorInputs.forEach((input: HTMLTextAreaElement) => { + editorInputs.forEach((input: any) => { const initialValue = decodeURIComponent(input.dataset.initialValue || ""); input.value = initialValue; // Set initial value for EasyMDE diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.list.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.list.ts index c47308496..5d9675bdd 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.list.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.list.ts @@ -96,7 +96,7 @@ const handleAddItem = (e: Event, container: HTMLElement, context: FormContext) = listGroup.insertAdjacentHTML("beforeend", itemMarkup); - var itemElement: HTMLElement = listGroup.querySelector(`[data-cms-form-field-item="${itemId}"]`) + const itemElement: HTMLElement | null = listGroup.querySelector(`[data-cms-form-field-item="${itemId}"]`) if (itemElement) { itemElement.addEventListener('dblclick', (e) => handleDoubleClick(e, context)); @@ -122,7 +122,7 @@ const getItemForm = async (el: HTMLElement) => { uri: contentNode.result.uri }) - var selected = pageTemplates.filter(pageTemplate => pageTemplate.template === getContentResponse?.result?.meta?.template) + var selected = pageTemplates.filter((pageTemplate : any) => pageTemplate.template === getContentResponse?.result?.meta?.template) const listContainer = el.closest("[data-cms-form-field-type='list']"); const fieldName = listContainer?.getAttribute('name'); @@ -134,7 +134,7 @@ const getItemForm = async (el: HTMLElement) => { if (!itemForm || itemForm.length === 0) { let itemTypes = (await getListItemTypes({})).result - var selectedItemType = itemTypes.filter(itemType => itemType.name === fieldName) + var selectedItemType = itemTypes.filter((itemType : any) => itemType.name === fieldName) itemForm = (selectedItemType.length === 1) ? selectedItemType[0].data?.form.fields : [] } @@ -162,25 +162,29 @@ const handleDoubleClick = async (event: Event, context: FormContext) => { title: 'Edit Item', fullscreen: true, form: form, - onCancel: (event) => { }, - onOk: async (event) => { + onCancel: (event: Event) => { }, + onOk: async (event: Event) => { var updateData = form.getRawData() el.setAttribute('data-cms-form-field-item-data', JSON.stringify(updateData)); const listContainer = el.closest("[data-cms-form-field-type='list']"); const nameField = listContainer?.getAttribute('data-name-field') || 'name'; - - el.querySelector('.object-name').textContent = updateData[nameField]; + const objectNameEl = el.querySelector('.object-name'); + if (!objectNameEl) return; + objectNameEl.textContent = updateData[nameField] || ""; } }); } } const getData = (context: FormContext) => { - var data = {} - context.formElement.querySelectorAll("[data-cms-form-field-type='list']").forEach((el: HTMLInputElement) => { - let value = [] - el.querySelectorAll("[data-cms-form-field-item]").forEach(itemEl => { + var data : any = {}; + if (!context.formElement) { + return data; + } + context.formElement.querySelectorAll("[data-cms-form-field-type='list']").forEach((el: any) => { + let value : any = [] + el.querySelectorAll("[data-cms-form-field-item]").forEach((itemEl: any) => { const itemData = itemEl.getAttribute('data-cms-form-field-item-data'); if (itemData) { value.push(JSON.parse(itemData)); @@ -198,7 +202,7 @@ const getData = (context: FormContext) => { } const init = (context: FormContext) => { - context.formElement.querySelectorAll("[data-cms-form-field-type='list']").forEach(listContainer => { + context.formElement?.querySelectorAll("[data-cms-form-field-type='list']").forEach(listContainer => { listContainer.querySelectorAll("[data-cms-form-field-item]").forEach(field => { field.addEventListener('dblclick', (e) => handleDoubleClick(e, context)); // Remove-Button-Listener setzen diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.mail.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.mail.ts index 66bf161f8..ca44fcadb 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.mail.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.mail.ts @@ -41,8 +41,11 @@ const createEmailField = (options: MailFieldOptions, value : string = '') => { }; const getData = (context : FormContext) => { - var data = {} - context.formElement.querySelectorAll("[data-cms-form-field-type='mail'] input").forEach((el : HTMLInputElement) => { + var data : any = {}; + if (!context.formElement) { + return data; + } + context.formElement.querySelectorAll("[data-cms-form-field-type='mail'] input").forEach((el : any) => { let value = el.value data[el.name] = { type: 'mail', diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.markdown.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.markdown.ts index fa5bc7527..85e728dcb 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.markdown.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.markdown.ts @@ -52,11 +52,15 @@ const createMarkdownField = (options: MarkdownFieldOptions, value: string = '') }; const getData = (context : FormContext) => { - const data = {}; + const data : any = {}; + const formElement = context.formElement; + if (!formElement) { + return data; + } - const editorInputs = context.formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); + const editorInputs = formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); - editorInputs.forEach((input: HTMLInputElement) => { + editorInputs.forEach((input: any) => { const editor = (input as any).cherryEditor; if (editor && editor.getMarkdown) { @@ -78,11 +82,15 @@ const getData = (context : FormContext) => { const init = async (context : FormContext) => { + const formElement = context.formElement; + if (!formElement) { + return; + } const cmsTagsMenu = await buildCmsTagsMenu(); - const editorInputs = context.formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); - editorInputs.forEach((input: HTMLInputElement) => { + const editorInputs = formElement.querySelectorAll('[data-cms-form-field-type="markdown"] input'); + editorInputs.forEach((input: any) => { const containerId = input.dataset.cherryId; const initialValue = decodeURIComponent(input.dataset.initialValue || ""); @@ -144,7 +152,7 @@ const buildCmsTagsMenu = async () => { const response = await getTagNames({}); const tagNames = response.result || []; - const submenuConfig = tagNames.map(tag => ({ + const submenuConfig = tagNames.map((tag: string) => ({ name: tag.charAt(0).toUpperCase() + tag.slice(1), value: tag, noIcon: true, @@ -158,7 +166,7 @@ const buildCmsTagsMenu = async () => { return window.Cherry.createMenuHook("CMS-Tags", { title: "CMS Tags", - onClick: (selection, tag) => { + onClick: (selection: string, tag : string) => { return `[[${tag}]]${selection || ""}[[/${tag}]]`; }, subMenuConfig: submenuConfig @@ -176,7 +184,7 @@ const cmsImageSelection = window.Cherry.createMenuHook("Image", { openFileBrowser({ type: "assets", fullscreen: false, - filter: (file) => { + filter: (file: any) => { return file.media || file.directory; }, onSelect: async (file: any) => { @@ -194,7 +202,7 @@ const cmsImageSelection = window.Cherry.createMenuHook("Image", { // select media format var mediaFormats = (await getMediaFormats({})).result || []; - var formatOptions = {}; + var formatOptions : any = {}; formatOptions["original"] = "Original"; mediaFormats.forEach((format : any) => { formatOptions[format.name] = format.name; diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.media.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.media.ts index ee4759496..76a0a242c 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.media.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.media.ts @@ -61,8 +61,12 @@ const createMediaField = (options: MediaFieldOptions, value : string = '') => { }; const getData = (context : FormContext) => { - const data = {}; + const data : any= {}; + if (!context.formElement) { + return data; + } + context.formElement.querySelectorAll("[data-cms-form-field-type='media']").forEach(wrapper => { const input = wrapper.querySelector(".cms-media-input-value") as HTMLInputElement; if (input) { @@ -76,7 +80,11 @@ const getData = (context : FormContext) => { }; const init = (context : FormContext) => { - context.formElement.querySelectorAll("[data-cms-form-field-type='media']").forEach(wrapper => { + if (!context.formElement) { + return; + } + + context.formElement.querySelectorAll("[data-cms-form-field-type='media']").forEach(wrapper => { const dropZone = wrapper.querySelector(".cms-drop-zone"); const input = wrapper.querySelector(".cms-media-input") as HTMLInputElement; const preview = wrapper.querySelector(".cms-media-image") as HTMLImageElement; @@ -85,18 +93,18 @@ const init = (context : FormContext) => { if (!input || !dropZone || !preview || !openMediaManager) return; // Handle file drop - dropZone.addEventListener("dragover", (e) => { + dropZone.addEventListener("dragover", (e : any) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.add("drag-over"); }); - dropZone.addEventListener("dragleave", (e) => { + dropZone.addEventListener("dragleave", (e : any) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove("drag-over"); }); - dropZone.addEventListener("drop", (e : DragEvent) => { + dropZone.addEventListener("drop", (e : any) => { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove("drag-over"); @@ -113,7 +121,14 @@ const init = (context : FormContext) => { // Handle file selection input.addEventListener("change", (e: Event) => { - const file = (e.target as HTMLInputElement).files[0]; + if (e.target === null) { + return; + } + var inputElement = e.target as HTMLInputElement; + if (inputElement.files == null) { + return; + } + const file = inputElement.files[0]; if (file) { preview.src = URL.createObjectURL(file); handleUpload(wrapper, file); @@ -125,7 +140,7 @@ const init = (context : FormContext) => { openMediaManager.onclick = () => { openFileBrowser({ type: "assets", - filter : (file) => { + filter : (file : any) => { return file.media || file.directory; }, onSelect: (file : any) => { @@ -152,21 +167,21 @@ const init = (context : FormContext) => { }); }; -const handleUpload = (wrapper, file) => { +const handleUpload = (wrapper : any, file : any) => { const inputValue = wrapper.querySelector(".cms-media-input-value"); uploadFileWithProgress({ uploadEndpoint: "/manager/upload2", file: file, uri: "not relevant for media fields", - onProgress: (percent) => { + onProgress: (percent: number) => { console.log(`Upload progress: ${percent}%`); }, - onSuccess: (data) => { + onSuccess: (data: any) => { if (data.filename) { inputValue.value = data.filename; // Set the input value to the uploaded file's name } }, - onError: (error) => { + onError: (error: any) => { console.error("Upload failed:", error); } }); diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.number.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.number.ts index d9bd294ae..2560d80a9 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.number.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.number.ts @@ -50,8 +50,12 @@ const createNumberField = (options: NumberFieldOptions, value: string = '') => { }; const getData = (context : FormContext) => { - const data = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='number'] input").forEach((el : HTMLInputElement) => { + const data : any = {}; + const formElement = context.formElement; + if (!formElement) { + return data; + } + formElement.querySelectorAll("[data-cms-form-field-type='number'] input").forEach((el : any) => { const value = el.value; data[el.name] = { type: 'number', diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.radio.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.radio.ts index ae6205303..d4bfdaf9f 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.radio.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.radio.ts @@ -60,9 +60,13 @@ const createRadioField = (options: RadioFieldOptions, value: string = '') => { }; const getData = (context : FormContext) => { - const data = {}; - - context.formElement.querySelectorAll("[data-cms-form-field-type='radio']").forEach(container => { + const data : any = {}; + const formElement = context.formElement; + if (!formElement) { + return data; + } + + formElement.querySelectorAll("[data-cms-form-field-type='radio']").forEach(container => { const name = (container.querySelector("input[type='radio']") as HTMLInputElement).name; const checked = container.querySelector("input[type='radio']:checked") as HTMLInputElement; if (checked) { diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.range.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.range.ts index 4723f8153..d538058bc 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.range.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.range.ts @@ -49,9 +49,13 @@ const createRangeField = (options: RangeFieldOptions, value : string = '') => { }; const getData = (context : FormContext) => { - const data = {}; - - context.formElement.querySelectorAll("[data-cms-form-field-type='range'] input").forEach((el : HTMLInputElement) => { + const data : any = {}; + const formElement = context.formElement; + if (!formElement) { + return data; + } + + formElement.querySelectorAll("[data-cms-form-field-type='range'] input").forEach((el : any) => { data[el.name] = { type: 'range', value: parseFloat(el.value) diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.reference.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.reference.ts index bfbafd1b6..05dd25d02 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.reference.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.reference.ts @@ -50,9 +50,14 @@ const createReferenceField = (options: ReferenceFieldOptions, value: string = '' }; const getData = (context: FormContext) => { - const data = {}; + const data : any = {}; - context.formElement.querySelectorAll("[data-cms-form-field-type='reference'] input").forEach((el: HTMLInputElement) => { + const formElement = context.formElement; + if (!formElement) { + return data; + } + + formElement.querySelectorAll("[data-cms-form-field-type='reference'] input").forEach((el: any) => { let value = el.value data[el.name] = { type: 'reference', @@ -63,7 +68,12 @@ const getData = (context: FormContext) => { }; const init = (context: FormContext) => { - context.formElement.querySelectorAll("[data-cms-form-field-type='reference']").forEach(wrapper => { + const formElement = context.formElement; + if (!formElement) { + return; + } + + formElement.querySelectorAll("[data-cms-form-field-type='reference']").forEach(wrapper => { const fileManager = wrapper.querySelector(".cms-reference-button") as HTMLButtonElement; if (!fileManager) return; @@ -81,7 +91,7 @@ const init = (context: FormContext) => { openFileBrowser({ type: "content", siteid: siteid, - filter: (file) => { + filter: (file : any) => { return file.content || file.directory; }, onSelect: (file: any) => { diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.select.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.select.ts index 8c180a9e8..e77f2d2f6 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.select.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.select.ts @@ -52,9 +52,9 @@ const createSelectField = (options: SelectFieldOptions, value: string = '') => { const getData = (context : FormContext) => { const data: Record = {}; - context.formElement - .querySelectorAll("[data-cms-form-field-type='select'] select") - .forEach((el: HTMLSelectElement) => { + context.formElement?. + querySelectorAll("[data-cms-form-field-type='select'] select") + .forEach((el: any) => { let value: any = el.value; // optional: type-konvertierung, aber fallback ist immer der echte Wert diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.text.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.text.ts index 6b9a8f570..37a8fc83e 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.text.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.text.ts @@ -40,8 +40,11 @@ const createTextField = (options: TextFieldOptions, value : string = '') => { }; const getData = (context : FormContext) => { - var data = {} - context.formElement.querySelectorAll("[data-cms-form-field-type='text'] input").forEach((el : HTMLInputElement) => { + var data : any = {} + if (!context.formElement) { + return data + } + context.formElement.querySelectorAll("[data-cms-form-field-type='text'] input").forEach((el : any) => { let value = el.value data[el.name] = { type: 'text', diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/field.textarea.ts b/modules/ui-module/src/main/ts/src/js/modules/form/field.textarea.ts index d8ae48f79..7af74fd89 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/field.textarea.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/field.textarea.ts @@ -40,8 +40,12 @@ const createTextAreaField = (options: TextAreaFieldOptions, value : string = '') }; const getData = (context : FormContext) => { - var data = {} - context.formElement.querySelectorAll("[data-cms-form-field-type='text'] textarea").forEach((el : HTMLInputElement) => { + var data : any = {} + if (context.formElement === null) { + console.error('Form element not found.'); + return data; + } + context.formElement.querySelectorAll("[data-cms-form-field-type='text'] textarea").forEach((el : any) => { let value = el.value data[el.name] = { type: 'textarea', diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts b/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts index 3f60415bc..7f1bbe532 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/forms.ts @@ -39,7 +39,7 @@ import { TextAreaField } from "@cms/modules/form/field.textarea.js"; import { ReferenceField } from "@cms/modules/form/field.reference.js"; -const createForm = (options) : Form => { +const createForm = (options : any) : Form => { const fields = options.fields || []; const values = options.values || {}; const formId = createID(); @@ -49,7 +49,7 @@ const createForm = (options) : Form => { fields: fields } - const fieldHtml = fields.map(field => { + const fieldHtml = fields.map((field : any) => { const val = values[field.name] || ''; switch (field.type) { case 'email': @@ -99,7 +99,7 @@ const createForm = (options) : Form => { `; - const init = (container) => { + const init = (container : any) => { if (typeof container === 'string') { container = document.querySelector(container); } @@ -110,6 +110,11 @@ const createForm = (options) : Form => { container.innerHTML = html; context.formElement = container.querySelector('form'); + if (!context.formElement) { + console.error('Form element not found.'); + return; + } + context.formElement.addEventListener('keydown', (e : KeyboardEvent) => { if (e.key === 'Enter' && (e.target as HTMLElement).tagName.toLowerCase() !== 'textarea') { e.preventDefault(); @@ -119,7 +124,7 @@ const createForm = (options) : Form => { context.formElement.addEventListener('submit', (e) => { e.preventDefault(); e.stopPropagation(); - context.formElement.classList.add('was-validated'); + context.formElement?.classList.add('was-validated'); }); CodeField.init(context) MarkdownField.init(context) @@ -166,12 +171,12 @@ const createForm = (options) : Form => { }; }; -const flattenFormData = (input) => { +const flattenFormData = (input : any) => { const result = {}; for (const key in input) { const value = input[key].value; const parts = key.split("."); - let current = result; + let current : any = result; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (i === parts.length - 1) { diff --git a/modules/ui-module/src/main/ts/src/js/modules/form/utils.ts b/modules/ui-module/src/main/ts/src/js/modules/form/utils.ts index f4b333f41..4ad694bb1 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/form/utils.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/form/utils.ts @@ -24,7 +24,7 @@ const utcToLocalDateTimeInputValue = (utcString : string) => { const date = new Date(utcString); if (isNaN(date.getTime())) return ""; - const pad = (n) => String(n).padStart(2, '0'); + const pad = (n : any) => String(n).padStart(2, '0'); const yyyy = date.getFullYear(); const MM = pad(date.getMonth() + 1); diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts index 9744751e3..21428f095 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts @@ -117,7 +117,7 @@ const initMessageHandlers = () => { executeScriptAction(cmd) } }); - frameMessenger.on('edit-sections', (payload) => { + frameMessenger.on('edit-sections', (payload : any) => { var cmd : any= { "module": window.manager.baseUrl + "/actions/page/edit-sections", "function": "runAction", diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/media.inject.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/media.inject.ts index 4e8f2848b..9ee020842 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/media.inject.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/media.inject.ts @@ -21,7 +21,7 @@ import { EDIT_ATTRIBUTES_ICON, IMAGE_ICON, MEDIA_CROP_ICON } from "@cms/modules/manager/toolbar-icons"; import frameMessenger from '@cms/modules/frameMessenger.js'; -const isSameDomainImage = (imgElement) => { +const isSameDomainImage = (imgElement : HTMLImageElement) => { if (!(imgElement instanceof HTMLImageElement)) { return false; // ist kein } @@ -78,7 +78,7 @@ export const initMediaUploadOverlay = (img: HTMLImageElement) => { } }); - overlay.addEventListener('click', (e) => { + overlay.addEventListener('click', (e: any) => { selectMedia(img.dataset.cmsMetaElement, img.dataset.cmsNodeUri); }); @@ -98,7 +98,7 @@ export const initContentMediaToolbar = (img: HTMLImageElement) => { } var toolbar = img.closest('[data-cms-toolbar]') as HTMLElement; - var parentToolbarDef = JSON.parse(toolbar.dataset.cmsToolbar); + var parentToolbarDef = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); if (!parentToolbarDef) { return; @@ -123,7 +123,7 @@ export const initMediaToolbar = (img: HTMLImageElement) => { return; } - var toolbarDefinition = JSON.parse(img.dataset.cmsMediaToolbar); + var toolbarDefinition = JSON.parse(img.dataset.cmsMediaToolbar || '{}'); initToolbar(img, toolbarDefinition); }; @@ -203,7 +203,7 @@ export const initToolbar = (img: HTMLImageElement, toolbarDefinition: any) => { }); }; -const selectMedia = (metaElement: string, uri?: string) => { +const selectMedia = (metaElement?: string, uri?: string) => { var command = { type: 'edit', payload: { diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts index 12cfbf9b0..27385e335 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/toolbar.inject.ts @@ -23,7 +23,7 @@ import { EDIT_ATTRIBUTES_ICON, EDIT_PAGE_ICON, SECTION_ADD_ICON, SECTION_DELETE_ const addSection = (event : Event) => { var toolbar = (event.target as HTMLElement).closest('[data-cms-toolbar]') as HTMLElement; - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar) + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar ||'{}') var command : any = { type: 'add-sectionEntry', @@ -36,7 +36,7 @@ const addSection = (event : Event) => { const deleteSection = (event: Event) => { var toolbar = (event.target as HTMLElement).closest('[data-cms-toolbar]') as HTMLElement; - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar) + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar ||'{}') var command = { type: 'delete-sectionEntry', @@ -49,7 +49,7 @@ const deleteSection = (event: Event) => { const setPublishForSection = (event: Event) => { var toolbar = (event.target as HTMLElement).closest('[data-cms-toolbar]') as HTMLElement; - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar) + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}') var action = (event.currentTarget as HTMLElement).getAttribute('data-cms-action'); @@ -65,7 +65,7 @@ const setPublishForSection = (event: Event) => { const orderSections = (event : Event) => { var toolbar = (event.target as HTMLElement).closest('[data-cms-toolbar]') as HTMLElement; - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar) + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}') var command = { type: 'edit-sections', @@ -79,7 +79,7 @@ const orderSections = (event : Event) => { const editContent = (event: Event) => { var toolbar = (event.target as HTMLElement).closest('[data-cms-toolbar]') as HTMLElement; - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar) + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}') var command : any = { type: 'edit', @@ -97,7 +97,7 @@ const editContent = (event: Event) => { const editAttributes = (event: Event) => { var toolbar = (event.target as HTMLElement).closest('[data-cms-toolbar]') as HTMLElement; - var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar) + var toolbarDefinition = JSON.parse(toolbar.dataset.cmsToolbar || '{}'); var command : any = { type: 'edit', @@ -135,7 +135,7 @@ const editAttributes = (event: Event) => { export const initToolbar = (container: HTMLElement) => { - var toolbarDefinition = JSON.parse(container.dataset.cmsToolbar) + var toolbarDefinition = JSON.parse(container.dataset.cmsToolbar || '{}'); if (!toolbarDefinition.actions) { return } @@ -164,7 +164,7 @@ export const initToolbar = (container: HTMLElement) => { } }); - toolbarDefinition.actions.forEach(action => { + toolbarDefinition.actions.forEach((action : any) => { if (action === "editContent") { const button = document.createElement('button'); button.setAttribute('data-cms-action', 'edit'); diff --git a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts index 514fde6b9..f4db95b10 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts @@ -36,11 +36,12 @@ const executeRemoteMethodCall = async (method : string, parameters : any) => { method: method, parameters: parameters } + const csrfToken = getCSRFToken(); var response = await fetch(window.manager.baseUrl + "/rpc", { method: "POST", headers: { 'Content-Type': 'application/json', - 'X-CSRF-Token': getCSRFToken() + ...(csrfToken && { 'X-CSRF-Token': csrfToken }) }, body: JSON.stringify(data) }) From 9647e38dd206af91aff2ac8853dbf5bec4eed9e3 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sun, 31 May 2026 21:46:10 +0200 Subject: [PATCH 2/5] update heartbeat time add todo --- .../condation/cms/modules/ui/http/auth/RefreshTokenHandler.java | 1 + modules/ui-module/src/main/ts/src/js/manager.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java index 7f7e1b8f6..a1ca36ead 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java @@ -62,6 +62,7 @@ public boolean handle(Request request, Response response, Callback callback) thr response.setStatus(200); Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(Map.of( "status", "ok", + // TODO: we can not access the name in this way "previewToken", TokenUtils.createToken(getUsername(request, moduleContext, requestContext), secret, Duration.ofHours(1), Duration.ofDays(7)) )), callback); } else { diff --git a/modules/ui-module/src/main/ts/src/js/manager.js b/modules/ui-module/src/main/ts/src/js/manager.js index 1372de511..9aa41e0bb 100644 --- a/modules/ui-module/src/main/ts/src/js/manager.js +++ b/modules/ui-module/src/main/ts/src/js/manager.js @@ -49,7 +49,7 @@ document.addEventListener("DOMContentLoaded", function () { setInterval(() => { heartbeat(); - }, 10 * 1000); + }, 5 *60 * 1000); //PreviewHistory.init("/"); //updateStateButton(); From 2e587ad2eeb25d96e4a96a6b0e3cc4cd1a420413 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Sun, 31 May 2026 21:47:54 +0200 Subject: [PATCH 3/5] fix get username --- .../modules/ui/http/auth/RefreshTokenHandler.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java index a1ca36ead..c0f705d65 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java @@ -26,7 +26,9 @@ import com.condation.cms.api.request.RequestContext; import com.condation.cms.modules.ui.http.JettyHandler; import com.condation.cms.modules.ui.utils.AuthUtil; +import com.condation.cms.modules.ui.utils.CookieUtil; import com.condation.cms.modules.ui.utils.TokenUtils; +import com.condation.cms.modules.ui.utils.UIConstants; import com.condation.cms.modules.ui.utils.json.UIGsonProvider; import java.time.Duration; import java.util.Map; @@ -59,11 +61,17 @@ public boolean handle(Request request, Response response, Callback callback) thr boolean refreshed = AuthUtil.refreshTokens(request, response, moduleContext, requestContext); if (refreshed) { var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); + + var authCookie = CookieUtil.getCookie(request, UIConstants.COOKIE_CMS_TOKEN); + + var token = authCookie.get().getValue(); + + var payload = TokenUtils.getPayload(token, secret); + response.setStatus(200); Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(Map.of( "status", "ok", - // TODO: we can not access the name in this way - "previewToken", TokenUtils.createToken(getUsername(request, moduleContext, requestContext), secret, Duration.ofHours(1), Duration.ofDays(7)) + "previewToken", TokenUtils.createToken(payload.get().username(), secret, Duration.ofHours(1), Duration.ofDays(7)) )), callback); } else { response.setStatus(401); From 006815e9ebf0f604385d6d7bcb998a0c0facf03a Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Mon, 1 Jun 2026 09:57:24 +0200 Subject: [PATCH 4/5] rework markdown renderer --- .../cms/content/markdown/CMSMarkdown.java | 7 +- .../cms/content/markdown/InlineBlock.java | 11 + .../markdown/rules/block/CodeBlockRule.java | 5 +- .../rules/inline/ImageInlineRule.java | 9 +- .../markdown/rules/inline/TextInlineRule.java | 3 +- .../content/markdown/utils/StringUtils.java | 44 ++++ .../cms/modules/ui/utils/MarkdownHelper.java | 54 +++++ .../modules/ui/utils/MarkdownHelperTest.java | 211 ++++++++++++++++++ test-server/hosts/demo/content/index.md | 3 +- 9 files changed, 336 insertions(+), 11 deletions(-) create mode 100644 modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MarkdownHelper.java create mode 100644 modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/MarkdownHelperTest.java diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/CMSMarkdown.java b/cms-content/src/main/java/com/condation/cms/content/markdown/CMSMarkdown.java index 3ce15c62f..8b54e3dda 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/CMSMarkdown.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/CMSMarkdown.java @@ -23,7 +23,6 @@ import com.condation.cms.api.request.RequestContextScope; import com.condation.cms.content.markdown.rules.block.ParagraphBlockRule; import com.condation.cms.content.markdown.rules.inline.TextInlineRule; -import com.condation.cms.content.markdown.utils.StringUtils; import java.io.IOException; import java.util.List; import java.util.function.Function; @@ -89,9 +88,7 @@ private String renderInlineElements(final String inline_md) throws IOException { } public String render(final String md) throws IOException { - // Escape input markdown - String escapedMd = StringUtils.escape(md); - List blocks = blockTokenizer.tokenize(escapedMd); + List blocks = blockTokenizer.tokenize(md); // Pre-size StringBuilder based on input to reduce allocations final StringBuilder htmlBuilder = new StringBuilder(md.length() + 256); @@ -164,6 +161,6 @@ public String render(final String md) throws IOException { } } - return StringUtils.unescape(htmlBuilder.toString()); + return htmlBuilder.toString(); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java index 2468e7b6d..dec7b8196 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java @@ -45,6 +45,17 @@ default boolean isPreview() { return requestContext != null && requestContext.has(IsPreviewFeature.class); } + default boolean isManagerPreview() { + if (!RequestContextScope.REQUEST_CONTEXT.isBound()) { + return false; + } + var requestContext = RequestContextScope.REQUEST_CONTEXT.get(); + if (requestContext == null || !requestContext.has(IsPreviewFeature.class)) { + return false; + } + return IsPreviewFeature.Mode.MANAGER.equals(requestContext.get(IsPreviewFeature.class).mode()); + } + default Optional getRequestContext () { if (!RequestContextScope.REQUEST_CONTEXT.isBound()) { return Optional.empty(); diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/CodeBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/CodeBlockRule.java index 3725dbe99..b1b56d374 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/CodeBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/CodeBlockRule.java @@ -25,6 +25,7 @@ import com.condation.cms.content.markdown.Block; import com.condation.cms.content.markdown.BlockElementRule; import com.condation.cms.content.markdown.InlineRenderer; +import com.condation.cms.content.markdown.utils.StringUtils; import com.google.common.html.HtmlEscapers; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -59,8 +60,8 @@ public String render(InlineRenderer inlineRenderer) { return "
%s
".formatted(language, escape(content)); } - private String escape (String html) { - return HtmlEscapers.htmlEscaper().escape(html); + private String escape(String html) { + return StringUtils.escapeToEntities(HtmlEscapers.htmlEscaper().escape(html)); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java index 9bbf2f4d1..d55d9ca44 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java @@ -22,6 +22,7 @@ */ +import com.condation.cms.api.feature.features.IsPreviewFeature; import com.condation.cms.api.feature.features.SiteMediaServiceFeature; import com.condation.cms.api.utils.ImageUtil; import com.condation.cms.content.markdown.InlineBlock; @@ -65,8 +66,12 @@ public String render() { } var uiSelector = ""; - if (isPreview()) { - uiSelector = " data-cms-ui-selector=\"content-image\" "; + if (isManagerPreview()) { + uiSelector = new StringBuilder() + .append(" data-cms-ui-selector=\"content-image\" ") + .append(" data-cms-md-start=\"").append(start).append("\" ") + .append(" data-cms-md-end=\"").append(end).append("\" ") + .toString(); } if (title != null && !"".equals(title.trim())) { diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/TextInlineRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/TextInlineRule.java index 23cc7e26e..16b4e84ae 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/TextInlineRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/TextInlineRule.java @@ -25,6 +25,7 @@ import com.condation.cms.content.markdown.InlineBlock; import com.condation.cms.content.markdown.InlineElementRule; import com.condation.cms.content.markdown.InlineElementTokenizer; +import com.condation.cms.content.markdown.utils.StringUtils; import com.google.common.base.Strings; /** @@ -44,7 +45,7 @@ public InlineBlock next(InlineElementTokenizer tokenizer, String md) { public static record TextBlock(int start, int end, String content) implements InlineBlock { @Override public String render() { - return content; + return StringUtils.escapeToEntities(content); } } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/utils/StringUtils.java b/cms-content/src/main/java/com/condation/cms/content/markdown/utils/StringUtils.java index 9569b72e3..73d62d8c8 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/utils/StringUtils.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/utils/StringUtils.java @@ -36,9 +36,13 @@ public class StringUtils { private static final Map ESCAPE = new HashMap<>(); + // Direct escape-sequence → HTML entity mapping (no placeholder needed) + private static final Map ESCAPE_TO_ENTITY = new HashMap<>(); + private static final String AMP_PLACEHOLDER = "AMP#PLACE#HOLDER"; private static final Pattern ESCAPE_PATTERN; + private static final Pattern ESCAPE_TO_ENTITY_PATTERN; private static final Pattern UNESCAPE_PATTERN; static { @@ -60,12 +64,31 @@ public class StringUtils { ESCAPE.put("\\!", AMP_PLACEHOLDER + "#33;"); ESCAPE.put("\\|", AMP_PLACEHOLDER + "#124;"); + ESCAPE_TO_ENTITY.put("\\#", "#"); + ESCAPE_TO_ENTITY.put("\\*", "*"); + ESCAPE_TO_ENTITY.put("\\`", "`"); + ESCAPE_TO_ENTITY.put("\\_", "_"); + ESCAPE_TO_ENTITY.put("\\{", "{"); + ESCAPE_TO_ENTITY.put("\\}", "}"); + ESCAPE_TO_ENTITY.put("\\[", "["); + ESCAPE_TO_ENTITY.put("\\]", "]"); + ESCAPE_TO_ENTITY.put("\\<", "<"); + ESCAPE_TO_ENTITY.put("\\>", ">"); + ESCAPE_TO_ENTITY.put("\\(", "("); + ESCAPE_TO_ENTITY.put("\\)", ")"); + ESCAPE_TO_ENTITY.put("\\+", "+"); + ESCAPE_TO_ENTITY.put("\\-", "-"); + ESCAPE_TO_ENTITY.put("\\.", "."); + ESCAPE_TO_ENTITY.put("\\!", "!"); + ESCAPE_TO_ENTITY.put("\\|", "|"); + // Build regex pattern: (\#|\*|\`|\_|...) - captures all escape sequences String regexPattern = ESCAPE.keySet().stream() .map(Pattern::quote) .reduce((a, b) -> a + "|" + b) .orElse(""); ESCAPE_PATTERN = Pattern.compile(regexPattern); + ESCAPE_TO_ENTITY_PATTERN = ESCAPE_PATTERN; // same pattern, different replacement map // Pattern for unescaping UNESCAPE_PATTERN = Pattern.compile(Pattern.quote(AMP_PLACEHOLDER)); @@ -106,6 +129,27 @@ public static String escape(String md) { return result.toString(); } + /** + * Converts markdown escape sequences (e.g. {@code \*}) directly to HTML entities + * (e.g. {@code *}). Used by TextBlock at render time so that positions in the + * original markdown string are not shifted by pre-processing. + */ + public static String escapeToEntities(String text) { + if (Strings.isNullOrEmpty(text)) { + return text; + } + Matcher matcher = ESCAPE_TO_ENTITY_PATTERN.matcher(text); + StringBuffer result = new StringBuffer(text.length() + 32); + while (matcher.find()) { + String replacement = ESCAPE_TO_ENTITY.get(matcher.group()); + if (replacement != null) { + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + } + matcher.appendTail(result); + return result.toString(); + } + public static String removeLeadingPipe(String s) { return s.replaceAll("^\\|+", ""); } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MarkdownHelper.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MarkdownHelper.java new file mode 100644 index 000000000..5ae043e17 --- /dev/null +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MarkdownHelper.java @@ -0,0 +1,54 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import java.util.Objects; + +/** + * + * @author thorstenmarx + */ +public class MarkdownHelper { + + public static String replaceRange(String markdown, + int start, + int end, + String replacement) { + + Objects.requireNonNull(markdown); + Objects.requireNonNull(replacement); + + if (start < 0 || end < start || end > markdown.length()) { + throw new IllegalArgumentException( + "Invalid range: start=" + start + ", end=" + end); + } + + StringBuilder sb = new StringBuilder( + markdown.length() - (end - start) + replacement.length()); + + sb.append(markdown, 0, start); + sb.append(replacement); + sb.append(markdown, end, markdown.length()); + + return sb.toString(); + } +} diff --git a/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/MarkdownHelperTest.java b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/MarkdownHelperTest.java new file mode 100644 index 000000000..8f71b5acd --- /dev/null +++ b/modules/ui-module/src/test/java/com/condation/cms/modules/ui/utils/MarkdownHelperTest.java @@ -0,0 +1,211 @@ +package com.condation.cms.modules.ui.utils; + +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class MarkdownHelperTest { + + @Test + void shouldReplaceMiddleSection() { + String markdown = "Hello World"; + + String result = MarkdownHelper.replaceRange( + markdown, + 6, + 11, + "CMS"); + + assertEquals("Hello CMS", result); + } + + @Test + void shouldReplaceAtBeginning() { + String markdown = "Hello World"; + + String result = MarkdownHelper.replaceRange( + markdown, + 0, + 5, + "Hi"); + + assertEquals("Hi World", result); + } + + @Test + void shouldReplaceAtEnd() { + String markdown = "Hello World"; + + String result = MarkdownHelper.replaceRange( + markdown, + 6, + 11, + "Universe"); + + assertEquals("Hello Universe", result); + } + + @Test + void shouldReplaceWholeString() { + String markdown = "Hello World"; + + String result = MarkdownHelper.replaceRange( + markdown, + 0, + markdown.length(), + "New Content"); + + assertEquals("New Content", result); + } + + @Test + void shouldInsertAtBeginning() { + String markdown = "World"; + + String result = MarkdownHelper.replaceRange( + markdown, + 0, + 0, + "Hello "); + + assertEquals("Hello World", result); + } + + @Test + void shouldInsertAtEnd() { + String markdown = "Hello"; + + String result = MarkdownHelper.replaceRange( + markdown, + markdown.length(), + markdown.length(), + " World"); + + assertEquals("Hello World", result); + } + + @Test + void shouldInsertInMiddle() { + String markdown = "HelloWorld"; + + String result = MarkdownHelper.replaceRange( + markdown, + 5, + 5, + " "); + + assertEquals("Hello World", result); + } + + @Test + void shouldRemoveSection() { + String markdown = "Hello World"; + + String result = MarkdownHelper.replaceRange( + markdown, + 5, + 11, + ""); + + assertEquals("Hello", result); + } + + @Test + void shouldReplaceMarkdownImage() { + String markdown = """ + # Article + + ![Team](/images/team.jpg) + + Some text. + """; + + String oldImage = "![Team](/images/team.jpg)"; + int start = markdown.indexOf(oldImage); + int end = start + oldImage.length(); + + String result = MarkdownHelper.replaceRange( + markdown, + start, + end, + "![Team](/images/new-team.jpg)"); + + assertTrue(result.contains("![Team](/images/new-team.jpg)")); + assertFalse(result.contains("![Team](/images/team.jpg)")); + } + + @Test + void shouldThrowForNegativeStart() { + assertThrows( + IllegalArgumentException.class, + () -> MarkdownHelper.replaceRange( + "test", + -1, + 2, + "x")); + } + + @Test + void shouldThrowWhenEndBeforeStart() { + assertThrows( + IllegalArgumentException.class, + () -> MarkdownHelper.replaceRange( + "test", + 3, + 2, + "x")); + } + + @Test + void shouldThrowWhenEndExceedsLength() { + assertThrows( + IllegalArgumentException.class, + () -> MarkdownHelper.replaceRange( + "test", + 0, + 10, + "x")); + } + + @Test + void shouldThrowForNullMarkdown() { + assertThrows( + NullPointerException.class, + () -> MarkdownHelper.replaceRange( + null, + 0, + 0, + "x")); + } + + @Test + void shouldThrowForNullReplacement() { + assertThrows( + NullPointerException.class, + () -> MarkdownHelper.replaceRange( + "test", + 0, + 0, + null)); + } +} diff --git a/test-server/hosts/demo/content/index.md b/test-server/hosts/demo/content/index.md index de12f0cc7..b7ed9138b 100644 --- a/test-server/hosts/demo/content/index.md +++ b/test-server/hosts/demo/content/index.md @@ -51,8 +51,9 @@ Theme: [[ext:theme_name]][[/ext:theme_name]] ```java +// its a comment System.out.println("Hello world!"); ``` ### say hello -[[ext:say_hello name="CondationCMS" /]] \ No newline at end of file +[[ext:say_hello name="CondationCMS" /]] From d4fc2ceee4c50fc99780b9f130bf5298d0189d98 Mon Sep 17 00:00:00 2001 From: Thorsten Marx Date: Mon, 1 Jun 2026 15:18:01 +0200 Subject: [PATCH 5/5] some reworks to edit content images outside of markdown editor --- .../condation/cms/content/markdown/Block.java | 19 ++-- .../cms/content/markdown/BlockTokenizer.java | 27 +++--- .../cms/content/markdown/CMSMarkdown.java | 77 +++++----------- .../cms/content/markdown/InlineBlock.java | 8 ++ .../markdown/InlineElementTokenizer.java | 46 +++++++--- .../cms/content/markdown/InlineRenderer.java | 17 ++-- .../cms/content/markdown/LocatedBlock.java | 32 +++++++ .../content/markdown/LocatedInlineBlock.java | 31 +++++++ .../rules/block/BlockquoteBlockRule.java | 4 +- .../markdown/rules/block/CodeBlockRule.java | 2 +- .../rules/block/DefinitionListBlockRule.java | 8 +- .../rules/block/HeadingBlockRule.java | 4 +- .../rules/block/HorizontalRuleBlockRule.java | 2 +- .../markdown/rules/block/ListBlockRule.java | 6 +- .../rules/block/ParagraphBlockRule.java | 4 +- .../markdown/rules/block/TableBlockRule.java | 6 +- .../markdown/rules/block/TagBlockRule.java | 4 +- .../rules/block/TaskListBlockRule.java | 2 +- .../rules/inline/ImageInlineRule.java | 17 ++-- .../rules/inline/ItalicInlineRule.java | 2 +- .../rules/inline/StrikethroughInlineRule.java | 2 +- .../rules/inline/StrongInlineRule.java | 2 +- .../content/markdown/BlockTokenizerTest.java | 48 +++++----- .../markdown/LargeBlockTokenizerTest.java | 2 +- .../block/DefinitionListBlockRuleTest.java | 4 +- .../block/HorizontalRuleBlockRuleTest.java | 8 +- .../rules/block/ListBlockRuleTest.java | 16 ++-- .../rules/block/TableBlockRuleTest.java | 4 +- .../rules/block/TagBlockRuleTest.java | 6 +- .../rules/block/TaskListBlockRuleTest.java | 4 +- .../RemoteContentEndpointsExtension.java | 42 +++++++++ .../ui/http/auth/RefreshTokenHandler.java | 18 ++-- .../cms/modules/ui/utils/AuthUtil.java | 29 +++--- .../cms/modules/ui/utils/MarkdownHelper.java | 3 + .../cms/modules/ui/utils/NumberUtils.java | 13 +++ .../actions/media/select-content-media.d.ts | 21 +++++ .../actions/media/select-content-media.js | 81 +++++++++++++++++ .../resources/manager/js/manager-inject.js | 1 - .../src/main/resources/manager/js/manager.js | 2 +- .../manager/js/modules/filebrowser.d.ts | 2 +- .../manager/js/modules/form/utils.d.ts | 4 +- .../manager/js/modules/localization.d.ts | 2 +- .../manager/manager.message.handlers.js | 13 +++ .../js/modules/manager/media.inject.js | 30 ++++++- .../manager/js/modules/preview.history.d.ts | 2 +- .../manager/js/modules/preview.utils.d.ts | 2 +- .../manager/js/modules/rpc/rpc-content.d.ts | 14 ++- .../manager/js/modules/rpc/rpc-content.js | 9 +- .../resources/manager/js/modules/rpc/rpc.d.ts | 3 + .../resources/manager/js/modules/rpc/rpc.js | 2 +- .../resources/manager/js/modules/state.js | 1 - .../manager/js/modules/ui-state.d.ts | 4 +- .../resources/manager/public/manager-login.js | 1 - .../actions/media/select-content-media.d.ts | 1 + .../actions/media/select-content-media.js | 81 +++++++++++++++++ .../src/main/ts/dist/js/manager-inject.js | 1 - .../ui-module/src/main/ts/dist/js/manager.js | 2 +- .../main/ts/dist/js/modules/filebrowser.d.ts | 2 +- .../main/ts/dist/js/modules/form/utils.d.ts | 4 +- .../main/ts/dist/js/modules/localization.d.ts | 2 +- .../manager/manager.message.handlers.js | 13 +++ .../dist/js/modules/manager/media.inject.js | 30 ++++++- .../ts/dist/js/modules/preview.history.d.ts | 2 +- .../ts/dist/js/modules/preview.utils.d.ts | 2 +- .../ts/dist/js/modules/rpc/rpc-content.d.ts | 14 ++- .../ts/dist/js/modules/rpc/rpc-content.js | 9 +- .../src/main/ts/dist/js/modules/rpc/rpc.d.ts | 3 + .../src/main/ts/dist/js/modules/rpc/rpc.js | 2 +- .../src/main/ts/dist/js/modules/state.js | 1 - .../src/main/ts/dist/js/modules/ui-state.d.ts | 4 +- .../src/main/ts/dist/public/manager-login.js | 1 - .../src/actions/media/select-content-media.ts | 88 +++++++++++++++++++ .../ui-module/src/main/ts/src/js/manager.js | 2 +- .../manager/manager.message.handlers.ts | 13 +++ .../ts/src/js/modules/manager/media.inject.ts | 31 ++++++- .../main/ts/src/js/modules/rpc/rpc-content.ts | 24 ++++- .../src/main/ts/src/js/modules/rpc/rpc.ts | 8 +- 77 files changed, 826 insertions(+), 227 deletions(-) create mode 100644 cms-content/src/main/java/com/condation/cms/content/markdown/LocatedBlock.java create mode 100644 cms-content/src/main/java/com/condation/cms/content/markdown/LocatedInlineBlock.java create mode 100644 modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts create mode 100644 modules/ui-module/src/main/resources/manager/actions/media/select-content-media.js create mode 100644 modules/ui-module/src/main/ts/dist/actions/media/select-content-media.d.ts create mode 100644 modules/ui-module/src/main/ts/dist/actions/media/select-content-media.js create mode 100644 modules/ui-module/src/main/ts/src/actions/media/select-content-media.ts diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/Block.java b/cms-content/src/main/java/com/condation/cms/content/markdown/Block.java index c961f289d..436b6cfc2 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/Block.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/Block.java @@ -10,25 +10,32 @@ * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * #L% */ - /** - * * @author t.marx */ public interface Block { int start(); int end(); - - String render (InlineRenderer inlineRenderer); + + /** + * Renders this block. {@code documentOffset} is the absolute position of + * this block's start in the original document — passed through to the + * {@link InlineRenderer} so inline elements can compute absolute positions. + */ + String render(InlineRenderer inlineRenderer, int documentOffset); + + default String render(InlineRenderer inlineRenderer) { + return render(inlineRenderer, 0); + } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/BlockTokenizer.java b/cms-content/src/main/java/com/condation/cms/content/markdown/BlockTokenizer.java index 798c90819..d8e25f46e 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/BlockTokenizer.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/BlockTokenizer.java @@ -10,12 +10,12 @@ * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * #L% @@ -28,7 +28,8 @@ /** * Block-level markdown tokenizer with recursion depth limit. - * Optimized to prevent stack overflow on pathological inputs. + * Returns {@link LocatedBlock} instances whose {@code absoluteStart}/ + * {@code absoluteEnd} are correct offsets into the original document string. * * @author t.marx */ @@ -38,23 +39,25 @@ public class BlockTokenizer { private final Options options; private static final int MAX_RECURSION_DEPTH = 100; - protected List tokenize(final String original_md) throws IOException { - return tokenizeWithDepth(original_md, 0); + protected List tokenize(final String original_md) throws IOException { + return tokenizeWithDepth(original_md, 0, 0); } /** - * Tokenizes markdown with recursion depth tracking. - * Throws exception if depth exceeds limit to prevent stack overflow. + * @param original_md the markdown substring to tokenize + * @param documentOffset cumulative character offset of this substring in the full document + * @param depth current recursion depth */ - private List tokenizeWithDepth(final String original_md, int depth) throws IOException { + private List tokenizeWithDepth(final String original_md, int documentOffset, int depth) throws IOException { if (depth > MAX_RECURSION_DEPTH) { throw new IOException("Maximum recursion depth exceeded in markdown parsing"); } var md = original_md.replaceAll("\r\n", "\n"); StringBuilder mdBuilder = new StringBuilder(md); + int offset = documentOffset; - final List blocks = new ArrayList<>(); + final List blocks = new ArrayList<>(); for (var blockRule : options.blockElementRules) { Block block = null; @@ -62,10 +65,12 @@ private List tokenizeWithDepth(final String original_md, int depth) throw if (block.start() != 0) { var before = mdBuilder.substring(0, block.start()); - blocks.addAll(tokenizeWithDepth(before, depth + 1)); + blocks.addAll(tokenizeWithDepth(before, offset, depth + 1)); + offset += block.start(); } - blocks.add(block); + blocks.add(new LocatedBlock(block, offset, offset + (block.end() - block.start()))); + offset += block.end() - block.start(); mdBuilder.delete(0, block.end()); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/CMSMarkdown.java b/cms-content/src/main/java/com/condation/cms/content/markdown/CMSMarkdown.java index 8b54e3dda..6e94734d9 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/CMSMarkdown.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/CMSMarkdown.java @@ -10,12 +10,12 @@ * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * #L% @@ -25,7 +25,6 @@ import com.condation.cms.content.markdown.rules.inline.TextInlineRule; import java.io.IOException; import java.util.List; -import java.util.function.Function; import java.util.function.Supplier; /** @@ -37,7 +36,6 @@ public class CMSMarkdown { private final BlockTokenizer blockTokenizer; - private final InlineElementTokenizer inlineTokenizer; private final List blockRules; @@ -46,22 +44,10 @@ public class CMSMarkdown { private final boolean parallelRendering; private final int parallelThreshold; - /** - * Creates a markdown renderer with default settings (parallel rendering - * enabled for 10+ blocks). - */ public CMSMarkdown(Options options) { this(options, true, 10); } - /** - * Creates a markdown renderer with custom parallel rendering configuration. - * - * @param options markdown rendering options - * @param parallelRendering enable parallel block rendering - * @param parallelThreshold minimum number of blocks to trigger parallel - * rendering - */ public CMSMarkdown(Options options, boolean parallelRendering, int parallelThreshold) { this.blockTokenizer = new BlockTokenizer(options); this.inlineTokenizer = new InlineElementTokenizer(options); @@ -73,32 +59,33 @@ public CMSMarkdown(Options options, boolean parallelRendering, int parallelThres inlineRules.addLast(new TextInlineRule()); } - private String renderInlineElements(final String inline_md) throws IOException { - List blocks = inlineTokenizer.tokenize(inline_md); + private String renderInlineElements(final String inline_md, int documentOffset) throws IOException { + List blocks = inlineTokenizer.tokenize(inline_md, documentOffset); - // Pre-size StringBuilder based on input length to reduce allocations final StringBuilder htmlBuilder = new StringBuilder(inline_md.length() + 128); - - // Use simple loop instead of streams for better performance - for (InlineBlock block : blocks) { - htmlBuilder.append(block.render()); + for (LocatedInlineBlock located : blocks) { + htmlBuilder.append(located.block().render(located.absoluteStart(), located.absoluteEnd())); } - return htmlBuilder.toString(); } + private String renderBlock(LocatedBlock located, InlineRenderer inlineRenderer, BlockRenderer blockRenderer) { + Block block = located.block(); + if (block instanceof BlockContainer blockContainer) { + return blockContainer.render(blockRenderer); + } + return block.render(inlineRenderer, located.absoluteStart()); + } + public String render(final String md) throws IOException { - List blocks = blockTokenizer.tokenize(md); + List blocks = blockTokenizer.tokenize(md); - // Pre-size StringBuilder based on input to reduce allocations final StringBuilder htmlBuilder = new StringBuilder(md.length() + 256); - // Create renderers once instead of as lambdas - InlineRenderer inlineRenderer = (content) -> { + InlineRenderer inlineRenderer = (content, documentOffset) -> { try { - return renderInlineElements(content); + return renderInlineElements(content, documentOffset); } catch (IOException ioe) { - // Log error but don't break rendering return ""; } }; @@ -106,31 +93,19 @@ public String render(final String md) throws IOException { try { return this.render(content); } catch (IOException e) { - // Log error but don't break rendering return ""; } }; - // Use parallel rendering for large documents (10+ blocks) - // For small documents, sequential is faster due to parallel overhead if (parallelRendering && blocks.size() >= parallelThreshold) { - // Capture ScopedValue on the calling thread BEFORE entering the parallel stream. - // ForkJoinPool worker threads do not inherit ScopedValue bindings, so we must - // capture the context here and explicitly re-bind it inside each worker lambda. final var capturedContext = RequestContextScope.REQUEST_CONTEXT.isBound() ? RequestContextScope.REQUEST_CONTEXT.get() : null; - // Parallel rendering: 2-4x faster on multi-core CPUs List renderedBlocks = blocks.parallelStream() - .map(block -> { - final Supplier renderBlockSupplier = () -> { - if (block instanceof BlockContainer blockContainer) { - return blockContainer.render(blockRenderer); - } else { - return block.render(inlineRenderer); - } - }; + .map(located -> { + final Supplier renderBlockSupplier = () -> + renderBlock(located, inlineRenderer, blockRenderer); try { if (capturedContext != null) { return ScopedValue.where(RequestContextScope.REQUEST_CONTEXT, capturedContext) @@ -144,20 +119,12 @@ public String render(final String md) throws IOException { }) .toList(); - // Append in order (toList() preserves order) for (String rendered : renderedBlocks) { htmlBuilder.append(rendered); } } else { - // Sequential rendering for small documents - for (Block block : blocks) { - String rendered; - if (block instanceof BlockContainer blockContainer) { - rendered = blockContainer.render(blockRenderer); - } else { - rendered = block.render(inlineRenderer); - } - htmlBuilder.append(rendered); + for (LocatedBlock located : blocks) { + htmlBuilder.append(renderBlock(located, inlineRenderer, blockRenderer)); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java index dec7b8196..b93b9a472 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineBlock.java @@ -37,6 +37,14 @@ public interface InlineBlock { String render(); + /** + * Renders with absolute document positions. Override for elements that need + * to embed position metadata (e.g. images). Defaults to {@link #render()}. + */ + default String render(int absoluteStart, int absoluteEnd) { + return render(); + } + default boolean isPreview() { if (!RequestContextScope.REQUEST_CONTEXT.isBound()) { return false; diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineElementTokenizer.java b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineElementTokenizer.java index bfe40b1fe..1c1fa5caf 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineElementTokenizer.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineElementTokenizer.java @@ -10,12 +10,12 @@ * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * #L% @@ -29,7 +29,8 @@ /** * Inline-level markdown tokenizer with recursion depth limit. - * Optimized to prevent stack overflow on pathological inputs. + * Returns {@link LocatedInlineBlock} instances whose absolute positions are + * correct offsets into the original document string. * * @author t.marx */ @@ -39,23 +40,39 @@ public class InlineElementTokenizer { private final Options options; private static final int MAX_RECURSION_DEPTH = 100; - public List tokenize(final String original_md) throws IOException { - return doTokenize(this, original_md, 0); + /** + * Tokenizes inline markdown without document-offset tracking (legacy entry point). + * Absolute positions will equal relative positions (documentOffset = 0). + */ + public List tokenize(final String original_md) throws IOException { + return tokenize(original_md, 0); } /** - * Tokenizes inline elements with recursion depth tracking. - * Throws exception if depth exceeds limit to prevent stack overflow. + * Tokenizes inline markdown with a known document offset. + * + * @param original_md inline markdown content (block body) + * @param documentOffset absolute start of this content in the full document */ - protected List doTokenize(final InlineElementTokenizer tokenizer, final String original_md, int depth) throws IOException { + public List tokenize(final String original_md, int documentOffset) throws IOException { + return doTokenize(this, original_md, documentOffset, 0); + } + + protected List doTokenize( + final InlineElementTokenizer tokenizer, + final String original_md, + int documentOffset, + int depth) throws IOException { + if (depth > MAX_RECURSION_DEPTH) { throw new IOException("Maximum recursion depth exceeded in inline parsing"); } var md = original_md.replaceAll("\r\n", "\n"); StringBuilder mdBuilder = new StringBuilder(md); + int offset = documentOffset; - final List blocks = new ArrayList<>(); + final List blocks = new ArrayList<>(); for (var blockRule : options.inlineElementRules) { InlineBlock block = null; @@ -63,16 +80,21 @@ protected List doTokenize(final InlineElementTokenizer tokenizer, f if (block.start() != 0) { var before = mdBuilder.substring(0, block.start()); - blocks.addAll(doTokenize(tokenizer, before, depth + 1)); + blocks.addAll(doTokenize(tokenizer, before, offset, depth + 1)); + offset += block.start(); } - blocks.add(block); + blocks.add(new LocatedInlineBlock(block, offset, offset + (block.end() - block.start()))); + offset += block.end() - block.start(); mdBuilder.delete(0, block.end()); } } if (mdBuilder.length() > 0) { - blocks.add(new TextInlineRule.TextBlock(0, mdBuilder.length(), mdBuilder.toString())); + blocks.add(new LocatedInlineBlock( + new TextInlineRule.TextBlock(0, mdBuilder.length(), mdBuilder.toString()), + offset, + offset + mdBuilder.length())); } return blocks; diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineRenderer.java b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineRenderer.java index 7ea39ef93..fc8b0cd73 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/InlineRenderer.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/InlineRenderer.java @@ -10,24 +10,29 @@ * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * #L% */ - /** + * Renders inline markdown content. The {@code documentOffset} is the absolute + * character position of {@code inline_md} in the full document, used to + * compute correct absolute positions for inline elements like images. * * @author t.marx */ public interface InlineRenderer { - - String render (String inline_md); - + + String render(String inline_md, int documentOffset); + + default String render(String inline_md) { + return render(inline_md, 0); + } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/LocatedBlock.java b/cms-content/src/main/java/com/condation/cms/content/markdown/LocatedBlock.java new file mode 100644 index 000000000..22485c9b2 --- /dev/null +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/LocatedBlock.java @@ -0,0 +1,32 @@ +package com.condation.cms.content.markdown; + +/*- + * #%L + * CMS Content + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +/** + * Wraps a {@link Block} with its absolute start/end positions in the original + * markdown document. The block's own {@code start()}/{@code end()} are relative + * to the substring the rule received; {@code absoluteStart}/{@code absoluteEnd} + * are correct offsets into the full document string. + * + * @author t.marx + */ +public record LocatedBlock(Block block, int absoluteStart, int absoluteEnd) {} diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/LocatedInlineBlock.java b/cms-content/src/main/java/com/condation/cms/content/markdown/LocatedInlineBlock.java new file mode 100644 index 000000000..0c88061de --- /dev/null +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/LocatedInlineBlock.java @@ -0,0 +1,31 @@ +package com.condation.cms.content.markdown; + +/*- + * #%L + * CMS Content + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +/** + * Wraps an {@link InlineBlock} with its absolute start/end positions in the + * original markdown document, combining the enclosing block's document offset + * with the inline element's position within the block content. + * + * @author t.marx + */ +public record LocatedInlineBlock(InlineBlock block, int absoluteStart, int absoluteEnd) {} diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/BlockquoteBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/BlockquoteBlockRule.java index 9bc0f623c..9ec52f09d 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/BlockquoteBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/BlockquoteBlockRule.java @@ -56,8 +56,8 @@ public static record BlockquoteBlock(int start, int end, String content) impleme @Override - public String render(InlineRenderer inlineRenderer) { - return "
%s
".formatted(inlineRenderer.render(content)); + public String render(InlineRenderer inlineRenderer, int documentOffset) { + return "
%s
".formatted(inlineRenderer.render(content, documentOffset)); } @Override diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/CodeBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/CodeBlockRule.java index b1b56d374..d09cf24ae 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/CodeBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/CodeBlockRule.java @@ -53,7 +53,7 @@ public Block next(final String md) { public static record CodeBlock (int start, int end, String content, String language) implements Block { @Override - public String render(InlineRenderer inlineRenderer) { + public String render(InlineRenderer inlineRenderer, int documentOffset) { if (language == null || "".equals(language)) { return "
%s
".formatted(escape(content)); } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/DefinitionListBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/DefinitionListBlockRule.java index 7f5c414f5..3872e8473 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/DefinitionListBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/DefinitionListBlockRule.java @@ -92,15 +92,15 @@ static record DefinitionList(String title, List values) { public static record DefinitionListBlock(int start, int end, DefinitionListContainer listContainer) implements Block { @Override - public String render(InlineRenderer inlineRenderer) { + public String render(InlineRenderer inlineRenderer, int documentOffset) { StringBuilder sb = new StringBuilder(); sb.append("
"); listContainer.lists().forEach(list -> { - sb.append("
").append(inlineRenderer.render(list.title())).append("
"); - + sb.append("
").append(inlineRenderer.render(list.title(), documentOffset)).append("
"); + list.values.forEach(item -> { - sb.append("
").append(inlineRenderer.render(item)).append("
"); + sb.append("
").append(inlineRenderer.render(item, documentOffset)).append("
"); }); }); diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/HeadingBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/HeadingBlockRule.java index 05e241868..efaf79128 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/HeadingBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/HeadingBlockRule.java @@ -54,11 +54,11 @@ public Block next(String md) { public static record HeadingBlock(int start, int end, String heading, int level, String id) implements Block { @Override - public String render(InlineRenderer inlineRenderer) { + public String render(InlineRenderer inlineRenderer, int documentOffset) { return "%s".formatted( level, id, - inlineRenderer.render(heading), + inlineRenderer.render(heading, documentOffset), level ); } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/HorizontalRuleBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/HorizontalRuleBlockRule.java index 867f81b0f..e47a901b2 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/HorizontalRuleBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/HorizontalRuleBlockRule.java @@ -49,7 +49,7 @@ public Block next(String md) { public static record HRBlock(int start, int end) implements Block { @Override - public String render(InlineRenderer inlineRenderer) { + public String render(InlineRenderer inlineRenderer, int documentOffset) { return "
"; } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ListBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ListBlockRule.java index ee691306d..ee042e6e3 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ListBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ListBlockRule.java @@ -74,13 +74,13 @@ public Block next(String md) { public static record ListBlock(int start, int end, List items, boolean ordered) implements Block { @Override - public String render(InlineRenderer inlineRenderer) { + public String render(InlineRenderer inlineRenderer, int documentOffset) { if (ordered) { return "
    %s
".formatted( - items.stream().map(item -> "
  • " + inlineRenderer.render(item) + "
  • ").collect(Collectors.joining())); + items.stream().map(item -> "
  • " + inlineRenderer.render(item, documentOffset) + "
  • ").collect(Collectors.joining())); } else { return "
      %s
    ".formatted( - items.stream().map(item -> "
  • " + inlineRenderer.render(item) + "
  • ").collect(Collectors.joining())); + items.stream().map(item -> "
  • " + inlineRenderer.render(item, documentOffset) + "
  • ").collect(Collectors.joining())); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ParagraphBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ParagraphBlockRule.java index 5b09e4d11..28ea2af98 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ParagraphBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/ParagraphBlockRule.java @@ -51,8 +51,8 @@ public Block next(String md) { public static record ParagraphBlock(int start, int end, String content) implements Block { @Override - public String render(InlineRenderer inlineRenderer) { - return "

    %s

    ".formatted(inlineRenderer.render(content)); + public String render(InlineRenderer inlineRenderer, int documentOffset) { + return "

    %s

    ".formatted(inlineRenderer.render(content, documentOffset)); } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TableBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TableBlockRule.java index eada92013..142036ea9 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TableBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TableBlockRule.java @@ -182,7 +182,7 @@ private String renderStyle (int index) { } @Override - public String render(InlineRenderer inlineRenderer) { + public String render(InlineRenderer inlineRenderer, int documentOffset) { StringBuilder sb = new StringBuilder(); sb.append(""); @@ -192,7 +192,7 @@ public String render(InlineRenderer inlineRenderer) { AtomicInteger index = new AtomicInteger(0); table.header.values.forEach((header) -> { sb.append(""); }); @@ -206,7 +206,7 @@ public String render(InlineRenderer inlineRenderer) { sb.append(""); row.values.forEach(items -> { sb.append(""); }); sb.append(""); diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TagBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TagBlockRule.java index e2a9805ca..1822efce2 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TagBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TagBlockRule.java @@ -60,7 +60,7 @@ public boolean has(String codeName) { public static record TagBlock(int start, int end, TagParser.TagInfo tagInfo) implements Block { @Override - public String render(InlineRenderer inlineRenderer) { + public String render(InlineRenderer inlineRenderer, int documentOffset) { List params = tagInfo.rawAttributes() .entrySet().stream() .filter(entry -> !entry.getKey().equals("_content")) @@ -72,7 +72,7 @@ public String render(InlineRenderer inlineRenderer) { .formatted( tagInfo.name(), String.join(" ", params), - inlineRenderer.render((String)tagInfo.rawAttributes().getOrDefault("_content", "")), + inlineRenderer.render((String)tagInfo.rawAttributes().getOrDefault("_content", ""), documentOffset), tagInfo.name() ); } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TaskListBlockRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TaskListBlockRule.java index 59e685f8e..cafa2873b 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TaskListBlockRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/block/TaskListBlockRule.java @@ -88,7 +88,7 @@ static record Item(String title, boolean checked) { public static record TaskListBlock(int start, int end, TaskList taskList) implements Block { @Override - public String render(InlineRenderer inlineRenderer) { + public String render(InlineRenderer inlineRenderer, int documentOffset) { StringBuilder sb = new StringBuilder(); sb.append("
      "); diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java index d55d9ca44..1274dde5d 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ImageInlineRule.java @@ -54,30 +54,35 @@ public static record ImageInlineBlock(int start, int end, String src, String alt @Override public String render() { + return render(start, end); + } + + @Override + public String render(int absoluteStart, int absoluteEnd) { var altText = alt; var requestContext = getRequestContext(); if (Strings.isNullOrEmpty(altText) && requestContext.isPresent()) { var imageUrl = ImageUtil.getRawPath(src, requestContext.get()); var media = requestContext.get().get(SiteMediaServiceFeature.class).mediaService().get(imageUrl); - + if (media != null && media.meta().containsKey("alt")) { altText = (String) media.meta().get("alt"); } } - + var uiSelector = ""; if (isManagerPreview()) { uiSelector = new StringBuilder() .append(" data-cms-ui-selector=\"content-image\" ") - .append(" data-cms-md-start=\"").append(start).append("\" ") - .append(" data-cms-md-end=\"").append(end).append("\" ") + .append(" data-cms-md-start=\"").append(absoluteStart).append("\" ") + .append(" data-cms-md-end=\"").append(absoluteEnd).append("\" ") .toString(); } - + if (title != null && !"".equals(title.trim())) { return "\"%s\"".formatted(src, altText, title, uiSelector); } - return "\"%s\"".formatted(src, altText, uiSelector); + return "\"%s\"".formatted(src, altText, uiSelector); } } } diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ItalicInlineRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ItalicInlineRule.java index 50aa9f8f9..e366f4958 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ItalicInlineRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/ItalicInlineRule.java @@ -51,7 +51,7 @@ public static record ItalicInlineBlock(InlineElementTokenizer tokenizer, int sta @Override public String render() { try { - var renderedContent = tokenizer.tokenize(content).stream().map(b -> b.render()).collect(Collectors.joining()); + var renderedContent = tokenizer.tokenize(content).stream().map(b -> b.block().render()).collect(Collectors.joining()); return "%s".formatted(renderedContent); } catch (IOException ex) { return "%s".formatted(content); diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/StrikethroughInlineRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/StrikethroughInlineRule.java index fef9a8925..d1d20b66a 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/StrikethroughInlineRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/StrikethroughInlineRule.java @@ -50,7 +50,7 @@ public static record StrikethroughInlineBlock(InlineElementTokenizer tokenizer, @Override public String render() { try { - var renderedContent = tokenizer.tokenize(content).stream().map(b -> b.render()).collect(Collectors.joining()); + var renderedContent = tokenizer.tokenize(content).stream().map(b -> b.block().render()).collect(Collectors.joining()); return "%s".formatted(renderedContent); } catch (IOException ex) { return "%s".formatted(content); diff --git a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/StrongInlineRule.java b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/StrongInlineRule.java index c1407a00c..1fc630831 100644 --- a/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/StrongInlineRule.java +++ b/cms-content/src/main/java/com/condation/cms/content/markdown/rules/inline/StrongInlineRule.java @@ -51,7 +51,7 @@ public static record StrongInlineBlock(InlineElementTokenizer tokenizer, int sta @Override public String render() { try { - var renderedContent = tokenizer.tokenize(content).stream().map(b -> b.render()).collect(Collectors.joining()); + var renderedContent = tokenizer.tokenize(content).stream().map(b -> b.block().render()).collect(Collectors.joining()); return "%s".formatted(renderedContent); } catch (IOException ex) { return "%s".formatted(content); diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/BlockTokenizerTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/BlockTokenizerTest.java index a97434ef7..228d4309e 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/BlockTokenizerTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/BlockTokenizerTest.java @@ -10,30 +10,26 @@ * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * #L% */ -import com.condation.cms.content.markdown.Options; -import com.condation.cms.content.markdown.BlockTokenizer; -import com.condation.cms.content.markdown.Block; import com.condation.cms.content.markdown.rules.block.CodeBlockRule; import com.condation.cms.content.markdown.rules.block.ParagraphBlockRule; import java.io.IOException; import java.util.List; import static org.assertj.core.api.Assertions.*; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; /** - * * @author t.marx */ public class BlockTokenizerTest extends MarkdownTest { @@ -51,53 +47,53 @@ public static void setup() { @Test void test_single_line() throws IOException { String content = load("block_single_line.md"); - List blocks = sut.tokenize(content); + List blocks = sut.tokenize(content); assertThat(blocks).hasSize(1); - assertThat(blocks.get(0)).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); - var pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(0); + assertThat(blocks.get(0).block()).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); + var pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(0).block(); assertThat(pb.content()).isEqualToIgnoringNewLines("Hallo"); } @Test void test_two_lines() throws IOException { String content = load("block_two_lines.md"); - List blocks = sut.tokenize(content); + List blocks = sut.tokenize(content); assertThat(blocks).hasSize(1); - assertThat(blocks.get(0)).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); - var pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(0); + assertThat(blocks.get(0).block()).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); + var pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(0).block(); assertThat(pb.content()).isEqualToIgnoringNewLines("Hallo\nLeute"); } @Test void test_two_blocks() throws IOException { String content = load("block_two_blocks.md"); - List blocks = sut.tokenize(content); + List blocks = sut.tokenize(content); assertThat(blocks).hasSize(2); - assertThat(blocks.get(0)).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); - assertThat(blocks.get(1)).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); - var pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(0); + assertThat(blocks.get(0).block()).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); + assertThat(blocks.get(1).block()).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); + var pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(0).block(); assertThat(pb.content()).isEqualToIgnoringNewLines("Hallo"); - pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(1); + pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(1).block(); assertThat(pb.content()).isEqualToIgnoringNewLines("Leute"); } - + @Test void test_code_paragraph() throws IOException { String content = load("block_code_paragraph.md"); - List blocks = sut.tokenize(content); + List blocks = sut.tokenize(content); assertThat(blocks).hasSize(4); - assertThat(blocks.get(0)).isInstanceOf(CodeBlockRule.CodeBlock.class); - assertThat(blocks.get(1)).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); - assertThat(blocks.get(2)).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); - assertThat(blocks.get(3)).isInstanceOf(CodeBlockRule.CodeBlock.class); - var cb = (CodeBlockRule.CodeBlock) blocks.get(0); + assertThat(blocks.get(0).block()).isInstanceOf(CodeBlockRule.CodeBlock.class); + assertThat(blocks.get(1).block()).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); + assertThat(blocks.get(2).block()).isInstanceOf(ParagraphBlockRule.ParagraphBlock.class); + assertThat(blocks.get(3).block()).isInstanceOf(CodeBlockRule.CodeBlock.class); + var cb = (CodeBlockRule.CodeBlock) blocks.get(0).block(); assertThat(cb.content()).isEqualToIgnoringNewLines("java.lang.System.out.println(\"Hello world!\");"); assertThat(cb.language()).isEqualToIgnoringNewLines("java"); - var pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(2); + var pb = (ParagraphBlockRule.ParagraphBlock) blocks.get(2).block(); assertThat(pb.content()).isEqualToIgnoringNewLines("Hallo"); } } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/LargeBlockTokenizerTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/LargeBlockTokenizerTest.java index c41487619..5d7053f74 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/LargeBlockTokenizerTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/LargeBlockTokenizerTest.java @@ -47,7 +47,7 @@ public static void setup() { @Test void test_large_file() throws IOException { String content = load("large_block.md"); - List blocks = sut.tokenize(content); + List blocks = sut.tokenize(content); assertThat(blocks).isNotEmpty(); } } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/DefinitionListBlockRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/DefinitionListBlockRuleTest.java index 92f924c43..58f7119c3 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/DefinitionListBlockRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/DefinitionListBlockRuleTest.java @@ -65,7 +65,7 @@ public void basic_test() { )) )); - var rendered = next.render((md) -> md); + var rendered = next.render((md, offset) -> md); Assertions.assertThat(rendered).isEqualToIgnoringWhitespace(expected); } @@ -109,7 +109,7 @@ public void mulitple_test() { )) )); - var rendered = next.render((md) -> md); + var rendered = next.render((md, offset) -> md); Assertions.assertThat(rendered).isEqualToIgnoringWhitespace(expected); } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/HorizontalRuleBlockRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/HorizontalRuleBlockRuleTest.java index 41ed0a743..c2781c1e1 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/HorizontalRuleBlockRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/HorizontalRuleBlockRuleTest.java @@ -50,7 +50,7 @@ void test_horizontal_rule(String input) { .isNotNull() .isInstanceOf(HorizontalRuleBlockRule.HRBlock.class); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      "); } @Test @@ -69,7 +69,7 @@ void test_horizontal_rule_with_before() { .isNotNull() .isInstanceOf(HorizontalRuleBlockRule.HRBlock.class); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      "); } @Test @@ -91,9 +91,9 @@ void test_horizontal_rule() { .isNotNull() .isInstanceOf(HorizontalRuleBlockRule.HRBlock.class); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      "); - Assertions.assertThat(next.render(value -> value)).isEqualToIgnoringWhitespace(expected); + Assertions.assertThat(next.render((value, offset) -> value)).isEqualToIgnoringWhitespace(expected); } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/ListBlockRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/ListBlockRuleTest.java index d9627388c..0f19954f9 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/ListBlockRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/ListBlockRuleTest.java @@ -49,7 +49,7 @@ void test_ordered_list() { .asInstanceOf(InstanceOfAssertFactories.type(ListBlockRule.ListBlock.class)) .hasFieldOrPropertyWithValue("items", List.of("Hallo", "Leute")); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      1. Hallo
      2. Leute
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      1. Hallo
      2. Leute
      "); } @Test @@ -65,7 +65,7 @@ void test_unordered_list_star() { .asInstanceOf(InstanceOfAssertFactories.type(ListBlockRule.ListBlock.class)) .hasFieldOrPropertyWithValue("items", List.of("Hallo", "Leute")); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      • Hallo
      • Leute
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      • Hallo
      • Leute
      "); } @Test @@ -81,7 +81,7 @@ void test_unordered_list_minus() { .asInstanceOf(InstanceOfAssertFactories.type(ListBlockRule.ListBlock.class)) .hasFieldOrPropertyWithValue("items", List.of("Hallo", "Leute")); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      • Hallo
      • Leute
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      • Hallo
      • Leute
      "); } @Test @@ -97,7 +97,7 @@ void test_unordered_list_plus() { .asInstanceOf(InstanceOfAssertFactories.type(ListBlockRule.ListBlock.class)) .hasFieldOrPropertyWithValue("items", List.of("Hallo", "Leute")); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      • Hallo
      • Leute
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      • Hallo
      • Leute
      "); } @Test @@ -113,7 +113,7 @@ void test_unordered_list_issue() { .asInstanceOf(InstanceOfAssertFactories.type(ListBlockRule.ListBlock.class)) .hasFieldOrPropertyWithValue("items", List.of("ul item 1", "ul item 2")); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      • ul item 1
      • ul item 2
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      • ul item 1
      • ul item 2
      "); } @Test @@ -129,7 +129,7 @@ void test_dot_issue_183() { .asInstanceOf(InstanceOfAssertFactories.type(ListBlockRule.ListBlock.class)) .hasFieldOrPropertyWithValue("items", List.of("first sentence. second sentence.", "item 2")); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      1. first sentence. second sentence.
      2. item 2
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      1. first sentence. second sentence.
      2. item 2
      "); } @Test @@ -145,7 +145,7 @@ void ordered_list_multiline_items() { .asInstanceOf(InstanceOfAssertFactories.type(ListBlockRule.ListBlock.class)) .hasFieldOrPropertyWithValue("items", List.of("first sentence.\nsecond sentence.", "item 2")); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      1. first sentence.\nsecond sentence.
      2. item 2
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      1. first sentence.\nsecond sentence.
      2. item 2
      "); } @Test @@ -161,6 +161,6 @@ void unordered_list_multiline_items() { .asInstanceOf(InstanceOfAssertFactories.type(ListBlockRule.ListBlock.class)) .hasFieldOrPropertyWithValue("items", List.of("first sentence.\nsecond sentence.", "item 2")); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("
      • first sentence.\nsecond sentence.
      • item 2
      "); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("
      • first sentence.\nsecond sentence.
      • item 2
      "); } } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TableBlockRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TableBlockRuleTest.java index 9b927502c..1e369e509 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TableBlockRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TableBlockRuleTest.java @@ -85,7 +85,7 @@ public void basic_test() { new TableBlockRule.Row(List.of("r2 / c1", "r2 / c2")) )); - var rendered = next.render((md) -> md); + var rendered = next.render((md, offset) -> md); Assertions.assertThat(rendered).isEqualToIgnoringWhitespace(expected); } @@ -144,7 +144,7 @@ public void align_test() { new TableBlockRule.Row(List.of("r2 / c1", "r2 / c2", "r2 / c3")) )); - var rendered = next.render((md) -> md); + var rendered = next.render((md, offset) -> md); Assertions.assertThat(rendered).isEqualToIgnoringWhitespace(expected); } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TagBlockRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TagBlockRuleTest.java index 198d64060..a4e17c44a 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TagBlockRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TagBlockRuleTest.java @@ -54,7 +54,7 @@ void long_form() { "_content", "Google" )); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("[[link url=\"https://google.de/\"]]Google[[/link]]"); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("[[link url=\"https://google.de/\"]]Google[[/link]]"); } @Test @@ -75,7 +75,7 @@ void short_form() { "url", "https://google.de/" )); - Assertions.assertThat(next.render((content) -> content)).isEqualTo("[[link url=\"https://google.de/\"]][[/link]]"); + Assertions.assertThat(next.render((content, offset) -> content)).isEqualTo("[[link url=\"https://google.de/\"]][[/link]]"); } @Test @@ -98,7 +98,7 @@ void test_issue () { "title", "Everybody loves little cats" )); - Assertions.assertThat(next.render((content) -> content)) + Assertions.assertThat(next.render((content, offset) -> content)) .isEqualTo("[[video id=\"y0sF5xhGreA\" title=\"Everybody loves little cats\" type=\"youtube\"]][[/video]]"); } diff --git a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TaskListBlockRuleTest.java b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TaskListBlockRuleTest.java index 504319dc0..4a2d02bf4 100644 --- a/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TaskListBlockRuleTest.java +++ b/cms-content/src/test/java/com/condation/cms/content/markdown/rules/block/TaskListBlockRuleTest.java @@ -64,7 +64,7 @@ public void basic_test() { )) ); - var rendered = next.render((md) -> md); + var rendered = next.render((md, offset) -> md); Assertions.assertThat(rendered).isEqualToIgnoringWhitespace(expected); } @@ -102,7 +102,7 @@ public void mulitple_test() { )) ); - var rendered = next.render((md) -> md); + var rendered = next.render((md, offset) -> md); Assertions.assertThat(rendered).isEqualToIgnoringWhitespace(expected); } diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java index 2074105be..d52eb9d95 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/extensionpoints/remotemethods/RemoteContentEndpointsExtension.java @@ -44,10 +44,13 @@ import java.util.Map; import lombok.extern.slf4j.Slf4j; import com.condation.cms.api.ui.annotations.RemoteMethod; +import com.condation.cms.api.ui.rpc.RPCException; import com.condation.cms.api.utils.SectionUtil; import com.condation.cms.content.SectionEntry; import com.condation.cms.modules.ui.utils.FormHelper; +import com.condation.cms.modules.ui.utils.MarkdownHelper; import com.condation.cms.modules.ui.utils.MetaConverter; +import com.condation.cms.modules.ui.utils.NumberUtils; import com.condation.cms.modules.ui.utils.UIFileNameUtil; import com.condation.cms.modules.ui.utils.UIPathUtil; import java.nio.file.Files; @@ -115,6 +118,45 @@ public Object setContent(Map parameters) { return result; } + + @RemoteMethod(name = "content.replace", permissions = {Permissions.CONTENT_EDIT}) + public Object replaceContent(Map parameters) throws RPCException { + final DB db = getContext().get(DBFeature.class).db(); + var contentBase = db.getReadOnlyFileSystem().resolve(Constants.Folders.CONTENT); + + var replacement = (String)parameters.get("content"); + int start = NumberUtils.toInt(parameters.getOrDefault("start", -1l)); + int end = NumberUtils.toInt(parameters.getOrDefault("end", -1l)); + var uri = (String) parameters.get("uri"); + + Map result = new HashMap<>(); + result.put("uri", uri); + + if (replacement == null) { + throw new RPCException("replacement must not be null"); + } + + var contentFile = contentBase.resolve(uri); + + if (contentFile != null) { + try { + ContentFileParser parser = new ContentFileParser(contentFile); + + var content = parser.getContent(); + + var updatedContent = MarkdownHelper.replaceRange(content, start, end, replacement); + + var filePath = db.getFileSystem().resolve(Constants.Folders.CONTENT).resolve(uri); + + YamlHeaderUpdater.saveMarkdownFileWithHeader(filePath, parser.getHeader(), updatedContent); + log.debug("file {} saved", uri); + } catch (IOException ex) { + log.error("", ex); + } + } + + return result; + } @RemoteMethod(name = "meta.set", permissions = {Permissions.CONTENT_EDIT}) public Object setMeta(Map parameters) { diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java index c0f705d65..7c10b4fcd 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/http/auth/RefreshTokenHandler.java @@ -26,9 +26,7 @@ import com.condation.cms.api.request.RequestContext; import com.condation.cms.modules.ui.http.JettyHandler; import com.condation.cms.modules.ui.utils.AuthUtil; -import com.condation.cms.modules.ui.utils.CookieUtil; import com.condation.cms.modules.ui.utils.TokenUtils; -import com.condation.cms.modules.ui.utils.UIConstants; import com.condation.cms.modules.ui.utils.json.UIGsonProvider; import java.time.Duration; import java.util.Map; @@ -58,16 +56,18 @@ public boolean handle(Request request, Response response, Callback callback) thr return false; } - boolean refreshed = AuthUtil.refreshTokens(request, response, moduleContext, requestContext); - if (refreshed) { + var newAuthToken = AuthUtil.refreshTokens(request, response, moduleContext, requestContext); + if (newAuthToken.isPresent()) { var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); - - var authCookie = CookieUtil.getCookie(request, UIConstants.COOKIE_CMS_TOKEN); - var token = authCookie.get().getValue(); + var payload = TokenUtils.getPayload(newAuthToken.get(), secret); + if (payload.isEmpty()) { + log.warn("Refresh succeeded but token payload could not be parsed"); + response.setStatus(401); + Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(Map.of("status", "error", "reason", "unauthorized")), callback); + return true; + } - var payload = TokenUtils.getPayload(token, secret); - response.setStatus(200); Content.Sink.write(response, true, UIGsonProvider.INSTANCE.toJson(Map.of( "status", "ok", diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/AuthUtil.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/AuthUtil.java index de35fc8e7..0607d198d 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/AuthUtil.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/AuthUtil.java @@ -48,7 +48,7 @@ public final class AuthUtil { private AuthUtil() { } - private static boolean tryRefresh(Request request, Response response, SiteModuleContext moduleContext, RequestContext requestContext) { + private static Optional tryRefresh(Request request, Response response, SiteModuleContext moduleContext, RequestContext requestContext) { var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); var refreshTokenCache = moduleContext.get(CacheManagerFeature.class).cacheManager().get( @@ -58,25 +58,27 @@ private static boolean tryRefresh(Request request, Response response, SiteModule var refreshCookie = CookieUtil.getCookie(request, UIConstants.COOKIE_CMS_REFRESH_TOKEN); if (refreshCookie.isEmpty()) { - return false; + return Optional.empty(); } var token = refreshCookie.get().getValue(); var payload = TokenUtils.getPayload(token, secret); if (payload.isPresent()) { - refreshTokenCache.invalidate(token); // best-effort invalidation; refresh should still work after restart if token is valid + refreshTokenCache.invalidate(token); Optional userOpt = moduleContext.get(InjectorFeature.class).injector().getInstance(UserService.class).byUsername(Realm.of("manager-users"), payload.get().username()); if (userOpt.isPresent()) { - updateCookies(userOpt.get(), response, requestContext, moduleContext); - return true; + return Optional.of(updateCookies(userOpt.get(), response, requestContext, moduleContext)); } } - return false; + return Optional.empty(); } - public static boolean refreshTokens(Request request, Response response, SiteModuleContext moduleContext, RequestContext requestContext) { + /** + * Returns the new auth token string if refresh succeeded, empty otherwise. + */ + public static Optional refreshTokens(Request request, Response response, SiteModuleContext moduleContext, RequestContext requestContext) { return tryRefresh(request, response, moduleContext, requestContext); } @@ -86,8 +88,7 @@ public static boolean checkAuthTokens(Request request, Response response, SiteMo var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); if (authCookie.isEmpty()) { - // try refresh - if (tryRefresh(request, response, moduleContext, requestContext)) { + if (tryRefresh(request, response, moduleContext, requestContext).isPresent()) { return true; } } else { @@ -96,8 +97,7 @@ public static boolean checkAuthTokens(Request request, Response response, SiteMo var payload = TokenUtils.getPayload(token, secret); if (payload.isEmpty()) { - // try refresh - if (tryRefresh(request, response, moduleContext, requestContext)) { + if (tryRefresh(request, response, moduleContext, requestContext).isPresent()) { return true; } } else { @@ -108,7 +108,10 @@ public static boolean checkAuthTokens(Request request, Response response, SiteMo return false; } - public static void updateCookies(User user, Response response, RequestContext requestContext, SiteModuleContext moduleContext) { + /** + * Creates and sets new auth/refresh/preview cookies. Returns the new auth token. + */ + public static String updateCookies(User user, Response response, RequestContext requestContext, SiteModuleContext moduleContext) { try { var secret = moduleContext.get(ConfigurationFeature.class).configuration().get(ServerConfiguration.class).serverProperties().secret(); @@ -142,6 +145,8 @@ public static void updateCookies(User user, Response response, RequestContext re new CacheManager.CacheConfig(1000l, Duration.ofDays(7)) ); refreshTokenCache.put(refreshToken, true); + + return authToken; } catch (Exception ex) { log.error("", ex); throw new RuntimeException(ex); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MarkdownHelper.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MarkdownHelper.java index 5ae043e17..d796e8e12 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MarkdownHelper.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/MarkdownHelper.java @@ -37,6 +37,9 @@ public static String replaceRange(String markdown, Objects.requireNonNull(markdown); Objects.requireNonNull(replacement); + // step is necessary because it is also in markdown renderer + markdown = markdown.replace("\r\n", "\n"); + if (start < 0 || end < start || end > markdown.length()) { throw new IllegalArgumentException( "Invalid range: start=" + start + ", end=" + end); diff --git a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/NumberUtils.java b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/NumberUtils.java index b5e063ec7..cb693a3ab 100644 --- a/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/NumberUtils.java +++ b/modules/ui-module/src/main/java/com/condation/cms/modules/ui/utils/NumberUtils.java @@ -39,4 +39,17 @@ public static long toLong(Object value) { throw new IllegalArgumentException("Invalid page value: " + value); }; } + + public static int toInt(Object value) { + return switch (value) { + case null -> + 1; + case Number n -> + n.intValue(); + case String s -> + Integer.parseInt(s); + default -> + throw new IllegalArgumentException("Invalid page value: " + value); + }; + } } diff --git a/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts b/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts new file mode 100644 index 000000000..6dc2ed246 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.d.ts @@ -0,0 +1,21 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.js b/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.js new file mode 100644 index 000000000..6c10ce9d9 --- /dev/null +++ b/modules/ui-module/src/main/resources/manager/actions/media/select-content-media.js @@ -0,0 +1,81 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { openFileBrowser } from "@cms/modules/filebrowser.js"; +import { i18n } from "@cms/modules/localization.js"; +import { getPreviewUrl, reloadPreview } from "@cms/modules/preview.utils.js"; +import { getContentNode, replaceContent } from "@cms/modules/rpc/rpc-content.js"; +import { showToast } from "@cms/modules/toast.js"; +export async function runAction(params) { + var uri = null; + if (params.options.uri) { + uri = params.options.uri; + } + else { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + uri = contentNode.result.uri; + } + openFileBrowser({ + type: "assets", + filter: (file) => { + return file.media || file.directory; + }, + onSelect: async (file) => { + if (file && file.uri) { + var selectedFile = file.uri; // Use the file's URI + if (file.uri.startsWith("/")) { + selectedFile = file.uri.substring(1); // Remove leading slash if present + } + var updateData = {}; + updateData[params.options.metaElement] = { + type: 'media', + value: selectedFile + }; + var options = { + uri: uri, + content: `![](${selectedFile}?format=small)`, + start: params.options.start, + end: params.options.end + }; + var replaceMedia = await replaceContent(options); + if (replaceMedia.result.error != null && replaceMedia.result.error === true) { + showToast({ + title: i18n.t('manager.actions.media.select-content-media.toast.title-error', "Media not updated"), + message: i18n.t('manager.actions.media.select-content-media.toast.message-error', "New media has not been updated successfully."), + type: 'error', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + else { + showToast({ + title: i18n.t('manager.actions.media.select-media.toast.title', "Media updated"), + message: i18n.t('manager.actions.media.select-media.toast.message', "New media has been updated successfully."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + } + } + }); +} diff --git a/modules/ui-module/src/main/resources/manager/js/manager-inject.js b/modules/ui-module/src/main/resources/manager/js/manager-inject.js index 2b11d2b1d..75083adbc 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager-inject.js +++ b/modules/ui-module/src/main/resources/manager/js/manager-inject.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/resources/manager/js/manager.js b/modules/ui-module/src/main/resources/manager/js/manager.js index 800a0a030..506b4e966 100644 --- a/modules/ui-module/src/main/resources/manager/js/manager.js +++ b/modules/ui-module/src/main/resources/manager/js/manager.js @@ -42,7 +42,7 @@ function heartbeat() { document.addEventListener("DOMContentLoaded", function () { setInterval(() => { heartbeat(); - }, 10 * 1000); + }, 10 * 60 * 1000); //PreviewHistory.init("/"); //updateStateButton(); activatePreviewOverlay(); diff --git a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts index 0869c773e..0c529bf7f 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/filebrowser.d.ts @@ -20,6 +20,6 @@ */ export function openFileBrowser(optionsParam: any): Promise; export namespace state { - let options: null; + let options: any; let currentFolder: string; } diff --git a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts index 23eeb2258..7d3d89dcb 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/form/utils.d.ts @@ -20,7 +20,7 @@ */ declare const createID: () => string; declare const utcToLocalDateTimeInputValue: (utcString: string) => string; -declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string | null; +declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string; declare function utcToLocalDateInputValue(utcString: string): string; -declare function getUTCDateFromInput(inputElement: HTMLInputElement): string | null; +declare function getUTCDateFromInput(inputElement: HTMLInputElement): string; export { createID, utcToLocalDateTimeInputValue, getUTCDateTimeFromInput, utcToLocalDateInputValue, getUTCDateFromInput }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts index e91051759..b1e393873 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/localization.d.ts @@ -21,7 +21,7 @@ export function localizeUi(): Promise; export namespace i18n { let _locale: any; - let _cache: null; + let _cache: any; /** * Loads and merges remote localizations with defaults. */ diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js index 083ac47a2..cef4be80c 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/manager.message.handlers.js @@ -47,6 +47,16 @@ const executeImageSelect = (payload) => { }; executeScriptAction(cmd); }; +const executeContentImageReplace = (payload) => { + const cmd = { + "module": window.manager.baseUrl + "/actions/media/select-content-media", + "function": "runAction", + "parameters": { + "options": payload.options ? payload.options : {} + } + }; + executeScriptAction(cmd); +}; const initMessageHandlers = () => { frameMessenger.on('preview:reload', (payload) => { }); @@ -88,6 +98,9 @@ const initMessageHandlers = () => { else if (payload.element === "image" && payload.editor === "select") { executeImageSelect(payload); } + else if (payload.element === "image" && payload.editor === "replace") { + executeContentImageReplace(payload); + } else if (payload.element === "image" && payload.editor === "focal-point") { var cmd = { "module": window.manager.baseUrl + "/actions/media/edit-focal-point", diff --git a/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.js b/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.js index 3b2e62255..c89fd2596 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/manager/media.inject.js @@ -90,9 +90,12 @@ export const initContentMediaToolbar = (img) => { } var toolbarDefinition = { "options": { - "uri": parentToolbarDef.uri + "uri": parentToolbarDef.uri, + "start": img.dataset.cmsMdStart || null, + "end": img.dataset.cmsMdEnd || null }, "actions": [ + "replace", "meta", "focalPoint" ] @@ -119,6 +122,15 @@ export const initToolbar = (img, toolbarDefinition) => { }); toolbar.appendChild(selectButton); } + if (toolbarDefinition.actions.includes('replace')) { + const replaceButton = document.createElement('button'); + replaceButton.innerHTML = IMAGE_ICON; + replaceButton.setAttribute("title", "Replace media"); + replaceButton.addEventListener('click', (event) => { + replaceMedia(toolbarDefinition.options.start, toolbarDefinition.options.end, toolbarDefinition.options.element, toolbarDefinition.options.uri); + }); + toolbar.appendChild(replaceButton); + } if (toolbarDefinition.actions.includes('meta')) { const metaButton = document.createElement('button'); metaButton.setAttribute('data-cms-action', 'editMediaForm'); @@ -174,6 +186,22 @@ export const initToolbar = (img, toolbarDefinition) => { positionToolbar(); }); }; +const replaceMedia = (start, end, metaElement, uri) => { + var command = { + type: 'edit', + payload: { + editor: "replace", + element: "image", + options: { + metaElement: metaElement, + uri: uri, + start: start, + end: end + } + } + }; + frameMessenger.send(window.parent, command); +}; const selectMedia = (metaElement, uri) => { var command = { type: 'edit', diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts index 6cfeb16ad..520ce2957 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.history.d.ts @@ -22,6 +22,6 @@ export namespace PreviewHistory { export { init }; export { navigatePreview }; } -declare function init(defaultUrl?: null): void; +declare function init(defaultUrl?: any): void; declare function navigatePreview(url: any, usePush?: boolean): void; export {}; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts index 20c850ee6..e7c2a28ba 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/preview.utils.d.ts @@ -23,4 +23,4 @@ export function deActivatePreviewOverlay(): void; export function getPreviewUrl(): any; export function reloadPreview(): void; export function loadPreview(url: any): void; -export function getPreviewFrame(): HTMLElement | null; +export function getPreviewFrame(): HTMLElement; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts index 26338623d..0df37da31 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.d.ts @@ -18,11 +18,23 @@ * along with this program. If not, see . * #L% */ +import { RPCResponse } from '@cms/modules/rpc/rpc.js'; declare const getContentNode: (options: any) => Promise; declare const getContent: (options: any) => Promise; declare const setContent: (options: any) => Promise; +export interface ReplaceContent { + error: boolean | null; + uri: string; +} +export interface ReplaceContentOptions { + uri: string; + content: string; + start: number; + end: number; +} +declare const replaceContent: (options: ReplaceContentOptions) => Promise>; declare const setMeta: (options: any) => Promise; declare const setMetaBatch: (options: any) => Promise; declare const addSection: (options: any) => Promise; declare const deleteSection: (options: any) => Promise; -export { getContentNode, getContent, setContent, setMeta, setMetaBatch, addSection, deleteSection }; +export { getContentNode, getContent, setContent, replaceContent, setMeta, setMetaBatch, addSection, deleteSection }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.js b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.js index 285c9e397..e0a97a880 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc-content.js @@ -40,6 +40,13 @@ const setContent = async (options) => { }; return await executeRemoteCall(data); }; +const replaceContent = async (options) => { + var data = { + method: "content.replace", + parameters: options + }; + return await executeRemoteCall(data); +}; const setMeta = async (options) => { var data = { method: "meta.set", @@ -68,4 +75,4 @@ const deleteSection = async (options) => { }; return await executeRemoteCall(data); }; -export { getContentNode, getContent, setContent, setMeta, setMetaBatch, addSection, deleteSection }; +export { getContentNode, getContent, setContent, replaceContent, setMeta, setMetaBatch, addSection, deleteSection }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts index 71e4a384c..753fc6d82 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.d.ts @@ -22,6 +22,9 @@ interface Options { method: string; parameters?: any; } +export interface RPCResponse { + result: T; +} declare const executeRemoteCall: (options: Options) => Promise; declare const executeRemoteMethodCall: (method: string, parameters: any) => Promise; export { executeRemoteCall, executeRemoteMethodCall }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js index 9974e2a6d..d1598ff2a 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/rpc/rpc.js @@ -40,7 +40,7 @@ const executeRemoteMethodCall = async (method, parameters) => { if (response.status === 403) { alert(i18n.t("ui.redirect.login", "You where logged out due to inactivity. Please log in again.")); window.location.href = window.manager.baseUrl + "/login"; - return; + throw new Error("Unauthorized"); } return await response.json(); }; diff --git a/modules/ui-module/src/main/resources/manager/js/modules/state.js b/modules/ui-module/src/main/resources/manager/js/modules/state.js index a0988236f..7274c0b66 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/state.js +++ b/modules/ui-module/src/main/resources/manager/js/modules/state.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts index 30e36cd4d..65018962d 100644 --- a/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts +++ b/modules/ui-module/src/main/resources/manager/js/modules/ui-state.d.ts @@ -20,11 +20,11 @@ */ export namespace UIStateManager { function setTabState(key: any, value: any): void; - function getTabState(key: any, defaultValue?: null): any; + function getTabState(key: any, defaultValue?: any): any; function setLocale(locale: any): void; function getLocale(): any; function removeTabState(key: any): void; function setAuthToken(token: any): void; - function getAuthToken(): string | null; + function getAuthToken(): string; function clearAuthToken(): void; } diff --git a/modules/ui-module/src/main/resources/manager/public/manager-login.js b/modules/ui-module/src/main/resources/manager/public/manager-login.js index 24aa077ae..6f21eee99 100644 --- a/modules/ui-module/src/main/resources/manager/public/manager-login.js +++ b/modules/ui-module/src/main/resources/manager/public/manager-login.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/dist/actions/media/select-content-media.d.ts b/modules/ui-module/src/main/ts/dist/actions/media/select-content-media.d.ts new file mode 100644 index 000000000..85620d3ab --- /dev/null +++ b/modules/ui-module/src/main/ts/dist/actions/media/select-content-media.d.ts @@ -0,0 +1 @@ +export declare function runAction(params: any): Promise; diff --git a/modules/ui-module/src/main/ts/dist/actions/media/select-content-media.js b/modules/ui-module/src/main/ts/dist/actions/media/select-content-media.js new file mode 100644 index 000000000..6c10ce9d9 --- /dev/null +++ b/modules/ui-module/src/main/ts/dist/actions/media/select-content-media.js @@ -0,0 +1,81 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ +import { openFileBrowser } from "@cms/modules/filebrowser.js"; +import { i18n } from "@cms/modules/localization.js"; +import { getPreviewUrl, reloadPreview } from "@cms/modules/preview.utils.js"; +import { getContentNode, replaceContent } from "@cms/modules/rpc/rpc-content.js"; +import { showToast } from "@cms/modules/toast.js"; +export async function runAction(params) { + var uri = null; + if (params.options.uri) { + uri = params.options.uri; + } + else { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }); + uri = contentNode.result.uri; + } + openFileBrowser({ + type: "assets", + filter: (file) => { + return file.media || file.directory; + }, + onSelect: async (file) => { + if (file && file.uri) { + var selectedFile = file.uri; // Use the file's URI + if (file.uri.startsWith("/")) { + selectedFile = file.uri.substring(1); // Remove leading slash if present + } + var updateData = {}; + updateData[params.options.metaElement] = { + type: 'media', + value: selectedFile + }; + var options = { + uri: uri, + content: `![](${selectedFile}?format=small)`, + start: params.options.start, + end: params.options.end + }; + var replaceMedia = await replaceContent(options); + if (replaceMedia.result.error != null && replaceMedia.result.error === true) { + showToast({ + title: i18n.t('manager.actions.media.select-content-media.toast.title-error', "Media not updated"), + message: i18n.t('manager.actions.media.select-content-media.toast.message-error', "New media has not been updated successfully."), + type: 'error', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + else { + showToast({ + title: i18n.t('manager.actions.media.select-media.toast.title', "Media updated"), + message: i18n.t('manager.actions.media.select-media.toast.message', "New media has been updated successfully."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview(); + } + } + } + }); +} diff --git a/modules/ui-module/src/main/ts/dist/js/manager-inject.js b/modules/ui-module/src/main/ts/dist/js/manager-inject.js index 2b11d2b1d..75083adbc 100644 --- a/modules/ui-module/src/main/ts/dist/js/manager-inject.js +++ b/modules/ui-module/src/main/ts/dist/js/manager-inject.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/dist/js/manager.js b/modules/ui-module/src/main/ts/dist/js/manager.js index 800a0a030..506b4e966 100644 --- a/modules/ui-module/src/main/ts/dist/js/manager.js +++ b/modules/ui-module/src/main/ts/dist/js/manager.js @@ -42,7 +42,7 @@ function heartbeat() { document.addEventListener("DOMContentLoaded", function () { setInterval(() => { heartbeat(); - }, 10 * 1000); + }, 10 * 60 * 1000); //PreviewHistory.init("/"); //updateStateButton(); activatePreviewOverlay(); diff --git a/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts index e10842a8d..34d9f9cb9 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/filebrowser.d.ts @@ -1,5 +1,5 @@ export function openFileBrowser(optionsParam: any): Promise; export namespace state { - let options: null; + let options: any; let currentFolder: string; } diff --git a/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts index 525b147b7..79c944de7 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/form/utils.d.ts @@ -1,6 +1,6 @@ declare const createID: () => string; declare const utcToLocalDateTimeInputValue: (utcString: string) => string; -declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string | null; +declare function getUTCDateTimeFromInput(inputElement: HTMLInputElement): string; declare function utcToLocalDateInputValue(utcString: string): string; -declare function getUTCDateFromInput(inputElement: HTMLInputElement): string | null; +declare function getUTCDateFromInput(inputElement: HTMLInputElement): string; export { createID, utcToLocalDateTimeInputValue, getUTCDateTimeFromInput, utcToLocalDateInputValue, getUTCDateFromInput }; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts index 9eeed5a15..0b22efabb 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/localization.d.ts @@ -1,7 +1,7 @@ export function localizeUi(): Promise; export namespace i18n { let _locale: any; - let _cache: null; + let _cache: any; /** * Loads and merges remote localizations with defaults. */ diff --git a/modules/ui-module/src/main/ts/dist/js/modules/manager/manager.message.handlers.js b/modules/ui-module/src/main/ts/dist/js/modules/manager/manager.message.handlers.js index 083ac47a2..cef4be80c 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/manager/manager.message.handlers.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/manager/manager.message.handlers.js @@ -47,6 +47,16 @@ const executeImageSelect = (payload) => { }; executeScriptAction(cmd); }; +const executeContentImageReplace = (payload) => { + const cmd = { + "module": window.manager.baseUrl + "/actions/media/select-content-media", + "function": "runAction", + "parameters": { + "options": payload.options ? payload.options : {} + } + }; + executeScriptAction(cmd); +}; const initMessageHandlers = () => { frameMessenger.on('preview:reload', (payload) => { }); @@ -88,6 +98,9 @@ const initMessageHandlers = () => { else if (payload.element === "image" && payload.editor === "select") { executeImageSelect(payload); } + else if (payload.element === "image" && payload.editor === "replace") { + executeContentImageReplace(payload); + } else if (payload.element === "image" && payload.editor === "focal-point") { var cmd = { "module": window.manager.baseUrl + "/actions/media/edit-focal-point", diff --git a/modules/ui-module/src/main/ts/dist/js/modules/manager/media.inject.js b/modules/ui-module/src/main/ts/dist/js/modules/manager/media.inject.js index 3b2e62255..c89fd2596 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/manager/media.inject.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/manager/media.inject.js @@ -90,9 +90,12 @@ export const initContentMediaToolbar = (img) => { } var toolbarDefinition = { "options": { - "uri": parentToolbarDef.uri + "uri": parentToolbarDef.uri, + "start": img.dataset.cmsMdStart || null, + "end": img.dataset.cmsMdEnd || null }, "actions": [ + "replace", "meta", "focalPoint" ] @@ -119,6 +122,15 @@ export const initToolbar = (img, toolbarDefinition) => { }); toolbar.appendChild(selectButton); } + if (toolbarDefinition.actions.includes('replace')) { + const replaceButton = document.createElement('button'); + replaceButton.innerHTML = IMAGE_ICON; + replaceButton.setAttribute("title", "Replace media"); + replaceButton.addEventListener('click', (event) => { + replaceMedia(toolbarDefinition.options.start, toolbarDefinition.options.end, toolbarDefinition.options.element, toolbarDefinition.options.uri); + }); + toolbar.appendChild(replaceButton); + } if (toolbarDefinition.actions.includes('meta')) { const metaButton = document.createElement('button'); metaButton.setAttribute('data-cms-action', 'editMediaForm'); @@ -174,6 +186,22 @@ export const initToolbar = (img, toolbarDefinition) => { positionToolbar(); }); }; +const replaceMedia = (start, end, metaElement, uri) => { + var command = { + type: 'edit', + payload: { + editor: "replace", + element: "image", + options: { + metaElement: metaElement, + uri: uri, + start: start, + end: end + } + } + }; + frameMessenger.send(window.parent, command); +}; const selectMedia = (metaElement, uri) => { var command = { type: 'edit', diff --git a/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts index 19f953414..02a1aa102 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/preview.history.d.ts @@ -2,6 +2,6 @@ export namespace PreviewHistory { export { init }; export { navigatePreview }; } -declare function init(defaultUrl?: null): void; +declare function init(defaultUrl?: any): void; declare function navigatePreview(url: any, usePush?: boolean): void; export {}; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts index 24beb8ecc..f2f167f34 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/preview.utils.d.ts @@ -3,4 +3,4 @@ export function deActivatePreviewOverlay(): void; export function getPreviewUrl(): any; export function reloadPreview(): void; export function loadPreview(url: any): void; -export function getPreviewFrame(): HTMLElement | null; +export function getPreviewFrame(): HTMLElement; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.d.ts index f33f4ad96..500f28b21 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.d.ts @@ -1,8 +1,20 @@ +import { RPCResponse } from '@cms/modules/rpc/rpc.js'; declare const getContentNode: (options: any) => Promise; declare const getContent: (options: any) => Promise; declare const setContent: (options: any) => Promise; +export interface ReplaceContent { + error: boolean | null; + uri: string; +} +export interface ReplaceContentOptions { + uri: string; + content: string; + start: number; + end: number; +} +declare const replaceContent: (options: ReplaceContentOptions) => Promise>; declare const setMeta: (options: any) => Promise; declare const setMetaBatch: (options: any) => Promise; declare const addSection: (options: any) => Promise; declare const deleteSection: (options: any) => Promise; -export { getContentNode, getContent, setContent, setMeta, setMetaBatch, addSection, deleteSection }; +export { getContentNode, getContent, setContent, replaceContent, setMeta, setMetaBatch, addSection, deleteSection }; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.js b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.js index 285c9e397..e0a97a880 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc-content.js @@ -40,6 +40,13 @@ const setContent = async (options) => { }; return await executeRemoteCall(data); }; +const replaceContent = async (options) => { + var data = { + method: "content.replace", + parameters: options + }; + return await executeRemoteCall(data); +}; const setMeta = async (options) => { var data = { method: "meta.set", @@ -68,4 +75,4 @@ const deleteSection = async (options) => { }; return await executeRemoteCall(data); }; -export { getContentNode, getContent, setContent, setMeta, setMetaBatch, addSection, deleteSection }; +export { getContentNode, getContent, setContent, replaceContent, setMeta, setMetaBatch, addSection, deleteSection }; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.d.ts index 285cce376..121bc30e4 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.d.ts @@ -2,6 +2,9 @@ interface Options { method: string; parameters?: any; } +export interface RPCResponse { + result: T; +} declare const executeRemoteCall: (options: Options) => Promise; declare const executeRemoteMethodCall: (method: string, parameters: any) => Promise; export { executeRemoteCall, executeRemoteMethodCall }; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.js b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.js index 9974e2a6d..d1598ff2a 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/rpc/rpc.js @@ -40,7 +40,7 @@ const executeRemoteMethodCall = async (method, parameters) => { if (response.status === 403) { alert(i18n.t("ui.redirect.login", "You where logged out due to inactivity. Please log in again.")); window.location.href = window.manager.baseUrl + "/login"; - return; + throw new Error("Unauthorized"); } return await response.json(); }; diff --git a/modules/ui-module/src/main/ts/dist/js/modules/state.js b/modules/ui-module/src/main/ts/dist/js/modules/state.js index a0988236f..7274c0b66 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/state.js +++ b/modules/ui-module/src/main/ts/dist/js/modules/state.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts b/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts index d064b5d45..cf3faa9ae 100644 --- a/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts +++ b/modules/ui-module/src/main/ts/dist/js/modules/ui-state.d.ts @@ -1,10 +1,10 @@ export namespace UIStateManager { function setTabState(key: any, value: any): void; - function getTabState(key: any, defaultValue?: null): any; + function getTabState(key: any, defaultValue?: any): any; function setLocale(locale: any): void; function getLocale(): any; function removeTabState(key: any): void; function setAuthToken(token: any): void; - function getAuthToken(): string | null; + function getAuthToken(): string; function clearAuthToken(): void; } diff --git a/modules/ui-module/src/main/ts/dist/public/manager-login.js b/modules/ui-module/src/main/ts/dist/public/manager-login.js index 24aa077ae..6f21eee99 100644 --- a/modules/ui-module/src/main/ts/dist/public/manager-login.js +++ b/modules/ui-module/src/main/ts/dist/public/manager-login.js @@ -1,4 +1,3 @@ -"use strict"; /*- * #%L * UI Module diff --git a/modules/ui-module/src/main/ts/src/actions/media/select-content-media.ts b/modules/ui-module/src/main/ts/src/actions/media/select-content-media.ts new file mode 100644 index 000000000..2ba89afd7 --- /dev/null +++ b/modules/ui-module/src/main/ts/src/actions/media/select-content-media.ts @@ -0,0 +1,88 @@ +/*- + * #%L + * UI Module + * %% + * Copyright (C) 2023 - 2026 CondationCMS + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * #L% + */ + +import { openFileBrowser } from "@cms/modules/filebrowser.js"; +import { i18n } from "@cms/modules/localization.js"; +import { getPreviewUrl, reloadPreview } from "@cms/modules/preview.utils.js"; +import { getContentNode, replaceContent, ReplaceContentOptions } from "@cms/modules/rpc/rpc-content.js"; +import { showToast } from "@cms/modules/toast.js"; + +export async function runAction(params : any) { + + + var uri : any = null + if (params.options.uri) { + uri = params.options.uri + } else { + const contentNode = await getContentNode({ + url: getPreviewUrl() + }) + uri = contentNode.result.uri + } + + openFileBrowser({ + type: "assets", + filter : (file: any) => { + return file.media || file.directory; + }, + onSelect: async (file: any) => { + + if (file && file.uri) { + + var selectedFile = file.uri; // Use the file's URI + if (file.uri.startsWith("/")) { + selectedFile = file.uri.substring(1); // Remove leading slash if present + } + + var updateData : any = {} + updateData[params.options.metaElement] = { + type: 'media', + value: selectedFile + } + var options: ReplaceContentOptions = { + uri : uri, + content: `![](${selectedFile}?format=small)`, + start: params.options.start, + end: params.options.end + } + + var replaceMedia = await replaceContent(options) + if (replaceMedia.result.error != null && replaceMedia.result.error === true) { + showToast({ + title: i18n.t('manager.actions.media.select-content-media.toast.title-error', "Media not updated"), + message: i18n.t('manager.actions.media.select-content-media.toast.message-error', "New media has not been updated successfully."), + type: 'error', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview() + } else { + showToast({ + title: i18n.t('manager.actions.media.select-media.toast.title', "Media updated"), + message: i18n.t('manager.actions.media.select-media.toast.message', "New media has been updated successfully."), + type: 'success', // optional: info | success | warning | error + timeout: 3000 + }); + reloadPreview() + } + } + } + }) +} diff --git a/modules/ui-module/src/main/ts/src/js/manager.js b/modules/ui-module/src/main/ts/src/js/manager.js index 9aa41e0bb..f0a3c73ca 100644 --- a/modules/ui-module/src/main/ts/src/js/manager.js +++ b/modules/ui-module/src/main/ts/src/js/manager.js @@ -49,7 +49,7 @@ document.addEventListener("DOMContentLoaded", function () { setInterval(() => { heartbeat(); - }, 5 *60 * 1000); + }, 10 * 60 * 1000); //PreviewHistory.init("/"); //updateStateButton(); diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts index 21428f095..712af5f3d 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/manager.message.handlers.ts @@ -50,6 +50,17 @@ const executeImageSelect = (payload: any) => { executeScriptAction(cmd); } +const executeContentImageReplace = (payload: any) => { + const cmd: any = { + "module": window.manager.baseUrl + "/actions/media/select-content-media", + "function": "runAction", + "parameters": { + "options": payload.options ? payload.options : {} + } + } + executeScriptAction(cmd); +} + const initMessageHandlers = () => { frameMessenger.on('preview:reload', (payload: any) => { @@ -89,6 +100,8 @@ const initMessageHandlers = () => { executeImageForm(payload); } else if (payload.element === "image" && payload.editor === "select") { executeImageSelect(payload); + } else if (payload.element === "image" && payload.editor === "replace") { + executeContentImageReplace(payload); } else if (payload.element === "image" && payload.editor === "focal-point") { var cmd: any = { "module": window.manager.baseUrl + "/actions/media/edit-focal-point", diff --git a/modules/ui-module/src/main/ts/src/js/modules/manager/media.inject.ts b/modules/ui-module/src/main/ts/src/js/modules/manager/media.inject.ts index 9ee020842..9887d106e 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/manager/media.inject.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/manager/media.inject.ts @@ -107,9 +107,12 @@ export const initContentMediaToolbar = (img: HTMLImageElement) => { var toolbarDefinition = { "options": { - "uri": parentToolbarDef.uri + "uri": parentToolbarDef.uri, + "start": img.dataset.cmsMdStart || null, + "end": img.dataset.cmsMdEnd || null }, "actions": [ + "replace", "meta", "focalPoint" ] @@ -142,6 +145,15 @@ export const initToolbar = (img: HTMLImageElement, toolbarDefinition: any) => { }); toolbar.appendChild(selectButton); } + if (toolbarDefinition.actions.includes('replace')) { + const replaceButton = document.createElement('button'); + replaceButton.innerHTML = IMAGE_ICON; + replaceButton.setAttribute("title", "Replace media"); + replaceButton.addEventListener('click', (event) => { + replaceMedia(toolbarDefinition.options.start, toolbarDefinition.options.end, toolbarDefinition.options.element, toolbarDefinition.options.uri); + }); + toolbar.appendChild(replaceButton); + } if (toolbarDefinition.actions.includes('meta')) { const metaButton = document.createElement('button'); metaButton.setAttribute('data-cms-action', 'editMediaForm'); @@ -203,6 +215,23 @@ export const initToolbar = (img: HTMLImageElement, toolbarDefinition: any) => { }); }; +const replaceMedia = (start : number, end : number, metaElement?: string, uri?: string) => { + var command = { + type: 'edit', + payload: { + editor: "replace", + element: "image", + options: { + metaElement: metaElement, + uri: uri, + start: start, + end: end + } + } + } + frameMessenger.send(window.parent, command); +} + const selectMedia = (metaElement?: string, uri?: string) => { var command = { type: 'edit', diff --git a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-content.ts b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-content.ts index 73891e88b..e1535ca11 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-content.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc-content.ts @@ -19,7 +19,7 @@ * #L% */ -import { executeRemoteCall } from '@cms/modules/rpc/rpc.js' +import { executeRemoteCall, RPCResponse } from '@cms/modules/rpc/rpc.js' const getContentNode = async (options : any) => { var data = { @@ -45,6 +45,26 @@ const setContent = async (options : any) => { return await executeRemoteCall(data); }; +export interface ReplaceContent { + error: boolean | null; + uri: string; +} + +export interface ReplaceContentOptions { + uri: string; + content: string; + start: number; + end: number; +} + +const replaceContent = async (options : ReplaceContentOptions): Promise> => { + var data = { + method: "content.replace", + parameters: options + } + return await executeRemoteCall(data); +}; + const setMeta = async (options : any) => { var data = { method: "meta.set", @@ -77,4 +97,4 @@ const deleteSection = async (options : any) => { return await executeRemoteCall(data); }; -export { getContentNode, getContent, setContent, setMeta, setMetaBatch, addSection, deleteSection }; +export { getContentNode, getContent, setContent, replaceContent, setMeta, setMetaBatch, addSection, deleteSection }; diff --git a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts index f4db95b10..5b7eee770 100644 --- a/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts +++ b/modules/ui-module/src/main/ts/src/js/modules/rpc/rpc.ts @@ -27,7 +27,11 @@ interface Options { parameters?: any; } -const executeRemoteCall = async (options: Options) => { +export interface RPCResponse { + result: T; +} + +const executeRemoteCall = async (options: Options) => { return executeRemoteMethodCall(options.method, options.parameters); }; @@ -49,7 +53,7 @@ const executeRemoteMethodCall = async (method : string, parameters : any) => { if (response.status === 403) { alert(i18n.t("ui.redirect.login", "You where logged out due to inactivity. Please log in again.")); window.location.href = window.manager.baseUrl + "/login"; - return; + throw new Error("Unauthorized"); } return await response.json();
    "); - sb.append(inlineRenderer.render(header)); + sb.append(inlineRenderer.render(header, documentOffset)); sb.append("
    "); - sb.append(inlineRenderer.render(items)); + sb.append(inlineRenderer.render(items, documentOffset)); sb.append("