UNPKG

@wordpress/block-library

Version:
194 lines (181 loc) 7.81 kB
/* wp:polyfill */ /** * WordPress dependencies */ import { escapeHTML } from '@wordpress/escape-html'; import { safeDecodeURI, getPath } from '@wordpress/url'; /** * Determines if an entity link should be severed based on URL changes. * * @param {string} originalUrl - The original URL * @param {string} newUrl - The new URL * @return {boolean} True if the entity link should be severed */ const shouldSeverEntityLink = (originalUrl, newUrl) => { if (!originalUrl || !newUrl) { return false; } const normalizePath = path => { if (!path) { return ''; } return path.replace(/\/+$/, ''); // Remove trailing slashes }; // Helper function to create URL objects with proper base handling const createUrlObject = (url, baseUrl = null) => { try { // Always provide a base URL - it will be ignored for absolute URLs // Use window.location.origin in browser, fallback for Node/tests const base = baseUrl || (typeof window !== 'undefined' ? window.location.origin : 'https://wordpress.org'); return new URL(url, base); } catch (error) { // If URL construction still fails, it's likely an invalid URL // and we should sever the entity link return null; } }; const originalUrlObj = createUrlObject(originalUrl); if (!originalUrlObj) { return true; } const newUrlObj = createUrlObject(newUrl, originalUrl); if (!newUrlObj) { return true; } // Move these declarations here, after the null checks const originalHostname = originalUrlObj.hostname; const newHostname = newUrlObj.hostname; const originalPath = normalizePath(getPath(originalUrlObj.toString())); const newPath = normalizePath(getPath(newUrlObj.toString())); // If hostname or path changed, sever the entity link if (originalHostname !== newHostname || originalPath !== newPath) { return true; } // Special handling for plain permalinks (query string post IDs) const originalP = originalUrlObj.searchParams.get('p'); const newP = newUrlObj.searchParams.get('p'); // If both are plain permalinks (with ?p= or ?page_id=), compare the IDs if (originalP && newP && originalP !== newP) { return true; } const originalPageId = originalUrlObj.searchParams.get('page_id'); const newPageId = newUrlObj.searchParams.get('page_id'); if (originalPageId && newPageId && originalPageId !== newPageId) { return true; } // If switching between ?p= and ?page_id=, or one is missing, sever if (originalP && newPageId || originalPageId && newP) { return true; } // If only query string or fragment changed, preserve the entity link return false; }; /** * @typedef {'post-type'|'custom'|'taxonomy'|'post-type-archive'} WPNavigationLinkKind */ /** * Navigation Link Block Attributes * * @typedef {Object} WPNavigationLinkBlockAttributes * * @property {string} [label] Link text. * @property {WPNavigationLinkKind} [kind] Kind is used to differentiate between term and post ids to check post draft status. * @property {string} [type] The type such as post, page, tag, category and other custom types. * @property {string} [rel] The relationship of the linked URL. * @property {number} [id] A post or term id. * @property {boolean} [opensInNewTab] Sets link target to _blank when true. * @property {string} [url] Link href. * @property {string} [title] Link title attribute. */ /** * Link Control onChange handler that updates block attributes when a setting is changed. * * @param {Object} updatedValue New block attributes to update. * @param {Function} setAttributes Block attribute update function. * @param {WPNavigationLinkBlockAttributes} blockAttributes Current block attributes. */ export const updateAttributes = (updatedValue = {}, setAttributes, blockAttributes = {}) => { var _newUrl$replace; const { label: originalLabel = '', kind: originalKind = '', type: originalType = '' } = blockAttributes; const { title: newLabel = '', // the title of any provided Post. label: newLabelFromLabel = '', // alternative to title url: newUrl, opensInNewTab, id: newID, kind: newKind = originalKind, type: newType = originalType } = updatedValue; // Use title if provided, otherwise fall back to label const finalNewLabel = newLabel || newLabelFromLabel; const newLabelWithoutHttp = finalNewLabel.replace(/http(s?):\/\//gi, ''); const newUrlWithoutHttp = (_newUrl$replace = newUrl?.replace(/http(s?):\/\//gi, '')) !== null && _newUrl$replace !== void 0 ? _newUrl$replace : ''; const useNewLabel = finalNewLabel && finalNewLabel !== originalLabel && // LinkControl without the title field relies // on the check below. Specifically, it assumes that // the URL is the same as a title. // This logic a) looks suspicious and b) should really // live in the LinkControl and not here. It's a great // candidate for future refactoring. newLabelWithoutHttp !== newUrlWithoutHttp; // Unfortunately this causes the escaping model to be inverted. // The escaped content is stored in the block attributes (and ultimately in the database), // and then the raw data is "recovered" when outputting into the DOM. // It would be preferable to store the **raw** data in the block attributes and escape it in JS. // Why? Because there isn't one way to escape data. Depending on the context, you need to do // different transforms. It doesn't make sense to me to choose one of them for the purposes of storage. // See also: // - https://github.com/WordPress/gutenberg/pull/41063 // - https://github.com/WordPress/gutenberg/pull/18617. const label = useNewLabel ? escapeHTML(finalNewLabel) : originalLabel || escapeHTML(newUrlWithoutHttp); // In https://github.com/WordPress/gutenberg/pull/24670 we decided to use "tag" in favor of "post_tag" const type = newType === 'post_tag' ? 'tag' : newType.replace('-', '_'); const isBuiltInType = ['post', 'page', 'tag', 'category'].indexOf(type) > -1; const isCustomLink = !newKind && !isBuiltInType || newKind === 'custom'; const kind = isCustomLink ? 'custom' : newKind; const attributes = { // Passed `url` may already be encoded. To prevent double encoding, decodeURI is executed to revert to the original string. ...(newUrl !== undefined ? { url: newUrl ? encodeURI(safeDecodeURI(newUrl)) : newUrl } : {}), ...(label && { label }), ...(undefined !== opensInNewTab && { opensInNewTab }), ...(kind && { kind }), ...(type && type !== 'URL' && { type }) }; // If the block's id is set then the menu item is linking to an entity. // Therefore, if the URL is set but a new ID is not provided, check if // the entity link should be severed based on URL changes. if (newUrl && !newID && blockAttributes.id) { const shouldSever = shouldSeverEntityLink(blockAttributes.url, newUrl); if (shouldSever) { attributes.id = undefined; // explicitly "unset" the ID. // When URL is manually changed in a way that severs the entity link, // update kind and type to "custom" to indicate this is now a custom link. attributes.kind = 'custom'; attributes.type = 'custom'; } } else if (newID && Number.isInteger(newID)) { attributes.id = newID; } else if (blockAttributes.id) { // If we have an existing ID and no URL change, ensure kind and type are preserved attributes.kind = kind; attributes.type = type; } setAttributes(attributes); }; //# sourceMappingURL=update-attributes.js.map