UNPKG

@empathyco/x-components

Version:
327 lines (324 loc) • 13.8 kB
import { objectFilter } from '@empathyco/x-utils'; import { defineComponent, inject, ref, computed, onMounted } from 'vue'; import { GlobalEvents } from 'vue-global-events'; import { use$x } from '../../../composables/use-_x.js'; import { useState } from '../../../composables/use-state.js'; import { isArrayEmpty } from '../../../utils/array.js'; import { initialUrlState } from '../store/initial-state.js'; import { urlXModule } from '../x-module.js'; /** * This component manages the browser URL parameters to preserve them through reloads and browser * history navigation. It allow to configure the default url parameter names using its attributes. * This component doesn't render elements to the DOM. * * @public */ var _sfc_main = defineComponent({ name: 'UrlHandler', components: { GlobalEvents, }, xModule: urlXModule.name, setup(_, { attrs }) { const $x = use$x(); const { initialExtraParams } = useState('url'); /** * The {@link SnippetConfig} provided by an ancestor. * * @internal */ const snippetConfig = inject('snippetConfig'); /** * Flag to know if the params were already loaded from the URL. * * @internal */ const urlLoaded = ref(false); /** * The page URL. It is used to compare against the current URL to check navigation state. * * @internal */ const url = ref(undefined); /** * Flag to know if the page has been persisted by the browser's back-forward cache. * * @internal */ const isPagePersisted = ref(false); /** * Computed to know which params we must get from URL. It gets the params names from the initial * state, to get all default params names, and also from the `$attrs` to get the extra params * names to take into account. * * @returns An array with the name of the params. * * @internal */ const managedParamsNames = computed(() => Object.keys({ ...initialUrlState, ...attrs })); /** * Returns the mapping of the param keys used in the URL is configured through $attrs. This way * we can support any param and extra param, no matters its name. * * @param paramName - The param name to get the Url key. * @returns The key used in the URL for the `paramName` passed. * * @internal */ const getUrlKey = (paramName) => { const paramValue = attrs[paramName]; return typeof paramValue === 'string' ? paramValue : paramName; }; /** * Deletes all the parameters in the passed URL. * * @param url - The URL to remove parameters from. * @internal */ const deleteUrlParameters = (url) => { managedParamsNames.value.forEach(paramName => url.searchParams.delete(getUrlKey(paramName))); }; /** * Sorts the params in a tuple array [key,value] to generate always the same URL with the params * in the same order. * * @param urlParams - The {@link UrlParams} to sort. * @returns An array of tuples with the key-value of each paramter, sorted by key. * @internal */ const sortParams = (urlParams) => { return Object.entries(urlParams).sort(([param1], [param2]) => { return param1 < param2 ? -1 : 1; }); }; /** * Set all the provided parameters to the url with the mapped key. * * @param url - The current URL. * @param urlParams - The list of parameters to add. * @remarks The params are filtered because there maybe received extra params which will not be * managed by URL. This is defined by the `managedParamsNames` computed. Also, the parameters * are sorted Alphabetically to produce always the same URL with the same parameters.This is * important for SEO purposes. * * @internal */ const setUrlParameters = (url, urlParams) => { // Only when there is a query the rest of the parameters are valid. if (!urlParams.query) { return; } const filteredParams = objectFilter(urlParams, paramName => managedParamsNames.value.includes(paramName)); const sortedParameters = sortParams(filteredParams); sortedParameters.forEach(([paramName, paramValue]) => { const urlParamKey = getUrlKey(paramName); if (Array.isArray(paramValue)) { paramValue.forEach(value => { url.searchParams.append(urlParamKey, String(value)); }); } else { url.searchParams.set(urlParamKey, String(paramValue)); } }); }; /** * Updates the browser URL with the passed `newUrlParams` and using the browser history method * passed as `historyMethod`. It only updates the browser history if the new URL is different * from the current. * * @param newUrlParams - The new params to add to the browser URL. * @param historyMethod - The browser history method used to add the new URL. * * @internal */ const updateUrl = (newUrlParams, historyMethod) => { if (urlLoaded.value) { const newUrl = new URL(window.location.href); deleteUrlParameters(newUrl); setUrlParameters(newUrl, newUrlParams); // Normalize '+' characters into '%20' for spaces in url params. newUrl.search = newUrl.search.replace(/\+/g, '%20'); if (newUrl.href !== window.location.href) { historyMethod({ ...window.history.state }, document.title, newUrl.href); } url.value = newUrl; } }; /** * Updates the browser URL with the new {@link UrlParams} using the history `pushState` method. * * @param newUrlParams - The new params to update browser URL. */ $x.on('PushableUrlStateUpdated', false).subscribe((newUrlParams) => { updateUrl(newUrlParams, window.history.pushState.bind(window.history)); }); /** * Updates the browser URL with the new {@link UrlParams} using the history `replaceState` * method. * * @param newUrlParams - The new params to update browser URL. */ $x.on('ReplaceableUrlStateUpdated', false).subscribe((newUrlParams) => { updateUrl(newUrlParams, window.history.replaceState.bind(window.history)); }); /** * Handler of the * [pageshow](https://developer.mozilla.org/en-US/docs/Web/API/Window/pageshow_event) * event. * * @remarks The pageshow event is listened to check if the browser has performed a navigation * using the back-forward cache. This information is available in the * PageTransitionEvent.persisted property. * * @param event - The page transition event. * @internal */ const onPageShow = (event) => { isPagePersisted.value = event.persisted; if (event.persisted) { // The internal url is reset due to the back-forward cache storing the previous value which // is no longer valid. url.value = undefined; } }; /** * Returns the URL param value parsed depending on its type in the initial store state. As we * can not know what type can have an extra param, all extra params are parsed as strings. We * know if it is an extra param because it is not in the initial state. * * @param name - The name of the param in {@link UrlParams}. * @param value - The `URLSearchParams` value as an arry of strings. * @returns The parsed value. * * @internal */ const parseUrlParam = (name, value) => { switch (typeof initialUrlState[name]) { case 'number': return Number(value[0]); case 'boolean': return value[0].toLowerCase() === 'true'; case 'string': return value[0]; default: // array return value; } }; /** * Gets the {@link UrlParams} from the URL, including only the params defined by `paramsNames`. * * @returns ParsedUrlParams obtained from URL. * @internal */ const parseUrlParams = () => { const urlSearchParams = new URL(window.location.href).searchParams; return managedParamsNames.value.reduce((params, name) => { const urlKey = getUrlKey(name); if (urlSearchParams.has(urlKey)) { if (name in initialUrlState) { const urlValue = urlSearchParams.getAll(urlKey); params.all[name] = parseUrlParam(name, urlValue); } else { params.all[name] = params.extra[name] = urlSearchParams.get(urlKey); } } return params; }, { all: { ...initialUrlState }, extra: { ...initialExtraParams.value } }); }; /** * Check if the navigation is from a product page. * * @remarks Due to Safari 14 not supporting the new and standard PerformanceNavigationTiming * API, we are falling back to the deprecated one, PerformanceNavigation. We also fallback to * this API whenever we get a navigationType equal to reload, because Safari has a bug that the * navigationType is permanently set to reload after you have reload the page and it never * resets. As some browsers have a back-forward cache implemented, we also take into account if * the page is persisted. * * @returns True if the navigation is from a product page, false otherwise. * @internal */ const isNavigatingFromPdp = () => { const isPagePersistedValue = isPagePersisted.value; const navigationEntries = window.performance.getEntriesByType('navigation'); const navigationType = navigationEntries[0]?.type; const useFallbackStrategy = !navigationEntries.length && (isArrayEmpty(navigationEntries) || navigationType === 'reload'); // Reset internal isPagePersisted property value isPagePersisted.value = false; if (useFallbackStrategy) { const isNavigatingInSpa = !!snippetConfig?.isSpa && navigationType === 'navigate'; return navigationType === 'back_forward' || isNavigatingInSpa || isPagePersistedValue; } else { const isNavigatingInSpa = !!snippetConfig?.isSpa && navigationType === 'navigate'; return navigationType === 'back_forward' || isNavigatingInSpa || isPagePersistedValue; } }; /** * Detects the {@link FeatureLocation} used to build the * {@link QueryOriginInit} data. * * @returns The {@link FeatureLocation}. * @internal */ const detectLocation = () => { const currentUrl = new URL(window.location.href); const previousUrl = url.value; url.value = currentUrl; const isInternalNavigation = previousUrl?.search !== currentUrl.search && previousUrl?.pathname === currentUrl.pathname; if (isInternalNavigation) { return 'url_history'; } if (isNavigatingFromPdp()) { return 'url_history_pdp'; } return 'external'; }; /** * Creates the wire metadata to include in every emitted {@link XEvent}. * * @returns The {@link WireMetadata}. * @internal */ const createWireMetadata = () => { return { feature: 'url', location: detectLocation(), }; }; /** * Emits the {@link UrlXEvents.ParamsLoadedFromUrl} XEvent, * the {@link UrlXEvents.ExtraParamsLoadedFromUrl} XEvent and, if there is query, also emits * the {@link XEventsTypes.UserOpenXProgrammatically}. * * @internal */ const emitEvents = () => { const { all, extra } = parseUrlParams(); const metadata = createWireMetadata(); $x.emit('ParamsLoadedFromUrl', all, metadata); $x.emit('ExtraParamsLoadedFromUrl', extra, metadata); if (all.query) { $x.emit('UserOpenXProgrammatically', undefined, metadata); } urlLoaded.value = true; }; /** * To emit the Url events just when the URL is load, and before the components mounted events * and state changes, we do it in the created of this component. */ onMounted(() => { emitEvents(); }); return { onPageShow, emitEvents, }; }, }); export { _sfc_main as default }; //# sourceMappingURL=url-handler.vue2.js.map