@easy-cms/core 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +34 -1
- package/dist/index.js +111 -0
- package/dist/live-preview.d.ts +33 -0
- package/dist/live-preview.js +24 -0
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
|
@@ -332,6 +332,14 @@ interface GlobalHooks {
|
|
|
332
332
|
readonly afterChange?: readonly AfterChangeHook[];
|
|
333
333
|
readonly afterRead?: readonly AfterReadHook[];
|
|
334
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Where a document is shown on the site, for live preview in the admin: an absolute URL or a
|
|
337
|
+
* path on the site's origin. Return `null` when the document has no page.
|
|
338
|
+
*/
|
|
339
|
+
type PreviewURL = (args: {
|
|
340
|
+
readonly doc: Record<string, unknown>;
|
|
341
|
+
readonly locale: AdminLocale;
|
|
342
|
+
}) => string | null | undefined;
|
|
335
343
|
interface VersionsConfig {
|
|
336
344
|
/** Versions kept per document; older ones are deleted. Default 50. */
|
|
337
345
|
readonly max?: number;
|
|
@@ -354,6 +362,8 @@ interface CollectionConfig {
|
|
|
354
362
|
* it is published again.
|
|
355
363
|
*/
|
|
356
364
|
readonly versions?: boolean | VersionsConfig;
|
|
365
|
+
/** Live preview: the page that shows a document, e.g. `({ doc }) => \`/posts/${doc.slug}\``. */
|
|
366
|
+
readonly preview?: PreviewURL;
|
|
357
367
|
readonly access?: CollectionAccess;
|
|
358
368
|
readonly hooks?: CollectionHooks;
|
|
359
369
|
}
|
|
@@ -364,6 +374,8 @@ interface GlobalConfig {
|
|
|
364
374
|
readonly drafts?: boolean;
|
|
365
375
|
/** Keep a snapshot of every save. See `CollectionConfig.versions`. */
|
|
366
376
|
readonly versions?: boolean | VersionsConfig;
|
|
377
|
+
/** Live preview: the page that shows the global. See `CollectionConfig.preview`. */
|
|
378
|
+
readonly preview?: PreviewURL;
|
|
367
379
|
readonly access?: GlobalAccess;
|
|
368
380
|
readonly hooks?: GlobalHooks;
|
|
369
381
|
}
|
|
@@ -771,6 +783,17 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
771
783
|
* a draft (publish it to put it live); access, hooks and validation run as for `update`.
|
|
772
784
|
*/
|
|
773
785
|
restoreVersion<S extends Slug<C>>(collection: S, id: ID, versionId: ID, options?: DepthOptions): Promise<Doc<C, S>>;
|
|
786
|
+
/**
|
|
787
|
+
* Live preview: the document as it would be read if `data` were saved on top of its current
|
|
788
|
+
* state (or as a new document when `id` is `null`), with relationships populated and
|
|
789
|
+
* `afterRead` hooks applied. Nothing is written. Also returns the page URL from `preview`.
|
|
790
|
+
*/
|
|
791
|
+
preview<S extends Slug<C>>(collection: S, id: ID | null, data: Record<string, unknown>, options?: DepthOptions): Promise<LivePreview<Doc<C, S>>>;
|
|
792
|
+
/** Live preview of a global. See `preview`. */
|
|
793
|
+
previewGlobal<S extends GSlug<C>>(slug: S, data: Record<string, unknown>, options?: DepthOptions): Promise<LivePreview<GDoc<C, S>>>;
|
|
794
|
+
/** Coerces preview input like a save would, keeping values that would fail validation. */
|
|
795
|
+
private previewDoc;
|
|
796
|
+
private previewURL;
|
|
774
797
|
private versionTarget;
|
|
775
798
|
private updateDocument;
|
|
776
799
|
delete<S extends Slug<C>>(collection: S, id: ID, options?: AccessOptions): Promise<Doc<C, S>>;
|
|
@@ -839,6 +862,12 @@ declare class EasyCMS<C extends Config = Config> {
|
|
|
839
862
|
private isTaken;
|
|
840
863
|
private checkReferences;
|
|
841
864
|
}
|
|
865
|
+
/** A document as live preview shows it, and the page to show it on. */
|
|
866
|
+
interface LivePreview<T> {
|
|
867
|
+
readonly doc: T;
|
|
868
|
+
/** From the collection's `preview` function; `null` without one. */
|
|
869
|
+
readonly url: string | null;
|
|
870
|
+
}
|
|
842
871
|
|
|
843
872
|
interface LoginArgs {
|
|
844
873
|
readonly email: string;
|
|
@@ -1076,6 +1105,8 @@ interface AdminCollection {
|
|
|
1076
1105
|
useAsTitle?: string;
|
|
1077
1106
|
drafts: boolean;
|
|
1078
1107
|
versions: boolean;
|
|
1108
|
+
/** Has live preview (`preview` in the config). */
|
|
1109
|
+
preview: boolean;
|
|
1079
1110
|
fields: AdminField[];
|
|
1080
1111
|
permissions: {
|
|
1081
1112
|
read: boolean;
|
|
@@ -1089,6 +1120,8 @@ interface AdminGlobal {
|
|
|
1089
1120
|
label?: Label;
|
|
1090
1121
|
drafts: boolean;
|
|
1091
1122
|
versions: boolean;
|
|
1123
|
+
/** Has live preview (`preview` in the config). */
|
|
1124
|
+
preview: boolean;
|
|
1092
1125
|
fields: AdminField[];
|
|
1093
1126
|
permissions: {
|
|
1094
1127
|
read: boolean;
|
|
@@ -1129,4 +1162,4 @@ declare const BUILTIN_COLLECTIONS: readonly ["users", "media"];
|
|
|
1129
1162
|
/** Checks a config and returns every problem found. An empty array means it is valid. */
|
|
1130
1163
|
declare function validateConfig(config: Config): ConfigIssue[];
|
|
1131
1164
|
|
|
1132
|
-
export { type Access, type AccessArgs, type AccessOptions, type AdminCollection, type AdminConfig, type AdminField, type AdminGlobal, type AdminLocale, type AdminSchema, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type BooleanField, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CollectionAccess, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_TOKEN_EXPIRATION, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DateField, type DepthOptions, EXTENSIONS, EasyCMS, EasyCMSError, type EmailField, FIELD_TYPES, type Field, type FieldAccess, type FieldAccessFn, type FieldError, type FieldType, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LoadConfigOptions, type LocalStorageOptions, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, PayloadTooLargeError, type Plugin, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, UnauthorizedError, type UpdateInput, type UploadConfig, type UploadField, ValidationError, type Version, type VersionSummary, type VersionsConfig, type Where, type WhereOperators, anyone, applyDefaults, consoleLogger, createEasyCMS, createRestHandler, defineConfig, fillMissing, findConfigFile, generateSlugs, generateTypes, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveConfig, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
|
|
1165
|
+
export { type Access, type AccessArgs, type AccessOptions, type AdminCollection, type AdminConfig, type AdminField, type AdminGlobal, type AdminLocale, type AdminSchema, type AfterChangeHook, type AfterDeleteHook, type AfterReadHook, type ArrayField, Auth, type AuthConfig, type AuthUser, BUILTIN_COLLECTIONS, type BeforeChangeHook, type BeforeDeleteHook, type BeforeValidateHook, type BooleanField, type BuiltinMediaCollection, type BuiltinUsersCollection, type BundleHints, CONFIG_FILE_NAMES, CSRF_COOKIE, CSRF_HEADER, type CollectionAccess, type CollectionConfig, type CollectionDocument, type CollectionHooks, type CollectionSlug, type Config, ConfigError, type ConfigIssue, type CreateEasyCMSOptions, type CreateInput, type CreateMigrationResult, DEFAULT_ADMIN_PATH, DEFAULT_API_PATH, DEFAULT_DEPTH, DEFAULT_MAX_FILE_SIZE, DEFAULT_MAX_VERSIONS, DEFAULT_TOKEN_EXPIRATION, type Database, type DatabaseAdapter, type DatabaseInitArgs, type DateField, type DepthOptions, EXTENSIONS, EasyCMS, EasyCMSError, type EmailField, FIELD_TYPES, type Field, type FieldAccess, type FieldAccessFn, type FieldError, type FieldType, type FieldValidate, type FieldValidateContext, type FieldValue, type FieldsInput, type FieldsValue, type FindArgs, type FindOptions, ForbiddenError, type GlobalAccess, type GlobalConfig, type GlobalDocument, type GlobalHooks, type GlobalInput, type GlobalSlug, type GroupField, type ID, INTERNAL_COLLECTIONS, type ImageSize, type InferCollection, type InferGlobal, type JsonField, type Label, type LivePreview, type LoadConfigOptions, type LocalStorageOptions, type Logger, type LoginArgs, MAX_DEPTH, MEDIA, MIN_PASSWORD_LENGTH, MIN_SECRET_LENGTH, type MediaDocument, type MediaSize, type MigrationInfo, NotFoundError, type NumberField, type Operation, type PaginatedDocs, PayloadTooLargeError, type Plugin, type PreviewURL, QueryError, type RawDocument, type ReadOptions, type RelationshipField, type ResolvedConfig, type RestHandler, type RestHandlerOptions, type RichTextDocument, type RichTextField, type Role, type RoutesConfig, SESSION_COOKIE, SchemaError, type SchemaMode, type SelectField, type SelectOption, type Session, type SlugField, type StorageAdapter, type StoredFile, type TextField, type TextareaField, TooManyRequestsError, UnauthorizedError, type UpdateInput, type UploadConfig, type UploadField, ValidationError, type Version, type VersionSummary, type VersionsConfig, type Where, type WhereOperators, anyone, applyDefaults, consoleLogger, createEasyCMS, createRestHandler, defineConfig, fillMissing, findConfigFile, generateSlugs, generateTypes, hashPassword, imageDimensions, importConfig, isAdmin, isLoggedIn, loadConfig, localStorage, mergeForUpdate, mimeAllowed, parseId, populate, readCookie, resolveConfig, silentLogger, singularize, slugify, sniffMimeType, validateConfig, validateFields, verifyPassword };
|
package/dist/index.js
CHANGED
|
@@ -1032,6 +1032,13 @@ function validateContainer(container, path, collectionSlugs, add) {
|
|
|
1032
1032
|
add(`${path}.versions.max`, "must be a positive integer");
|
|
1033
1033
|
}
|
|
1034
1034
|
}
|
|
1035
|
+
if (container.preview !== void 0 && typeof container.preview !== "function") {
|
|
1036
|
+
add(
|
|
1037
|
+
`${path}.preview`,
|
|
1038
|
+
"must be a function ({ doc }) => url",
|
|
1039
|
+
"preview: ({ doc }) => `/posts/${doc.slug}`"
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1035
1042
|
const reserved = new Set(SYSTEM_FIELD_NAMES);
|
|
1036
1043
|
if (container.drafts) reserved.add("status");
|
|
1037
1044
|
validateFields2(container.fields, `${path}.fields`, reserved, collectionSlugs, add);
|
|
@@ -1825,6 +1832,91 @@ var EasyCMS = class {
|
|
|
1825
1832
|
options
|
|
1826
1833
|
);
|
|
1827
1834
|
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Live preview: the document as it would be read if `data` were saved on top of its current
|
|
1837
|
+
* state (or as a new document when `id` is `null`), with relationships populated and
|
|
1838
|
+
* `afterRead` hooks applied. Nothing is written. Also returns the page URL from `preview`.
|
|
1839
|
+
*/
|
|
1840
|
+
async preview(collection2, id, data, options = {}) {
|
|
1841
|
+
const config = this.collection(collection2);
|
|
1842
|
+
const guard = guardOf(options);
|
|
1843
|
+
const raw = asObject(data, collection2);
|
|
1844
|
+
let base;
|
|
1845
|
+
let parsed;
|
|
1846
|
+
if (id === null) {
|
|
1847
|
+
if (guard.enforce) {
|
|
1848
|
+
const allowed2 = await evaluateAccess(config.access?.create, { user: guard.user, data: raw });
|
|
1849
|
+
if (allowed2 !== true) throw deny(guard.user);
|
|
1850
|
+
}
|
|
1851
|
+
} else {
|
|
1852
|
+
parsed = parseId(id);
|
|
1853
|
+
const existing = parsed === void 0 ? null : await this.db.findById({ collection: collection2, id: parsed });
|
|
1854
|
+
if (!existing || parsed === void 0) throw new NotFoundError(collection2, id);
|
|
1855
|
+
await this.checkDocumentAccess(config, "update", guard, parsed, raw);
|
|
1856
|
+
[base] = await this.withDrafts(config, [existing]);
|
|
1857
|
+
}
|
|
1858
|
+
const { input } = splitPassword(config, raw);
|
|
1859
|
+
const filtered = await filterInput(
|
|
1860
|
+
config.fields,
|
|
1861
|
+
input,
|
|
1862
|
+
this.fieldChecker("update", guard, parsed, input)
|
|
1863
|
+
);
|
|
1864
|
+
const merged = base ? mergeForUpdate(config.fields, base, filtered) : applyDefaults(config.fields, filtered);
|
|
1865
|
+
if (config.drafts) merged.status = filtered.status ?? base?.status ?? "draft";
|
|
1866
|
+
const doc = await this.previewDoc(config, generateSlugs(config.fields, merged), {
|
|
1867
|
+
id: parsed ?? 0,
|
|
1868
|
+
createdAt: base?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1869
|
+
});
|
|
1870
|
+
const [out] = await this.output(config, [doc], guard, { ...options, draft: true });
|
|
1871
|
+
return { doc: out, url: this.previewURL(config, out) };
|
|
1872
|
+
}
|
|
1873
|
+
/** Live preview of a global. See `preview`. */
|
|
1874
|
+
async previewGlobal(slug, data, options = {}) {
|
|
1875
|
+
const config = this.global(slug);
|
|
1876
|
+
const guard = guardOf(options);
|
|
1877
|
+
await this.checkGlobalAccess(config, "update", guard);
|
|
1878
|
+
const raw = asObject(data, slug);
|
|
1879
|
+
const input = await filterInput(
|
|
1880
|
+
config.fields,
|
|
1881
|
+
raw,
|
|
1882
|
+
this.fieldChecker("update", guard, void 0, raw)
|
|
1883
|
+
);
|
|
1884
|
+
const saved = await this.db.findGlobal({ slug }) ?? {};
|
|
1885
|
+
const current = await this.globalDraft(config, saved);
|
|
1886
|
+
const merged = generateSlugs(
|
|
1887
|
+
config.fields,
|
|
1888
|
+
applyDefaults(config.fields, mergeForUpdate(config.fields, current, input))
|
|
1889
|
+
);
|
|
1890
|
+
if (config.drafts) merged.status = input.status ?? current.status ?? "draft";
|
|
1891
|
+
const doc = await this.previewDoc(config, merged, { id: 0 });
|
|
1892
|
+
const [out] = await this.output(config, [doc], guard, { ...options, draft: true });
|
|
1893
|
+
const { id: _id, ...global2 } = out;
|
|
1894
|
+
return { doc: global2, url: this.previewURL(config, global2) };
|
|
1895
|
+
}
|
|
1896
|
+
/** Coerces preview input like a save would, keeping values that would fail validation. */
|
|
1897
|
+
async previewDoc(config, data, system) {
|
|
1898
|
+
const { data: clean } = await validateFields(config.fields, data, {
|
|
1899
|
+
operation: "update",
|
|
1900
|
+
root: data,
|
|
1901
|
+
skipRequired: true
|
|
1902
|
+
});
|
|
1903
|
+
return {
|
|
1904
|
+
...data,
|
|
1905
|
+
...clean,
|
|
1906
|
+
...config.drafts ? { status: data.status } : {},
|
|
1907
|
+
...system,
|
|
1908
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
previewURL(config, doc) {
|
|
1912
|
+
if (!config.preview) return null;
|
|
1913
|
+
try {
|
|
1914
|
+
return config.preview({ doc, locale: this.config.admin.locale }) ?? null;
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
this.logger.error(`preview URL of "${config.slug}" failed: ${error.message}`);
|
|
1917
|
+
return null;
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1828
1920
|
async versionTarget(collection2, id, options) {
|
|
1829
1921
|
const config = this.collection(collection2);
|
|
1830
1922
|
if (!versionLimit(config)) throw new QueryError(`"${collection2}" has no versions`);
|
|
@@ -2499,6 +2591,7 @@ async function collection(config, user) {
|
|
|
2499
2591
|
slug: config.slug,
|
|
2500
2592
|
drafts: config.drafts === true,
|
|
2501
2593
|
versions: Boolean(config.versions),
|
|
2594
|
+
preview: typeof config.preview === "function",
|
|
2502
2595
|
fields: await serializeFields(config.fields, new FieldAccessChecker("update", { user })),
|
|
2503
2596
|
permissions: {
|
|
2504
2597
|
read: await allowed(config.access?.read, user),
|
|
@@ -2516,6 +2609,7 @@ async function global(config, user) {
|
|
|
2516
2609
|
slug: config.slug,
|
|
2517
2610
|
drafts: config.drafts === true,
|
|
2518
2611
|
versions: Boolean(config.versions),
|
|
2612
|
+
preview: typeof config.preview === "function",
|
|
2519
2613
|
fields: await serializeFields(config.fields, new FieldAccessChecker("update", { user })),
|
|
2520
2614
|
permissions: {
|
|
2521
2615
|
read: await allowed(config.access?.read, user),
|
|
@@ -2792,6 +2886,13 @@ async function route(cms, ctx, method, segments, options) {
|
|
|
2792
2886
|
if (second !== void 0 && third !== void 0) {
|
|
2793
2887
|
return documentAction(cms, ctx, method, collection2, second, segments.slice(2));
|
|
2794
2888
|
}
|
|
2889
|
+
if (second === "preview") {
|
|
2890
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
2891
|
+
const body = await readJson(ctx.request);
|
|
2892
|
+
return {
|
|
2893
|
+
body: await cms.preview(collection2, null, body, { ...access, ...parseDepth(ctx.url) })
|
|
2894
|
+
};
|
|
2895
|
+
}
|
|
2795
2896
|
if (second === void 0) {
|
|
2796
2897
|
if (method === "GET") {
|
|
2797
2898
|
const query = parseListQuery(ctx.url);
|
|
@@ -2843,6 +2944,11 @@ async function documentAction(cms, ctx, method, collection2, id, path) {
|
|
|
2843
2944
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
2844
2945
|
return { body: await cms.restoreVersion(collection2, id, versionId, { ...access, ...depth }) };
|
|
2845
2946
|
}
|
|
2947
|
+
if (path.length === 1 && action === "preview") {
|
|
2948
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
2949
|
+
const body = await readJson(ctx.request);
|
|
2950
|
+
return { body: await cms.preview(collection2, id, body, { ...access, ...depth }) };
|
|
2951
|
+
}
|
|
2846
2952
|
if (path.length === 1 && (action === "unpublish" || action === "discard-draft")) {
|
|
2847
2953
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
2848
2954
|
const options = { ...access, ...depth };
|
|
@@ -2870,6 +2976,11 @@ async function globalAction(cms, ctx, method, slug, path) {
|
|
|
2870
2976
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
2871
2977
|
return { body: await cms.restoreGlobalVersion(slug, versionId, { ...access, ...depth }) };
|
|
2872
2978
|
}
|
|
2979
|
+
if (path.length === 1 && action === "preview") {
|
|
2980
|
+
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
2981
|
+
const body = await readJson(ctx.request);
|
|
2982
|
+
return { body: await cms.previewGlobal(slug, body, { ...access, ...depth }) };
|
|
2983
|
+
}
|
|
2873
2984
|
if (path.length === 1 && (action === "unpublish" || action === "discard-draft")) {
|
|
2874
2985
|
if (method !== "POST") throw methodNotAllowed(ctx, "POST");
|
|
2875
2986
|
const options = { ...access, ...depth };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Sent by the admin to the preview frame whenever the form changes. */
|
|
2
|
+
interface LivePreviewMessage<T = Record<string, unknown>> {
|
|
3
|
+
readonly type: 'easy-cms:preview';
|
|
4
|
+
/** Collection slug, or `undefined` for a global. */
|
|
5
|
+
readonly collection?: string;
|
|
6
|
+
/** Global slug, or `undefined` for a collection. */
|
|
7
|
+
readonly global?: string;
|
|
8
|
+
/** The document as it would be read after saving, relationships populated. */
|
|
9
|
+
readonly doc: T;
|
|
10
|
+
}
|
|
11
|
+
/** Sent by the page to the admin when it is ready, so the admin sends the current state. */
|
|
12
|
+
declare const READY_MESSAGE = "easy-cms:preview-ready";
|
|
13
|
+
interface LivePreviewOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Origin(s) of the admin allowed to send updates. Default: the page's own origin, which fits
|
|
16
|
+
* the Nuxt and Next.js adapters (admin and site share it). Set it for a standalone server,
|
|
17
|
+
* e.g. `'https://cms.example.com'`.
|
|
18
|
+
*/
|
|
19
|
+
readonly origin?: string | readonly string[];
|
|
20
|
+
}
|
|
21
|
+
/** True when the page is shown inside a frame (e.g. the admin's preview). */
|
|
22
|
+
declare function isLivePreview(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Calls `onChange` with the document each time the admin's form changes. Returns a function
|
|
25
|
+
* that stops listening. Does nothing outside a frame, so it is safe to call on every page.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* const stop = subscribeLivePreview<Post>((doc) => render(doc))
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare function subscribeLivePreview<T = Record<string, unknown>>(onChange: (doc: T, message: LivePreviewMessage<T>) => void, options?: LivePreviewOptions): () => void;
|
|
32
|
+
|
|
33
|
+
export { type LivePreviewMessage, type LivePreviewOptions, READY_MESSAGE, isLivePreview, subscribeLivePreview };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/live-preview.ts
|
|
2
|
+
var READY_MESSAGE = "easy-cms:preview-ready";
|
|
3
|
+
function isLivePreview() {
|
|
4
|
+
return typeof window !== "undefined" && window.parent !== window;
|
|
5
|
+
}
|
|
6
|
+
function subscribeLivePreview(onChange, options = {}) {
|
|
7
|
+
if (!isLivePreview()) return () => {
|
|
8
|
+
};
|
|
9
|
+
const allowed = [options.origin ?? window.location.origin].flat();
|
|
10
|
+
const listener = (event) => {
|
|
11
|
+
if (!allowed.includes(event.origin)) return;
|
|
12
|
+
const message = event.data;
|
|
13
|
+
if (message?.type !== "easy-cms:preview" || typeof message.doc !== "object") return;
|
|
14
|
+
onChange(message.doc, message);
|
|
15
|
+
};
|
|
16
|
+
window.addEventListener("message", listener);
|
|
17
|
+
window.parent.postMessage({ type: READY_MESSAGE }, "*");
|
|
18
|
+
return () => window.removeEventListener("message", listener);
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
READY_MESSAGE,
|
|
22
|
+
isLivePreview,
|
|
23
|
+
subscribeLivePreview
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easy-cms/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Embedded, code-first headless CMS for Nuxt and Next.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./live-preview": {
|
|
14
|
+
"types": "./dist/live-preview.d.ts",
|
|
15
|
+
"default": "./dist/live-preview.js"
|
|
16
|
+
},
|
|
13
17
|
"./package.json": "./package.json"
|
|
14
18
|
},
|
|
15
19
|
"files": [
|