@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
JavaScript
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: `
(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: `
(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