From ab90ab0cf485c40732f010e1c6a6c327d629a5bf Mon Sep 17 00:00:00 2001 From: Florin Raducan Date: Wed, 4 Sep 2024 09:21:12 +0300 Subject: [PATCH 1/6] #486247 - General fixes --- blocks/accordion/accordion.js | 2 +- blocks/column/column.js | 5 +++-- blocks/navigation/navigation.js | 8 +++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/blocks/accordion/accordion.js b/blocks/accordion/accordion.js index bb7ce8ed..ac3bf1da 100644 --- a/blocks/accordion/accordion.js +++ b/blocks/accordion/accordion.js @@ -10,7 +10,7 @@ export default class Accordion extends ComponentBase { button: { componentName: 'button', loaderConfig: { - targetsSelectorsPrefix: ':scope > :is(:nth-child(even)) >', + targetsSelectorsPrefix: ':scope > :is(:nth-child(even))', }, }, }, diff --git a/blocks/column/column.js b/blocks/column/column.js index fdc1eb08..6b42bc56 100644 --- a/blocks/column/column.js +++ b/blocks/column/column.js @@ -5,12 +5,13 @@ export default class Column extends ComponentBase { connected() { this.position = parseInt(this.dataset.position, 10); - this.dataset.justify ??= 'stretch'; this.calculateGridTemplateColumns(); } calculateGridTemplateColumns() { - this.style.setProperty('justify-content', this.dataset.justify); + if (this.dataset.justify) { + this.style.setProperty('justify-content', this.dataset.justify); + } if (this.position) { const parent = this.parentElement; const children = Array.from(parent.children); diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js index 9aecf808..f312a30b 100644 --- a/blocks/navigation/navigation.js +++ b/blocks/navigation/navigation.js @@ -23,7 +23,7 @@ export default class Navigation extends Column { compact: true, }, }, - x: { + s: { data: { compact: true, }, @@ -116,6 +116,7 @@ export default class Navigation extends Column { this.navButton.setAttribute('type', 'button'); this.navButton.innerHTML = ``; this.navIcon = this.navButton.querySelector('raqn-icon'); + this.navButton.addEventListener('click', () => { this.isActive = !this.isActive; this.classList.toggle('active'); @@ -124,6 +125,7 @@ export default class Navigation extends Column { blockBodyScroll(this.isActive); this.closeAllLevels(); }); + return this.navButton; } @@ -188,9 +190,9 @@ export default class Navigation extends Column { let whileCurrentLevel = currentLevel; while (whileCurrentLevel <= activeLevel) { const activeElem = this.active[currentLevel]; - + activeElem.classList.remove('active'); - const accordion = activeElem.querySelector('raqn-accordion'); + const accordion = activeElem.querySelector('raqn-accordion'); const control = accordion.querySelector('.accordion-control'); accordion.toggleControl(control); this.active[whileCurrentLevel] = null; From c105569dce2fb8e7d9a7e8927dcff46dc295fa78 Mon Sep 17 00:00:00 2001 From: Florin Raducan Date: Wed, 4 Sep 2024 09:22:19 +0300 Subject: [PATCH 2/6] #486247 - Update stylelint --- .stylelintrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.stylelintrc.json b/.stylelintrc.json index e1a82932..e44ce00c 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -3,6 +3,7 @@ "rules": { "no-descending-specificity": null, "custom-property-pattern": null, + "declaration-block-no-redundant-longhand-properties": null, "selector-class-pattern": [ "^[a-z]([-]?[a-z0-9]+)*(__[a-z0-9]([-]?[a-z0-9]+)*)?(--[a-z0-9]([-]?[a-z0-9]+)*)?$", { From 2d45849f57728c5a78ac3b9f9778de9cdb98fa17 Mon Sep 17 00:00:00 2001 From: Florin Raducan Date: Wed, 4 Sep 2024 09:49:45 +0300 Subject: [PATCH 3/6] #486247 - Add grid and grid-item components --- blocks/button/button.css | 2 +- blocks/grid-item/grid-item.css | 65 ++++++++ blocks/grid-item/grid-item.js | 90 ++++++++++ blocks/grid/grid.css | 39 +++++ blocks/grid/grid.js | 280 ++++++++++++++++++++++++++++++++ blocks/icon/icon.css | 2 +- blocks/theming/theming.js | 4 +- scripts/component-base.js | 97 ++++++++--- scripts/init.js | 45 ++++- scripts/libs.js | 82 +++++++++- scripts/libs/external-config.js | 29 ++-- styles/styles.css | 242 +++++++++++++-------------- 12 files changed, 795 insertions(+), 182 deletions(-) create mode 100644 blocks/grid-item/grid-item.css create mode 100644 blocks/grid-item/grid-item.js create mode 100644 blocks/grid/grid.css create mode 100644 blocks/grid/grid.js diff --git a/blocks/button/button.css b/blocks/button/button.css index e5111379..c02d1cad 100644 --- a/blocks/button/button.css +++ b/blocks/button/button.css @@ -16,7 +16,7 @@ raqn-button { raqn-button :where(a, button) { display: inline-flex; - line-height: var(--icon-size, 1em); + line-height: var(--icon-size, 1); background: var(--accent-background, #000); color: var(--accent-text, #fff); text-transform: none; diff --git a/blocks/grid-item/grid-item.css b/blocks/grid-item/grid-item.css new file mode 100644 index 00000000..36dffc43 --- /dev/null +++ b/blocks/grid-item/grid-item.css @@ -0,0 +1,65 @@ +raqn-grid-item { + --grid-item-justify: initial; + --grid-item-align: initial; + --grid-item-order: initial; + + grid-column: initial; + grid-row: initial; + grid-area: initial; + justify-self: var(--grid-item-justify); + align-self: var(--grid-item-align); + order: var(--grid-item-order); +} + +/* Make grid item sticky */ +raqn-grid-item[data-sticky='true' i] { + position: sticky; + top: var(--header-height); +} + +/* End */ + +/* Start Make entire item clickable + * if the first anchor in the grid item is italic + */ +raqn-grid-item:has(> p:first-child > em:only-child > a:only-child) { + position: relative; +} + +raqn-grid-item:has(> p:first-child > em:only-child > a:only-child) > p:first-child a { + position: absolute; + inset-block-end: 0; + inset-inline-end: 0; + width: 100%; + height: 100%; + color: transparent; + user-select: none; + z-index: 1; +} + +/* Make other anchor or buttons in the grid item still accessible */ +raqn-grid-item:has(> p:first-child > em:only-child > a:only-child) :where(a, button) { + position: relative; + z-index: 2; +} + +raqn-grid-item > p:first-child:has(> em:only-child > a:only-child) { + margin: 0; +} + +/* End */ + +/* Start Remove unwanted spacing from elements inside grid item */ +raqn-grid-item > :first-child { + margin-block-start: 0; +} + +raqn-grid-item > :last-child { + margin-block-end: 0; +} + +raqn-grid-item > p:first-child:has(> em:only-child > a:only-child) + * { + margin-block-start: 0; +} + +/* End */ diff --git a/blocks/grid-item/grid-item.js b/blocks/grid-item/grid-item.js new file mode 100644 index 00000000..312a2398 --- /dev/null +++ b/blocks/grid-item/grid-item.js @@ -0,0 +1,90 @@ +import ComponentBase from '../../scripts/component-base.js'; + +export default class Grid extends ComponentBase { + static observedAttributes = [ + 'data-level', + 'data-order', + 'data-sticky', + 'data-column', + 'data-row', + 'data-area', + 'data-justify', + 'data-align', + ]; + + nestedComponentsConfig = {}; + + attributesValues = { + all: { + data: { + level: 1, + }, + }, + }; + + setDefaults() { + super.setDefaults(); + this.gridParent = null; + } + + get siblingsItems() { + return this.gridParent.gridItems.filter((x) => x !== this); + } + + get logicalOrder() { + return this.gridParent.gridItems.indexOf(this) + 1; + } + + get areaName() { + return `item-${this.logicalOrder}`; + } + + // This method is called by the gridParent when a grid-template-areas is set. + setAutoAreaName(add = true) { + if (add) { + this.dataset.area = this.areaName; + } else { + delete this.dataset.area; + } + } + + connected() { + this.gridParent ??= this.parentElement; + } + + onAttributeOrderChanged({ oldValue, newValue }) { + this.setStyleProp('--grid-item-order', oldValue, newValue); + } + + // grid-column doesn't work with value from css variable; + onAttributeColumnChanged({ oldValue, newValue }) { + this.setStyleProp('grid-column', oldValue, newValue); + } + + // grid-row doesn't work with value from css variable; + onAttributeRowChanged({ oldValue, newValue }) { + this.setStyleProp('grid-row', oldValue, newValue); + } + + // grid-area doesn't work with value from css variable; + onAttributeAreaChanged({ oldValue, newValue }) { + this.setStyleProp('grid-area', oldValue, newValue); + } + + onAttributeJustifyChanged({ oldValue, newValue }) { + this.setStyleProp('--grid-item-justify', oldValue, newValue); + } + + onAttributeAlignChanged({ oldValue, newValue }) { + this.setStyleProp('--grid-item-align', oldValue, newValue); + } + + setStyleProp(prop, oldValue, newValue) { + if (oldValue === newValue) return; + if (newValue) { + this.style.setProperty(prop, newValue); + } else { + this.style.removeProperty(prop); + } + } +} diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css new file mode 100644 index 00000000..1282aae4 --- /dev/null +++ b/blocks/grid/grid.css @@ -0,0 +1,39 @@ +raqn-grid { + /* Set to initial to prevent inheritance for nested grids */ + --grid-height: initial; + --grid-width: 100%; + --grid-justify-items: initial; + --grid-align-items: initial; + --grid-justify-content: initial; + --grid-align-content: initial; + --grid-columns: initial; + --grid-rows: initial; + --grid-auto-columns: initial; + --grid-auto-rows: initial; + --grid-tpl-areas: initial; + --grid-tpl-columns: repeat(var(--grid-columns, 2), 1fr); + --grid-tpl-rows: repeat(var(--grid-rows, 0), 1fr); + + display: grid; + + /* defaults to 2 columns */ + grid-template-columns: var(--grid-tpl-columns); + grid-template-rows: var(--grid-tpl-rows); + grid-template-areas: var(--grid-tpl-areas); + grid-auto-columns: var(--grid-auto-columns); + grid-auto-rows: var(--grid-auto-rows); + gap: var(--gap, 20px); + justify-items: var(--grid-justify-items); + align-items: var(--grid-align-items); + justify-content: var(--grid-justify-content); + align-content: var(--grid-align-content); + height: var(--grid-height); +} + +/* + * First level grids will (as any other block) will act as a container + * and width should not be applied. + */ +raqn-grid:not(main > div > raqn-grid) { + width: var(--grid-width); +} diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js new file mode 100644 index 00000000..f236daa6 --- /dev/null +++ b/blocks/grid/grid.js @@ -0,0 +1,280 @@ +import ComponentBase from '../../scripts/component-base.js'; +import { stringToJsVal } from '../../scripts/libs.js'; +import component from '../../scripts/init.js'; + +export default class Grid extends ComponentBase { + static observedAttributes = [ + 'data-level', + 'data-height', + 'data-width', + 'data-reverse', + 'data-columns', // value can be any valid css value or a number which creates as many equal columns + 'data-rows', // value can be any valid css value or a number which creates as many equal rows + 'data-auto-columns', + 'data-auto-rows', + 'data-areas', + 'data-justify-items', + 'data-align-items', + 'data-justify-content', + 'data-align-content', + ]; + + nestedComponentsConfig = {}; + + attributesValues = { + all: { + data: { + level: 1, + }, + }, + }; + + setDefaults() { + super.setDefaults(); + // this.gridItems = []; + } + + get gridItems() { + return [...this.children]; + } + + onAttributeHeightChanged({ oldValue, newValue }) { + this.setStyleProp('height', oldValue, newValue); + } + + onAttributeWidthChanged({ oldValue, newValue }) { + this.setStyleProp('width', oldValue, newValue); + } + + async onAttributeReverseChanged({ oldValue, newValue }) { + // await for initialization because access to this.gridItems is required; + await this.initialization; + + if (oldValue === newValue) return; + + const val = stringToJsVal(newValue); + const reverse = val === true || newValue === 'alternate'; + + let items = this.gridItems; + + switch (val) { + case true: + items = [...this.gridItems].reverse(); + break; + case 'alternate': + items = this.alternateReverse(); + break; + default: + items = this.gridItems; + break; + } + + items.forEach((item, index) => { + if (reverse) { + item.dataset.order = index + 1; + } else { + delete item.dataset.order; + } + }); + } + + // swaps every 2 items [1,2,3,4,5,6,7] => [2,1,4,3,6,5,7] + alternateReverse() { + return this.gridItems.reduce((acc, x, i, arr) => { + if ((i + 1) % 2) { + if (arr.length === i + 1) acc.push(x); + return acc; + } + acc.push(x, arr[i - 1]); + return acc; + }, []); + } + + onAttributeColumnsChanged({ oldValue, newValue }) { + this.setRowsOrColumns('columns', oldValue, newValue); + } + + onAttributeRowsChanged({ oldValue, newValue }) { + this.setRowsOrColumns('rows', oldValue, newValue); + } + + setRowsOrColumns(name, oldValue, newValue) { + if (oldValue === newValue) return; + + const tplCol = `--grid-tpl-${name}`; + const col = `--grid-${name}`; + + if (!newValue) { + this.style.removeProperty(tplCol); + this.style.removeProperty(col); + return; + } + + const nrOfCols = Number(newValue); + + if (nrOfCols) { + this.style.removeProperty(tplCol); + this.style.setProperty(col, nrOfCols); + return; + } + + this.style.removeProperty(col); + this.style.setProperty(tplCol, newValue.replace(/-+/g, ' ')); + } + + onAttributeAutoColumnsChanged({ oldValue, newValue }) { + this.setStyleProp('auto-columns', oldValue, newValue); + } + + onAttributeAutoRowsChanged({ oldValue, newValue }) { + this.setStyleProp('auto-rows', oldValue, newValue); + } + + /** + * Grid areas names should be defined using the following pattern: + * `item-{index}` + * where index is the grid item position starting from 1 as a child element of the grid; + * + * The grid-template-area should be defined with an equal amount of areas as the number of grid items. + * + * The grid-area with the same pattern is automatically set on the grid item when grid-template-area is set + * on the grid using data-areas attribute; + */ + async onAttributeAreasChanged({ oldValue, newValue }) { + // await for initialization because access to this.gridItems is required; + await this.initialization; + const cleanValue = newValue.replace(/"\s+"/g, '" "').replace(/\n+|^\s+|\s+$/g, ''); + + // For validation check if the areas template includes all grid items + const missingItems = []; + + this.gridItems.forEach((item) => { + const areaCheck = cleanValue?.includes(item.areaName); + item.setAutoAreaName(!!cleanValue && areaCheck); + + if (cleanValue && !areaCheck) { + missingItems.push(item.areaName); + } + }); + + if (missingItems.length) { + console.warn(`The following items are not included in the areas template: ${missingItems.join(',')}`, this); + } + + this.setStyleProp('tpl-areas', oldValue, cleanValue); + } + + onAttributeJustifyItemsChanged({ oldValue, newValue }) { + this.setStyleProp('justify-items', oldValue, newValue); + } + + onAttributeAlignItemsChanged({ oldValue, newValue }) { + this.setStyleProp('align-items', oldValue, newValue); + } + + onAttributeJustifyContentChanged({ oldValue, newValue }) { + this.setStyleProp('justify-content', oldValue, newValue); + } + + onAttributeAlignContentChanged({ oldValue, newValue }) { + this.setStyleProp('align-content', oldValue, newValue); + } + + setStyleProp(name, oldValue, newValue) { + if (oldValue === newValue) return; + const prop = `--grid-${name}`; + if (newValue) { + this.style.setProperty(prop, newValue); + } else { + this.style.removeProperty(prop); + } + } + + /** + * hide grid if there are no grid items + */ + async connected() { + await this.collectGridItemsFromBlocks(); + } + + ready() { + this.cleanGridItems(); + } + + cleanGridItems() { + // Get all the grid items and remove any non grid item element. + return [...this.children].filter((child) => child.matches('raqn-grid-item') || child.remove()); + } + + async collectGridItemsFromBlocks() { + if (!this.isInitAsBlock) return; + + await this.recursiveItems(this.nextElementSibling); + // this.append(...this.gridItems); + } + + async recursiveItems(elem, children = []) { + if (!elem) return; + if (this.isForbiddenGridItem(elem)) return; + if (this.isForbiddenBlockGrid(elem)) return; + if (this.isForbiddenRaqnGrid(elem)) return; + + if (this.isThisGridItem(elem)) { + await this.createGridItem([...children], [...elem.classList]); + await this.recursiveItems(elem.nextElementSibling, []); + elem.remove(); + return; + } + + children.push(elem); + + await this.recursiveItems(elem.nextElementSibling, children); + } + + getLevel(elem = this) { + return Number(elem.dataset.level); + } + + getLevelFromClass(elem) { + const levelClass = [...elem.classList].find((cls) => cls.startsWith('data-level-')) || 'data-level-1'; + return Number(levelClass.slice('data-level-'.length)); + } + + isGridItem(elem) { + return elem.tagName === 'DIV' && elem.classList.contains('grid-item'); + } + + isThisGridItem(elem) { + return this.isGridItem(elem) && this.getLevelFromClass(elem) === this.getLevel(); + } + + isForbiddenGridItem(elem) { + return this.isGridItem(elem) && this.getLevelFromClass(elem) > this.getLevel(); + } + + isBlockGrid(elem) { + return elem.tagName === 'DIV' && elem.classList.contains('grid'); + } + + isRaqnGrid(elem) { + return elem.tagName === 'RAQN-GRID'; + } + + isForbiddenRaqnGrid(elem) { + return this.isRaqnGrid(elem) && this.getLevel() >= this.getLevel(elem); + } + + isForbiddenBlockGrid(elem) { + return this.isBlockGrid(elem) && this.getLevelFromClass(elem) <= this.getLevel(); + } + + async createGridItem(children, configByClasses) { + await component.loadAndDefine('grid-item'); + const tempGridItem = document.createElement('raqn-grid-item'); + tempGridItem.init({ configByClasses }); + tempGridItem.gridParent = this; + tempGridItem.append(...children); + this.gridItems.push(tempGridItem); + this.append(tempGridItem); + } +} diff --git a/blocks/icon/icon.css b/blocks/icon/icon.css index 72ea9a03..2bc09b42 100644 --- a/blocks/icon/icon.css +++ b/blocks/icon/icon.css @@ -2,7 +2,7 @@ raqn-icon { display: inline-flex; text-align: center; font-size: 1.2em; - line-height: 1.2em; + line-height: 1; width: var(--icon-size, 1.2em); height: var(--icon-size, 1.2em); justify-content: var(--icon-align, start); diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js index d30e7604..e6985253 100644 --- a/blocks/theming/theming.js +++ b/blocks/theming/theming.js @@ -1,5 +1,5 @@ import ComponentBase from '../../scripts/component-base.js'; -import { flat, getBreakPoints, getMediaQuery, getMeta, metaTags, readValue, unflat } from '../../scripts/libs.js'; +import { flat, getBreakPoints, getMediaQuery, getMeta, metaTags, readValue, unFlat } from '../../scripts/libs.js'; const k = Object.keys; @@ -102,7 +102,7 @@ export default class Theming extends ComponentBase { defineVariations() { const names = k(this.variations); const result = names.reduce((a, name) => { - const unflatted = unflat(this.variations[name]); + const unflatted = unFlat(this.variations[name]); return ( a + this.reduceViewports(unflatted, (actionData) => { diff --git a/scripts/component-base.js b/scripts/component-base.js index 5582c745..eda84263 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -7,10 +7,12 @@ import { capitalizeCaseAttr, deepMerge, classToFlat, - unflat, + unFlat, isObject, flatAsValue, flat, + mergeUniqueArrays, + getBlocksAndGrids, } from './libs.js'; import { externalConfig } from './libs/external-config.js'; @@ -65,10 +67,16 @@ export default class ComponentBase extends HTMLElement { nestedComponents: [], // from inner html blocks innerComponents: [], + // from inner html blocks + innerGrids: [], }; + // set only if content is loaded externally this.innerBlocks = []; + // set only if content is loaded externally + this.innerGrids = []; this.initError = null; this.breakpoints = getBreakPoints(); + this.dataAttributesKeys = this.setDataAttributesKeys(); // use the this.extendConfig() method to extend the default config this.config = { @@ -126,6 +134,19 @@ export default class ComponentBase extends HTMLElement { this.onBreakpointChange = this.onBreakpointChange.bind(this); } + async setDataAttributesKeys() { + const { observedAttributes } = await this.Handler; + this.dataAttributesKeys = observedAttributes.map((dataAttr) => { + const [, key] = dataAttr.split('data-'); + + return { + data: dataAttr, + noData: key, + noDataCamelCase: camelCaseAttr(key), + }; + }); + } + // ! Needs to be called after the element is created; async init(initOptions) { try { @@ -155,13 +176,14 @@ export default class ComponentBase extends HTMLElement { * use the data attr values as default for attributesValues */ setInitialAttributesValues() { - const initialAttributesValues = { all: {} }; + const initialAttributesValues = { all: { data: {} } }; - this.Handler.observedAttributes.map((dataAttr) => { - const [, key] = dataAttr.split('data-'); - const value = this.dataset[key]; + this.dataAttributesKeys.forEach(({ noData, noDataCamelCase }) => { + const value = this.dataset[noDataCamelCase]; + // TODO check this if (typeof value === 'undefined') return {}; - initialAttributesValues.all[key] = value; + const initialValue = unFlat({ [noData]: value }); + initialAttributesValues.all.data = deepMerge({}, initialAttributesValues.all.data, initialValue); return initialAttributesValues; }); @@ -232,7 +254,7 @@ export default class ComponentBase extends HTMLElement { } async buildExternalConfig() { - let configByClasses = this.initOptions.configByClasses || []; + let configByClasses = mergeUniqueArrays(this.initOptions.configByClasses, this.classList); // normalize the configByClasses to serializable format const { byName } = getBreakPoints(); configByClasses = configByClasses @@ -250,14 +272,22 @@ export default class ComponentBase extends HTMLElement { let values = classToFlat(configByClasses); // get the external config - - const configs = unflat(await externalConfig.getConfig(this.componentName, values.config)); - - if (!this.overrideExternalConfig) { - values = deepMerge({}, configs, values); - } else { + // TODO With the unFlatten approach of setting this.attributesValues there is an increased amount of processing + // each time a viewport changes when we need to flatten again the values + // better approach would be to generate this.attributesValues in the final state needed by each time of data: + // - class - as arrays with unique values + // - data - as flatten values with camel case keys + // - attributes - as flatten values with hyphen separated keys. + // for anything else set them flatten as they come from from external config + const configs = unFlat(await externalConfig.getConfig(this.componentName, values.config)); + + if (this.overrideExternalConfig) { + // Used for preview functionality values = deepMerge({}, configs, this.attributesValues, values); + } else { + values = deepMerge({}, configs, values); } + delete values.config; // add to attributesValues @@ -326,14 +356,11 @@ export default class ComponentBase extends HTMLElement { // Add only supported data attributes from observedAttributes; // Sometimes the order in which the attributes are set matters. // Control the order by using the order of the observedAttributes. - this.Handler.observedAttributes.forEach((dataAttr) => { - const [, key] = dataAttr.split('data-'); - const camelCaseAttribute = camelCaseAttr(key); - - if (typeof values[key] !== 'undefined') { - this.dataset[camelCaseAttribute] = values[key]; + this.dataAttributesKeys.forEach(({ noData, noDataCamelCase }) => { + if (typeof values[noData] !== 'undefined') { + this.dataset[noDataCamelCase] = values[noData]; } else { - delete this.dataset[camelCaseAttribute]; + delete this.dataset[noDataCamelCase]; } }); } @@ -344,7 +371,7 @@ export default class ComponentBase extends HTMLElement { // classes can be serialized as a string or an object if (isObject(className)) { - // if an object is passed, it's flat and splited + // if an object is passed, it's flat and splitted this.classList.add(...flatAsValue(className).split(' ')); } else if (className) { // strings are added as is @@ -359,7 +386,7 @@ export default class ComponentBase extends HTMLElement { const values = flat(entries); // transformed into values as {col-direction: 2, columns: 2} Object.keys(values).forEach((key) => { - // camelCaseAttr converst col-direction into colDirection + // camelCaseAttr converts col-direction into colDirection this.setAttribute(key, values[key]); }); } @@ -412,6 +439,7 @@ export default class ComponentBase extends HTMLElement { async initChildComponents() { await Promise.allSettled([this.initNestedComponents(), this.initInnerBlocks()]); + await this.initInnerGrids(); } async initNestedComponents() { @@ -437,14 +465,24 @@ export default class ComponentBase extends HTMLElement { async initInnerBlocks() { if (!this.innerBlocks.length) return; - const innerBlocksSettings = this.innerBlocks.map((block) => ({ targets: [block] })); - this.childComponents.innerComponents = await component.multiInit(innerBlocksSettings); + + this.childComponents.innerComponents = await component.multiInit(this.innerBlocks); const { allInitialized } = this.childComponents.innerComponents; const { hideOnChildrenError } = this.config; this.hideWithError(!allInitialized && hideOnChildrenError, 'has-inner-error'); } + async initInnerGrids() { + if (!this.innerGrids.length) return; + + this.childComponents.innerGrids = await component.multiSequentialInit(this.innerGrids); + + const { allInitialized } = this.childComponents.innerGrids; + const { hideOnChildrenError } = this.config; + this.hideWithError(!allInitialized && hideOnChildrenError, 'has-inner-error'); + } + async loadDependencies() { if (!this.dependencies.length) return; component.multiLoadAndDefine(this.dependencies); @@ -464,7 +502,7 @@ export default class ComponentBase extends HTMLElement { if (response.ok) { this.fragmentContent = response.text(); await this.addFragmentContent(); - this.setInnerBlocks(); + this.setInnerBlocksAndGrids(); } } @@ -472,8 +510,13 @@ export default class ComponentBase extends HTMLElement { this.innerHTML = await this.fragmentContent; } - setInnerBlocks() { - this.innerBlocks = [...this.querySelectorAll(globalConfig.blockSelector)]; + // Set only if content is loaded externally; + setInnerBlocksAndGrids() { + const { blocks, grids } = getBlocksAndGrids( + [...this.querySelectorAll(globalConfig.blockSelector)].map((elem) => component.getBlockData(elem)), + ); + this.innerBlocks = blocks; + this.innerGrids = grids; } queryElements() { diff --git a/scripts/init.js b/scripts/init.js index 5d3f9b94..6171e3b0 100644 --- a/scripts/init.js +++ b/scripts/init.js @@ -1,5 +1,13 @@ import ComponentLoader from './component-loader.js'; -import { globalConfig, metaTags, eagerImage, getMeta, getMetaGroup, mergeUniqueArrays } from './libs.js'; +import { + globalConfig, + metaTags, + eagerImage, + getMeta, + getMetaGroup, + mergeUniqueArrays, + getBlocksAndGrids, +} from './libs.js'; const component = { async init(settings) { @@ -43,6 +51,24 @@ const component = { return status; }, + async multiSequentialInit(settings) { + const initialized = []; + const sequentialInit = async (set) => { + if (!set.length) return; + const initializing = await this.init(set.shift()); + initialized.unshift(initializing); + sequentialInit(set); + }; + + await sequentialInit([...settings]); + + const status = { + allInitialized: initialized.every((c) => !(c.initError || c.failedInstances.length)), + instances: initialized, + }; + return status; + }, + async loadAndDefine(componentName) { const status = await new ComponentLoader({ componentName }).loadAndDefine(); return { componentName, status }; @@ -62,12 +88,11 @@ const component = { getBlockData(block) { const tagName = block.tagName.toLowerCase(); const lcp = block.classList.contains('lcp'); - const orignalClasses = block.getAttribute('class'); let componentName = tagName; if (!globalConfig.semanticBlocks.includes(tagName)) { componentName = block.classList.item(0); } - return { targets: [block], componentName, lcp, orignalClasses }; + return { targets: [block], componentName, lcp }; }, }; @@ -150,7 +175,11 @@ export const onLoadComponents = { }, setLazyBlocks() { - this.lazyBlocks = this.blocksData.filter((data) => !this.findLcp(data)); + const allLazy = this.blocksData.filter((data) => !this.findLcp(data)); + const { grids, blocks } = getBlocksAndGrids(allLazy); + + this.lazyBlocks = blocks; + this.grids = grids; }, findLcp(data) { @@ -160,13 +189,17 @@ export const onLoadComponents = { ); }, - initBlocks() { + async initBlocks() { // Keep the page hidden until specific components are initialized to prevent CLS component.multiInit(this.lcpBlocks).then(() => { window.postMessage({ message: 'raqn:components:loaded' }); document.body.style.setProperty('display', 'block'); }); - component.multiInit(this.lazyBlocks); + + await component.multiInit(this.lazyBlocks); + // grids must be initialized sequentially starting from the deepest level. + // all the blocks that will be contained by the grids must be already initialized before they are added to the grids. + component.multiSequentialInit(this.grids); }, }; diff --git a/scripts/libs.js b/scripts/libs.js index 11c0be2b..89bcadf1 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -1,9 +1,14 @@ export const globalConfig = { semanticBlocks: ['header', 'footer'], - blockSelector: '[class]:not(style, [class^="config-" i])', + blockSelector: ` + [class]:not( + style, + [class^="config-" i], + [class^="grid-item" i] + )`, breakpoints: { xs: 0, - s: 320, + s: 480, m: 768, l: 1024, xl: 1280, @@ -214,6 +219,7 @@ export function stringToArray(val, options) { export function readValue(data, extend = {}) { const k = Object.keys; const keys = k(data[0]).filter((item) => item !== 'key'); + return data.reduce((acc, row) => { const mainKey = row.key; keys.reduce((a, key) => { @@ -266,7 +272,12 @@ export function deepMerge(origin, ...toMerge) { if (isOnlyObject(origin) && isOnlyObject(merge)) { Object.keys(merge).forEach((key) => { if (isOnlyObject(merge[key])) { - if (!origin[key]) Object.assign(origin, { [key]: {} }); + const noKeyInOrigin = !origin[key]; + // overwrite origin non object values with objects + const overwriteOriginWithObject = !isOnlyObject(origin[key]) && isOnlyObject(merge[key]); + if (noKeyInOrigin || overwriteOriginWithObject) { + Object.assign(origin, { [key]: {} }); + } deepMerge(origin[key], merge[key]); } else { Object.assign(origin, { [key]: merge[key] }); @@ -295,13 +306,13 @@ export function loadModule(urlWithoutExtension, loadCSS = true) { } }).catch((error) => // eslint-disable-next-line no-console - console.log('could not load module style', urlWithoutExtension, error), + console.log('Could not load module style', urlWithoutExtension, error), ); return { css, js }; } catch (error) { // eslint-disable-next-line no-console - console.log('could not load module', urlWithoutExtension, error); + console.log('Could not load module', urlWithoutExtension, error); } return { css: Promise.resolve(), js: Promise.resolve() }; } @@ -436,13 +447,28 @@ export function flatAsValue(data, sep = '-') { .trim(); } +export function flatAsClasses(data, sep = '-') { + return Object.entries(data) + .reduce((acc, [key, value]) => { + const accm = acc ? `${acc} ` : ''; + if (isObject(value)) { + const flatSubValues = flatAsClasses(value, sep); + // add current key as prefix to sublevel flatten values + const valuesWithKey = flatSubValues.replace(/^|\s/g, ` ${key}${sep}`).trim(); + return `${accm}${valuesWithKey}`; + } + return `${accm}${key}${sep}${value}`; + }, '') + .trim(); +} + /** * unFlattenProperties: convert objects from subkeys as strings {'a-b-c-d':1} to tree {a:{b:{c:{d:1}}}} * * @param {Object} obj - Object to unflatten * */ -export function unflat(f, sep = '-') { +export function unFlat(f, sep = '-') { const un = {}; // for each key create objects Object.keys(f).forEach((key) => { @@ -460,7 +486,7 @@ export function unflat(f, sep = '-') { } export const classToFlat = (classes = [], valueLength = 1, extend = {}) => - unflat( + unFlat( classes.reduce((acc, c) => { const length = c.split('-').length - valueLength; const key = c.split('-').slice(0, length).join('-'); @@ -475,3 +501,45 @@ export function blockBodyScroll(boolean) { const { noScroll } = globalConfig.classes; document.body.classList.toggle(noScroll, boolean); } + +// Separate any other blocks from grids and grid-item because: +// grids must be initialized only after all the other blocks are initialized +// grid-item component are going to be generated and initialized by the grid component and should be excluded from blocks. +export function getBlocksAndGrids(elements) { + const blocksAndGrids = elements.reduce( + (acc, block) => { + // exclude grid items + if (block.componentName === 'grid-item') return acc; + if (block.componentName === 'grid') { + // separate grids + acc.grids.push(block); + } else { + // separate the rest of blocks + acc.blocks.push(block); + } + return acc; + }, + { grids: [], blocks: [] }, + ); + + // if a grid doesn't specify its level will default to level 1 + const getGridLevel = (elem) => { + const levelClass = [...elem.classList].find((cls) => cls.startsWith('data-level-')) || 'data-level-1'; + return Number(levelClass.slice('data-level-'.length)); + }; + + // Based on how each gird is identifying it's own grid items, the grid initialization + // must be done starting from the deepest level grids. + // This is because each grid can contain other grids in their grid-items + // To achieve this infinite nesting each grid deeper than level 1 must specify their level of + // nesting with the data-level= option e.g data-level=2 + blocksAndGrids.grids.sort(({ targets: [elemA] }, { targets: [elemB] }) => { + const levelA = getGridLevel(elemA); + const levelB = getGridLevel(elemB); + if (levelA <= levelB) return 1; + if (levelA > levelB) return -1; + return 0; + }); + + return blocksAndGrids; +} diff --git a/scripts/libs/external-config.js b/scripts/libs/external-config.js index fd5f7862..3b7ae352 100644 --- a/scripts/libs/external-config.js +++ b/scripts/libs/external-config.js @@ -1,27 +1,16 @@ -import { getMeta, metaTags, readValue } from '../libs.js'; +import { getMeta, metaTags, readValue, deepMerge } from '../libs.js'; window.raqnComponentsMasterConfig = window.raqnComponentsMasterConfig || null; // eslint-disable-next-line import/prefer-default-export export const externalConfig = { - defaultConfig(rawConfig = []) { - return { - attributesValues: {}, - nestedComponentsConfig: {}, - props: {}, - config: {}, - rawConfig, - hasBreakpointsValues: false, - }; - }, - async getConfig(componentName, configName = 'default') { - if (!window.raqnComponentsMasterConfig) { - window.raqnComponentsMasterConfig = await this.loadConfig(); - } + window.raqnComponentsMasterConfig ??= await this.loadConfig(); const componentConfig = window.raqnComponentsMasterConfig?.[componentName]; const parsedConfig = componentConfig?.[configName]; - if (parsedConfig) return parsedConfig; + + // return copy of object to prevent mutation of raqnComponentsMasterConfig; + if (parsedConfig) return deepMerge({}, parsedConfig); return {}; }, @@ -49,17 +38,19 @@ export const externalConfig = { }, simplifiedConfig() { - window.raqnParsedConfigs = window.raqnParsedConfigs || {}; - if (window.raqnComponentsConfig) { + if (window.raqnComponentsConfig && !window.raqnParsedConfigs) { + window.raqnParsedConfigs ??= {}; + Object.keys(window.raqnComponentsConfig).forEach((key) => { if (!window.raqnComponentsConfig[key]) return; const { data } = window.raqnComponentsConfig[key]; - if (data && data.length > 0) { + if (data?.length) { window.raqnParsedConfigs[key] = window.raqnParsedConfigs[key] || {}; window.raqnParsedConfigs[key] = readValue(data, window.raqnParsedConfigs[key]); } }); } + return window.raqnParsedConfigs; }, }; diff --git a/styles/styles.css b/styles/styles.css index 743467c6..aa414fb3 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -15,207 +15,217 @@ box-sizing: border-box; } -img { - width: 100%; - object-fit: cover; +html, +body, +div, +span, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +button, +img, +small, +strong, +sub, +sup, +tt, +b, +u, +i, +dl, +dt, +dd, +ol, +ul, +li, +label, +legend, +caption { + vertical-align: baseline; +} + +html { + font-size: 100%; } html, body { - width: 100%; - height: 100%; + min-width: 100%; + min-height: 100%; margin: 0; padding: 0; +} + +body { + display: none; + background: var(--background, #fff); + padding: 0; + margin: 0; + width: 100%; +} + +body.no-scroll { + block-size: 100vh; + overflow: hidden; + position: relative; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +body > * { color: var(--text, #000); font-family: var(--p-font-family, roboto); - font-size: var(--p-font-size, 16px); + font-size: var(--p-font-size, 1rem); font-weight: var(--p-font-weight, normal); font-style: var(--p-font-style, normal); - line-height: var(--p-line-height, 1.2em); + line-height: var(--p-line-height, 1.2); +} + +header { + min-height: var(--header-height, 64px); + display: grid; + background: var(--header-background, #fff); +} + +head:has(meta[name='header'][content='false' i]) + body > header, +head:has(meta[name='footer'][content='false' i]) + body > footer { + display: none; +} + +main { + background: var(--background, #fff); + padding: 0; + margin: 0; + width: 100%; + position: relative; + min-height: 100%; +} + +h1, +h2, +h3, +h4 { + color: var(--title, #000); } h1 { font-family: var(--h1-font-family, roboto); - font-size: var(--h1-font-size, 16px); + font-size: var(--h1-font-size, 1rem); font-weight: var(--h1-font-weight, normal); font-style: var(--h1-font-style, normal); - line-height: var(--h1-line-height, 1.2em); + line-height: var(--h1-line-height, 1.2); } h2 { font-family: var(--h2-font-family, roboto); - font-size: var(--h2-font-size, 16px); + font-size: var(--h2-font-size, 1rem); font-weight: var(--h2-font-weight, normal); font-style: var(--h2-font-style, normal); - line-height: var(--h2-line-height, 1.2em); + line-height: var(--h2-line-height, 1.2); } h3 { font-family: var(--h3-font-family, roboto); - font-size: var(--h3-font-size, 16px); + font-size: var(--h3-font-size, 1rem); font-weight: var(--h3-font-weight, normal); font-style: var(--h3-font-style, normal); - line-height: var(--h3-line-height, 1.3em); + line-height: var(--h3-line-height, 1.3); } h5 { font-family: var(--h5-font-family, roboto); - font-size: var(--h5-font-size, 16px); + font-size: var(--h5-font-size, 1rem); font-weight: var(--h5-font-weight, normal); font-style: var(--h5-font-style, normal); - line-height: var(--h5-line-height, 1.5em); + line-height: var(--h5-line-height, 1.5); } h4 { font-family: var(--h4-font-family, roboto); - font-size: var(--h4-font-size, 16px); + font-size: var(--h4-font-size, 1rem); font-weight: var(--h4-font-weight, normal); font-style: var(--h4-font-style, normal); - line-height: var(--h4-line-height, 1.4em); + line-height: var(--h4-line-height, 1.4); } h6 { font-family: var(--h6-font-family, roboto); - font-size: var(--h6-font-size, 16px); + font-size: var(--h6-font-size, 1rem); font-weight: var(--h6-font-weight, normal); font-style: var(--h6-font-style, normal); - line-height: var(--h6-line-height, 1.6em); + line-height: var(--h6-line-height, 1.6); } label { font-family: var(--label-font-family, roboto); - font-size: var(--label-font-size, 16px); + font-size: var(--label-font-size, 1rem); font-weight: var(--label-font-weight, normal); font-style: var(--label-font-style, normal); - line-height: var(--label-line-height, 1.6em); -} - -body { - display: none; - background: var(--background, #fff); - padding: 0; - margin: 0; - width: 100%; + line-height: var(--label-line-height, 1.6); } -main { - background: var(--background, #fff); - padding: 0; - margin: 0; +img { width: 100%; - position: relative; - min-height: 100%; -} - -body.no-scroll { - block-size: 100vh; - overflow: hidden; - position: relative; - top: 0; - left: 0; - right: 0; - bottom: 0; -} - -html, -body, -div, -span, -h1, -h2, -h3, -h4, -h5, -h6, -p, -blockquote, -pre, -a, -button, -img, -small, -strong, -sub, -sup, -tt, -b, -u, -i, -dl, -dt, -dd, -ol, -ul, -li, -label, -legend, -caption { - font-size: 100%; - vertical-align: baseline; -} - -h1, -h2, -h3, -h4 { - color: var(--title, #000); -} - -header { - min-height: var(--header-height, 64px); - display: grid; - background: var(--header-background, #fff); + object-fit: cover; } -head:has(meta[name='header'][content='false' i]) + body > header, -head:has(meta[name='footer'][content='false' i]) + body > footer { - display: none; +picture, +img { + display: block; + max-width: 100%; + height: auto; } -/* Use :where() to give lower specificity in order to not overwrite any display option set on the web component tag */ +/* Set default block style to all raqn web components + Use :where() to give lower specificity in order to not overwrite any display option set on the web component tag +*/ :where([raqnWebComponent]) { display: block; } +/* Container: make all content act as a container where background of the container is limited to the content area */ main > div > *:not(.full-width) { margin-inline: max(calc((100% - var(--max-width)) / 2 - var(--padding-container)), var(--padding-container)); padding-inline: var(--padding-container); } +/* Change the above behavior by setting the full-with class on the block. This will make the background take the full with of the page */ .full-width { padding-inline: var(--container-width); } +/* TODO Check if this is still needed */ main > div > div { background: var(--background, #fff); color: var(--text, #000); padding: var(--padding, 0); } +/* TODO Check if this is still needed */ main > div > div > div { max-width: var(--max-width, 100%); margin: var(--margin, 0 auto); width: 100%; } -.breadcrumbs { - display: grid; - grid-template-columns: var(--grid-template-columns, 1fr); - gap: var(--gap, 20px); - align-items: center; - justify-items: start; - min-height: var(--font-size, 1.2em); -} - a { align-items: center; color: var(--highlight, inherit); text-decoration: none; font-family: var(--a-font-family, roboto); - font-size: var(--a-font-size, 16px); + font-size: var(--a-font-size, 1rem); font-weight: var(--a-font-weight, normal); font-style: var(--a-font-style, normal); - line-height: var(--a-line-height, 1.2em); + line-height: var(--a-line-height, 1.2); } a:hover { @@ -230,10 +240,10 @@ button { align-items: center; text-decoration: none; font-family: var(--button-font-family, roboto); - font-size: var(--button-font-size, 16px); + font-size: var(--button-font-size, 1rem); font-weight: var(--button-font-weight, normal); font-style: var(--button-font-style, normal); - line-height: var(--button-line-height, 1.2em); + line-height: var(--button-line-height, 1.2); } .raqn-grid { @@ -246,13 +256,7 @@ button { justify-items: start; } -picture, -img { - display: block; - max-width: 100%; - height: auto; -} - +/* Hide raqn web components based on different states */ [isloading], .hide-with-error, .hide { From 670959d29faf63468146dbc79b1c50e258e6dc9b Mon Sep 17 00:00:00 2001 From: Florin Raducan Date: Wed, 4 Sep 2024 10:25:05 +0300 Subject: [PATCH 4/6] #486247 - Add color and background to grid --- blocks/grid/grid.css | 4 ++++ styles/styles.css | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css index 1282aae4..ba44b35b 100644 --- a/blocks/grid/grid.css +++ b/blocks/grid/grid.css @@ -13,6 +13,8 @@ raqn-grid { --grid-tpl-areas: initial; --grid-tpl-columns: repeat(var(--grid-columns, 2), 1fr); --grid-tpl-rows: repeat(var(--grid-rows, 0), 1fr); + --grid-background: var(--background, black); + --grid-color: var(--text, white); display: grid; @@ -28,6 +30,8 @@ raqn-grid { justify-content: var(--grid-justify-content); align-content: var(--grid-align-content); height: var(--grid-height); + background: var(--grid-background); + color: var(--grid-color); } /* diff --git a/styles/styles.css b/styles/styles.css index aa414fb3..ed8c5c68 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -194,8 +194,7 @@ img { /* Container: make all content act as a container where background of the container is limited to the content area */ main > div > *:not(.full-width) { - margin-inline: max(calc((100% - var(--max-width)) / 2 - var(--padding-container)), var(--padding-container)); - padding-inline: var(--padding-container); + margin-inline: var(--container-width); } /* Change the above behavior by setting the full-with class on the block. This will make the background take the full with of the page */ From e216f2d66872fcb323828d3bba6a1b3236d034b9 Mon Sep 17 00:00:00 2001 From: Florin Raducan Date: Wed, 4 Sep 2024 13:37:34 +0300 Subject: [PATCH 5/6] #486247 - Code cleanup --- blocks/grid/grid.js | 6 +----- scripts/component-base.js | 2 +- styles/styles.css | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js index f236daa6..b46a68d5 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -31,7 +31,6 @@ export default class Grid extends ComponentBase { setDefaults() { super.setDefaults(); - // this.gridItems = []; } get gridItems() { @@ -158,6 +157,7 @@ export default class Grid extends ComponentBase { }); if (missingItems.length) { + // eslint-disable-next-line no-console console.warn(`The following items are not included in the areas template: ${missingItems.join(',')}`, this); } @@ -190,9 +190,6 @@ export default class Grid extends ComponentBase { } } - /** - * hide grid if there are no grid items - */ async connected() { await this.collectGridItemsFromBlocks(); } @@ -210,7 +207,6 @@ export default class Grid extends ComponentBase { if (!this.isInitAsBlock) return; await this.recursiveItems(this.nextElementSibling); - // this.append(...this.gridItems); } async recursiveItems(elem, children = []) { diff --git a/scripts/component-base.js b/scripts/component-base.js index eda84263..db5071eb 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -213,7 +213,7 @@ export default class ComponentBase extends HTMLElement { this.setAttribute('isloading', ''); try { this.initialized = this.getAttribute('initialized'); - this.initSubscriptions(); // must subscribe each time the element is added to the document + this.initSubscriptions(); // must subscribe each type the element is added to the document if (!this.initialized) { await this.initOnConnected(); this.setAttribute('id', this.uuid); diff --git a/styles/styles.css b/styles/styles.css index ed8c5c68..140625a6 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -197,7 +197,7 @@ main > div > *:not(.full-width) { margin-inline: var(--container-width); } -/* Change the above behavior by setting the full-with class on the block. This will make the background take the full with of the page */ +/* Change the above behavior by setting the full-with class on the block. This will make the background take the full width of the page */ .full-width { padding-inline: var(--container-width); } From ff8c206e97b860d363acb8f7d80ccfd8144352ca Mon Sep 17 00:00:00 2001 From: Florin Raducan Date: Thu, 5 Sep 2024 13:26:24 +0300 Subject: [PATCH 6/6] 486247 - code cleanup --- scripts/component-base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/component-base.js b/scripts/component-base.js index db5071eb..a3b749dd 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -180,7 +180,7 @@ export default class ComponentBase extends HTMLElement { this.dataAttributesKeys.forEach(({ noData, noDataCamelCase }) => { const value = this.dataset[noDataCamelCase]; - // TODO check this + if (typeof value === 'undefined') return {}; const initialValue = unFlat({ [noData]: value }); initialAttributesValues.all.data = deepMerge({}, initialAttributesValues.all.data, initialValue);