From 63f5c1a1c086bccd38d6855d8fb1376ce7b157c9 Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 26 Sep 2026 12:29:12 +0200 Subject: [PATCH 1/8] refactor(web): rebuild web-embed-pdf on the @simplepdf/embed core, window.simplePDF unchanged --- package-lock.json | 95 ++++++++++++- web/package.json | 6 +- web/rollup.config.js | 4 +- web/src/shared.ts | 309 ++++++++++++++----------------------------- web/src/types.ts | 23 +--- 5 files changed, 203 insertions(+), 234 deletions(-) diff --git a/package-lock.json b/package-lock.json index f028ec76..8a1c04a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ }, "embed": { "name": "@simplepdf/embed", - "version": "0.6.0", + "version": "0.7.0", "license": "MIT", "devDependencies": { "@tanstack/ai": "^0.38.0", @@ -1484,6 +1484,67 @@ "node": ">= 8" } }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/plugin-terser": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", @@ -2318,6 +2379,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -2894,6 +2962,16 @@ "node": ">=6" } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3442,6 +3520,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5698,10 +5783,10 @@ }, "react": { "name": "@simplepdf/react-embed-pdf", - "version": "1.12.0", + "version": "1.13.0", "license": "MIT", "dependencies": { - "@simplepdf/embed": "0.6.0" + "@simplepdf/embed": "0.7.0" }, "devDependencies": { "@rollup/plugin-terser": "^0.4.4", @@ -6658,7 +6743,11 @@ "name": "@simplepdf/web-embed-pdf", "version": "1.8.4", "license": "MIT", + "dependencies": { + "@simplepdf/embed": "0.7.0" + }, "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^0.4.4", "@types/jsdom": "^21.1.7", "jsdom": "^26.0.0", diff --git a/web/package.json b/web/package.json index 3e4fb783..56b0e726 100644 --- a/web/package.json +++ b/web/package.json @@ -31,6 +31,7 @@ "start": "rollup -c -w" }, "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-terser": "^0.4.4", "@types/jsdom": "^21.1.7", "jsdom": "^26.0.0", @@ -50,5 +51,8 @@ "typescript", "npm", "pdf" - ] + ], + "dependencies": { + "@simplepdf/embed": "0.7.0" + } } diff --git a/web/rollup.config.js b/web/rollup.config.js index 0afab22c..2df3df01 100644 --- a/web/rollup.config.js +++ b/web/rollup.config.js @@ -1,5 +1,6 @@ import typescript from 'rollup-plugin-typescript2'; import terser from '@rollup/plugin-terser'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; import pkg from './package.json' with { type: 'json' }; @@ -7,6 +8,7 @@ function createOutputConfig({ file, minify }) { return { file, format: 'umd', + inlineDynamicImports: true, name: 'simplePDF', strict: true, plugins: minify ? [terser({ format: { comments: false } })] : [], @@ -22,6 +24,6 @@ export default { minify: true, }), ], - plugins: [typescript()], + plugins: [nodeResolve({ browser: true }), typescript()], external: [], }; diff --git a/web/src/shared.ts b/web/src/shared.ts index b7fe923f..c890b9bb 100644 --- a/web/src/shared.ts +++ b/web/src/shared.ts @@ -1,15 +1,26 @@ -import { EditorConfig, EditorContext, Locale, ConfigSetter, OutgoingIframeEvent, IncomingIframeEvent } from './types'; +import { createEmbed, EmbedConfigError, type WebMCPOptions } from '@simplepdf/embed'; +import type { ConfigSetter, EditorConfig, EditorContext, Locale } from './types'; const MODAL_ID = 'simplePDF_modal' as const; const MODAL_CLOSE_BUTTON_ID = 'simplePDF_modal_close_button' as const; const MODAL_STYLE_ID = 'simplePDF_modal_style' as const; const IFRAME_ID = 'simplePDF_iframe' as const; +const IFRAME_CONTAINER_SELECTOR = `#${MODAL_ID} .simplePDF_iframeContainer`; const UNEXPECTED_ERROR_INITIALIZATION = 'Unexpected: window.simplePDF not initialized'; -const UNEXPECTED_ERROR_IFRAME_NOT_INSTANTIATED = 'Unexpected: SimplePDF iframe not instantiated'; + +const SUPPORTED_LOCALES = { + de: true, + en: true, + es: true, + fr: true, + it: true, + ja: true, + nl: true, + pt: true, +} satisfies Record; const editorContext: EditorContext = { - getFromConfig: (key: 'companyIdentifier' | 'locale') => window.simplePDF?.config?.[key] ?? null, log: (message: string, details: Record) => { const debugAttribute = document.currentScript?.getAttribute('debug'); const isDebug = debugAttribute !== null && debugAttribute !== undefined; @@ -21,17 +32,11 @@ const editorContext: EditorContext = { console.warn(`@simplepdf/web-embed-pdf: ${message}`, details); }, autoOpenListeners: window.simplePDF?._ctx.listenersMap ?? new Map(), - outgoingEventsQueue: [], - isIframeReady: false, - getEditor: () => { - return { - iframe: document.getElementById(IFRAME_ID) as HTMLIFrameElement | null, - modal: document.getElementById(MODAL_ID) as HTMLDivElement | null, - styles: document.getElementById(MODAL_STYLE_ID) as HTMLStyleElement | null, - }; - }, + activeEmbed: null, }; +const readScriptAttribute = (name: string): string | null => document.currentScript?.getAttribute(name) ?? null; + const isSimplePDFLink = (url: string) => { const regex = /^https:\/\/[^.]+\.simplepdf\.com(\/[^\/]+)?\/(form|documents)\/.+/; return regex.test(url); @@ -39,6 +44,8 @@ const isSimplePDFLink = (url: string) => { const isPDFLink = (url: string) => url.endsWith('.pdf'); +const isLocale = (value: string): value is Locale => Object.prototype.hasOwnProperty.call(SUPPORTED_LOCALES, value); + const getLocale = (): Locale => { const languageCode = (() => { try { @@ -49,66 +56,50 @@ const getLocale = (): Locale => { } })(); - const inputLocale = (editorContext.getFromConfig('locale') ?? - document.currentScript?.getAttribute('locale') ?? - languageCode ?? - 'en') as Locale; - - switch (inputLocale) { - case 'en': - case 'de': - case 'es': - case 'fr': - case 'it': - case 'pt': - return inputLocale; - default: - inputLocale satisfies never; - return 'en'; + const inputLocale = window.simplePDF?.config?.locale ?? readScriptAttribute('locale') ?? languageCode ?? 'en'; + + return isLocale(inputLocale) ? inputLocale : 'en'; +}; + +const readWebMCPAttribute = (): WebMCPOptions | null => { + const webMCPAttribute = readScriptAttribute('webmcp'); + if (webMCPAttribute === null) { + return null; } + + return webMCPAttribute === 'false' ? { enabled: false } : { enabled: true }; }; +const scriptBaseDomain = readScriptAttribute('baseDomain'); +const scriptWebMCP = readWebMCPAttribute(); + export const config: EditorConfig = { locale: getLocale(), - companyIdentifier: - editorContext.getFromConfig('companyIdentifier') ?? - document.currentScript?.getAttribute('companyIdentifier') ?? - 'embed', + companyIdentifier: window.simplePDF?.config?.companyIdentifier ?? readScriptAttribute('companyIdentifier') ?? 'embed', autoOpen: false, + ...(scriptBaseDomain !== null ? { baseDomain: scriptBaseDomain } : {}), + ...(scriptWebMCP !== null ? { webMCP: scriptWebMCP } : {}), }; export const setConfig: ConfigSetter = (params) => { - let config = window.simplePDF?.config; - - if (!config) { + if (!window.simplePDF) { throw Error(UNEXPECTED_ERROR_INITIALIZATION); } - Object.keys(params).forEach((paramKey) => { - const configKey = paramKey as keyof EditorConfig; - const configValue = params[configKey]; - - if (configValue === undefined) { - return; + if (params.autoOpen !== undefined) { + editorContext.log('Update config', { configKey: 'autoOpen', configValue: params.autoOpen }); + if (params.autoOpen) { + enableAutoOpen(); + } else { + disableAutoOpen(); } + } - editorContext.log('Update config', { configKey, configValue }); - - switch (configKey) { - case 'autoOpen': { - if (configValue === true) { - enableAutoOpen(); - } else { - disableAutoOpen(); - } - break; - } - default: - break; - } - - (config[configKey] as any) = configValue as any; - }); + const definedParams = Object.fromEntries( + Object.entries(params).filter(([, configValue]) => configValue !== undefined), + ); + editorContext.log('Update config', definedParams); + Object.assign(config, definedParams); return config; }; @@ -139,116 +130,19 @@ export const getSimplePDFElements = (document: Document): Element[] => { return [...getNonAnchors(), ...getAnchors()]; }; -export const closeEditor = (): void => { - removeIframe(); +const removeModal = (): void => { + document.getElementById(MODAL_ID)?.remove(); + document.getElementById(MODAL_STYLE_ID)?.remove(); document.body.style.overflow = 'initial'; }; -const eventsListener = (event: MessageEvent) => { - const { getEditor, outgoingEventsQueue, getFromConfig, log } = editorContext; - const iframe = getEditor().iframe; - const editorDomain = `https://${getFromConfig('companyIdentifier')}.simplepdf.com`; - - const eventOrigin = new URL(event.origin).origin; - const iframeOrigin = new URL(editorDomain).origin; - - if (eventOrigin !== iframeOrigin) { - log('Incoming message from untrusted origin', { eventOrigin, iframeOrigin }); - return; - } - - const isTrustedIframe = event.source === iframe?.contentWindow; - - if (!isTrustedIframe) { - log('Incoming message from untrusted iframe', { eventOrigin, iframeOrigin }); - return; - } - - const payload: IncomingIframeEvent | null = (() => { - try { - return JSON.parse(event.data); - } catch (e) { - console.error('Failed to parse iFrame event payload'); - return null; - } - })(); - - switch (payload?.type) { - case 'EDITOR_READY': - editorContext.isIframeReady = true; - outgoingEventsQueue.forEach((queuedEvent) => { - sendEventToIframe(queuedEvent); - }); - outgoingEventsQueue.length = 0; - return; - case 'DOCUMENT_LOADED': - case 'SUBMISSION_SENT': - default: - return; - } -}; - -const onIframeLoaded = () => { - window.addEventListener('message', eventsListener); -}; - -const removeIframe = () => { - window.removeEventListener('message', eventsListener); - editorContext.getEditor().modal?.remove(); - editorContext.getEditor().styles?.remove(); - editorContext.isIframeReady = false; - editorContext.outgoingEventsQueue.length = 0; +export const closeEditor = (): void => { + editorContext.activeEmbed?.lifecycle.dispose(); + editorContext.activeEmbed = null; + removeModal(); }; -function sendEventToIframe(event: OutgoingIframeEvent) { - const { outgoingEventsQueue, log, isIframeReady } = editorContext; - const editorDomainURL = new URL(editorContext.getEditor().iframe?.src ?? ''); - - if (isIframeReady) { - log('Send iframe event', { event }); - editorContext.getEditor().iframe?.contentWindow?.postMessage(JSON.stringify(event), editorDomainURL.origin); - return; - } - - log('Push event to queue', { event }); - outgoingEventsQueue.push(event); -} - -export const openEditor = ({ href, context }: { href: string | null; context?: Record }): void => { - const { getFromConfig, log, getEditor } = editorContext; - - if (getEditor().iframe) { - log('Editor already opened', {}); - return; - } - - const companyIdentifier = getFromConfig('companyIdentifier'); - const locale = getFromConfig('locale'); - - const encodedContext = (() => { - if (!context) { - return null; - } - - try { - return encodeURIComponent(btoa(JSON.stringify(context))); - } catch (e) { - log(`Failed to encode the context: ${JSON.stringify(e)}`, { context }); - return null; - } - })(); - - const iframeURL = new URL(`/${locale}/editor`, `https://${companyIdentifier}.simplepdf.com`); - - if (href) { - iframeURL.searchParams.set('loadingPlaceholder', 'true'); - } - - if (encodedContext) { - iframeURL.searchParams.set('context', encodedContext); - } - - const modal = ` +const MODAL_HTML = `