UNPKG

@limitless-angular/sanity

Version:

A powerful Angular library for Sanity.io integration, featuring Portable Text rendering and optimized image loading.

337 lines (331 loc) 16.1 kB
import * as i0 from '@angular/core'; import { input, signal, computed, inject, effect, untracked, Component, ChangeDetectionStrategy, PLATFORM_ID } from '@angular/core'; import { Location, isPlatformBrowser } from '@angular/common'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { Router, NavigationEnd } from '@angular/router'; import { enableVisualEditing } from '@sanity/visual-editing'; import { filter } from 'rxjs/operators'; /** * From: https://github.com/vercel/next.js/blob/5469e6427b54ab7e9876d4c85b47f9c3afdc5c1f/packages/next/src/shared/lib/router/utils/path-has-prefix.ts#L10-L17 * Checks if a given path starts with a given prefix. It ensures it matches * exactly without containing extra chars. e.g. prefix /docs should replace * for /docs, /docs/, /docs/a but not /docsss * @param path The path to check. * @param prefix The prefix to check against. */ function pathHasPrefix(path, prefix) { if (typeof path !== 'string') { return false; } const { pathname } = parsePath(path); return pathname === prefix || pathname.startsWith(`${prefix}/`); } /** * From: https://github.com/vercel/next.js/blob/5469e6427b54ab7e9876d4c85b47f9c3afdc5c1f/packages/next/src/shared/lib/router/utils/parse-path.ts#L6-L22 * Given a path this function will find the pathname, query and hash and return * them. This is useful to parse full paths on the client side. * @param path A path to parse e.g. /foo/bar?id=1#hash */ function parsePath(path) { const hashIndex = path.indexOf('#'); const queryIndex = path.indexOf('?'); const hasQuery = queryIndex > -1 && (hashIndex < 0 || queryIndex < hashIndex); if (hasQuery || hashIndex > -1) { return { pathname: path.substring(0, hasQuery ? queryIndex : hashIndex), query: hasQuery ? path.substring(queryIndex, hashIndex > -1 ? hashIndex : undefined) : '', hash: hashIndex > -1 ? path.slice(hashIndex) : '', }; } return { pathname: path, query: '', hash: '' }; } /** * From: https://github.com/vercel/next.js/blob/5469e6427b54ab7e9876d4c85b47f9c3afdc5c1f/packages/next/src/shared/lib/router/utils/add-path-prefix.ts#L3C1-L14C2 * Adds the provided prefix to the given path. It first ensures that the path * is indeed starting with a slash. */ function addPathPrefix(path, prefix) { if (!path.startsWith('/') || !prefix) { return path; } // If the path is exactly '/' then return just the prefix if (path === '/' && prefix) { return prefix; } const { pathname, query, hash } = parsePath(path); return `${prefix}${pathname}${query}${hash}`; } /** * From: https://github.com/vercel/next.js/blob/5469e6427b54ab7e9876d4c85b47f9c3afdc5c1f/packages/next/src/shared/lib/router/utils/remove-path-prefix.ts#L3-L39 * Given a path and a prefix it will remove the prefix when it exists in the * given path. It ensures it matches exactly without containing extra chars * and if the prefix is not there it will be noop. * * @param path The path to remove the prefix from. * @param prefix The prefix to be removed. */ function removePathPrefix(path, prefix) { // If the path doesn't start with the prefix we can return it as is. This // protects us from situations where the prefix is a substring of the path // prefix such as: // // For prefix: /blog // // /blog -> true // /blog/ -> true // /blog/1 -> true // /blogging -> false // /blogging/ -> false // /blogging/1 -> false if (!pathHasPrefix(path, prefix)) { return path; } // Remove the prefix from the path via slicing. const withoutPrefix = path.slice(prefix.length); // If the path without the prefix starts with a `/` we can return it as is. if (withoutPrefix.startsWith('/')) { return withoutPrefix; } // If the path without the prefix doesn't start with a `/` we need to add it // back to the path to make sure it's a valid path. return `/${withoutPrefix}`; } /** * From: https://github.com/vercel/next.js/blob/dfe7fc03e2268e7cb765dce6a89e02c831c922d5/packages/next/src/client/normalize-trailing-slash.ts#L16 * Normalizes the trailing slash of a path according to the `trailingSlash` option * in `next.config.js`. */ const normalizePathTrailingSlash = (path, trailingSlash) => { const { pathname, query, hash } = parsePath(path); if (trailingSlash) { if (pathname.endsWith('/')) { return `${pathname}${query}${hash}`; } return `${pathname}/${query}${hash}`; } return `${removeTrailingSlash(pathname)}${query}${hash}`; }; /** * From: https://github.com/vercel/next.js/blob/dfe7fc03e2268e7cb765dce6a89e02c831c922d5/packages/next/src/shared/lib/router/utils/remove-trailing-slash.ts#L8 * Removes the trailing slash for a given route or page path. Preserves the * root page. Examples: * - `/foo/bar/` -> `/foo/bar` * - `/foo/bar` -> `/foo/bar` * - `/` -> `/` */ function removeTrailingSlash(route) { return route.replace(/\/$/, '') || '/'; } class VisualEditingClientComponent { constructor() { this.refresh = input(); this.zIndex = input(); this.basePath = input('', { transform: (value) => value ?? '', }); this.trailingSlash = input(false, { transform: (value) => Boolean(value), }); this.navigate = signal(undefined); this.currentUrl = computed(() => { const urlTree = this.router.parseUrl(this.router.url); const pathname = urlTree.root.children['primary']?.segments .map((segment) => segment.path) .join('/') || '/'; const searchParams = new URLSearchParams(urlTree.queryParams).toString(); return normalizePathTrailingSlash(addPathPrefix(`${pathname}${searchParams ? `?${searchParams}` : ''}`, this.basePath()), this.trailingSlash()); }); this.location = inject(Location); this.router = inject(Router); this.defaultRefresh = (payload) => { switch (payload.source) { case 'manual': return payload.livePreviewEnabled ? this.manualFastRefresh() : this.manualFallbackRefresh(); case 'mutation': return payload.livePreviewEnabled ? this.mutationFastRefresh() : this.mutationFallbackRefresh(); default: // eslint-disable-next-line no-case-declarations const error = new Error('Unknown refresh source'); // eslint-disable-next-line @typescript-eslint/no-explicit-any error.details = { cause: payload }; throw error; } }; effect(() => { const zIndex = this.zIndex(); const refresh = this.refresh(); const basePath = this.basePath(); return untracked(() => { const disable = enableVisualEditing({ zIndex, refresh: refresh || this.defaultRefresh, history: { subscribe: (_navigate) => { this.navigate.set(_navigate); return () => this.navigate.set(undefined); }, update: (update) => { switch (update.type) { case 'push': return this.router.navigateByUrl(removePathPrefix(update.url, basePath)); case 'pop': return this.location.back(); case 'replace': return this.router.navigateByUrl(removePathPrefix(update.url, basePath), { replaceUrl: true }); default: throw new Error(`Unknown update type: ${update.type}`); } }, }, }); return () => disable(); }); }); effect(() => { const currentNavigate = this.navigate(); const url = this.currentUrl(); untracked(() => { if (currentNavigate) { currentNavigate({ type: 'push', url, }); } }); }); this.router.events .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed()) .subscribe(() => { const currentNavigate = this.navigate(); const url = this.currentUrl(); untracked(() => { if (currentNavigate) { currentNavigate({ type: 'push', url, }); } }); }); } manualFastRefresh() { console.debug('Live preview is setup, refreshing the view without refetching cached data'); // In Angular, we don't have a direct equivalent to router.refresh() // You might need to implement a custom solution here // TODO: check alternative return Promise.resolve(); } manualFallbackRefresh() { console.debug('No loaders in live mode detected, or preview kit setup, revalidating root layout'); return Promise.resolve(); // TODO: check alternative // return revalidateRootLayout(); } mutationFastRefresh() { console.debug('Live preview is setup, mutation is skipped assuming its handled by the live preview'); return false; } mutationFallbackRefresh() { console.debug('No loaders in live mode detected, or preview kit setup, revalidating root layout'); return Promise.resolve(); // TODO: check alternative // return revalidateRootLayout(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: VisualEditingClientComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.0.3", type: VisualEditingClientComponent, isStandalone: true, selector: "visual-editing-client", inputs: { refresh: { classPropertyName: "refresh", publicName: "refresh", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, trailingSlash: { classPropertyName: "trailingSlash", publicName: "trailingSlash", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: VisualEditingClientComponent, decorators: [{ type: Component, args: [{ // eslint-disable-next-line @angular-eslint/component-selector selector: 'visual-editing-client', template: '', changeDetection: ChangeDetectionStrategy.OnPush, }] }], ctorParameters: () => [] }); class VisualEditingComponent { constructor() { this.refresh = input(); this.zIndex = input(); this.basePath = input(undefined); this.trailingSlash = input(); this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); this.autoBasePath = computed(() => { if (typeof this.basePath() === 'string') { return undefined; } try { const detectedBasePath = !this.isBrowser ? process.env['__NEXT_ROUTER_BASEPATH'] : undefined; if (detectedBasePath) { console.log(`Detected next basePath as ${JSON.stringify(detectedBasePath)} by reading "process.env.__NEXT_ROUTER_BASEPATH". If this is incorrect then you can set it manually with the basePath input on the VisualEditing component.`); } return detectedBasePath; } catch (err) { console.error('Failed detecting basePath', err); return undefined; } }); this.autoTrailingSlash = computed(() => { if (typeof this.trailingSlash() === 'boolean') { return undefined; } try { const detectedTrailingSlash = Boolean(!this.isBrowser && process.env['__NEXT_TRAILING_SLASH']); if (detectedTrailingSlash) { console.log(`Detected next trailingSlash as ${JSON.stringify(detectedTrailingSlash)} by reading "process.env.__NEXT_TRAILING_SLASH". If this is incorrect then you can set it manually with the trailingSlash input on the VisualEditing component.`); } return detectedTrailingSlash; } catch (err) { console.error('Failed detecting trailingSlash', err); return undefined; } }); this.computedBasePath = computed(() => this.basePath() ?? this.autoBasePath()); this.computedTrailingSlash = computed(() => this.trailingSlash() ?? this.autoTrailingSlash()); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: VisualEditingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.3", type: VisualEditingComponent, isStandalone: true, selector: "visual-editing", inputs: { refresh: { classPropertyName: "refresh", publicName: "refresh", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null }, trailingSlash: { classPropertyName: "trailingSlash", publicName: "trailingSlash", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: ` @if (isBrowser) { <visual-editing-client [refresh]="refresh()" [zIndex]="zIndex()" [basePath]="computedBasePath()" [trailingSlash]="computedTrailingSlash()" /> } `, isInline: true, dependencies: [{ kind: "component", type: VisualEditingClientComponent, selector: "visual-editing-client", inputs: ["refresh", "zIndex", "basePath", "trailingSlash"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.3", ngImport: i0, type: VisualEditingComponent, decorators: [{ type: Component, args: [{ // eslint-disable-next-line @angular-eslint/component-selector selector: 'visual-editing', imports: [VisualEditingClientComponent], template: ` @if (isBrowser) { <visual-editing-client [refresh]="refresh()" [zIndex]="zIndex()" [basePath]="computedBasePath()" [trailingSlash]="computedTrailingSlash()" /> } `, changeDetection: ChangeDetectionStrategy.OnPush, }] }] }); /** * Generated bundle index. Do not edit. */ export { VisualEditingComponent }; //# sourceMappingURL=limitless-angular-sanity-visual-editing.mjs.map