wp/wp-includes/js/dist/script-modules/interactivity-router/index.js
changeset 22 8c2e4d02f4ef
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wp/wp-includes/js/dist/script-modules/interactivity-router/index.js	Fri Sep 05 18:52:52 2025 +0200
@@ -0,0 +1,515 @@
+import * as __WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__ from "@wordpress/interactivity";
+/******/ var __webpack_modules__ = ({
+
+/***/ 317:
+/***/ ((module) => {
+
+module.exports = import("@wordpress/a11y");;
+
+/***/ })
+
+/******/ });
+/************************************************************************/
+/******/ // The module cache
+/******/ var __webpack_module_cache__ = {};
+/******/ 
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/ 	// Check if module is in cache
+/******/ 	var cachedModule = __webpack_module_cache__[moduleId];
+/******/ 	if (cachedModule !== undefined) {
+/******/ 		return cachedModule.exports;
+/******/ 	}
+/******/ 	// Create a new module (and put it into the cache)
+/******/ 	var module = __webpack_module_cache__[moduleId] = {
+/******/ 		// no module.id needed
+/******/ 		// no module.loaded needed
+/******/ 		exports: {}
+/******/ 	};
+/******/ 
+/******/ 	// Execute the module function
+/******/ 	__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
+/******/ 
+/******/ 	// Return the exports of the module
+/******/ 	return module.exports;
+/******/ }
+/******/ 
+/************************************************************************/
+/******/ /* webpack/runtime/define property getters */
+/******/ (() => {
+/******/ 	// define getter functions for harmony exports
+/******/ 	__webpack_require__.d = (exports, definition) => {
+/******/ 		for(var key in definition) {
+/******/ 			if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
+/******/ 				Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
+/******/ 			}
+/******/ 		}
+/******/ 	};
+/******/ })();
+/******/ 
+/******/ /* webpack/runtime/hasOwnProperty shorthand */
+/******/ (() => {
+/******/ 	__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
+/******/ })();
+/******/ 
+/************************************************************************/
+var __webpack_exports__ = {};
+
+// EXPORTS
+__webpack_require__.d(__webpack_exports__, {
+  o: () => (/* binding */ actions),
+  w: () => (/* binding */ state)
+});
+
+;// external "@wordpress/interactivity"
+var x = (y) => {
+	var x = {}; __webpack_require__.d(x, y); return x
+} 
+var y = (x) => (() => (x))
+const interactivity_namespaceObject = x({ ["getConfig"]: () => (__WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__.getConfig), ["privateApis"]: () => (__WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__.privateApis), ["store"]: () => (__WEBPACK_EXTERNAL_MODULE__wordpress_interactivity_8e89b257__.store) });
+;// ./node_modules/@wordpress/interactivity-router/build-module/head.js
+/**
+ * The cache of prefetched stylesheets and scripts.
+ */
+const headElements = new Map();
+
+/**
+ * Helper to update only the necessary tags in the head.
+ *
+ * @async
+ * @param newHead The head elements of the new page.
+ */
+const updateHead = async newHead => {
+  // Helper to get the tag id store in the cache.
+  const getTagId = tag => tag.id || tag.outerHTML;
+
+  // Map incoming head tags by their content.
+  const newHeadMap = new Map();
+  for (const child of newHead) {
+    newHeadMap.set(getTagId(child), child);
+  }
+  const toRemove = [];
+
+  // Detect nodes that should be added or removed.
+  for (const child of document.head.children) {
+    const id = getTagId(child);
+    // Always remove styles and links as they might change.
+    if (child.nodeName === 'LINK' || child.nodeName === 'STYLE') {
+      toRemove.push(child);
+    } else if (newHeadMap.has(id)) {
+      newHeadMap.delete(id);
+    } else if (child.nodeName !== 'SCRIPT' && child.nodeName !== 'META') {
+      toRemove.push(child);
+    }
+  }
+  await Promise.all([...headElements.entries()].filter(([, {
+    tag
+  }]) => tag.nodeName === 'SCRIPT').map(async ([url]) => {
+    await import(/* webpackIgnore: true */url);
+  }));
+
+  // Prepare new assets.
+  const toAppend = [...newHeadMap.values()];
+
+  // Apply the changes.
+  toRemove.forEach(n => n.remove());
+  document.head.append(...toAppend);
+};
+
+/**
+ * Fetches and processes head assets (stylesheets and scripts) from a specified document.
+ *
+ * @async
+ * @param doc The document from which to fetch head assets. It should support standard DOM querying methods.
+ *
+ * @return Returns an array of HTML elements representing the head assets.
+ */
+const fetchHeadAssets = async doc => {
+  const headTags = [];
+
+  // We only want to fetch module scripts because regular scripts (without
+  // `async` or `defer` attributes) can depend on the execution of other scripts.
+  // Scripts found in the head are blocking and must be executed in order.
+  const scripts = doc.querySelectorAll('script[type="module"][src]');
+  scripts.forEach(script => {
+    const src = script.getAttribute('src');
+    if (!headElements.has(src)) {
+      // add the <link> elements to prefetch the module scripts
+      const link = doc.createElement('link');
+      link.rel = 'modulepreload';
+      link.href = src;
+      document.head.append(link);
+      headElements.set(src, {
+        tag: script
+      });
+    }
+  });
+  const stylesheets = doc.querySelectorAll('link[rel=stylesheet]');
+  await Promise.all(Array.from(stylesheets).map(async tag => {
+    const href = tag.getAttribute('href');
+    if (!href) {
+      return;
+    }
+    if (!headElements.has(href)) {
+      try {
+        const response = await fetch(href);
+        const text = await response.text();
+        headElements.set(href, {
+          tag,
+          text
+        });
+      } catch (e) {
+        // eslint-disable-next-line no-console
+        console.error(e);
+      }
+    }
+    const headElement = headElements.get(href);
+    const styleElement = doc.createElement('style');
+    styleElement.textContent = headElement.text;
+    headTags.push(styleElement);
+  }));
+  return [doc.querySelector('title'), ...doc.querySelectorAll('style'), ...headTags];
+};
+
+;// ./node_modules/@wordpress/interactivity-router/build-module/index.js
+var _getConfig$navigation;
+/**
+ * WordPress dependencies
+ */
+
+
+/**
+ * Internal dependencies
+ */
+
+const {
+  directivePrefix,
+  getRegionRootFragment,
+  initialVdom,
+  toVdom,
+  render,
+  parseServerData,
+  populateServerData,
+  batch
+} = (0,interactivity_namespaceObject.privateApis)('I acknowledge that using private APIs means my theme or plugin will inevitably break in the next version of WordPress.');
+// Check if the navigation mode is full page or region based.
+const navigationMode = (_getConfig$navigation = (0,interactivity_namespaceObject.getConfig)('core/router').navigationMode) !== null && _getConfig$navigation !== void 0 ? _getConfig$navigation : 'regionBased';
+
+// The cache of visited and prefetched pages, stylesheets and scripts.
+const pages = new Map();
+
+// Helper to remove domain and hash from the URL. We are only interesting in
+// caching the path and the query.
+const getPagePath = url => {
+  const u = new URL(url, window.location.href);
+  return u.pathname + u.search;
+};
+
+// Fetch a new page and convert it to a static virtual DOM.
+const fetchPage = async (url, {
+  html
+}) => {
+  try {
+    if (!html) {
+      const res = await window.fetch(url);
+      if (res.status !== 200) {
+        return false;
+      }
+      html = await res.text();
+    }
+    const dom = new window.DOMParser().parseFromString(html, 'text/html');
+    return regionsToVdom(dom);
+  } catch (e) {
+    return false;
+  }
+};
+
+// Return an object with VDOM trees of those HTML regions marked with a
+// `router-region` directive.
+const regionsToVdom = async (dom, {
+  vdom
+} = {}) => {
+  const regions = {
+    body: undefined
+  };
+  let head;
+  if (false) {}
+  if (navigationMode === 'regionBased') {
+    const attrName = `data-${directivePrefix}-router-region`;
+    dom.querySelectorAll(`[${attrName}]`).forEach(region => {
+      const id = region.getAttribute(attrName);
+      regions[id] = vdom?.has(region) ? vdom.get(region) : toVdom(region);
+    });
+  }
+  const title = dom.querySelector('title')?.innerText;
+  const initialData = parseServerData(dom);
+  return {
+    regions,
+    head,
+    title,
+    initialData
+  };
+};
+
+// Render all interactive regions contained in the given page.
+const renderRegions = async page => {
+  if (false) {}
+  if (navigationMode === 'regionBased') {
+    const attrName = `data-${directivePrefix}-router-region`;
+    batch(() => {
+      populateServerData(page.initialData);
+      document.querySelectorAll(`[${attrName}]`).forEach(region => {
+        const id = region.getAttribute(attrName);
+        const fragment = getRegionRootFragment(region);
+        render(page.regions[id], fragment);
+      });
+    });
+  }
+  if (page.title) {
+    document.title = page.title;
+  }
+};
+
+/**
+ * Load the given page forcing a full page reload.
+ *
+ * The function returns a promise that won't resolve, useful to prevent any
+ * potential feedback indicating that the navigation has finished while the new
+ * page is being loaded.
+ *
+ * @param href The page href.
+ * @return Promise that never resolves.
+ */
+const forcePageReload = href => {
+  window.location.assign(href);
+  return new Promise(() => {});
+};
+
+// Listen to the back and forward buttons and restore the page if it's in the
+// cache.
+window.addEventListener('popstate', async () => {
+  const pagePath = getPagePath(window.location.href); // Remove hash.
+  const page = pages.has(pagePath) && (await pages.get(pagePath));
+  if (page) {
+    await renderRegions(page);
+    // Update the URL in the state.
+    state.url = window.location.href;
+  } else {
+    window.location.reload();
+  }
+});
+
+// Initialize the router and cache the initial page using the initial vDOM.
+// Once this code is tested and more mature, the head should be updated for
+// region based navigation as well.
+if (false) {}
+pages.set(getPagePath(window.location.href), Promise.resolve(regionsToVdom(document, {
+  vdom: initialVdom
+})));
+
+// Check if the link is valid for client-side navigation.
+const isValidLink = ref => ref && ref instanceof window.HTMLAnchorElement && ref.href && (!ref.target || ref.target === '_self') && ref.origin === window.location.origin && !ref.pathname.startsWith('/wp-admin') && !ref.pathname.startsWith('/wp-login.php') && !ref.getAttribute('href').startsWith('#') && !new URL(ref.href).searchParams.has('_wpnonce');
+
+// Check if the event is valid for client-side navigation.
+const isValidEvent = event => event && event.button === 0 &&
+// Left clicks only.
+!event.metaKey &&
+// Open in new tab (Mac).
+!event.ctrlKey &&
+// Open in new tab (Windows).
+!event.altKey &&
+// Download.
+!event.shiftKey && !event.defaultPrevented;
+
+// Variable to store the current navigation.
+let navigatingTo = '';
+let hasLoadedNavigationTextsData = false;
+const navigationTexts = {
+  loading: 'Loading page, please wait.',
+  loaded: 'Page Loaded.'
+};
+const {
+  state,
+  actions
+} = (0,interactivity_namespaceObject.store)('core/router', {
+  state: {
+    url: window.location.href,
+    navigation: {
+      hasStarted: false,
+      hasFinished: false
+    }
+  },
+  actions: {
+    /**
+     * Navigates to the specified page.
+     *
+     * This function normalizes the passed href, fetches the page HTML if
+     * needed, and updates any interactive regions whose contents have
+     * changed. It also creates a new entry in the browser session history.
+     *
+     * @param href                               The page href.
+     * @param [options]                          Options object.
+     * @param [options.force]                    If true, it forces re-fetching the URL.
+     * @param [options.html]                     HTML string to be used instead of fetching the requested URL.
+     * @param [options.replace]                  If true, it replaces the current entry in the browser session history.
+     * @param [options.timeout]                  Time until the navigation is aborted, in milliseconds. Default is 10000.
+     * @param [options.loadingAnimation]         Whether an animation should be shown while navigating. Default to `true`.
+     * @param [options.screenReaderAnnouncement] Whether a message for screen readers should be announced while navigating. Default to `true`.
+     *
+     * @return  Promise that resolves once the navigation is completed or aborted.
+     */
+    *navigate(href, options = {}) {
+      const {
+        clientNavigationDisabled
+      } = (0,interactivity_namespaceObject.getConfig)();
+      if (clientNavigationDisabled) {
+        yield forcePageReload(href);
+      }
+      const pagePath = getPagePath(href);
+      const {
+        navigation
+      } = state;
+      const {
+        loadingAnimation = true,
+        screenReaderAnnouncement = true,
+        timeout = 10000
+      } = options;
+      navigatingTo = href;
+      actions.prefetch(pagePath, options);
+
+      // Create a promise that resolves when the specified timeout ends.
+      // The timeout value is 10 seconds by default.
+      const timeoutPromise = new Promise(resolve => setTimeout(resolve, timeout));
+
+      // Don't update the navigation status immediately, wait 400 ms.
+      const loadingTimeout = setTimeout(() => {
+        if (navigatingTo !== href) {
+          return;
+        }
+        if (loadingAnimation) {
+          navigation.hasStarted = true;
+          navigation.hasFinished = false;
+        }
+        if (screenReaderAnnouncement) {
+          a11ySpeak('loading');
+        }
+      }, 400);
+      const page = yield Promise.race([pages.get(pagePath), timeoutPromise]);
+
+      // Dismiss loading message if it hasn't been added yet.
+      clearTimeout(loadingTimeout);
+
+      // Once the page is fetched, the destination URL could have changed
+      // (e.g., by clicking another link in the meantime). If so, bail
+      // out, and let the newer execution to update the HTML.
+      if (navigatingTo !== href) {
+        return;
+      }
+      if (page && !page.initialData?.config?.['core/router']?.clientNavigationDisabled) {
+        yield renderRegions(page);
+        window.history[options.replace ? 'replaceState' : 'pushState']({}, '', href);
+
+        // Update the URL in the state.
+        state.url = href;
+
+        // Update the navigation status once the the new page rendering
+        // has been completed.
+        if (loadingAnimation) {
+          navigation.hasStarted = false;
+          navigation.hasFinished = true;
+        }
+        if (screenReaderAnnouncement) {
+          a11ySpeak('loaded');
+        }
+
+        // Scroll to the anchor if exits in the link.
+        const {
+          hash
+        } = new URL(href, window.location.href);
+        if (hash) {
+          document.querySelector(hash)?.scrollIntoView();
+        }
+      } else {
+        yield forcePageReload(href);
+      }
+    },
+    /**
+     * Prefetches the page with the passed URL.
+     *
+     * The function normalizes the URL and stores internally the fetch
+     * promise, to avoid triggering a second fetch for an ongoing request.
+     *
+     * @param url             The page URL.
+     * @param [options]       Options object.
+     * @param [options.force] Force fetching the URL again.
+     * @param [options.html]  HTML string to be used instead of fetching the requested URL.
+     */
+    prefetch(url, options = {}) {
+      const {
+        clientNavigationDisabled
+      } = (0,interactivity_namespaceObject.getConfig)();
+      if (clientNavigationDisabled) {
+        return;
+      }
+      const pagePath = getPagePath(url);
+      if (options.force || !pages.has(pagePath)) {
+        pages.set(pagePath, fetchPage(pagePath, {
+          html: options.html
+        }));
+      }
+    }
+  }
+});
+
+/**
+ * Announces a message to screen readers.
+ *
+ * This is a wrapper around the `@wordpress/a11y` package's `speak` function. It handles importing
+ * the package on demand and should be used instead of calling `ally.speak` direacly.
+ *
+ * @param messageKey The message to be announced by assistive technologies.
+ */
+function a11ySpeak(messageKey) {
+  if (!hasLoadedNavigationTextsData) {
+    hasLoadedNavigationTextsData = true;
+    const content = document.getElementById('wp-script-module-data-@wordpress/interactivity-router')?.textContent;
+    if (content) {
+      try {
+        const parsed = JSON.parse(content);
+        if (typeof parsed?.i18n?.loading === 'string') {
+          navigationTexts.loading = parsed.i18n.loading;
+        }
+        if (typeof parsed?.i18n?.loaded === 'string') {
+          navigationTexts.loaded = parsed.i18n.loaded;
+        }
+      } catch {}
+    } else {
+      // Fallback to localized strings from Interactivity API state.
+      // @todo This block is for Core < 6.7.0. Remove when support is dropped.
+
+      // @ts-expect-error
+      if (state.navigation.texts?.loading) {
+        // @ts-expect-error
+        navigationTexts.loading = state.navigation.texts.loading;
+      }
+      // @ts-expect-error
+      if (state.navigation.texts?.loaded) {
+        // @ts-expect-error
+        navigationTexts.loaded = state.navigation.texts.loaded;
+      }
+    }
+  }
+  const message = navigationTexts[messageKey];
+  Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 317)).then(({
+    speak
+  }) => speak(message),
+  // Ignore failures to load the a11y module.
+  () => {});
+}
+
+// Add click and prefetch to all links.
+if (false) {}
+
+var __webpack_exports__actions = __webpack_exports__.o;
+var __webpack_exports__state = __webpack_exports__.w;
+export { __webpack_exports__actions as actions, __webpack_exports__state as state };