diff --git a/layout-service/src/main/java/io/meeds/layout/model/LayoutModel.java b/layout-service/src/main/java/io/meeds/layout/model/LayoutModel.java index d5ef7b54d..975c61950 100644 --- a/layout-service/src/main/java/io/meeds/layout/model/LayoutModel.java +++ b/layout-service/src/main/java/io/meeds/layout/model/LayoutModel.java @@ -22,6 +22,8 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -30,6 +32,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty.Access; +import org.apache.commons.lang3.StringUtils; + import org.exoplatform.portal.config.model.Application; import org.exoplatform.portal.config.model.ApplicationBackgroundStyle; import org.exoplatform.portal.config.model.ApplicationState; @@ -132,6 +136,14 @@ public class LayoutModel { private String appBackgroundAttachment; + private Integer appMarginTop; + + private Integer appMarginRight; + + private Integer appMarginBottom; + + private Integer appMarginLeft; + private String textTitleColor; private String textTitleFontSize; @@ -272,6 +284,42 @@ public LayoutModel(ModelObject model, PortletInstanceService portletInstanceServ init(model, portletInstanceService, portletInstanceContext); } + /** + * Application margins are custom properties on the platform scale where 20 + * means "no extra margin". An application whose cssClass still carries + * Vuetify spacing tokens in their cssClass, written either by + * the editors from a 0-neutral attribute or by ModelStyle at import from a + * 20-neutral page-XML attribute: the two attributes disagree, the tokens do + * not (mt-n1 rendered -4px in both). The token is therefore the source of a + * legacy margin: value = N x 4 + 20, and the tokens are stripped from the + * class exposed to every Vue consumer (view-time layout and editors). + */ + private static final Pattern SPACING_TOKEN_PATTERN = Pattern.compile("(^| )(mt|mr|mb|ml|ms|me)-((md|lg|xl)-)?(n?)(\\d{1,2})(?= |$)"); + + private static final int MARGIN_NEUTRAL = 20; + + private void convertLegacyMarginTokens() { + if (StringUtils.isBlank(this.cssClass)) { + return; + } + Matcher matcher = SPACING_TOKEN_PATTERN.matcher(this.cssClass); + boolean legacy = false; + while (matcher.find()) { + legacy = true; + int value = Integer.parseInt(matcher.group(6)) * 4 * ("n".equals(matcher.group(5)) ? -1 : 1) + MARGIN_NEUTRAL; + switch (matcher.group(2)) { + case "mt" -> this.marginTop = value; + case "mb" -> this.marginBottom = value; + case "me", "mr" -> this.marginRight = value; + default -> this.marginLeft = value; + } + } + if (legacy) { + String stripped = SPACING_TOKEN_PATTERN.matcher(this.cssClass).replaceAll(" ").replaceAll(" {2,}", " ").trim(); + this.cssClass = StringUtils.isBlank(stripped) ? null : stripped; + } + } + private void init(ModelObject model, PortletInstanceService portletInstanceService, PortletInstanceContext portletInstanceContext) { // NOSONAR ModelStyle cssStyle = model.getCssStyle(); if (cssStyle != null) { @@ -379,6 +427,10 @@ private void init(ModelObject model, PortletInstanceService portletInstanceServi this.appBackgroundSize = appCssStyle.getBackgroundSize(); this.appBackgroundRepeat = appCssStyle.getBackgroundRepeat(); this.appBackgroundAttachment = appCssStyle.getBackgroundAttachment(); + this.appMarginTop = appCssStyle.getMarginTop(); + this.appMarginRight = appCssStyle.getMarginRight(); + this.appMarginBottom = appCssStyle.getMarginBottom(); + this.appMarginLeft = appCssStyle.getMarginLeft(); } if (model instanceof Page page) { this.editPermission = page.getEditPermission(); @@ -400,6 +452,7 @@ private void init(ModelObject model, PortletInstanceService portletInstanceServi this.width = application.getWidth(); this.height = application.getHeight(); this.cssClass = application.getCssClass(); + convertLegacyMarginTokens(); this.showInfoBar = application.getShowInfoBar(); this.showApplicationState = application.getShowApplicationState(); this.showApplicationMode = application.getShowApplicationMode(); diff --git a/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java b/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java index 25db564c8..a2eea0aca 100644 --- a/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java +++ b/layout-service/src/main/java/io/meeds/layout/util/EntityMapper.java @@ -251,7 +251,11 @@ private static ModelStyle mapToStyle(LayoutModel layoutModel) { private static ApplicationBackgroundStyle mapToAppStyle(LayoutModel layoutModel) { ApplicationBackgroundStyle cssStyle = null; if (StringUtils.isNotBlank(layoutModel.getAppBackgroundColor()) - || StringUtils.isNotBlank(layoutModel.getAppBackgroundImage())) { + || StringUtils.isNotBlank(layoutModel.getAppBackgroundImage()) + || layoutModel.getAppMarginTop() != null + || layoutModel.getAppMarginRight() != null + || layoutModel.getAppMarginBottom() != null + || layoutModel.getAppMarginLeft() != null) { cssStyle = new ApplicationBackgroundStyle(); cssStyle.setBackgroundColor(layoutModel.getAppBackgroundColor()); cssStyle.setBackgroundImage(layoutModel.getAppBackgroundImage()); @@ -260,6 +264,10 @@ private static ApplicationBackgroundStyle mapToAppStyle(LayoutModel layoutModel) cssStyle.setBackgroundSize(layoutModel.getAppBackgroundSize()); cssStyle.setBackgroundRepeat(layoutModel.getAppBackgroundRepeat()); cssStyle.setBackgroundAttachment(layoutModel.getAppBackgroundAttachment()); + cssStyle.setMarginTop(layoutModel.getAppMarginTop()); + cssStyle.setMarginRight(layoutModel.getAppMarginRight()); + cssStyle.setMarginBottom(layoutModel.getAppMarginBottom()); + cssStyle.setMarginLeft(layoutModel.getAppMarginLeft()); } return cssStyle; } diff --git a/layout-service/src/test/java/io/meeds/layout/model/LayoutModelTest.java b/layout-service/src/test/java/io/meeds/layout/model/LayoutModelTest.java new file mode 100644 index 000000000..7e0d1bcd8 --- /dev/null +++ b/layout-service/src/test/java/io/meeds/layout/model/LayoutModelTest.java @@ -0,0 +1,93 @@ +/** + * This file is part of the Meeds project (https://meeds.io/). + * + * Copyright (C) 2020 - 2026 Meeds Association contact@meeds.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package io.meeds.layout.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +import org.exoplatform.portal.config.model.Application; +import org.exoplatform.portal.config.model.ModelStyle; +import org.exoplatform.portal.config.model.TransientApplicationState; + +/** + * Legacy application margins are read once, server-side, from + * the Vuetify spacing tokens still stored in the cssClass (value = N x 4 + 20 + * on the platform scale), whatever scale the stored attribute used, and the + * tokens are stripped from the class exposed to the Vue consumers. + */ +public class LayoutModelTest { + + @Test + public void shouldReadLegacyMarginsFromXmlImportedApplicationTokens() { + // page XML: 20-neutral attribute next to the tokens ModelStyle generated at import (mt-n1 = -4px) + LayoutModel model = new LayoutModel(application(16, 8, 4, 0, "mt-n1 mb-n3 me-n4 ms-n5 TEST-class")); + assertEquals(16, model.getMarginTop()); + assertEquals(8, model.getMarginBottom()); + assertEquals(4, model.getMarginRight()); + assertEquals(0, model.getMarginLeft()); + assertEquals("TEST-class", model.getCssClass()); + } + + @Test + public void shouldReadLegacyMarginsFromEditorSavedApplicationTokens() { + // editor before 7.3.0.30: 0-neutral attribute next to the tokens the editor wrote for the same -4px + LayoutModel model = new LayoutModel(application(-4, -12, -16, -20, "mt-n1 mb-n3 me-n4 ms-n5")); + assertEquals(16, model.getMarginTop()); + assertEquals(8, model.getMarginBottom()); + assertEquals(4, model.getMarginRight()); + assertEquals(0, model.getMarginLeft()); + assertNull(model.getCssClass()); + } + + @Test + public void shouldKeepPlatformScaleMarginsWithoutTokens() { + LayoutModel model = new LayoutModel(application(32, 20, null, null, "brtr-2 TEST-class")); + assertEquals(32, model.getMarginTop()); + assertEquals(20, model.getMarginBottom()); + assertNull(model.getMarginRight()); + // Application.getCssClass() prefixes the style tokens with a space (pre-existing): only the content matters here + assertEquals("brtr-2 TEST-class", model.getCssClass().trim()); + } + + @Test + public void shouldReadPositiveAndBreakpointTokens() { + LayoutModel model = new LayoutModel(application(null, null, null, null, "mt-3 mb-0 mr-md-2 ml-1")); + assertEquals(32, model.getMarginTop()); + assertEquals(20, model.getMarginBottom()); + assertEquals(28, model.getMarginRight()); + assertEquals(24, model.getMarginLeft()); + assertNull(model.getCssClass()); + } + + private Application application(Integer top, Integer bottom, Integer right, Integer left, String cssClass) { + Application application = new Application("storageId"); + application.setState(new TransientApplicationState("layout/Test")); + ModelStyle style = new ModelStyle(); + style.setMarginTop(top); + style.setMarginBottom(bottom); + style.setMarginRight(right); + style.setMarginLeft(left); + application.setCssStyle(style); + application.setCssClass(cssClass); + return application; + } + +} diff --git a/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties b/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties index 8d494d351..0f49e6e71 100644 --- a/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties +++ b/layout-webapp/src/main/resources/locale/portlet/LayoutEditor_en.properties @@ -50,26 +50,7 @@ layout.pageDraftSavedSuccessfully=Page draft saved successfully layout.pageOutdatedError=The current page version seems to be outdated. Please try again by refreshing the page or check if another session is ongoing (another browser tab) or contact the support services. layout.switchToDesktop=Switch to Desktop View layout.switchToMobile=Switch to Mobile View -layout.margins=Margins -layout.top=Top -layout.right=Right -layout.bottom=Bottom -layout.left=Left -layout.border=Border -layout.borderColor=Color -layout.borderSize=Size -layout.boxShadow=Shadow -layout.borderRadius=Radius -layout.ok=Ok layout.view=View -layout.sameForAllSides=Same for all sides -layout.differentForEachSide=Different for each side -layout.sameForAllCorners=Same for all corners -layout.differentForEachCorner=Different for each corner -layout.topRight=Top-Right -layout.topLeft=Top-Left -layout.bottomRight=Bottom-Right -layout.bottomLeft=Bottom-Left layout.chooseATemplate=Choose a template layout.fixedSectionTypeChoice=Fixed Section layout.dynamicSectionTypeChoice=Dynamic Section @@ -397,42 +378,13 @@ portletInstance.label.close=Close layout.editPortletInstance=Edit portlet instance {0} -layout.uploadBackgroundImageTitle=Upload a background image -layout.deleteBackgroundImageTitle=Delete the background image layout.background=Background layout.backgroundColor=Background color layout.backgroundImage=Background image layout.image=Image -layout.imageSizeTitle=Size -layout.imageSizeCover=Fill, cropped -layout.imageSizeContain=Fit, whole image -layout.imageRepeatTitle=Repeat -layout.imageRepeat=Repeat -layout.imageNoRepeat=No repeat -layout.imageScrollingTitle=Scrolling -layout.imageScrollingFixed=Stays fixed -layout.imageScrollingScroll=Scrolls with page layout.editPageProperties=Edit page design layout.fullWindow=Full window -layout.imagePositionTitle=Position -layout.imagePositionCenter=Center -layout.imagePositionTopLeft=Top left -layout.imagePositionTopRight=Top right -layout.imagePositionBottomLeft=Bottom left -layout.imagePositionBottomRight=Bottom right layout.gradient=Gradient -layout.gradientFrom=From -layout.gradientTo=To -layout.linearGradient=Linear Gradient -layout.radialGradient=Radial Gradient -layout.angularGradient=Angular Gradient -layout.gradientDirectionTopToBottom=Top to Bottom -layout.gradientDirectionLeftToRight=Left to Right -layout.gradientCornerTopLeft=Top Left -layout.gradientCornerTopRight=Top Right -layout.gradientCornerBottomRight=Bottom Right -layout.gradientCornerBottomLeft=Bottom Left -layout.gradientRatio=Ratio layout.saveDraft=Save layout.saveDraftTooltip=Save page as draft layout.saveSiteDraftTooltip=Save site as draft @@ -449,13 +401,6 @@ layout.draftConfirmQuestion=Do you want to resume the draft? layout.draftConfirmOkLabel=Resume Draft layout.draftConfirmCancelLabel=Create New layout.pageBeingEditedByYouInOther=This version of the page is outdated and has been replaced by another one. -layout.textStyle=Text -layout.textColorTitle=Title -layout.textColorHeader=Header -layout.textColorBody=Body -layout.textColorSubtitle=Subtitle -layout.textBackground=Text Background -layout.color=Solid Color layout.applicationStyling=Application Styling layout.globalPageDesign=Global Page Design layout.globalPageBackground=Page Background @@ -665,7 +610,5 @@ layout.adjustPortalPageWidth=Adjust Page width layout.editSiteLayout=Edit Site Layout layout.editSiteNavigation=Edit Site Navigation layout.pageMargins=Page Margins -layout.scrollTopColor=Top -layout.scrollMiddleColor=On scroll layout.siteDesign=Site Design layout.siteBackground=Site Background diff --git a/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml b/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml index 0561a4237..9b6fb0a56 100644 --- a/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml +++ b/layout-webapp/src/main/webapp/WEB-INF/gatein-resources.xml @@ -34,6 +34,7 @@ LayoutEditor Enterprise LayoutEditor + StylingInputs @@ -41,6 +42,7 @@ SiteLayoutEditor Enterprise LayoutEditor + StylingInputs @@ -48,6 +50,7 @@ SectionEditor Enterprise LayoutEditor + StylingInputs @@ -55,6 +58,28 @@ SectionTemplates Enterprise LayoutEditor + StylingInputs + + + + layout + PageTemplatesManagement + Enterprise + StylingInputs + + + + layout + Portlets + Enterprise + StylingInputs + + + + layout + PortletEditor + Enterprise + StylingInputs @@ -65,6 +90,9 @@ commonVueComponents + + stylingUtils + translationField @@ -200,6 +228,9 @@ commonLayoutComponents + + stylingInputs + vueDraggable @@ -244,6 +275,9 @@ commonLayoutComponents + + stylingInputs + vueDraggable @@ -288,6 +322,9 @@ commonLayoutComponents + + stylingInputs + vueDraggable @@ -395,6 +432,9 @@ commonLayoutComponents + + stylingInputs + attachImage @@ -426,6 +466,9 @@ commonLayoutComponents + + stylingInputs + attachImage @@ -472,6 +515,9 @@ commonLayoutComponents + + stylingInputs + attachImage @@ -541,6 +587,9 @@ commonLayoutComponents + + stylingInputs + attachImage diff --git a/layout-webapp/src/main/webapp/skin/less/editor.less b/layout-webapp/src/main/webapp/skin/less/editor.less index 723462cd7..5abae746a 100644 --- a/layout-webapp/src/main/webapp/skin/less/editor.less +++ b/layout-webapp/src/main/webapp/skin/less/editor.less @@ -117,16 +117,6 @@ background-size: 4px 4px !important; } -.layout-color-picker-swatch { - background-image: - linear-gradient(45deg, #bbb 25%, transparent 25%), - linear-gradient(-45deg, #bbb 25%, transparent 25%), - linear-gradient(45deg, transparent 75%, #bbb 75%), - linear-gradient(-45deg, transparent 75%, #bbb 75%); - background-size: 10px 10px; - background-position: 0 0, 0 5px, 5px -5px, -5px 0; -} - .layout-section { .layout-section-hover { diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/ContainerBase.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/ContainerBase.vue index e263bbbdd..b04ebd3fa 100644 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/ContainerBase.vue +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/base/ContainerBase.vue @@ -132,6 +132,12 @@ export default { type: Boolean, default: false, }, + // Page editor: the page container is rendered with the site style but must preview the applications' default + // margins set in Edit Page (appMargin*), as the view-time page style does + pageAppMargins: { + type: Boolean, + default: false, + }, pageStyle: { type: Boolean, default: false, @@ -199,6 +205,7 @@ export default { noBackgroundStyle: this.noBackgroundStyle, siteStyle: this.siteStyle, pageStyle: this.pageStyle, + pageAppMargins: this.pageAppMargins, sectionStyle: this.sectionStyle, appStyle: !this.noApplicationStyle, dynamicWidth: this.dynamicWidth, diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Application.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Application.vue index 567010eef..2aa818652 100644 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Application.vue +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/content/container/Application.vue @@ -227,6 +227,7 @@ export default { isApplicationBackground: true, isApplicationScroll: true, appStyle: true, + applicationMargins: true, }); }, moveEnd() { diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditPageDrawer.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditPageDrawer.vue index cfaa90b7d..dc782a350 100644 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditPageDrawer.vue +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/components/drawer/EditPageDrawer.vue @@ -140,6 +140,13 @@ {{ $t('layout.applicationStyling') }} + - - \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundInput.vue deleted file mode 100644 index 189edc383..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundInput.vue +++ /dev/null @@ -1,605 +0,0 @@ - - - \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundMarginInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundMarginInput.vue deleted file mode 100644 index ad25db563..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundMarginInput.vue +++ /dev/null @@ -1,227 +0,0 @@ - - - diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundRadiusInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundRadiusInput.vue deleted file mode 100644 index ae2475136..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BackgroundRadiusInput.vue +++ /dev/null @@ -1,227 +0,0 @@ - - - diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderInput.vue deleted file mode 100644 index c3d4fee13..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderInput.vue +++ /dev/null @@ -1,135 +0,0 @@ - - - \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusInput.vue deleted file mode 100644 index 7b194dc47..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusInput.vue +++ /dev/null @@ -1,176 +0,0 @@ - - - \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusSelector.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusSelector.vue deleted file mode 100644 index 9c1844b5b..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/BorderRadiusSelector.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/ColorPicker.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/ColorPicker.vue deleted file mode 100644 index f4fd462f4..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/ColorPicker.vue +++ /dev/null @@ -1,133 +0,0 @@ - - - diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/MarginInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/MarginInput.vue deleted file mode 100644 index 9335d47e3..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/MarginInput.vue +++ /dev/null @@ -1,204 +0,0 @@ - - - \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/SectionMarginInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/SectionMarginInput.vue deleted file mode 100644 index 1b0d5cb8a..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/SectionMarginInput.vue +++ /dev/null @@ -1,198 +0,0 @@ - - - \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/TextBackgroundInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/TextBackgroundInput.vue deleted file mode 100644 index ae5c94b88..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/TextBackgroundInput.vue +++ /dev/null @@ -1,505 +0,0 @@ - - - diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/TextInput.vue b/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/TextInput.vue deleted file mode 100644 index b79aa8c8d..000000000 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/components/form/TextInput.vue +++ /dev/null @@ -1,395 +0,0 @@ - - - \ No newline at end of file diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/initComponents.js b/layout-webapp/src/main/webapp/vue-app/common-layout/initComponents.js index a628a6a9c..100d3e787 100644 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/initComponents.js +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/initComponents.js @@ -18,18 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import BorderRadiusSelector from './components/form/BorderRadiusSelector.vue'; -import ColorPicker from './components/form/ColorPicker.vue'; -import BackgroundImageAttachment from './components/form/BackgroundImageAttachment.vue'; -import BackgroundInput from './components/form/BackgroundInput.vue'; -import BackgroundMarginInput from './components/form/BackgroundMarginInput.vue'; -import BackgroundRadiusInput from './components/form/BackgroundRadiusInput.vue'; -import TextInput from './components/form/TextInput.vue'; -import TextBackgroundInput from './components/form/TextBackgroundInput.vue'; -import MarginInput from './components/form/MarginInput.vue'; -import SectionMarginInput from './components/form/SectionMarginInput.vue'; -import BorderInput from './components/form/BorderInput.vue'; -import BorderRadiusInput from './components/form/BorderRadiusInput.vue'; import SectionTemplate from './components/form/SectionTemplate.vue'; import ContainerExtension from './components/content/base/ContainerExtension.vue'; @@ -62,9 +50,23 @@ import EditSectionDrawer from './components/drawer/EditSectionDrawer.vue'; import EditPortletDialog from './components/dialog/EditPortletDialog.vue'; +// The styling inputs live in social's shared 'stylingInputs' module, a dependency of the editor +// portlets only (the view-time page renderer loads this module too and must not download them). They stay +// reachable under their historical layout-editor-* tags through async components resolved at first mount. +function sharedStylingInput(tag) { + return (resolve, reject) => { + const component = Vue.options.components[tag]; + if (component) { + resolve(component); + } else { + reject(new Error(`Shared styling input '${tag}' is not registered: the stylingInputs module must be loaded by this portlet`)); + } + }; +} + const components = { - 'layout-editor-color-picker': ColorPicker, - 'layout-editor-border-radius-selector': BorderRadiusSelector, + 'layout-editor-color-picker': sharedStylingInput('styling-color-picker'), + 'layout-editor-border-radius-selector': sharedStylingInput('styling-border-radius-selector'), 'layout-editor-container': Container, 'layout-editor-container-extension': ContainerExtension, 'layout-editor-container-base': ContainerBase, @@ -85,16 +87,16 @@ const components = { 'layout-editor-page-edit-drawer': EditPageDrawer, 'layout-editor-section-edit-drawer': EditSectionDrawer, 'layout-editor-portlet-edit-dialog': EditPortletDialog, - 'layout-editor-background-image-attachment': BackgroundImageAttachment, - 'layout-editor-background-input': BackgroundInput, - 'layout-editor-background-margin-input': BackgroundMarginInput, - 'layout-editor-background-radius-input': BackgroundRadiusInput, - 'layout-editor-text-input': TextInput, - 'layout-editor-text-background-input': TextBackgroundInput, - 'layout-editor-margin-input': MarginInput, - 'layout-editor-section-margin-input': SectionMarginInput, - 'layout-editor-border-input': BorderInput, - 'layout-editor-border-radius-input': BorderRadiusInput, + 'layout-editor-background-image-attachment': sharedStylingInput('styling-background-image-attachment'), + 'layout-editor-background-input': sharedStylingInput('styling-background-input'), + 'layout-editor-background-margin-input': sharedStylingInput('styling-background-margin-input'), + 'layout-editor-background-radius-input': sharedStylingInput('styling-background-radius-input'), + 'layout-editor-text-input': sharedStylingInput('styling-text-input'), + 'layout-editor-text-background-input': sharedStylingInput('styling-text-background-input'), + 'layout-editor-margin-input': sharedStylingInput('styling-margin-input'), + 'layout-editor-section-margin-input': sharedStylingInput('styling-section-margin-input'), + 'layout-editor-border-input': sharedStylingInput('styling-border-input'), + 'layout-editor-border-radius-input': sharedStylingInput('styling-border-radius-input'), 'layout-editor-application-card': ApplicationCard, 'layout-editor-application-category-card': ApplicationCategoryCard, 'layout-editor-application-menu': ApplicationMenu, diff --git a/layout-webapp/src/main/webapp/vue-app/common-layout/js/LayoutUtils.js b/layout-webapp/src/main/webapp/vue-app/common-layout/js/LayoutUtils.js index 728e79a7d..758d6aac6 100644 --- a/layout-webapp/src/main/webapp/vue-app/common-layout/js/LayoutUtils.js +++ b/layout-webapp/src/main/webapp/vue-app/common-layout/js/LayoutUtils.js @@ -95,6 +95,10 @@ export const containerModel = { appBackgroundSize: null, appBackgroundRepeat: null, appBackgroundAttachment: null, + appMarginTop: null, + appMarginRight: null, + appMarginBottom: null, + appMarginLeft: null, textTitleColor: null, textTitleFontSize: null, textTitleFontWeight: null, @@ -354,15 +358,10 @@ export function applyContainerStyle(container, containerStyle) { Vue.set(container, 'marginBottom', containerStyle.marginBottom === 0 || containerStyle.marginBottom ? containerStyle.marginBottom : null); Vue.set(container, 'marginLeft', containerStyle.marginLeft === 0 || containerStyle.marginLeft ? containerStyle.marginLeft : null); } else { + // margins are rendered through --appMargin* custom properties from the stored margin attributes; + // the Vuetify spacing classes older layouts carry are stripped (and never written again) container.cssClass = container.cssClass.replace(new RegExp('(^| )(mt|mr|mb|ml|ms|me)-((md|lg|xl)-)?n?[0-9]{1,2}', 'g'), '').replace(/ +/g, ' '); - if (containerStyle.marginTop === 0 || containerStyle.marginTop) { - container.cssClass += ` mt-${containerStyle.marginTop >= 0 ? '' : 'n'}${Math.abs(parseInt(Math.max(-64, Math.min(containerStyle.marginTop || 0, 64)) / 4))}`; - container.cssClass += ` me-${containerStyle.marginRight >= 0 ? '' : 'n'}${Math.abs(parseInt(Math.max(-64, Math.min(containerStyle.marginRight || 0, 64)) / 4))}`; - container.cssClass += ` mb-${containerStyle.marginBottom >= 0 ? '' : 'n'}${Math.abs(parseInt(Math.max(-64, Math.min(containerStyle.marginBottom || 0, 64)) / 4))}`; - container.cssClass += ` ms-${containerStyle.marginLeft >= 0 ? '' : 'n'}${Math.abs(parseInt(Math.max(-64, Math.min(containerStyle.marginLeft || 0, 64)) / 4))}`; - } } - container.cssClass = container.cssClass.replace(new RegExp('(^| )(brtr|brtl|brbr|brbl)-[0-9]', 'g'), '').replace(/ +/g, ' '); Vue.set(container, 'cssClass', container.cssClass); @@ -432,6 +431,10 @@ export function applyContainerStyle(container, containerStyle) { Vue.set(container, 'appBackgroundSize', containerStyle.appBackgroundSize || null); Vue.set(container, 'appBackgroundRepeat', containerStyle.appBackgroundRepeat || null); Vue.set(container, 'appBackgroundAttachment', containerStyle.appBackgroundAttachment || null); + Vue.set(container, 'appMarginTop', containerStyle.appMarginTop === 0 ? 0 : containerStyle.appMarginTop || null); + Vue.set(container, 'appMarginRight', containerStyle.appMarginRight === 0 ? 0 : containerStyle.appMarginRight || null); + Vue.set(container, 'appMarginBottom', containerStyle.appMarginBottom === 0 ? 0 : containerStyle.appMarginBottom || null); + Vue.set(container, 'appMarginLeft', containerStyle.appMarginLeft === 0 ? 0 : containerStyle.appMarginLeft || null); Vue.set(container, 'backgroundColor', containerStyle.backgroundColor || null); Vue.set(container, 'backgroundImage', containerStyle.backgroundImage || null); Vue.set(container, 'backgroundEffect', containerStyle.backgroundEffect || null); @@ -1025,18 +1028,8 @@ export function parseContainerStyle(container) { && container.template !== sidebarTemplate && container.template !== pageBodyTemplate && container.template !== sectionsParentTemplate) { - const marginMatches = container?.cssClass?.match?.(new RegExp('(^| )(mt|mr|mb|ml|ms|me)-((md|lg|xl)-)?n?[0-9]{1,2}', 'g')) || []; - if (marginMatches?.length) { - container.marginTop = parseInt(marginMatches.find(c => c.search(/mt-n?\d+/) >= 0)?.replace?.('mt-n', '-')?.replace?.('mt-', '') || 0) * 4; - container.marginRight = parseInt(marginMatches.find(c => c.search(/me-n?\d+/) >= 0)?.replace?.('me-n', '-')?.replace?.('me-', '') || 0) * 4; - container.marginBottom = parseInt(marginMatches.find(c => c.search(/mb-n?\d+/) >= 0)?.replace?.('mb-n', '-')?.replace?.('mb-', '') || 0) * 4; - container.marginLeft = parseInt(marginMatches.find(c => c.search(/ms-n?\d+/) >= 0)?.replace?.('ms-n', '-')?.replace?.('ms-', '') || 0) * 4; - } else { - container.marginTop = null; - container.marginRight = null; - container.marginBottom = null; - container.marginLeft = null; - } + // margins live in the margin attributes only, on the platform scale (20 = no extra margin); + // a legacy application is converted once, server-side, by LayoutModel, so the attributes are the source of truth } const radiusMatches = container?.cssClass?.match?.(new RegExp('(^| )(brtr|brtl|brbr|brbl)-[0-9]', 'g')) || []; diff --git a/layout-webapp/src/main/webapp/vue-app/common/js/ApplicationUtils.js b/layout-webapp/src/main/webapp/vue-app/common/js/ApplicationUtils.js index 3536c1477..4810860f3 100644 --- a/layout-webapp/src/main/webapp/vue-app/common/js/ApplicationUtils.js +++ b/layout-webapp/src/main/webapp/vue-app/common/js/ApplicationUtils.js @@ -27,6 +27,24 @@ export function installApplication(navUri, applicationStorageId, applicationElem .then(applicationContent => handleApplicationContent(applicationContent, applicationElement, applicationMode)); } +const GRADIENT_COLOR_PATTERN = /#[0-9a-fA-F]{3,8}|rgba?\([^)]*\)/; + +export function isTopBanner(container) { + return !!container?.cssClass?.includes?.('layout-banner-top-section'); +} + +export function firstGradientColor(effect) { + const match = effect?.match?.(GRADIENT_COLOR_PATTERN); + return match ? match[0] : null; +} + +export function isTransparentColor(color) { + return color === 'transparent' || (color?.length === 9 && color.toUpperCase().endsWith('00')); +} + +// Margins are rendered through --appMargin* custom properties (getStyle); the Vuetify spacing classes that +// older layouts stored for the same margins are dropped at render time so that they do not apply twice + export function getStyle(container, options) { const style = {}; const backgroundLayerValues = !options.noBackgroundStyle && parseBackgroundLayerValues(container.cssClass); @@ -298,14 +316,17 @@ export function getStyle(container, options) { style['--appWidthScroll'] = 'auto'; } } + // Application and page levels write the applications' border variables (a page's "Application styling > Border" + // is the default of its applications, inherited by custom property); sections paint their own border + const applicationBorder = options.appStyle || options.pageStyle; if (container.borderColor) { - style[options.appStyle && '--appBorderColor' || 'border-color'] = container.borderColor; + style[applicationBorder && '--appBorderColor' || 'border-color'] = container.borderColor; if (container.borderSize) { - style[options.appStyle && '--appBorderSize' || 'border-size'] = `${container.borderSize}px`; + style[applicationBorder && '--appBorderSize' || 'border-size'] = `${container.borderSize}px`; } } if (container.boxShadow === 'true') { - style[options.appStyle && '--appBoxShadow' || 'box-shadow'] = '0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 0px 1px 8px 0px rgba(0, 0, 0, 0.12)'; + style[applicationBorder && '--appBoxShadow' || 'box-shadow'] = '0px 3px 3px -2px rgba(0, 0, 0, 0.2), 0px 3px 4px 0px rgba(0, 0, 0, 0.14), 0px 1px 8px 0px rgba(0, 0, 0, 0.12)'; } } if (!options.noBackgroundStyle @@ -330,9 +351,18 @@ export function getStyle(container, options) { style['--sectionBackgroundColorScroll'] = colors[1]; } else { style['background-color'] = container.backgroundColor; - if (container.cssClass?.includes?.('layout-sticky-section')) { - style['--sectionBackgroundColorScroll'] = container.backgroundColor; - } + // Colour kept once the section is fixed on scroll: the section's own, whether it is fixed by its + // own option or by the platform-wide Topbar default (body.topbar-sticky); a no-op when nothing is sticky + style['--sectionBackgroundColorScroll'] = container.backgroundColor; + } + if (isTopBanner(container) && container.backgroundEffect) { + // The Topbar background offers no gradient option. A stored gradient is ignored, never + // rewritten: its first colour is shown, otherwise the platform Topbar colour + const fallbackColor = firstGradientColor(container.backgroundEffect) + || (container.backgroundColor && !isTransparentColor(container.backgroundColor) && container.backgroundColor) + || 'var(--allPagesTopBarBackgroundColor)'; + style['background-color'] = fallbackColor; + style['--sectionBackgroundColorScroll'] = fallbackColor; } } } else if (container.backgroundEffect || container.backgroundImage) { @@ -347,8 +377,9 @@ export function getStyle(container, options) { } } - if (container.backgroundEffect && container.backgroundImage) { - style[options.isApplicationBackground && '--appBackgroundImage' || 'background-image'] = `url(${container.backgroundImage}),${container.backgroundEffect}`; + const backgroundEffect = isTopBanner(container) ? null : container.backgroundEffect; + if (backgroundEffect && container.backgroundImage) { + style[options.isApplicationBackground && '--appBackgroundImage' || 'background-image'] = `url(${container.backgroundImage}),${backgroundEffect}`; } else if (container.backgroundImage) { if (options.siteStyle) { document.body.style.setProperty('--allPagesBackgroundImage', `url(${container.backgroundImage})`); @@ -357,7 +388,7 @@ export function getStyle(container, options) { } else { style[options.isApplicationBackground && '--appBackgroundImage' || 'background-image'] = `url(${container.backgroundImage})`; } - } else if (container.backgroundEffect) { + } else if (backgroundEffect) { if (options.siteStyle) { document.body.style.setProperty('--allPagesBackgroundImage', container.backgroundEffect); } else if (options.pageStyle) { @@ -451,6 +482,23 @@ export function getStyle(container, options) { document.body.style.removeProperty('--allPagesBackgroundAttachment'); siteBackgroundOverridden = false; } + // Margins are custom properties at every level, on the platform scale (20 = no extra margin), rendered by one + // skin rule: calc(var(--appMargin*, var(--allPagesAppMargin*, 20px)) - 20px). The stored attribute travels as is: + // a legacy application (spacing tokens) is converted once, server-side, by LayoutModel before it reaches here. + // Page level (pageStyle): default margins of the applications of the page (appMargin*), inherited by every + // application below. Application level (applicationMargins, set by the application renderers only): its own + // margins (margin*), on its own element. Sections, cells and the site container carry margin* fields of their + // own scale and must not write --appMargin*, which their applications would inherit. + // pageAppMargins: the page editor renders the page container with the site style and passes this option so that + // the Edit Page margins preview there as they render at view time (pageStyle). + if (options.applicationMargins || options.pageStyle || options.pageAppMargins) { + ['Top', 'Right', 'Bottom', 'Left'].forEach(side => { + const margin = options.applicationMargins ? container[`margin${side}`] : container[`appMargin${side}`]; + if (margin === 0 || margin) { + style[`--appMargin${side}`] = `${Number(margin)}px`; + } + }); + } if (container.appBackgroundColor) { style['--appBackgroundColor'] = container.appBackgroundColor; } @@ -490,51 +538,14 @@ export function getStyle(container, options) { return style; } -// Background margin/radius (EXIP-88427) can't be stored as dedicated ModelStyle -// fields since ModelStyle is an external org.exoplatform.portal class we can't -// extend from this repo, so the 8 values are opaquely encoded as cssClass tokens -// (same pattern already used to mirror marginTop/radiusTopRight into mt-/brtr- -// classes, under a prefix that can't collide with those). -const BACKGROUND_LAYER_TOKEN_PREFIXES = { - marginTop: 'layout-bg-margin-top', - marginRight: 'layout-bg-margin-right', - marginBottom: 'layout-bg-margin-bottom', - marginLeft: 'layout-bg-margin-left', - radiusTopRight: 'layout-bg-radius-tr', - radiusTopLeft: 'layout-bg-radius-tl', - radiusBottomRight: 'layout-bg-radius-br', - radiusBottomLeft: 'layout-bg-radius-bl', -}; - +// The cssClass token helpers of the background layer (EXIP-88427) live in social's shared 'stylingUtils' +// module (Vue.prototype.$stylingUtils), consumed by the styling inputs and by this renderer export function parseBackgroundLayerValues(cssClass) { - if (!cssClass) { - return null; - } - const values = {}; - let found = false; - Object.entries(BACKGROUND_LAYER_TOKEN_PREFIXES).forEach(([key, prefix]) => { - const match = cssClass.match(new RegExp(`(?:^| )${prefix}-([0-9]+)(?: |$)`)); - if (match) { - values[key] = parseInt(match[1]); - found = true; - } - }); - return found ? values : null; + return Vue.prototype.$stylingUtils.parseBackgroundLayerValues(cssClass); } export function setBackgroundLayerValues(container, partialValues) { - // Merge with whatever's already encoded so that the margin component's - // writes never clobber the radius component's tokens (and vice-versa) - - // each only knows about its own 4 values, not the other's. - const values = { ...parseBackgroundLayerValues(container.cssClass), ...partialValues }; - const prefixesPattern = Object.values(BACKGROUND_LAYER_TOKEN_PREFIXES).join('|'); - let cssClass = (container.cssClass || '').replace(new RegExp(`(^| )(${prefixesPattern})-[0-9]+`, 'g'), '').replace(/ {2,}/g, ' ').trim(); - Object.entries(BACKGROUND_LAYER_TOKEN_PREFIXES).forEach(([key, prefix]) => { - if (values[key] || values[key] === 0) { - cssClass += ` ${prefix}-${values[key]}`; - } - }); - container.cssClass = cssClass.trim(); + return Vue.prototype.$stylingUtils.setBackgroundLayerValues(container, partialValues); } // The site background paints on the scroll container itself (see the diff --git a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/PageBody.vue b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/PageBody.vue index 845b4fa52..c5f115fed 100644 --- a/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/PageBody.vue +++ b/layout-webapp/src/main/webapp/vue-app/layout-editor/components/content/container/PageBody.vue @@ -26,7 +26,8 @@ :index="index" :length="childrenSize" :draggable="childrenSize > 1" - site-style /> + site-style + page-app-margins />