From 9e8321a23ac623dcd00c7d80cb60d8067645972d Mon Sep 17 00:00:00 2001 From: Florin Raducan Date: Mon, 25 Nov 2024 16:43:33 +0200 Subject: [PATCH] Cleanup and fixes --- blocks/accordion/accordion.js | 3 - blocks/button/button.js | 39 +---- blocks/grid-item/grid-item.css | 8 + blocks/grid/grid.css | 11 ++ blocks/grid/grid.js | 3 - blocks/header/header.js | 12 -- blocks/icon/icon.js | 21 ++- blocks/navigation/navigation.js | 89 +++++----- blocks/popup-trigger/popup-trigger.js | 75 +++++---- blocks/popup/popup.css | 1 + blocks/popup/popup.js | 42 +++-- .../sidekick-tools-palette.js | 3 +- blocks/theming/theming.js | 6 +- head.html | 67 +++++--- scripts/component-base.js | 159 ++++++++++++------ scripts/component-list/component-list.js | 58 ++++--- scripts/index.js | 11 +- scripts/libs.js | 19 ++- scripts/libs/external-config.js | 15 +- styles/styles.css | 21 ++- 20 files changed, 380 insertions(+), 283 deletions(-) diff --git a/blocks/accordion/accordion.js b/blocks/accordion/accordion.js index cab0fe39..13117990 100644 --- a/blocks/accordion/accordion.js +++ b/blocks/accordion/accordion.js @@ -1,9 +1,6 @@ import ComponentBase from '../../scripts/component-base.js'; -import { componentList } from '../../scripts/component-list/component-list.js'; export default class Accordion extends ComponentBase { - dependencies = componentList.accordion.module.dependencies; - init() { super.init(); this.setAttribute('role', 'navigation'); diff --git a/blocks/button/button.js b/blocks/button/button.js index 80888205..3496713c 100644 --- a/blocks/button/button.js +++ b/blocks/button/button.js @@ -1,40 +1,3 @@ import ComponentBase from '../../scripts/component-base.js'; -export default class Button extends ComponentBase { - extendConfig() { - return [ - ...super.extendConfig(), - { - selectors: { - anchor: ':scope > a', - ariaText: ':scope > a:has(> raqn-icon, > .icon) > strong', - }, - }, - ]; - } - - init() { - super.init(); - this.queryElements(); - this.wrapText(); - this.addAriaText(); - } - - wrapText() { - const { anchor, ariaText } = this.elements; - const wrap = document.createElement('span'); - if (ariaText) return; - if (!anchor.childNodes) return; - const label = [...anchor.childNodes].find(({ nodeName }) => nodeName === '#text'); - if (!label) return; - wrap.textContent = label.textContent; - label.replaceWith(wrap); - } - - addAriaText() { - const { anchor, ariaText } = this.elements; - if (!ariaText) return; - anchor.setAttribute('aria-label', ariaText.textContent); - ariaText.remove(); - } -} +export default class Button extends ComponentBase {} diff --git a/blocks/grid-item/grid-item.css b/blocks/grid-item/grid-item.css index 36dffc43..0e6ec4f9 100644 --- a/blocks/grid-item/grid-item.css +++ b/blocks/grid-item/grid-item.css @@ -9,6 +9,14 @@ raqn-grid-item { justify-self: var(--grid-item-justify); align-self: var(--grid-item-align); order: var(--grid-item-order); + margin-block-start: var(--grid-item-margin-block-start); + margin-block-end: var(--grid-item-margin-block-end); + margin-inline-start: var(--grid-item-margin-inline-start); + margin-inline-end: var(--grid-item-margin-inline-end); + padding-block-start: var(--grid-item-padding-block-start); + padding-block-end: var(--grid-item-padding-block-end); + padding-inline-start: var(--grid-item-padding-inline-start); + padding-inline-end: var(--grid-item-padding-inline-end); } /* Make grid item sticky */ diff --git a/blocks/grid/grid.css b/blocks/grid/grid.css index 3948be10..4c95a4c1 100644 --- a/blocks/grid/grid.css +++ b/blocks/grid/grid.css @@ -12,6 +12,17 @@ raqn-grid { --grid-background: var(--background, black); --grid-color: var(--text, white); + /* Option to add margins/paddings for grid to grid-items + Values are set to initial prevent unwanted inheritance. */ + --grid-item-margin-block-start: initial; + --grid-item-margin-block-end: initial; + --grid-item-margin-inline-start: initial; + --grid-item-margin-inline-end: initial; + --grid-item-padding-block-start: initial; + --grid-item-padding-block-end: initial; + --grid-item-padding-inline-start: initial; + --grid-item-padding-inline-end: initial; + display: grid; /* defaults to 2 columns */ diff --git a/blocks/grid/grid.js b/blocks/grid/grid.js index 13bbb735..732228e3 100644 --- a/blocks/grid/grid.js +++ b/blocks/grid/grid.js @@ -1,13 +1,10 @@ import ComponentBase from '../../scripts/component-base.js'; -import { componentList } from '../../scripts/component-list/component-list.js'; import { stringToJsVal } from '../../scripts/libs.js'; export default class Grid extends ComponentBase { // only one attribute is observed rest is set as css variables directly static observedAttributes = ['data-reverse']; - dependencies = componentList.grid.module.dependencies; - async onAttributeReverseChanged({ oldValue, newValue }) { await this.initialization; diff --git a/blocks/header/header.js b/blocks/header/header.js index 030dea66..762e69bc 100644 --- a/blocks/header/header.js +++ b/blocks/header/header.js @@ -1,12 +1,9 @@ import ComponentBase from '../../scripts/component-base.js'; import { eagerImage, getMeta, metaTags } from '../../scripts/libs.js'; -import { componentList } from '../../scripts/component-list/component-list.js'; const metaHeader = getMeta(metaTags.header.metaName); export default class Header extends ComponentBase { - dependencies = componentList.header.module.dependencies; - attributesValues = { all: { class: ['color-primary'], @@ -15,15 +12,6 @@ export default class Header extends ComponentBase { fragmentPath = `${metaHeader}.plain.html`; - extendConfig() { - return [ - ...super.extendConfig(), - { - addToTargetMethod: 'append', - }, - ]; - } - async init() { super.init(); eagerImage(this, 1); diff --git a/blocks/icon/icon.js b/blocks/icon/icon.js index 45ff1526..e1af4e74 100644 --- a/blocks/icon/icon.js +++ b/blocks/icon/icon.js @@ -48,17 +48,11 @@ export default class Icon extends ComponentBase { async onAttributeIconChanged({ oldValue, newValue }) { if (oldValue === newValue) return; - // ! The initial and active icon names are separated with a double underline - // ! The active icon is optional; if (!newValue) return; - const [initial, active] = newValue.split('__'); + const { initial, active, loadActiveIcon, loadInitialIcon } = this.getIcons(newValue); this.#initialIcon = initial; this.#activeIcon = active || null; - // Start loading both icons; - const loadInitialIcon = this.loadIcon(this.#initialIcon); - const loadActiveIcon = this.#activeIcon ? this.loadIcon(this.#activeIcon) : null; - const isActiveWithIcon = this.isActive && this.#activeIcon; // Wait only for the current icon if (isActiveWithIcon) { @@ -81,6 +75,19 @@ export default class Icon extends ComponentBase { this.innerHTML = this.template(iconName); } + getIcons(icon) { + // ! The initial and active icon names are separated with a double underline + // ! The active icon is optional; + const [initial, active] = icon.split('__'); + + return { + initial, + active, + loadInitialIcon: this.loadIcon(initial), + loadActiveIcon: active ? this.loadIcon(active) : null, + }; + } + // Load icon can be used externally to load additional icons in the cache async loadIcon(iconName) { // this.iconName = icon; diff --git a/blocks/navigation/navigation.js b/blocks/navigation/navigation.js index b2075c8a..2a291dc5 100644 --- a/blocks/navigation/navigation.js +++ b/blocks/navigation/navigation.js @@ -9,7 +9,7 @@ export default class Navigation extends ComponentBase { isActive = false; - #navContentInit = false; + navContentInit = false; navCompactedContentInit = false; @@ -39,9 +39,9 @@ export default class Navigation extends ComponentBase { async init() { super.init(); - this.navContent = this.querySelector('ul'); + this.elements.navContent = this.querySelector('ul'); this.innerHTML = ''; - this.navCompactedContent = this.navContent.cloneNode(true); // the clone need to be done before `this.navContent` is modified + this.elements.navCompactedContent = this.elements.navContent.cloneNode(true); // the clone need to be done before `this.navContent` is modified this.nav = document.createElement('nav'); this.isCompact = this.dataset.compact === 'true'; this.append(this.nav); @@ -56,24 +56,24 @@ export default class Navigation extends ComponentBase { } setupNav() { - if (!this.#navContentInit) { - this.#navContentInit = true; - this.setupClasses(this.navContent); + if (!this.navContentInit) { + this.navContentInit = true; + this.setupClasses(this.elements.navContent); } - this.navButton?.remove(); - this.nav.append(this.navContent); + this.nav.append(this.elements.navContent); } async setupCompactedNav() { + const { navCompactedContent } = this.elements; + if (!this.navCompactedContentInit) { loadAndDefine(componentList.accordion); this.navCompactedContentInit = true; - this.setupClasses(this.navCompactedContent, true); - this.navCompactedContent.addEventListener('click', (e) => this.activate(e)); + this.setupClasses(navCompactedContent, true); } - this.prepend(this.createButton()); - this.nav.append(this.navCompactedContent); + this.nav.append(navCompactedContent); + this.addCompactedListeners(); } onAttributeCompactChanged({ oldValue, newValue }) { @@ -85,13 +85,7 @@ export default class Navigation extends ComponentBase { if (this.isCompact) { this.setupCompactedNav(); } else { - if (this.navButton) { - this.isActive = false; - this.classList.remove('active'); - this.navButton.removeAttribute('aria-expanded'); - this.navIcon.dataset.active = this.isActive; - this.closeAllLevels(); - } + this.cleanCompactedNav(); this.setupNav(); } } @@ -103,25 +97,16 @@ export default class Navigation extends ComponentBase { } createButton() { - this.navButton = document.createElement('button'); - this.navButton.setAttribute('aria-label', 'Menu'); - this.navButton.setAttribute('aria-expanded', 'false'); - this.navButton.setAttribute('aria-controls', 'navigation'); - this.navButton.setAttribute('aria-haspopup', 'true'); - 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'); - this.navButton.setAttribute('aria-expanded', this.isActive); - this.navIcon.dataset.active = this.isActive; - blockBodyScroll(this.isActive); - this.closeAllLevels(); - }); - - return this.navButton; + this.elements.navButton = document.createElement('button'); + const { navButton } = this.elements; + navButton.setAttribute('aria-label', 'Menu'); + navButton.setAttribute('aria-expanded', 'false'); + navButton.setAttribute('aria-controls', 'navigation'); + navButton.setAttribute('aria-haspopup', 'true'); + navButton.setAttribute('type', 'button'); + navButton.innerHTML = ``; + this.elements.navIcon = navButton.querySelector('raqn-icon'); + return navButton; } addIcon(elem) { @@ -158,6 +143,34 @@ export default class Navigation extends ComponentBase { }); } + addCompactedListeners() { + const { navCompactedContent, navButton } = this.elements; + navCompactedContent.addEventListener('click', (e) => this.activate(e)); + navButton.addEventListener('click', (e) => this.toggleNav(e)); + } + + toggleNav() { + const { navIcon, navButton } = this.elements; + this.isActive = !this.isActive; + this.classList.toggle('active'); + navButton.setAttribute('aria-expanded', this.isActive); + navIcon.dataset.active = this.isActive; + blockBodyScroll(this.isActive); + this.closeAllLevels(); + } + + cleanCompactedNav() { + if (!this.navCompactedContentInit) return; + const { navIcon, navButton } = this.elements; + + this.isActive = false; + this.classList.remove('active'); + navButton.removeAttribute('aria-expanded'); + navIcon.dataset.active = this.isActive; + this.closeAllLevels(); + navButton.remove(); + } + activate(e) { if (e.target.tagName.toLowerCase() === 'raqn-icon' || e.target.closest('raqn-icon')) { e.preventDefault(); diff --git a/blocks/popup-trigger/popup-trigger.js b/blocks/popup-trigger/popup-trigger.js index 6f4af659..650adb2e 100644 --- a/blocks/popup-trigger/popup-trigger.js +++ b/blocks/popup-trigger/popup-trigger.js @@ -7,10 +7,14 @@ export default class PopupTrigger extends ComponentBase { isClosePopupTrigger = false; - ariaLabel = null; - popupSourceUrl = null; + popupConfigId = null; + + elements = { + popup: null, + }; + get isActive() { return this.dataset.active === 'true'; } @@ -24,27 +28,40 @@ export default class PopupTrigger extends ComponentBase { triggerIcon: 'raqn-icon', }, closePopupIdentifier: '#popup-close', + triggerPopupIdentifier: '#popup-trigger', }, ]; } init() { - this.setAction(); - this.queryElements(); - this.addListeners(); + this.setAction(this.dataset.action); + super.init(); } - setAction() { - const { closePopupIdentifier } = this.config; - const anchorUrl = new URL(this.dataset.action, window.location.origin); + setAction(action) { + const sourceUrl = URL.parse(action, window.location.origin); + if (!sourceUrl) { + // eslint-disable-next-line no-console + console.warn(`The value provided is not a valid path: ${action}`); + return; + } + + const { closePopupIdentifier, triggerPopupIdentifier } = this.config; - if (anchorUrl.hash === closePopupIdentifier) { + if (sourceUrl.hash === closePopupIdentifier) { this.isClosePopupTrigger = true; - this.dataset.action = anchorUrl.hash; + return; } + + this.popupSourceUrl = sourceUrl.pathname; + + const [, configId] = sourceUrl.hash.split(`${triggerPopupIdentifier}-`); + + if (configId) this.popupConfigId = configId; } addListeners() { + super.addListeners(); this.elements.popupBtn.addEventListener('click', (e) => { e.preventDefault(); this.dataset.active = !this.isActive; @@ -52,21 +69,11 @@ export default class PopupTrigger extends ComponentBase { } onAttributeActionChanged({ oldValue, newValue }) { - if (this.isClosePopupTrigger) { - return; - } + if (!this.initialized) return; + if (this.isClosePopupTrigger) return; if (oldValue === newValue) return; - let sourceUrl; - try { - sourceUrl = new URL(newValue, window.location.origin); - } catch (error) { - // eslint-disable-next-line no-console - console.warn('The value provided is not a valid path', error); - return; - } - - this.popupSourceUrl = sourceUrl.pathname; + this.setAction(newValue); if (this.popup) { this.popup.dataset.url = this.popupSourceUrl; @@ -92,10 +99,8 @@ export default class PopupTrigger extends ComponentBase { if (this.isClosePopupTrigger) return; if (!this.isActive) return; - this.popup = await this.createPopup(); + await this.createPopup(); this.addPopupToPage(); - // the icon is initialize async by page loader - // this.triggerIcon = this.querySelector('raqn-icon'); // Reassign to just toggle after the popup is created; this.loadPopup = this.togglePopup; @@ -103,18 +108,20 @@ export default class PopupTrigger extends ComponentBase { } async createPopup() { - loadAndDefine(componentList.popup); + await loadAndDefine(componentList.popup); const popupEl = document.createElement('raqn-popup'); - popupEl.dataset.action = this.popupSourceUrl; + popupEl.dataset.url = this.popupSourceUrl; popupEl.dataset.active = true; - // Set the popupTrigger property of the popup component to this trigger instance - popupEl.popupTrigger = this; - return popupEl; + // link popup with popup-trigger + popupEl.elements.popupTrigger = this; + if (this.popupConfigId) popupEl.setAttribute('config-id', this.popupConfigId); + + this.elements.popup = popupEl; } togglePopup() { - this.popup.dataset.active = this.isActive; + this.elements.popup.dataset.active = this.isActive; this.elements.popupBtn.setAttribute('aria-expanded', this.isActive); if (this.elements.triggerIcon) { this.elements.triggerIcon.dataset.active = this.isActive; @@ -125,7 +132,7 @@ export default class PopupTrigger extends ComponentBase { } addPopupToPage() { - if (!this.popup) return; - document.body.append(this.popup); + if (!this.elements.popup) return; + document.body.append(this.elements.popup); } } diff --git a/blocks/popup/popup.css b/blocks/popup/popup.css index dbfe8eab..23942890 100644 --- a/blocks/popup/popup.css +++ b/blocks/popup/popup.css @@ -14,6 +14,7 @@ raqn-popup { } raqn-popup:has(.popup__base--flyout) { + --popup-grid-area-size: 6; --popup-grid-area-start: calc(13 - var(--popup-grid-area-size)); --popup-close-btn-area-size: calc(var(--popup-close-btn-padding) * 2); --popup-close-btn-icon-size: calc(var(--popup-close-btn-padding)); diff --git a/blocks/popup/popup.js b/blocks/popup/popup.js index 2fb59ae4..d25382b2 100644 --- a/blocks/popup/popup.js +++ b/blocks/popup/popup.js @@ -18,11 +18,13 @@ export default class Popup extends ComponentBase { configPopupAttributes = ['data-type', 'data-size', 'data-offset', 'data-height']; - /** - * Optional special property to set a reference to a popupTrigger element which controls this popup. - * This will automatically control the states of the popupTrigger based on popup interaction. - */ - popupTrigger = null; + elements = { + /** + * Optional special property to set a reference to a popupTrigger element which controls this popup. + * This will automatically control the states of the popupTrigger based on popup interaction. + */ + popupTrigger: null, + }; get isActive() { return this.dataset.active !== 'true'; @@ -32,6 +34,7 @@ export default class Popup extends ComponentBase { return [ ...super.extendConfig(), { + addFragmentContentOnInit: false, showCloseBtn: true, selectors: { popupBase: '.popup__base', @@ -40,6 +43,7 @@ export default class Popup extends ComponentBase { popupContent: '.popup__content', popupOverlay: '.popup__overlay', popupCloseBtn: '.popup__close-btn', + fragmentTarget: '.popup__container', }, elements: { sourceUrlAnchor: 'a', @@ -54,15 +58,17 @@ export default class Popup extends ComponentBase { } setBinds() { + super.setBinds(); this.closeOnEsc = this.closeOnEsc.bind(this); } - onInit() { + init() { this.showPopup(false); this.createPopupHtml(); - this.setUrlFromTarget(); this.queryElements(); + this.addListeners(); focusTrap(this.elements.popupContainer, { dynamicContent: true }); + this.activeOnConnect(); } createPopupHtml() { @@ -96,13 +102,8 @@ export default class Popup extends ComponentBase { `; } - addContentFromTarget() { - const { target } = this.initOptions; - - this.elements.popupContent.append(...target.childNodes); - } - addListeners() { + super.addListeners(); this.elements.popupCloseBtn.addEventListener('click', () => { this.dataset.active = false; }); @@ -111,23 +112,21 @@ export default class Popup extends ComponentBase { }); } - connected() { - this.activeOnConnect(); - } - activeOnConnect() { if (this.isActive) return; - popupState.closeActivePopup(); + + this.openPopup(); + /* popupState.closeActivePopup(); popupState.activePopup = this; blockBodyScroll(true); this.showPopup(true); this.toggleCloseOnEsc(true); - focusFirstElementInContainer(this.elements.popupContainer); + focusFirstElementInContainer(this.elements.popupContainer); */ } async addFragmentContent() { - this.elements.popupContent.innerHTML = await this.fragmentContent; + this.elements.popupContent.append(...this.fragmentContent); } setInnerBlocks() { @@ -224,7 +223,6 @@ export default class Popup extends ComponentBase { blockBodyScroll(true); await this.addFragmentContent(); this.setInnerBlocks(); - await this.initChildComponents(); this.showPopup(true); this.updatePopupTrigger(true); this.toggleCloseOnEsc(true); @@ -247,7 +245,7 @@ export default class Popup extends ComponentBase { } updatePopupTrigger(isActive) { - if (this.popupTrigger) this.popupTrigger.dataset.active = isActive; + if (this.elements.popupTrigger) this.elements.popupTrigger.dataset.active = isActive; } showPopup(boolean) { diff --git a/blocks/sidekick-tools-palette/sidekick-tools-palette.js b/blocks/sidekick-tools-palette/sidekick-tools-palette.js index 783cb47a..b2fe5ebe 100644 --- a/blocks/sidekick-tools-palette/sidekick-tools-palette.js +++ b/blocks/sidekick-tools-palette/sidekick-tools-palette.js @@ -376,7 +376,8 @@ export default class SidekickToolsPalette extends ComponentBase { }); } - connected() { + init() { + super.init(); this.initPalette(); } } diff --git a/blocks/theming/theming.js b/blocks/theming/theming.js index c1fe0a29..ddf22b71 100644 --- a/blocks/theming/theming.js +++ b/blocks/theming/theming.js @@ -11,7 +11,6 @@ import { getBaseUrl, runTasks, } from '../../scripts/libs.js'; -import { externalConfig } from '../../scripts/libs/external-config.js'; const k = Object.keys; @@ -189,10 +188,7 @@ ${k(f) return {}; } - const response = - name === 'component' - ? externalConfig.loadConfig(true) // use the loader to prevent duplicated calls - : await fetch(`${name !== 'fontface' ? base : ''}${content}.json`); + const response = await fetch(`${name !== 'fontface' ? base : ''}${content}.json`); return this.processFragment(response, name); }), ); diff --git a/head.html b/head.html index aa013b0d..37d621a1 100644 --- a/head.html +++ b/head.html @@ -1,35 +1,51 @@ + + - - + diff --git a/scripts/component-base.js b/scripts/component-base.js index 04adaf45..d5dc0961 100644 --- a/scripts/component-base.js +++ b/scripts/component-base.js @@ -7,11 +7,11 @@ import { isObject, deepMerge, deepMergeByType, - unFlat, stringToArray, mergeUniqueArrays, stringToJsVal, runTasks, + loadAndDefine, } from './libs.js'; import { componentList } from './component-list/component-list.js'; import { externalConfig } from './libs/external-config.js'; @@ -23,8 +23,7 @@ export default class ComponentBase extends HTMLElement { // The order of observedAttributes is the order in which the values from config are added. static observedAttributes = []; - // dependencies must to be added and checked locally for cases when components are created inside other components - static dependencies = componentList[this.componentName]?.module?.dependencies || []; + static dependencies; // dynamically added to each constructor in `loadDependencies()` initialization; // set as promise in constructor(); @@ -60,6 +59,9 @@ export default class ComponentBase extends HTMLElement { // All settings which are not in `attributesValues` which might require extension in extended components should be in the config. // Use the `extendConfig()` method to extend the config config = { + addFragmentContentOnInit: true, + hideOnInitError: true, + listenBreakpoints: false, selectors: {}, classes: { showLoader: 'show-loader', @@ -69,18 +71,20 @@ export default class ComponentBase extends HTMLElement { dispatches: { initialized: (uuid) => `initialized:${uuid}`, }, - listenBreakpoints: false, - hideOnInitError: true, // All the component attributes which are not in the `observedAttributes` knownAttributes: { configId: 'config-id', isLoading: 'isloading', raqnwebcomponent: 'raqnwebcomponent', }, + }; + + mergeMethods = { // Merge options for non object values in `attributesValues` - attributesMergeMethods: { + forAttributesValues: { '**.class': (a, b) => mergeUniqueArrays(a, b), }, + forConfig: null, }; dataAttributesKeys = this.constructor.observedAttributes.flatMap((data) => { @@ -95,9 +99,12 @@ export default class ComponentBase extends HTMLElement { constructor() { super(); + this.constructor.instancesRef ??= []; + this.constructor.instancesRef.push(this); this.setInitializationPromise(); this.setDefaults(); this.setBinds(); + this.loadDependencies(); } /** @@ -119,6 +126,19 @@ export default class ComponentBase extends HTMLElement { this.onBreakpointChange = this.onBreakpointChange.bind(this); } + loadDependencies() { + if (this.constructor.dependencies) return; + + this.constructor.dependencies = componentList[this.componentName]?.module?.dependencies || []; + this.constructor.dependencies.forEach((dependency) => { + if (!componentList[dependency]?.module?.path) return; + if (window.raqnComponents[this.webComponentName]) return; + setTimeout(() => { + loadAndDefine(componentList[dependency]); + }, 0); + }); + } + // Build-in method called after the element is added to the DOM. async connectedCallback() { const { knownAttributes, hideOnInitError, dispatches } = this.config; @@ -150,22 +170,37 @@ export default class ComponentBase extends HTMLElement { /** * Do not overwrite this method unless absolutely needed. */ async onConnected() { - await this.initSettings(); - await this.loadFragment(this.fragmentPath); - await this.init(); + await runTasks.call( + this, + null, + this.initSettings, + async function loadFragment() { + await this.loadFragment(this.fragmentPath); + }, + this.init, + ); } /** - * Use this method to add the component's functionality */ + * Use this method to add the component's functionality + * If the functionality can generate long blocking tasks consider using runTasks() */ async init() { + this.queryElements(); await this.addListeners(); } async initSettings() { - this.extendConfigRunner({ field: 'config', method: 'extendConfig' }); - this.setInitialAttributesValues(); - await this.buildExternalConfig(); - this.runConfigsByViewport(); // set the values for current breakpoint + await runTasks.call( + this, + null, + function extendConfig() { + this.extendConfigRunner({ field: 'mergeMethods', method: 'extendMergeMethods' }); + this.extendConfigRunner({ field: 'config', method: 'extendConfig' }); + }, + this.setInitialAttributesValues, + this.buildExternalConfig, + this.runConfigsByViewport, // set the values for current breakpoint + ); } // Using the `method` which returns an array of objects it's easier to extend @@ -173,7 +208,11 @@ export default class ComponentBase extends HTMLElement { extendConfigRunner({ field, method }) { const conf = this[method]?.(); if (conf.length <= 1) return; - this[field] = deepMerge({}, ...conf); + this[field] = deepMergeByType(this.mergeMethods.forConfig, {}, ...conf); + } + + extendMergeMethods() { + return [...(super.mergeMethods?.() || []), this.mergeMethods]; } extendConfig() { @@ -217,7 +256,7 @@ export default class ComponentBase extends HTMLElement { }); this.attributesValues = deepMergeByType( - this.config.attributesMergeMethods, + this.mergeMethods.forAttributesValues, {}, this.attributesValues, initialAttributesValues, @@ -225,28 +264,30 @@ export default class ComponentBase extends HTMLElement { } async buildExternalConfig() { - const unFlatConfig = unFlat(await externalConfig.getConfig(this.componentName, this.configId)); + const extConfig = await externalConfig.getConfig(this.componentName, this.configId); + /** + * Any options which are not required to use `attributeChangedCallback` + * with different values per breakpoint should be added to this.config */ + const configExternal = extConfig.config; + if (configExternal) { + delete extConfig.config; + deepMergeByType(this.mergeMethods.forConfig, {}, this.config, configExternal); + } // turn classes to array - Object.values(unFlatConfig).forEach((value) => { + Object.values(extConfig).forEach((value) => { if (typeof value.class === 'string') { value.class = stringToArray(value.class, { divider: ' ' }); } }); - const toMerge = [this.attributesValues, unFlatConfig]; + const toMerge = [this.attributesValues, extConfig]; if (this.overrideExternalConfig) toMerge.reverse(); this.attributesValues = deepMergeByType(this.config.attributesMergeMethods, {}, ...toMerge); } - onBreakpointChange(e) { - if (e.matches) { - this.runConfigsByViewport(); - } - } - currentAttributesValues() { const { name } = this.breakpoints.active; const currentAttrValues = deepMergeByType( @@ -319,13 +360,6 @@ export default class ComponentBase extends HTMLElement { }); } - // TODO handle this part - applySetting(config) { - // delete the setting to run only once on init - delete this.attributesValues.all.setting; - deepMerge(this.config, config); - } - /** * Attributes are assigned before the `connectedCallback` is triggered. * In some cases a check for `this.initialized` inside `onAttribute${capitalizedAttr}Changed` might be required @@ -358,12 +392,6 @@ export default class ComponentBase extends HTMLElement { return attribute?.[activeBrName] ?? attribute?.all; } - addListeners() { - if (Object.keys(this.attributesValues).length >= 1) { - listenBreakpointChange(this.onBreakpointChange); - } - } - async loadFragment(path) { if (typeof path !== 'string') return; const response = await this.getFragment(path); @@ -377,27 +405,46 @@ export default class ComponentBase extends HTMLElement { async processFragment(response) { if (response.ok) { this.fragmentContent = await response.text(); - await this.addFragmentContent(); + await runTasks.call( + this, + null, + this.fragmentVirtualDom, + this.fragmentVirtualDomManipulation, + this.renderFragment, + this.addFragmentContent, + ); } } - async addFragmentContent() { - await runTasks.call( - this, - null, - function fragmentVirtualDom() { - const element = document.createElement('div'); - element.innerHTML = this.fragmentContent; - return generateVirtualDom(element.childNodes); - }, - // eslint-disable-next-line prefer-arrow-callback - async function fragmentVirtualDomManipulation({ fragmentVirtualDom }) { - await generalManipulation(fragmentVirtualDom); - }, - function renderFragment({ fragmentVirtualDom }) { - this.append(...renderVirtualDom(fragmentVirtualDom)); - }, - ); + fragmentVirtualDom() { + const element = document.createElement('div'); + element.innerHTML = this.fragmentContent; + return generateVirtualDom(element.childNodes); + } + + async fragmentVirtualDomManipulation({ fragmentVirtualDom }) { + await generalManipulation(fragmentVirtualDom); + } + + renderFragment({ fragmentVirtualDom }) { + this.fragmentContent = renderVirtualDom(fragmentVirtualDom); + return { stopTaskRun: !this.config.addFragmentContentOnInit }; + } + + addFragmentContent() { + this.append(...this.fragmentContent); + } + + addListeners() { + if (Object.keys(this.attributesValues).length >= 1) { + listenBreakpointChange(this.onBreakpointChange); + } + } + + onBreakpointChange(e) { + if (e.matches) { + this.runConfigsByViewport(); + } } queryElements() { diff --git a/scripts/component-list/component-list.js b/scripts/component-list/component-list.js index 90ef9795..b1afe17e 100644 --- a/scripts/component-list/component-list.js +++ b/scripts/component-list/component-list.js @@ -184,11 +184,25 @@ export const componentList = { }, button: { tag: 'raqn-button', - method: 'replace', filterNode(node) { if (node.tag === 'p' && node.hasOnlyChild('a')) return true; return false; }, + transform(node) { + node.tag = this.tag; + + const [textNode] = node.firstChild.queryAll((n) => n.tag === 'textNode', { queryLevel: 2 }); + const [ariaLabel] = node.firstChild.queryAll( + (n) => n.tag === 'strong' && [n.nextSibling?.tag, n.previousSibling?.tag].includes('raqn-icon'), + { queryLevel: 1 }, + ); + if (!ariaLabel && textNode) { + textNode.tag = 'span'; + } else if (ariaLabel && textNode) { + node.firstChild.attributes['aria-label'] = textNode.text; + ariaLabel.remove(); + } + }, module: { path: '/blocks/button/button', priority: 0, @@ -197,37 +211,36 @@ export const componentList = { 'popup-trigger': { tag: 'raqn-popup-trigger', method: 'replaceWith', + popupHash: '#popup-trigger', + closeHash: '#popup-close', filterNode(node) { if (node.tag === 'a') { if (node.parentNode.tag === 'raqn-button') { const { href } = node.attributes; - const hash = href.substring(href.indexOf('#')); - if (['#popup-trigger', '#popup-close'].includes(hash)) return true; + const [, hash] = href.split(/(?=#)/g); + if ([this.popupHash, this.closeHash].some((item) => hash?.startsWith(item))) { + return true; + } } } return false; }, transform(node) { - const { href } = node.attributes; - const hash = href.substring(href.indexOf('#')); + let { href } = node.attributes; + const [, hash] = href.split(/(?=#)/g); + href = hash.startsWith(this.closeHash) ? this.closeHash : href; + node.tag = 'button'; + node.attributes['aria-expanded'] = 'false'; + node.attributes['aria-haspopup'] = 'false'; + delete node.attributes.href; return [ { - tag: 'raqn-popup-trigger', + tag: this.tag, attributes: { - 'data-action': hash, + 'data-action': href, }, - children: [ - { - tag: 'button', - attributes: { - 'aria-expanded': 'false', - 'aria-haspopup': 'true', - type: 'button', - }, - children: [...node.children], - }, - ], + children: [node], }, { processChildren: true }, ]; @@ -298,11 +311,4 @@ export const componentList = { }, }; -export const injectedComponents = [ - { - tag: 'div', - class: ['theming'], - children: [], - attributes: [], - }, -]; +export const injectedComponents = []; diff --git a/scripts/index.js b/scripts/index.js index 72d0e981..548a97f9 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -1,6 +1,6 @@ import { generateVirtualDom, renderVirtualDom } from './render/dom.js'; import { pageManipulation, templateManipulation } from './render/dom-manipulations.js'; -import { getMeta, metaTags, runTasks, isTemplatePage, previewModule } from './libs.js'; +import { getMeta, metaTags, runTasks, isTemplatePage, previewModule, loadAndDefine } from './libs.js'; await previewModule(import.meta); @@ -9,6 +9,7 @@ export default { runTasks.call( this, // all the tasks bellow will be bound to this object when called. null, + this.addTheming, this.generatePageVirtualDom, this.pageVirtualDomManipulation, this.loadAndProcessTemplate, @@ -16,6 +17,14 @@ export default { ); }, + // add theming early + addTheming() { + document.head.append(document.createElement('raqn-theming')); + setTimeout(() => { + loadAndDefine({ tag: 'raqn-theming', module: { path: '/blocks/theming/theming' } }); + }, 0); + }, + generatePageVirtualDom() { window.raqnVirtualDom = generateVirtualDom(document.body.childNodes); document.body.innerHTML = ''; diff --git a/scripts/libs.js b/scripts/libs.js index d22c549e..91c91ad8 100644 --- a/scripts/libs.js +++ b/scripts/libs.js @@ -92,8 +92,8 @@ export const metaTags = { fallbackContent: '/configs/layout', // contentType: 'path without extension', }, - themeConfigComponent: { - metaName: 'theme-config-component', + componentsConfig: { + metaName: 'components-config', fallbackContent: '/configs/components-config', // contentType: 'path without extension', }, @@ -366,13 +366,14 @@ export function deepMerge(origin, ...toMerge) { export function deepMergeByType(keyPathMethods, origin, ...toMerge) { if (!toMerge.length) return origin; const merge = toMerge.shift(); + const keyPathMethodsCopy = keyPathMethods || {}; const pathsArrays = - keyPathMethods?.pathsArrays || - Object.entries(keyPathMethods).flatMap(([key, method]) => { + keyPathMethodsCopy?.pathsArrays || + Object.entries(keyPathMethodsCopy).flatMap(([key, method]) => { if (key === 'currentPath') return []; return [[key.split('.').map((k) => k.split('|')), method]]; }); - const { currentPath = [] } = keyPathMethods; + const { currentPath = [] } = keyPathMethodsCopy; if (isOnlyObject(origin) && isOnlyObject(merge)) { Object.keys(merge).forEach((key) => { @@ -462,6 +463,14 @@ export function loadModule(urlWithoutExtension, { loadCSS = true, loadJS = true return modules; } +/** + * When creating elements that require properties to be set on them + * either await for this method when loading the component + * or use `await customElements.whenDefined('component-tag');` + * Otherwise properties set on the element before it was defined will be overwritten + * with the defaults from the class. + * This is not required for attributes. + */ export async function loadAndDefine(componentConfig) { const { tag, module: { path, loadJS, loadCSS } = {} } = componentConfig; if (window.raqnComponents[tag]) { diff --git a/scripts/libs/external-config.js b/scripts/libs/external-config.js index 960a4311..d01aa604 100644 --- a/scripts/libs/external-config.js +++ b/scripts/libs/external-config.js @@ -1,4 +1,4 @@ -import { getMeta, metaTags, readValue, deepMerge, getBaseUrl } from '../libs.js'; +import { getMeta, metaTags, readValue, deepMerge, getBaseUrl, unFlat } from '../libs.js'; window.raqnComponentsMasterConfig = window.raqnComponentsMasterConfig || null; @@ -8,17 +8,19 @@ export const externalConfig = { const configNameFallback = configName || 'default'; window.raqnComponentsMasterConfig ??= await this.loadConfig(); const componentConfig = window.raqnComponentsMasterConfig?.[componentName]; - const parsedConfig = componentConfig?.[configNameFallback]; + if (componentConfig?.[configNameFallback]) { + componentConfig[configNameFallback] = unFlat(componentConfig?.[configNameFallback]); + // return copy of object to prevent mutation of raqnComponentsMasterConfig; + return deepMerge({}, componentConfig[configNameFallback]); + } - // return copy of object to prevent mutation of raqnComponentsMasterConfig; - if (parsedConfig) return deepMerge({}, parsedConfig); return {}; }, async loadConfig(rawConfig) { window.raqnComponentsConfig ??= (async () => { const { - themeConfigComponent: { metaName }, + componentsConfig: { metaName }, themeConfig, } = metaTags; const metaConfigPath = getMeta(metaName); @@ -40,6 +42,7 @@ export const externalConfig = { } catch (error) { // eslint-disable-next-line no-console console.error(error); + return {}; } return result; })(); @@ -57,7 +60,7 @@ export const externalConfig = { if (!window.raqnComponentsConfig[key]) return; const { data } = window.raqnComponentsConfig[key]; if (data?.length) { - window.raqnParsedConfigs[key] = window.raqnParsedConfigs[key] || {}; + window.raqnParsedConfigs[key] ??= {}; window.raqnParsedConfigs[key] = readValue(data, window.raqnParsedConfigs[key]); } }); diff --git a/styles/styles.css b/styles/styles.css index c5bc6c21..78dcfdd9 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -199,10 +199,29 @@ img { } /* 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 + Use :where() to give lower specificity in order to not overwrite any styles set on the web component tag */ :where([raqnwebcomponent]) { display: block; + + /* Add defaults to initial to prevent inheritance */ + --margin-block-start: initial; + --margin-block-end: initial; + --margin-inline-start: initial; + --margin-inline-end: initial; + --padding-block-start: initial; + --padding-block-end: initial; + --padding-inline-start: initial; + --padding-inline-end: initial; + + margin-block-start: var(--margin-block-start); + margin-block-end: var(--margin-block-end); + margin-inline-start: var(--margin-inline-start); + margin-inline-end: var(--margin-inline-end); + padding-block-start: var(--padding-block-start); + padding-block-end: var(--padding-block-end); + padding-inline-start: var(--padding-inline-start); + padding-inline-end: var(--padding-inline-end); } /* Container: make all content act as a container where background of the container is limited to the content area */