diff --git a/bin/build_package.ts b/bin/build_package.ts index 08c079b56d2..98ec13e2249 100644 --- a/bin/build_package.ts +++ b/bin/build_package.ts @@ -6,9 +6,8 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { parseArgs } from 'node:util'; import * as LightningCSS from 'lightningcss'; -import * as rollup from 'rollup'; import { globSync } from 'tinyglobby'; -import { getRollupConfiguration } from './rollup.ts'; +import { build } from 'tsdown'; const args = parseArgs({ allowPositionals: true, @@ -34,7 +33,7 @@ async function main() { process.exit(1); } - const packageData = await import(path.join(packageRoot, 'package.json'), {with: { type: 'json'}}); + const packageData = await import(path.join(packageRoot, 'package.json'), { with: { type: 'json' } }); const packageName = packageData.name; const srcDir = path.join(packageRoot, 'src'); const distDir = path.join(packageRoot, 'dist'); @@ -44,13 +43,7 @@ async function main() { process.exit(1); } - if (fs.existsSync(distDir)) { - console.log(`Cleaning up the "${distDir}" directory...`); - await fs.promises.rm(distDir, { recursive: true }); - await fs.promises.mkdir(distDir); - } - - const inputScriptFiles = [ + const inputFiles = [ ...globSync(path.join(srcDir, '*controller.ts')), ...(['@symfony/ux-react', '@symfony/ux-vue', '@symfony/ux-svelte'].includes(packageName) ? [path.join(srcDir, 'loader.ts'), path.join(srcDir, 'components.ts')] @@ -58,93 +51,72 @@ async function main() { ...(packageName === '@symfony/stimulus-bundle' ? [path.join(srcDir, 'loader.ts'), path.join(srcDir, 'controllers.ts')] : []), + ...(packageData?.config?.css_source ? [packageData.config.css_source] : []), ]; - const inputStyleFile = packageData.config?.css_source; - const buildCss = async () => { - if (!inputStyleFile) { - return; - } - const inputStyleFileDist = path.resolve(distDir, `${path.basename(inputStyleFile, '.css')}.min.css`); - - console.log('Minifying CSS...'); - const css = await fs.promises.readFile(inputStyleFile, 'utf-8'); - const { code: minified } = LightningCSS.transform({ - filename: path.basename(inputStyleFile, '.css'), - code: Buffer.from(css), - minify: true, - sourceMap: false, // TODO: Maybe we can add source maps later? :) - }); - await fs.promises.writeFile(inputStyleFileDist, minified); - }; + const external = []; - if (inputScriptFiles.length === 0) { - console.error( - `No input files found for package "${packageName}" (directory "${packageRoot}").\nEnsure you have at least a file matching the pattern "src/*_controller.ts", or manually specify input files in "${import.meta.filename}" file.` - ); - process.exit(1); - } + inputFiles.forEach((file) => { + // custom handling for StimulusBundle + if (file.includes('StimulusBundle/assets/src/loader.ts')) { + external.push('./controllers.js'); + } - const rollupConfig = getRollupConfiguration({ - packageRoot, - inputFiles: inputScriptFiles, - isWatch, - additionalPlugins: [ - ...(isWatch && inputStyleFile - ? [ - { - name: 'watcher', - buildStart(this: rollup.PluginContext) { - this.addWatchFile(inputStyleFile); - }, - }, - ] - : []), - ], + // React, Vue + if (file.includes('assets/src/loader.ts')) { + external.push('./components.js'); + } }); - if (isWatch) { - console.log( - `Watching for JavaScript${inputStyleFile ? ' and CSS' : ''} files modifications in "${srcDir}" directory...` - ); - - const watcher = rollup.watch(rollupConfig); - watcher.on('event', (event) => { - if (event.code === 'ERROR') { - console.error('Error during build:', event.error); - } - - if ((event.code === 'BUNDLE_END' || event.code === 'ERROR') && event.result) { - event.result.close(); - } - }); - watcher.on('change', async (id, { event }) => { - if (event === 'update') { - console.log('Files were modified, rebuilding...'); - } - - if (inputStyleFile && id === inputStyleFile) { - await buildCss(); + build({ + entry: inputFiles, + outDir: distDir, + clean: true, + outputOptions: { + cssEntryFileNames: '[name].min.css', + }, + external, + format: 'esm', + platform: 'browser', + tsconfig: path.join(import.meta.dirname, '../tsconfig.packages.json'), + // The target should be kept in sync with `tsconfig.packages.json` file. + // In the future, I hope the target will be read from the `tsconfig.packages.json` file, but for now we need to specify it manually. + target: 'es2022', + watch: isWatch, + plugins: [ + // Since minifying files is not configurable per file, we need to use a custom plugin to handle CSS minification. + { + name: 'minimize-css', + transform: { + filter: { + id: /\.css$/, + }, + handler (code, id) { + const { code: minifiedCode } = LightningCSS.transform({ + filename: path.basename(id), + code: Buffer.from(code), + minify: true, + sourceMap: false, + }); + + return { code: minifiedCode.toString(), map: null }; + } + }, + }, + ], + hooks: { + async 'build:done'() { + // TODO: Idk why, but when we build a CSS file (e.g. `style.css`), it also generate an empty JS file (e.g. `style.js`). + if (packageData?.config?.css_source) { + const unwantedJsFile = path.join(distDir, path.basename(packageData.config.css_source, '.css') + '.js'); + await fs.promises.rm(unwantedJsFile, { force: true }); + } } - }); - } else { - console.log(`Building JavaScript files from ${packageName} package...`); - const start = Date.now(); - - if (typeof rollupConfig.output === 'undefined' || Array.isArray(rollupConfig.output)) { - console.error( - `The rollup configuration for package "${packageName}" does not contain a valid output configuration.` - ); - process.exit(1); } - - const bundle = await rollup.rollup(rollupConfig); - await bundle.write(rollupConfig.output); - - await buildCss(); - - console.log(`Done in ${((Date.now() - start) / 1000).toFixed(3)} seconds.`); - } + }).catch((error) => { + console.error('Error during build:', error); + process.exit(1); + }); } main(); diff --git a/bin/rollup.ts b/bin/rollup.ts deleted file mode 100644 index 398878628fc..00000000000 --- a/bin/rollup.ts +++ /dev/null @@ -1,143 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import commonjs from '@rollup/plugin-commonjs'; -import resolve from '@rollup/plugin-node-resolve'; -import typescript from '@rollup/plugin-typescript'; -import type { Plugin, RollupOptions } from 'rollup'; -import { globSync } from 'tinyglobby'; - -/** - * Guarantees that any files imported from a peer dependency are treated as an external. - * - * For example, if we import `chart.js/auto`, that would not normally - * match the "chart.js" we pass to the "externals" config. This plugin - * catches that case and adds it as an external. - * - * Inspired by https://github.com/oat-sa/rollup-plugin-wildcard-external - */ -function wildcardExternalsPlugin(peerDependencies: Array): Plugin { - return { - name: 'wildcard-externals', - resolveId(source, importer) { - if (importer) { - let matchesExternal = false; - peerDependencies.forEach((peerDependency) => { - if (source.includes(`/${peerDependency}/`)) { - matchesExternal = true; - } - }); - - if (matchesExternal) { - return { - id: source, - external: true, - moduleSideEffects: true, - }; - } - } - - return null; // other ids should be handled as usually - }, - }; -} - -/** - * Moves the generated TypeScript declaration files to the correct location. - * - * This could probably be configured in the TypeScript plugin. - */ -function moveTypescriptDeclarationsPlugin(packageRoot: string): Plugin { - return { - name: 'move-ts-declarations', - writeBundle: async () => { - const isBridge = packageRoot.includes('src/Bridge'); - const globPattern = path.join('dist', '**', 'assets', 'src', '**/*.d.ts'); - const files = globSync(globPattern); - - files.forEach((file) => { - const relativePath = file; - // a bit odd, but remove first 7 or 4 directories, which will leave only the relative path to the file - // ex: dist/Chartjs/assets/src/controller.d.ts' => 'dist/controller.d.ts' - const targetFile = relativePath.replace( - `${relativePath - .split('/') - .slice(1, isBridge ? 7 : 4) - .join('/')}/`, - '' - ); - if (!fs.existsSync(path.dirname(targetFile))) { - fs.mkdirSync(path.dirname(targetFile), { recursive: true }); - } - fs.renameSync(file, targetFile); - }); - }, - }; -} - -export function getRollupConfiguration({ - packageRoot, - inputFiles, - isWatch, - additionalPlugins, -}: { - packageRoot: string; - inputFiles: string[]; - isWatch: boolean; - additionalPlugins: Array; -}): RollupOptions { - const packagePath = path.join(packageRoot, 'package.json'); - const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8')); - const peerDependencies = [ - '@hotwired/stimulus', - ...(packageData.peerDependencies ? Object.keys(packageData.peerDependencies) : []), - ]; - - inputFiles.forEach((file) => { - // custom handling for StimulusBundle - if (file.includes('StimulusBundle/assets/src/loader.ts')) { - peerDependencies.push('./controllers.js'); - } - - // React, Vue - if (file.includes('assets/src/loader.ts')) { - peerDependencies.push('./components.js'); - } - }); - - const outDir = path.join(packageRoot, 'dist'); - - return { - input: inputFiles, - output: { - dir: outDir, - entryFileNames: '[name].js', - format: 'esm', - }, - external: peerDependencies, - plugins: [ - resolve(), - typescript({ - filterRoot: '.', - tsconfig: path.join(import.meta.dirname, '..', 'tsconfig.packages.json'), - noEmitOnError: !isWatch, - include: [ - 'src/**/*.ts', - // TODO: Remove for the next major release - // "@rollup/plugin-typescript" v11.0.0 fixed an issue (https://github.com/rollup/plugins/pull/1310) that - // cause a breaking change for UX React users, the dist file requires "react-dom/client" instead of "react-dom" - // and it will break for users using the Symfony AssetMapper without Symfony Flex (for automatic "importmap.php" upgrade). - '**/node_modules/react-dom/client.js', - ], - compilerOptions: { - outDir: outDir, - declaration: true, - emitDeclarationOnly: true, - }, - }), - commonjs(), - wildcardExternalsPlugin(peerDependencies), - moveTypescriptDeclarationsPlugin(packageRoot), - ...additionalPlugins, - ], - }; -} diff --git a/package.json b/package.json index 92f574c8434..8c04637972d 100644 --- a/package.json +++ b/package.json @@ -14,16 +14,14 @@ }, "devDependencies": { "@biomejs/biome": "^2.0.4", - "@rollup/plugin-commonjs": "^28.0.6", - "@rollup/plugin-node-resolve": "^16.0.1", - "@rollup/plugin-typescript": "^12.1.4", + "@oxc-project/runtime": "^0.77.3", "@testing-library/dom": "catalog:", "@testing-library/jest-dom": "catalog:", "@types/node": "^22.6.0", "lightningcss": "^1.28.2", "playwright": "^1.47.0", - "rollup": "^4.44.1", "tinyglobby": "^0.2.14", + "tsdown": "^0.12.9", "vitest": "catalog:" }, "version": "2.27.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 731d040acfd..00e723002cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,15 +49,9 @@ importers: '@biomejs/biome': specifier: ^2.0.4 version: 2.1.2 - '@rollup/plugin-commonjs': - specifier: ^28.0.6 - version: 28.0.6(rollup@4.45.1) - '@rollup/plugin-node-resolve': - specifier: ^16.0.1 - version: 16.0.1(rollup@4.45.1) - '@rollup/plugin-typescript': - specifier: ^12.1.4 - version: 12.1.4(rollup@4.45.1)(tslib@2.8.1)(typescript@5.8.3) + '@oxc-project/runtime': + specifier: ^0.77.3 + version: 0.77.3 '@testing-library/dom': specifier: 'catalog:' version: 10.4.0 @@ -73,12 +67,12 @@ importers: playwright: specifier: ^1.47.0 version: 1.54.1 - rollup: - specifier: ^4.44.1 - version: 4.45.1 tinyglobby: specifier: ^0.2.14 version: 0.2.14 + tsdown: + specifier: ^0.12.9 + version: 0.12.9(typescript@5.8.3) vitest: specifier: 'catalog:' version: 3.2.4(@types/node@22.16.5)(@vitest/browser@3.2.4)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(terser@5.43.1) @@ -252,10 +246,6 @@ importers: version: 3.2.4(@types/node@22.16.5)(@vitest/browser@3.2.4)(jsdom@26.1.0)(lightningcss@1.30.1)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(terser@5.43.1) src/LiveComponent/assets: - dependencies: - idiomorph: - specifier: ^0.3.0 - version: 0.3.0 devDependencies: '@hotwired/stimulus': specifier: ^3.0.0 @@ -272,6 +262,9 @@ importers: '@types/node-fetch': specifier: ^2.6.2 version: 2.6.12 + idiomorph: + specifier: ^0.3.0 + version: 0.3.0 jsdom: specifier: 'catalog:' version: 26.1.0 @@ -962,6 +955,15 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@emnapi/core@1.4.5': + resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} + + '@emnapi/runtime@1.4.5': + resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} + + '@emnapi/wasi-threads@1.0.4': + resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -1344,6 +1346,9 @@ packages: resolution: {integrity: sha512-9bw/wBL7pblsnOCIqvn1788S9o4h+cC5HWXg0Xhh0dOzsZ53IyfmBM+FYqpDDPbm0xjCqEqvCITloF3Dm4TXRQ==} engines: {node: '>=18'} + '@napi-rs/wasm-runtime@1.0.1': + resolution: {integrity: sha512-KVlQ/jgywZpixGCKMNwxStmmbYEMyokZpCf2YuIChhfJA2uqfAKNEM8INz7zzTo55iEXfBhIIs3VqYyqzDLj8g==} + '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -1359,51 +1364,99 @@ packages: '@orchidjs/unicode-variants@1.1.2': resolution: {integrity: sha512-5DobW1CHgnBROOEpFlEXytED5OosEWESFvg/VYmH0143oXcijYTprRYJTs+55HzGM4IqxiLFSuqEzu9mPNwVsA==} + '@oxc-project/runtime@0.77.2': + resolution: {integrity: sha512-oqzN82vVbqK6BnUuYDlBMlMr8mEeysMn/P8HbiB3j5rD04JvIfONCfh6SbtJTxhp1C4cjLi1evrtVTIptrln7Q==} + engines: {node: '>=6.9.0'} + + '@oxc-project/runtime@0.77.3': + resolution: {integrity: sha512-vsC/ewcGJ7xXnnwZkku7rpPH5Lxb5g4J+V6lD9eBTnRLmXVXM7Qu50y+ozD+UD5IXaSoVOvVMGTT4YSNCz2MQQ==} + engines: {node: '>=6.9.0'} + + '@oxc-project/types@0.77.2': + resolution: {integrity: sha512-+ZFWJF8ZBTOIO5PiNohNIw7JBzJCybScfrhLh65tcHCAtqaQkVDonjRD1HmMV/RF3rtt3r88hzSyTqvXs4j7vw==} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} - '@rolldown/pluginutils@1.0.0-beta.27': - resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@quansync/fs@0.1.3': + resolution: {integrity: sha512-G0OnZbMWEs5LhDyqy2UL17vGhSVHkQIfVojMtEWVenvj0V5S84VBgy86kJIuNsGDp2p7sTKlpSIpBUWdC35OKg==} + engines: {node: '>=20.0.0'} - '@rollup/plugin-commonjs@28.0.6': - resolution: {integrity: sha512-XSQB1K7FUU5QP+3lOQmVCE3I0FcbbNvmNT4VJSj93iUjayaARrTQeoRdiYQoftAJBLrR9t2agwAd3ekaTgHNlw==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true + '@rolldown/binding-android-arm64@1.0.0-beta.28': + resolution: {integrity: sha512-hLb7k11KBXtO8xc7DO1OWriXWM/2FKv/R510NChqpzoI6au2aJbGUQTKJw4D8Mj7oHfY2Nwzy+sJBgWx/P8IKw==} + cpu: [arm64] + os: [android] - '@rollup/plugin-node-resolve@16.0.1': - resolution: {integrity: sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true + '@rolldown/binding-darwin-arm64@1.0.0-beta.28': + resolution: {integrity: sha512-yRhjS3dcjfAasnJ2pTyCVm5rtfOmkGIglrFh+n9J7Zi4owJFsVVpbY7dOE3T1Op3mQ94apGN+Twtv6CIk6GFIQ==} + cpu: [arm64] + os: [darwin] - '@rollup/plugin-typescript@12.1.4': - resolution: {integrity: sha512-s5Hx+EtN60LMlDBvl5f04bEiFZmAepk27Q+mr85L/00zPDn1jtzlTV6FWn81MaIwqfWzKxmOJrBWHU6vtQyedQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.14.0||^3.0.0||^4.0.0 - tslib: '*' - typescript: '>=3.7.0' - peerDependenciesMeta: - rollup: - optional: true - tslib: - optional: true + '@rolldown/binding-darwin-x64@1.0.0-beta.28': + resolution: {integrity: sha512-eOX0pjz++yVdqcDqnoZeVXUHxak2AcEgQBlEKJYaeJj+O5V3r3wSnlDVSkgD6YEAHo2IlIa89+qFHv529esY6w==} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.28': + resolution: {integrity: sha512-WV1QYVMkkp/568iaEBoZhD1axFLhSO+ybCJlbmHkTFMub4wb5bmKtfuaBgjUVDDSB6JfZ6UL3Z0Q9VVHENOgsg==} + cpu: [x64] + os: [freebsd] - '@rollup/pluginutils@5.2.0': - resolution: {integrity: sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.28': + resolution: {integrity: sha512-ug/Wh9Tz4XB/CsYvaI2r5uC3vE3zrP5iDIsD+uEgFPV71BOQOfXFgZbC1zv+J1adkzWACr578aGQqW9jRj0gVA==} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.28': + resolution: {integrity: sha512-h3hzQuP+5l47wxn9+A39n1Q3i4mAvbNFJCZ8EZLrkqfsecfeZ5btIbDJTVAIQTy+uPr7uluAHIf11Jw+YkWjOQ==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.28': + resolution: {integrity: sha512-oW5LydGtfdT8TI5HTybxi1DdMCXCmVE1ak4VrSmVKsbBZyE0bDgL1UvTS1OOvuq4PM24zQHIuSNOpgLXgVj4vQ==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-ohos@1.0.0-beta.28': + resolution: {integrity: sha512-yeAAPMgssEkTCouUSYLrSWm+EXYBFI+ZTe8BVQkY5le51OCbqFNibtYkKZNHZBdhNRjWcSKSIuXN4MAXBz1j+g==} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.28': + resolution: {integrity: sha512-xWsylmva9L4ZFc28A9VGlF9fnrFpVxVC+kKqrBoqz2l/p5b4zRoFNtnSecivnxuPeR5Ga6W6lnpwGeWDvqBZ1Q==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.28': + resolution: {integrity: sha512-IdtRNm70EH7/1mjqXJc4pa2MoAxo/xl9hN8VySG9BQuYfhQz+JDC+FZBc+krlVUO3cTJz/o4xI/x4kA+rLKTwA==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.28': + resolution: {integrity: sha512-jS2G0+GtUCVcglCefScxgXeLJal0UAvVwvpy3reoC07K16k8WM/lXoYsZdpw34d5ONg0XcZpcokzA9R5K2o0lQ==} engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.28': + resolution: {integrity: sha512-K6SO4e48aqpE/E6iEaXYG1kVX3owLierZUngP44f7s6WcnNUXsX8aborZZkKDKjgfk654M/EjSI7riPQXfynIA==} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.28': + resolution: {integrity: sha512-IIAecHvlUY/oxADfA6sZFfmRx0ajY+U1rAPFT77COp11kf7irUJeD9GskFzCm+7Wm+q8Vogyh0KWqqd6f5Azgg==} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.28': + resolution: {integrity: sha512-eMGdPBhNkylib+7eaeC69axEjg5Y1Vie5LoKDBVaZ71jYTmtrUdna9PTUblkCIChNTQKlgxpi/eCaYmhId0aYA==} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rolldown/pluginutils@1.0.0-beta.28': + resolution: {integrity: sha512-fe3/1HZ3qJmXvkGv1kacKq2b+x9gbcyF1hnmLBVrRFEQWoOcRapQjXf8+hgyxI0EJAbnKEtrp5yhohQCFCjycw==} '@rollup/rollup-android-arm-eabi@4.45.1': resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} @@ -1573,6 +1626,9 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} + '@types/aria-query@4.2.2': resolution: {integrity: sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==} @@ -1644,9 +1700,6 @@ packages: '@types/react@18.3.23': resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -1789,6 +1842,10 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansis@4.1.0: + resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} + engines: {node: '>=14'} + aria-query@4.2.2: resolution: {integrity: sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==} engines: {node: '>=6.0'} @@ -1804,6 +1861,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-kit@2.1.1: + resolution: {integrity: sha512-mfh6a7gKXE8pDlxTvqIc/syH/P3RkzbOF6LeHdcKztLEzYe6IMsRCL7N8vI7hqTGWNxpkCuuRTpT21xNWqhRtQ==} + engines: {node: '>=20.18.0'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -1811,6 +1872,9 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + birpc@2.5.0: + resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} + browserslist@4.25.1: resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1854,6 +1918,10 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1879,9 +1947,6 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1939,6 +2004,9 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1954,12 +2022,25 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + engines: {node: '>=0.3.1'} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dts-resolver@2.1.1: + resolution: {integrity: sha512-3BiGFhB6mj5Kv+W2vdJseQUYW+SKVzAFJL6YNP6ursbrwy1fXHRotfHi3xLNxe4wZl/K8qbAFeCDjZLjzqxxRw==} + engines: {node: '>=20.18.0'} + peerDependencies: + oxc-resolver: '>=11.0.0' + peerDependenciesMeta: + oxc-resolver: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1970,6 +2051,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2098,6 +2183,9 @@ packages: headers-polyfill@4.0.3: resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} @@ -2124,32 +2212,26 @@ packages: intl-messageformat@10.7.16: resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==} - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - is-node-process@1.2.0: resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} - is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} jest-canvas-mock@2.5.2: resolution: {integrity: sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==} + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2352,9 +2434,6 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -2404,6 +2483,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} @@ -2423,6 +2505,10 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -2444,9 +2530,24 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} + rolldown-plugin-dts@0.13.14: + resolution: {integrity: sha512-wjNhHZz9dlN6PTIXyizB6u/mAg1wEFMW9yw7imEVe3CxHSRnNHVyycIX0yDEOVJfDNISLPbkCIPEpFpizy5+PQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + '@typescript/native-preview': '>=7.0.0-dev.20250601.1' + rolldown: ^1.0.0-beta.9 + typescript: ^5.0.0 + vue-tsc: ^2.2.0 || ^3.0.0 + peerDependenciesMeta: + '@typescript/native-preview': + optional: true + typescript: + optional: true + vue-tsc: + optional: true + + rolldown@1.0.0-beta.28: + resolution: {integrity: sha512-QOANlVluwwrLP5snQqKfC2lv/KJphMkjh4V0gpw0K40GdKmhd8eShIGOJNAC51idk5cn3xI08SZTRWj0R2XlDw==} hasBin: true rollup@4.45.1: @@ -2475,6 +2576,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2529,10 +2635,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - svelte-hmr@0.15.3: resolution: {integrity: sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==} engines: {node: ^12.20 || ^14.13.1 || >= 16} @@ -2560,6 +2662,9 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -2605,6 +2710,28 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + tsdown@0.12.9: + resolution: {integrity: sha512-MfrXm9PIlT3saovtWKf/gCJJ/NQCdE0SiREkdNC+9Qy6UHhdeDPxnkFaBD7xttVUmgp0yUHtGirpoLB+OVLuLA==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + publint: ^0.3.0 + typescript: ^5.0.0 + unplugin-lightningcss: ^0.4.0 + unplugin-unused: ^0.5.0 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + publint: + optional: true + typescript: + optional: true + unplugin-lightningcss: + optional: true + unplugin-unused: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2632,6 +2759,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + unconfig@7.3.2: + resolution: {integrity: sha512-nqG5NNL2wFVGZ0NA/aCFw0oJ2pxSf1lwg4Z5ill8wd7K4KX/rQbHlwbh+bjctXL5Ly1xtzHenHGOK0b+lG6JVg==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -3022,6 +3152,22 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@emnapi/core@1.4.5': + dependencies: + '@emnapi/wasi-threads': 1.0.4 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.4.5': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.4': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -3275,6 +3421,13 @@ snapshots: strict-event-emitter: 0.5.1 optional: true + '@napi-rs/wasm-runtime@1.0.1': + dependencies: + '@emnapi/core': 1.4.5 + '@emnapi/runtime': 1.4.5 + '@tybys/wasm-util': 0.10.0 + optional: true + '@open-draft/deferred-promise@2.2.0': optional: true @@ -3293,48 +3446,65 @@ snapshots: '@orchidjs/unicode-variants@1.1.2': {} - '@polka/url@1.0.0-next.29': {} + '@oxc-project/runtime@0.77.2': {} - '@rolldown/pluginutils@1.0.0-beta.27': {} + '@oxc-project/runtime@0.77.3': {} - '@rollup/plugin-commonjs@28.0.6(rollup@4.45.1)': - dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.45.1) - commondir: 1.0.1 - estree-walker: 2.0.2 - fdir: 6.4.6(picomatch@4.0.3) - is-reference: 1.2.1 - magic-string: 0.30.17 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.45.1 + '@oxc-project/types@0.77.2': {} - '@rollup/plugin-node-resolve@16.0.1(rollup@4.45.1)': - dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.45.1) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.10 - optionalDependencies: - rollup: 4.45.1 + '@polka/url@1.0.0-next.29': {} - '@rollup/plugin-typescript@12.1.4(rollup@4.45.1)(tslib@2.8.1)(typescript@5.8.3)': + '@quansync/fs@0.1.3': dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.45.1) - resolve: 1.22.10 - typescript: 5.8.3 - optionalDependencies: - rollup: 4.45.1 - tslib: 2.8.1 + quansync: 0.2.10 + + '@rolldown/binding-android-arm64@1.0.0-beta.28': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-beta.28': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.28': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.28': + optional: true - '@rollup/pluginutils@5.2.0(rollup@4.45.1)': + '@rolldown/binding-linux-arm64-ohos@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.28': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.28': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.28': dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.45.1 + '@napi-rs/wasm-runtime': 1.0.1 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.28': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.28': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.28': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rolldown/pluginutils@1.0.0-beta.28': {} '@rollup/rollup-android-arm-eabi@4.45.1': optional: true @@ -3499,6 +3669,11 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@tybys/wasm-util@0.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@types/aria-query@4.2.2': {} '@types/aria-query@5.0.4': {} @@ -3577,8 +3752,6 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.1.3 - '@types/resolve@1.20.2': {} - '@types/statuses@2.0.6': optional: true @@ -3759,6 +3932,8 @@ snapshots: ansi-styles@5.2.0: {} + ansis@4.1.0: {} + aria-query@4.2.2: dependencies: '@babel/runtime': 7.27.6 @@ -3772,10 +3947,17 @@ snapshots: assertion-error@2.0.1: {} + ast-kit@2.1.1: + dependencies: + '@babel/parser': 7.28.0 + pathe: 2.0.3 + asynckit@0.4.0: {} axobject-query@4.1.0: {} + birpc@2.5.0: {} + browserslist@4.25.1: dependencies: caniuse-lite: 1.0.30001727 @@ -3821,6 +4003,10 @@ snapshots: check-error@2.1.1: {} + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + cli-width@4.1.0: optional: true @@ -3852,8 +4038,6 @@ snapshots: commander@2.20.3: optional: true - commondir@1.0.1: {} - convert-source-map@2.0.0: {} cookie@0.7.2: @@ -3900,6 +4084,8 @@ snapshots: deepmerge@4.3.1: {} + defu@6.1.4: {} + delayed-stream@1.0.0: {} delegate-it@6.2.1: @@ -3910,10 +4096,14 @@ snapshots: detect-libc@2.0.4: {} + diff@8.0.2: {} + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} + dts-resolver@2.1.1: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -3925,6 +4115,8 @@ snapshots: emoji-regex@8.0.0: optional: true + empathic@2.0.0: {} + entities@4.5.0: {} entities@6.0.1: {} @@ -4082,6 +4274,8 @@ snapshots: headers-polyfill@4.0.3: optional: true + hookable@5.5.3: {} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 @@ -4115,24 +4309,14 @@ snapshots: '@formatjs/icu-messageformat-parser': 2.11.2 tslib: 2.8.1 - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - is-fullwidth-code-point@3.0.0: optional: true - is-module@1.0.0: {} - is-node-process@1.2.0: optional: true is-potential-custom-element-name@1.0.1: {} - is-reference@1.2.1: - dependencies: - '@types/estree': 1.0.8 - is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -4142,6 +4326,8 @@ snapshots: cssfontparser: 1.2.1 moo-color: 1.0.3 + jiti@2.4.2: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -4320,8 +4506,6 @@ snapshots: dependencies: entities: 6.0.1 - path-parse@1.0.7: {} - path-to-regexp@6.3.0: optional: true @@ -4373,6 +4557,8 @@ snapshots: punycode@2.3.1: {} + quansync@0.2.10: {} + querystringify@2.2.0: optional: true @@ -4390,6 +4576,8 @@ snapshots: dependencies: loose-envify: 1.4.0 + readdirp@4.1.2: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -4407,11 +4595,44 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve@1.22.10: + rolldown-plugin-dts@0.13.14(rolldown@1.0.0-beta.28)(typescript@5.8.3): dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 + '@babel/generator': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.1 + ast-kit: 2.1.1 + birpc: 2.5.0 + debug: 4.4.1 + dts-resolver: 2.1.1 + get-tsconfig: 4.10.1 + rolldown: 1.0.0-beta.28 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - oxc-resolver + - supports-color + + rolldown@1.0.0-beta.28: + dependencies: + '@oxc-project/runtime': 0.77.2 + '@oxc-project/types': 0.77.2 + '@rolldown/pluginutils': 1.0.0-beta.28 + ansis: 4.1.0 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-beta.28 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.28 + '@rolldown/binding-darwin-x64': 1.0.0-beta.28 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.28 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.28 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.28 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.28 + '@rolldown/binding-linux-arm64-ohos': 1.0.0-beta.28 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.28 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.28 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.28 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.28 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.28 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.28 rollup@4.45.1: dependencies: @@ -4460,6 +4681,8 @@ snapshots: semver@6.3.1: {} + semver@7.7.2: {} + siginfo@2.0.0: {} signal-exit@4.1.0: @@ -4516,8 +4739,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} - svelte-hmr@0.15.3(svelte@4.2.20): dependencies: svelte: 4.2.20 @@ -4558,6 +4779,8 @@ snapshots: tinyexec@0.3.2: {} + tinyexec@1.0.1: {} + tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.3) @@ -4600,6 +4823,29 @@ snapshots: dependencies: punycode: 2.3.1 + tsdown@0.12.9(typescript@5.8.3): + dependencies: + ansis: 4.1.0 + cac: 6.7.14 + chokidar: 4.0.3 + debug: 4.4.1 + diff: 8.0.2 + empathic: 2.0.0 + hookable: 5.5.3 + rolldown: 1.0.0-beta.28 + rolldown-plugin-dts: 0.13.14(rolldown@1.0.0-beta.28)(typescript@5.8.3) + semver: 7.7.2 + tinyexec: 1.0.1 + tinyglobby: 0.2.14 + unconfig: 7.3.2 + optionalDependencies: + typescript: 5.8.3 + transitivePeerDependencies: + - '@typescript/native-preview' + - oxc-resolver + - supports-color + - vue-tsc + tslib@2.8.1: {} tsx@4.20.3: @@ -4621,6 +4867,13 @@ snapshots: typescript@5.8.3: {} + unconfig@7.3.2: + dependencies: + '@quansync/fs': 0.1.3 + defu: 6.1.4 + jiti: 2.4.2 + quansync: 0.2.10 + undici-types@6.21.0: {} universalify@0.2.0: diff --git a/src/Autocomplete/assets/dist/controller.d.ts b/src/Autocomplete/assets/dist/controller.d.ts index d5cc6b3e272..662c4f94481 100644 --- a/src/Autocomplete/assets/dist/controller.d.ts +++ b/src/Autocomplete/assets/dist/controller.d.ts @@ -1,56 +1,60 @@ -import { Controller } from '@hotwired/stimulus'; -import TomSelect from 'tom-select'; -export interface AutocompletePreConnectOptions { - options: any; +import { Controller } from "@hotwired/stimulus"; +import TomSelect from "tom-select"; + +//#region src/controller.d.ts +interface AutocompletePreConnectOptions { + options: any; } -export interface AutocompleteConnectOptions { - tomSelect: TomSelect; - options: any; +interface AutocompleteConnectOptions { + tomSelect: TomSelect; + options: any; } -export default class extends Controller { - #private; - static values: { - url: StringConstructor; - optionsAsHtml: BooleanConstructor; - loadingMoreText: StringConstructor; - noResultsFoundText: StringConstructor; - noMoreResultsText: StringConstructor; - createOptionText: StringConstructor; - minCharacters: NumberConstructor; - tomSelectOptions: ObjectConstructor; - preload: StringConstructor; - }; - readonly urlValue: string; - readonly optionsAsHtmlValue: boolean; - readonly loadingMoreTextValue: string; - readonly noMoreResultsTextValue: string; - readonly noResultsFoundTextValue: string; - readonly createOptionTextValue: string; - readonly minCharactersValue: number; - readonly hasMinCharactersValue: boolean; - readonly tomSelectOptionsValue: object; - readonly hasPreloadValue: boolean; - readonly preloadValue: string; - tomSelect: TomSelect; - private mutationObserver; - private isObserving; - private hasLoadedChoicesPreviously; - private originalOptions; - initialize(): void; - connect(): void; - initializeTomSelect(): void; - disconnect(): void; - urlValueChanged(): void; - private getMaxOptions; - get selectElement(): HTMLSelectElement | null; - get formElement(): HTMLInputElement | HTMLSelectElement; - private dispatchEvent; - get preload(): string | boolean; - private resetTomSelect; - private changeTomSelectDisabledState; - private startMutationObserver; - private stopMutationObserver; - private onMutations; - private createOptionsDataStructure; - private areOptionsEquivalent; +declare class export_default extends Controller { + #private; + static values: { + url: StringConstructor; + optionsAsHtml: BooleanConstructor; + loadingMoreText: StringConstructor; + noResultsFoundText: StringConstructor; + noMoreResultsText: StringConstructor; + createOptionText: StringConstructor; + minCharacters: NumberConstructor; + tomSelectOptions: ObjectConstructor; + preload: StringConstructor; + }; + readonly urlValue: string; + readonly optionsAsHtmlValue: boolean; + readonly loadingMoreTextValue: string; + readonly noMoreResultsTextValue: string; + readonly noResultsFoundTextValue: string; + readonly createOptionTextValue: string; + readonly minCharactersValue: number; + readonly hasMinCharactersValue: boolean; + readonly tomSelectOptionsValue: object; + readonly hasPreloadValue: boolean; + readonly preloadValue: string; + tomSelect: TomSelect; + private mutationObserver; + private isObserving; + private hasLoadedChoicesPreviously; + private originalOptions; + initialize(): void; + connect(): void; + initializeTomSelect(): void; + disconnect(): void; + urlValueChanged(): void; + private getMaxOptions; + get selectElement(): HTMLSelectElement | null; + get formElement(): HTMLInputElement | HTMLSelectElement; + private dispatchEvent; + get preload(): string | boolean; + private resetTomSelect; + private changeTomSelectDisabledState; + private startMutationObserver; + private stopMutationObserver; + private onMutations; + private createOptionsDataStructure; + private areOptionsEquivalent; } +//#endregion +export { AutocompleteConnectOptions, AutocompletePreConnectOptions, export_default as default }; \ No newline at end of file diff --git a/src/Autocomplete/assets/dist/controller.js b/src/Autocomplete/assets/dist/controller.js index 62ca5d44b47..8089283b7d8 100644 --- a/src/Autocomplete/assets/dist/controller.js +++ b/src/Autocomplete/assets/dist/controller.js @@ -1,410 +1,325 @@ -import { Controller } from '@hotwired/stimulus'; -import TomSelect from 'tom-select'; +import { Controller } from "@hotwired/stimulus"; +import TomSelect from "tom-select"; -/****************************************************************************** -Copyright (c) Microsoft Corporation. - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -***************************************************************************** */ -/* global Reflect, Promise, SuppressedError, Symbol, Iterator */ - - -function __classPrivateFieldGet(receiver, state, kind, f) { - if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); - if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); - return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); -} - -typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { - var e = new Error(message); - return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; -}; - -var _default_1_instances, _default_1_getCommonConfig, _default_1_createAutocomplete, _default_1_createAutocompleteWithHtmlContents, _default_1_createAutocompleteWithRemoteData, _default_1_stripTags, _default_1_mergeConfigs, _default_1_normalizePluginsToHash, _default_1_createTomSelect; -class default_1 extends Controller { - constructor() { - super(...arguments); - _default_1_instances.add(this); - this.isObserving = false; - this.hasLoadedChoicesPreviously = false; - this.originalOptions = []; - _default_1_normalizePluginsToHash.set(this, (plugins) => { - if (Array.isArray(plugins)) { - return plugins.reduce((acc, plugin) => { - if (typeof plugin === 'string') { - acc[plugin] = {}; - } - if (typeof plugin === 'object' && plugin.name) { - acc[plugin.name] = plugin.options || {}; - } - return acc; - }, {}); - } - return plugins; - }); - } - initialize() { - if (!this.mutationObserver) { - this.mutationObserver = new MutationObserver((mutations) => { - this.onMutations(mutations); - }); - } - } - connect() { - if (this.selectElement) { - this.originalOptions = this.createOptionsDataStructure(this.selectElement); - } - this.initializeTomSelect(); - } - initializeTomSelect() { - if (this.selectElement) { - this.selectElement.setAttribute('data-skip-morph', ''); - } - if (this.urlValue) { - this.tomSelect = __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_createAutocompleteWithRemoteData).call(this, this.urlValue, this.hasMinCharactersValue ? this.minCharactersValue : null); - return; - } - if (this.optionsAsHtmlValue) { - this.tomSelect = __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_createAutocompleteWithHtmlContents).call(this); - return; - } - this.tomSelect = __classPrivateFieldGet(this, _default_1_instances, "m", _default_1_createAutocomplete).call(this); - this.startMutationObserver(); - } - disconnect() { - this.stopMutationObserver(); - let currentSelectedValues = []; - if (this.selectElement) { - if (this.selectElement.multiple) { - currentSelectedValues = Array.from(this.selectElement.options) - .filter((option) => option.selected) - .map((option) => option.value); - } - else { - currentSelectedValues = [this.selectElement.value]; - } - } - this.tomSelect.destroy(); - if (this.selectElement) { - if (this.selectElement.multiple) { - Array.from(this.selectElement.options).forEach((option) => { - option.selected = currentSelectedValues.includes(option.value); - }); - } - else { - this.selectElement.value = currentSelectedValues[0]; - } - } - } - urlValueChanged() { - this.resetTomSelect(); - } - getMaxOptions() { - return this.selectElement ? this.selectElement.options.length : 50; - } - get selectElement() { - if (!(this.element instanceof HTMLSelectElement)) { - return null; - } - return this.element; - } - get formElement() { - if (!(this.element instanceof HTMLInputElement) && !(this.element instanceof HTMLSelectElement)) { - throw new Error('Autocomplete Stimulus controller can only be used on an or or where there are multiple +* elements. In those cases, it will return the "full", final value +* for the model, which includes previously-selected values. +*/ function getValueFromElement(element, valueStore) { - if (element instanceof HTMLInputElement) { - if (element.type === 'checkbox') { - const modelNameData = getModelDirectiveFromElement(element, false); - if (modelNameData !== null) { - const modelValue = valueStore.get(modelNameData.action); - if (Array.isArray(modelValue)) { - return getMultipleCheckboxValue(element, modelValue); - } - if (Object(modelValue) === modelValue) { - return getMultipleCheckboxValue(element, Object.values(modelValue)); - } - } - if (element.hasAttribute('value')) { - return element.checked ? element.getAttribute('value') : null; - } - return element.checked; - } - return inputValue(element); - } - if (element instanceof HTMLSelectElement) { - if (element.multiple) { - return Array.from(element.selectedOptions).map((el) => el.value); - } - return element.value; - } - if (element.dataset.value) { - return element.dataset.value; - } - if ('value' in element) { - return element.value; - } - if (element.hasAttribute('value')) { - return element.getAttribute('value'); - } - return null; + if (element instanceof HTMLInputElement) { + if (element.type === "checkbox") { + const modelNameData = getModelDirectiveFromElement(element, false); + if (modelNameData !== null) { + const modelValue = valueStore.get(modelNameData.action); + if (Array.isArray(modelValue)) return getMultipleCheckboxValue(element, modelValue); + if (Object(modelValue) === modelValue) return getMultipleCheckboxValue(element, Object.values(modelValue)); + } + if (element.hasAttribute("value")) return element.checked ? element.getAttribute("value") : null; + return element.checked; + } + return inputValue(element); + } + if (element instanceof HTMLSelectElement) { + if (element.multiple) return Array.from(element.selectedOptions).map((el) => el.value); + return element.value; + } + if (element.dataset.value) return element.dataset.value; + if ("value" in element) return element.value; + if (element.hasAttribute("value")) return element.getAttribute("value"); + return null; } +/** +* Adapted from https://github.com/livewire/livewire +*/ function setValueOnElement(element, value) { - if (element instanceof HTMLInputElement) { - if (element.type === 'file') { - return; - } - if (element.type === 'radio') { - element.checked = element.value == value; - return; - } - if (element.type === 'checkbox') { - if (Array.isArray(value)) { - element.checked = value.some((val) => val == element.value); - } - else if (element.hasAttribute('value')) { - element.checked = element.value == value; - } - else { - element.checked = value; - } - return; - } - } - if (element instanceof HTMLSelectElement) { - const arrayWrappedValue = [].concat(value).map((value) => { - return `${value}`; - }); - Array.from(element.options).forEach((option) => { - option.selected = arrayWrappedValue.includes(option.value); - }); - return; - } - value = value === undefined ? '' : value; - element.value = value; + if (element instanceof HTMLInputElement) { + if (element.type === "file") return; + if (element.type === "radio") { + element.checked = element.value == value; + return; + } + if (element.type === "checkbox") { + if (Array.isArray(value)) element.checked = value.some((val) => val == element.value); + else if (element.hasAttribute("value")) element.checked = element.value == value; + else element.checked = value; + return; + } + } + if (element instanceof HTMLSelectElement) { + const arrayWrappedValue = [].concat(value).map((value$1) => { + return `${value$1}`; + }); + Array.from(element.options).forEach((option) => { + option.selected = arrayWrappedValue.includes(option.value); + }); + return; + } + value = value === void 0 ? "" : value; + element.value = value; } +/** +* Fetches *all* "data-model" directives for a given element. +* +* @param element +*/ function getAllModelDirectiveFromElements(element) { - if (!element.dataset.model) { - return []; - } - const directives = parseDirectives(element.dataset.model); - directives.forEach((directive) => { - if (directive.args.length > 0) { - throw new Error(`The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.`); - } - directive.action = normalizeModelName(directive.action); - }); - return directives; + if (!element.dataset.model) return []; + const directives = parseDirectives(element.dataset.model); + directives.forEach((directive) => { + if (directive.args.length > 0) throw new Error(`The data-model="${element.dataset.model}" format is invalid: it does not support passing arguments to the model.`); + directive.action = normalizeModelName(directive.action); + }); + return directives; } function getModelDirectiveFromElement(element, throwOnMissing = true) { - const dataModelDirectives = getAllModelDirectiveFromElements(element); - if (dataModelDirectives.length > 0) { - return dataModelDirectives[0]; - } - if (element.getAttribute('name')) { - const formElement = element.closest('form'); - if (formElement && 'model' in formElement.dataset) { - const directives = parseDirectives(formElement.dataset.model || '*'); - const directive = directives[0]; - if (directive.args.length > 0) { - throw new Error(`The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.`); - } - directive.action = normalizeModelName(element.getAttribute('name')); - return directive; - } - } - if (!throwOnMissing) { - return null; - } - throw new Error(`Cannot determine the model name for "${getElementAsTagText(element)}": the element must either have a "data-model" (or "name" attribute living inside a
).`); + const dataModelDirectives = getAllModelDirectiveFromElements(element); + if (dataModelDirectives.length > 0) return dataModelDirectives[0]; + if (element.getAttribute("name")) { + const formElement = element.closest("form"); + if (formElement && "model" in formElement.dataset) { + const directives = parseDirectives(formElement.dataset.model || "*"); + const directive = directives[0]; + if (directive.args.length > 0) throw new Error(`The data-model="${formElement.dataset.model}" format is invalid: it does not support passing arguments to the model.`); + directive.action = normalizeModelName(element.getAttribute("name")); + return directive; + } + } + if (!throwOnMissing) return null; + throw new Error(`Cannot determine the model name for "${getElementAsTagText(element)}": the element must either have a "data-model" (or "name" attribute living inside a ).`); } +/** +* Does the given element "belong" to the given component. +* +* To "belong" the element needs to: +* A) Live inside the component element (of course) +* B) NOT also live inside a child component +*/ function elementBelongsToThisComponent(element, component) { - if (component.element === element) { - return true; - } - if (!component.element.contains(element)) { - return false; - } - const closestLiveComponent = element.closest('[data-controller~="live"]'); - return closestLiveComponent === component.element; + if (component.element === element) return true; + if (!component.element.contains(element)) return false; + const closestLiveComponent = element.closest("[data-controller~=\"live\"]"); + return closestLiveComponent === component.element; } function cloneHTMLElement(element) { - const newElement = element.cloneNode(true); - if (!(newElement instanceof HTMLElement)) { - throw new Error('Could not clone element'); - } - return newElement; + const newElement = element.cloneNode(true); + if (!(newElement instanceof HTMLElement)) throw new Error("Could not clone element"); + return newElement; } function htmlToElement(html) { - const template = document.createElement('template'); - html = html.trim(); - template.innerHTML = html; - if (template.content.childElementCount > 1) { - throw new Error(`Component HTML contains ${template.content.childElementCount} elements, but only 1 root element is allowed.`); - } - const child = template.content.firstElementChild; - if (!child) { - throw new Error('Child not found'); - } - if (!(child instanceof HTMLElement)) { - throw new Error(`Created element is not an HTMLElement: ${html.trim()}`); - } - return child; + const template = document.createElement("template"); + html = html.trim(); + template.innerHTML = html; + if (template.content.childElementCount > 1) throw new Error(`Component HTML contains ${template.content.childElementCount} elements, but only 1 root element is allowed.`); + const child = template.content.firstElementChild; + if (!child) throw new Error("Child not found"); + if (!(child instanceof HTMLElement)) throw new Error(`Created element is not an HTMLElement: ${html.trim()}`); + return child; } const getMultipleCheckboxValue = (element, currentValues) => { - const finalValues = [...currentValues]; - const value = inputValue(element); - const index = currentValues.indexOf(value); - if (element.checked) { - if (index === -1) { - finalValues.push(value); - } - return finalValues; - } - if (index > -1) { - finalValues.splice(index, 1); - } - return finalValues; + const finalValues = [...currentValues]; + const value = inputValue(element); + const index = currentValues.indexOf(value); + if (element.checked) { + if (index === -1) finalValues.push(value); + return finalValues; + } + if (index > -1) finalValues.splice(index, 1); + return finalValues; }; const inputValue = (element) => element.dataset.value ? element.dataset.value : element.value; +/** +* Checks whether the given element is a textual input (input[type=text/email/...]). +*/ function isTextualInputElement(el) { - return el instanceof HTMLInputElement && ['text', 'email', 'password', 'search', 'tel', 'url'].includes(el.type); + return el instanceof HTMLInputElement && [ + "text", + "email", + "password", + "search", + "tel", + "url" + ].includes(el.type); } +/** +* Checks whether the given element is a textarea. +*/ function isTextareaElement(el) { - return el instanceof HTMLTextAreaElement; + return el instanceof HTMLTextAreaElement; } +/** +* Checks whether the given element is a numerical input (input[type=number] or input[type=range]). +*/ function isNumericalInputElement(element) { - return element instanceof HTMLInputElement && ['number', 'range'].includes(element.type); + return element instanceof HTMLInputElement && ["number", "range"].includes(element.type); } -class HookManager { - constructor() { - this.hooks = new Map(); - } - register(hookName, callback) { - const hooks = this.hooks.get(hookName) || []; - hooks.push(callback); - this.hooks.set(hookName, hooks); - } - unregister(hookName, callback) { - const hooks = this.hooks.get(hookName) || []; - const index = hooks.indexOf(callback); - if (index === -1) { - return; - } - hooks.splice(index, 1); - this.hooks.set(hookName, hooks); - } - triggerHook(hookName, ...args) { - const hooks = this.hooks.get(hookName) || []; - hooks.forEach((callback) => callback(...args)); - } -} - -// base IIFE to define idiomorph -var Idiomorph = (function () { - - //============================================================================= - // AND NOW IT BEGINS... - //============================================================================= - let EMPTY_SET = new Set(); - - // default configuration values, updatable by users now - let defaults = { - morphStyle: "outerHTML", - callbacks : { - beforeNodeAdded: noOp, - afterNodeAdded: noOp, - beforeNodeMorphed: noOp, - afterNodeMorphed: noOp, - beforeNodeRemoved: noOp, - afterNodeRemoved: noOp, - beforeAttributeUpdated: noOp, - - }, - head: { - style: 'merge', - shouldPreserve: function (elt) { - return elt.getAttribute("im-preserve") === "true"; - }, - shouldReAppend: function (elt) { - return elt.getAttribute("im-re-append") === "true"; - }, - shouldRemove: noOp, - afterHeadMorphed: noOp, - } - }; - - //============================================================================= - // Core Morphing Algorithm - morph, morphNormalizedContent, morphOldNodeTo, morphChildren - //============================================================================= - function morph(oldNode, newContent, config = {}) { - - if (oldNode instanceof Document) { - oldNode = oldNode.documentElement; - } - - if (typeof newContent === 'string') { - newContent = parseContent(newContent); - } - - let normalizedContent = normalizeContent(newContent); - - let ctx = createMorphContext(oldNode, normalizedContent, config); - - return morphNormalizedContent(oldNode, normalizedContent, ctx); - } - - function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { - if (ctx.head.block) { - let oldHead = oldNode.querySelector('head'); - let newHead = normalizedNewContent.querySelector('head'); - if (oldHead && newHead) { - let promises = handleHeadElement(newHead, oldHead, ctx); - // when head promises resolve, call morph again, ignoring the head tag - Promise.all(promises).then(function () { - morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { - head: { - block: false, - ignore: true - } - })); - }); - return; - } - } - - if (ctx.morphStyle === "innerHTML") { - - // innerHTML, so we are only updating the children - morphChildren(normalizedNewContent, oldNode, ctx); - return oldNode.children; - - } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { - // otherwise find the best element match in the new content, morph that, and merge its siblings - // into either side of the best match - let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); - - // stash the siblings that will need to be inserted on either side of the best match - let previousSibling = bestMatch?.previousSibling; - let nextSibling = bestMatch?.nextSibling; - - // morph it - let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); - - if (bestMatch) { - // if there was a best match, merge the siblings in too and return the - // whole bunch - return insertSiblings(previousSibling, morphedNode, nextSibling); - } else { - // otherwise nothing was added to the DOM - return [] - } - } else { - throw "Do not understand how to morph style " + ctx.morphStyle; - } - } - - - /** - * @param possibleActiveElement - * @param ctx - * @returns {boolean} - */ - function ignoreValueOfActiveElement(possibleActiveElement, ctx) { - return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement; - } - - /** - * @param oldNode root node to merge content into - * @param newContent new content to merge - * @param ctx the merge context - * @returns {Element} the element that ended up in the DOM - */ - function morphOldNodeTo(oldNode, newContent, ctx) { - if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) { - if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; - - oldNode.remove(); - ctx.callbacks.afterNodeRemoved(oldNode); - return null; - } else if (!isSoftMatch(oldNode, newContent)) { - if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; - if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode; - - oldNode.parentElement.replaceChild(newContent, oldNode); - ctx.callbacks.afterNodeAdded(newContent); - ctx.callbacks.afterNodeRemoved(oldNode); - return newContent; - } else { - if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode; - - if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") { - handleHeadElement(newContent, oldNode, ctx); - } else { - syncNodeFrom(newContent, oldNode, ctx); - if (!ignoreValueOfActiveElement(oldNode, ctx)) { - morphChildren(newContent, oldNode, ctx); - } - } - ctx.callbacks.afterNodeMorphed(oldNode, newContent); - return oldNode; - } - } - - /** - * This is the core algorithm for matching up children. The idea is to use id sets to try to match up - * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but - * by using id sets, we are able to better match up with content deeper in the DOM. - * - * Basic algorithm is, for each node in the new content: - * - * - if we have reached the end of the old parent, append the new content - * - if the new content has an id set match with the current insertion point, morph - * - search for an id set match - * - if id set match found, morph - * - otherwise search for a "soft" match - * - if a soft match is found, morph - * - otherwise, prepend the new node before the current insertion point - * - * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved - * with the current node. See findIdSetMatch() and findSoftMatch() for details. - * - * @param {Element} newParent the parent element of the new content - * @param {Element } oldParent the old content that we are merging the new content into - * @param ctx the merge context - */ - function morphChildren(newParent, oldParent, ctx) { - - let nextNewChild = newParent.firstChild; - let insertionPoint = oldParent.firstChild; - let newChild; - - // run through all the new content - while (nextNewChild) { - - newChild = nextNewChild; - nextNewChild = newChild.nextSibling; - - // if we are at the end of the exiting parent's children, just append - if (insertionPoint == null) { - if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; - - oldParent.appendChild(newChild); - ctx.callbacks.afterNodeAdded(newChild); - removeIdsFromConsideration(ctx, newChild); - continue; - } - - // if the current node has an id set match then morph - if (isIdSetMatch(newChild, insertionPoint, ctx)) { - morphOldNodeTo(insertionPoint, newChild, ctx); - insertionPoint = insertionPoint.nextSibling; - removeIdsFromConsideration(ctx, newChild); - continue; - } - - // otherwise search forward in the existing old children for an id set match - let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); - - // if we found a potential match, remove the nodes until that point and morph - if (idSetMatch) { - insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); - morphOldNodeTo(idSetMatch, newChild, ctx); - removeIdsFromConsideration(ctx, newChild); - continue; - } - - // no id set match found, so scan forward for a soft match for the current node - let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); - - // if we found a soft match for the current node, morph - if (softMatch) { - insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); - morphOldNodeTo(softMatch, newChild, ctx); - removeIdsFromConsideration(ctx, newChild); - continue; - } - - // abandon all hope of morphing, just insert the new child before the insertion point - // and move on - if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; - - oldParent.insertBefore(newChild, insertionPoint); - ctx.callbacks.afterNodeAdded(newChild); - removeIdsFromConsideration(ctx, newChild); - } - - // remove any remaining old nodes that didn't match up with new content - while (insertionPoint !== null) { - - let tempNode = insertionPoint; - insertionPoint = insertionPoint.nextSibling; - removeNode(tempNode, ctx); - } - } - - //============================================================================= - // Attribute Syncing Code - //============================================================================= - - /** - * @param attr {String} the attribute to be mutated - * @param to {Element} the element that is going to be updated - * @param updateType {("update"|"remove")} - * @param ctx the merge context - * @returns {boolean} true if the attribute should be ignored, false otherwise - */ - function ignoreAttribute(attr, to, updateType, ctx) { - if(attr === 'value' && ctx.ignoreActiveValue && to === document.activeElement){ - return true; - } - return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false; - } - - /** - * syncs a given node with another node, copying over all attributes and - * inner element state from the 'from' node to the 'to' node - * - * @param {Element} from the element to copy attributes & state from - * @param {Element} to the element to copy attributes & state to - * @param ctx the merge context - */ - function syncNodeFrom(from, to, ctx) { - let type = from.nodeType; - - // if is an element type, sync the attributes from the - // new node into the new node - if (type === 1 /* element type */) { - const fromAttributes = from.attributes; - const toAttributes = to.attributes; - for (const fromAttribute of fromAttributes) { - if (ignoreAttribute(fromAttribute.name, to, 'update', ctx)) { - continue; - } - if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) { - to.setAttribute(fromAttribute.name, fromAttribute.value); - } - } - // iterate backwards to avoid skipping over items when a delete occurs - for (let i = toAttributes.length - 1; 0 <= i; i--) { - const toAttribute = toAttributes[i]; - if (ignoreAttribute(toAttribute.name, to, 'remove', ctx)) { - continue; - } - if (!from.hasAttribute(toAttribute.name)) { - to.removeAttribute(toAttribute.name); - } - } - } - - // sync text nodes - if (type === 8 /* comment */ || type === 3 /* text */) { - if (to.nodeValue !== from.nodeValue) { - to.nodeValue = from.nodeValue; - } - } - - if (!ignoreValueOfActiveElement(to, ctx)) { - // sync input values - syncInputValue(from, to, ctx); - } - } - - /** - * @param from {Element} element to sync the value from - * @param to {Element} element to sync the value to - * @param attributeName {String} the attribute name - * @param ctx the merge context - */ - function syncBooleanAttribute(from, to, attributeName, ctx) { - if (from[attributeName] !== to[attributeName]) { - let ignoreUpdate = ignoreAttribute(attributeName, to, 'update', ctx); - if (!ignoreUpdate) { - to[attributeName] = from[attributeName]; - } - if (from[attributeName]) { - if (!ignoreUpdate) { - to.setAttribute(attributeName, from[attributeName]); - } - } else { - if (!ignoreAttribute(attributeName, to, 'remove', ctx)) { - to.removeAttribute(attributeName); - } - } - } - } - - /** - * NB: many bothans died to bring us information: - * - * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js - * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 - * - * @param from {Element} the element to sync the input value from - * @param to {Element} the element to sync the input value to - * @param ctx the merge context - */ - function syncInputValue(from, to, ctx) { - if (from instanceof HTMLInputElement && - to instanceof HTMLInputElement && - from.type !== 'file') { - - let fromValue = from.value; - let toValue = to.value; - - // sync boolean attributes - syncBooleanAttribute(from, to, 'checked', ctx); - syncBooleanAttribute(from, to, 'disabled', ctx); - - if (!from.hasAttribute('value')) { - if (!ignoreAttribute('value', to, 'remove', ctx)) { - to.value = ''; - to.removeAttribute('value'); - } - } else if (fromValue !== toValue) { - if (!ignoreAttribute('value', to, 'update', ctx)) { - to.setAttribute('value', fromValue); - to.value = fromValue; - } - } - } else if (from instanceof HTMLOptionElement) { - syncBooleanAttribute(from, to, 'selected', ctx); - } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { - let fromValue = from.value; - let toValue = to.value; - if (ignoreAttribute('value', to, 'update', ctx)) { - return; - } - if (fromValue !== toValue) { - to.value = fromValue; - } - if (to.firstChild && to.firstChild.nodeValue !== fromValue) { - to.firstChild.nodeValue = fromValue; - } - } - } - - //============================================================================= - // the HEAD tag can be handled specially, either w/ a 'merge' or 'append' style - //============================================================================= - function handleHeadElement(newHeadTag, currentHead, ctx) { - - let added = []; - let removed = []; - let preserved = []; - let nodesToAppend = []; - - let headMergeStyle = ctx.head.style; - - // put all new head elements into a Map, by their outerHTML - let srcToNewHeadNodes = new Map(); - for (const newHeadChild of newHeadTag.children) { - srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); - } - - // for each elt in the current head - for (const currentHeadElt of currentHead.children) { - - // If the current head element is in the map - let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); - let isReAppended = ctx.head.shouldReAppend(currentHeadElt); - let isPreserved = ctx.head.shouldPreserve(currentHeadElt); - if (inNewContent || isPreserved) { - if (isReAppended) { - // remove the current version and let the new version replace it and re-execute - removed.push(currentHeadElt); - } else { - // this element already exists and should not be re-appended, so remove it from - // the new content map, preserving it in the DOM - srcToNewHeadNodes.delete(currentHeadElt.outerHTML); - preserved.push(currentHeadElt); - } - } else { - if (headMergeStyle === "append") { - // we are appending and this existing element is not new content - // so if and only if it is marked for re-append do we do anything - if (isReAppended) { - removed.push(currentHeadElt); - nodesToAppend.push(currentHeadElt); - } - } else { - // if this is a merge, we remove this content since it is not in the new head - if (ctx.head.shouldRemove(currentHeadElt) !== false) { - removed.push(currentHeadElt); - } - } - } - } - - // Push the remaining new head elements in the Map into the - // nodes to append to the head tag - nodesToAppend.push(...srcToNewHeadNodes.values()); - - let promises = []; - for (const newNode of nodesToAppend) { - let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild; - if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { - if (newElt.href || newElt.src) { - let resolve = null; - let promise = new Promise(function (_resolve) { - resolve = _resolve; - }); - newElt.addEventListener('load', function () { - resolve(); - }); - promises.push(promise); - } - currentHead.appendChild(newElt); - ctx.callbacks.afterNodeAdded(newElt); - added.push(newElt); - } - } - - // remove all removed elements, after we have appended the new elements to avoid - // additional network requests for things like style sheets - for (const removedElement of removed) { - if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { - currentHead.removeChild(removedElement); - ctx.callbacks.afterNodeRemoved(removedElement); - } - } - - ctx.head.afterHeadMorphed(currentHead, {added: added, kept: preserved, removed: removed}); - return promises; - } - - function noOp() { - } - - /* - Deep merges the config object and the Idiomoroph.defaults object to - produce a final configuration object - */ - function mergeDefaults(config) { - let finalConfig = {}; - // copy top level stuff into final config - Object.assign(finalConfig, defaults); - Object.assign(finalConfig, config); - - // copy callbacks into final config (do this to deep merge the callbacks) - finalConfig.callbacks = {}; - Object.assign(finalConfig.callbacks, defaults.callbacks); - Object.assign(finalConfig.callbacks, config.callbacks); - - // copy head config into final config (do this to deep merge the head) - finalConfig.head = {}; - Object.assign(finalConfig.head, defaults.head); - Object.assign(finalConfig.head, config.head); - return finalConfig; - } - - function createMorphContext(oldNode, newContent, config) { - config = mergeDefaults(config); - return { - target: oldNode, - newContent: newContent, - config: config, - morphStyle: config.morphStyle, - ignoreActive: config.ignoreActive, - ignoreActiveValue: config.ignoreActiveValue, - idMap: createIdMap(oldNode, newContent), - deadIds: new Set(), - callbacks: config.callbacks, - head: config.head - } - } - - function isIdSetMatch(node1, node2, ctx) { - if (node1 == null || node2 == null) { - return false; - } - if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) { - if (node1.id !== "" && node1.id === node2.id) { - return true; - } else { - return getIdIntersectionCount(ctx, node1, node2) > 0; - } - } - return false; - } - - function isSoftMatch(node1, node2) { - if (node1 == null || node2 == null) { - return false; - } - return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName - } - - function removeNodesBetween(startInclusive, endExclusive, ctx) { - while (startInclusive !== endExclusive) { - let tempNode = startInclusive; - startInclusive = startInclusive.nextSibling; - removeNode(tempNode, ctx); - } - removeIdsFromConsideration(ctx, endExclusive); - return endExclusive.nextSibling; - } - - //============================================================================= - // Scans forward from the insertionPoint in the old parent looking for a potential id match - // for the newChild. We stop if we find a potential id match for the new child OR - // if the number of potential id matches we are discarding is greater than the - // potential id matches for the new child - //============================================================================= - function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { - - // max id matches we are willing to discard in our search - let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); - - let potentialMatch = null; - - // only search forward if there is a possibility of an id match - if (newChildPotentialIdCount > 0) { - let potentialMatch = insertionPoint; - // if there is a possibility of an id match, scan forward - // keep track of the potential id match count we are discarding (the - // newChildPotentialIdCount must be greater than this to make it likely - // worth it) - let otherMatchCount = 0; - while (potentialMatch != null) { - - // If we have an id match, return the current potential match - if (isIdSetMatch(newChild, potentialMatch, ctx)) { - return potentialMatch; - } - - // computer the other potential matches of this new content - otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent); - if (otherMatchCount > newChildPotentialIdCount) { - // if we have more potential id matches in _other_ content, we - // do not have a good candidate for an id match, so return null - return null; - } - - // advanced to the next old content child - potentialMatch = potentialMatch.nextSibling; - } - } - return potentialMatch; - } - - //============================================================================= - // Scans forward from the insertionPoint in the old parent looking for a potential soft match - // for the newChild. We stop if we find a potential soft match for the new child OR - // if we find a potential id match in the old parents children OR if we find two - // potential soft matches for the next two pieces of new content - //============================================================================= - function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { - - let potentialSoftMatch = insertionPoint; - let nextSibling = newChild.nextSibling; - let siblingSoftMatchCount = 0; - - while (potentialSoftMatch != null) { - - if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) { - // the current potential soft match has a potential id set match with the remaining new - // content so bail out of looking - return null; - } - - // if we have a soft match with the current node, return it - if (isSoftMatch(newChild, potentialSoftMatch)) { - return potentialSoftMatch; - } - - if (isSoftMatch(nextSibling, potentialSoftMatch)) { - // the next new node has a soft match with this node, so - // increment the count of future soft matches - siblingSoftMatchCount++; - nextSibling = nextSibling.nextSibling; - - // If there are two future soft matches, bail to allow the siblings to soft match - // so that we don't consume future soft matches for the sake of the current node - if (siblingSoftMatchCount >= 2) { - return null; - } - } - - // advanced to the next old content child - potentialSoftMatch = potentialSoftMatch.nextSibling; - } - - return potentialSoftMatch; - } - - function parseContent(newContent) { - let parser = new DOMParser(); - - // remove svgs to avoid false-positive matches on head, etc. - let contentWithSvgsRemoved = newContent.replace(/]*>|>)([\s\S]*?)<\/svg>/gim, ''); - - // if the newContent contains a html, head or body tag, we can simply parse it w/o wrapping - if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { - let content = parser.parseFromString(newContent, "text/html"); - // if it is a full HTML document, return the document itself as the parent container - if (contentWithSvgsRemoved.match(/<\/html>/)) { - content.generatedByIdiomorph = true; - return content; - } else { - // otherwise return the html element as the parent container - let htmlElement = content.firstChild; - if (htmlElement) { - htmlElement.generatedByIdiomorph = true; - return htmlElement; - } else { - return null; - } - } - } else { - // if it is partial HTML, wrap it in a template tag to provide a parent element and also to help - // deal with touchy tags like tr, tbody, etc. - let responseDoc = parser.parseFromString("", "text/html"); - let content = responseDoc.body.querySelector('template').content; - content.generatedByIdiomorph = true; - return content - } - } - - function normalizeContent(newContent) { - if (newContent == null) { - // noinspection UnnecessaryLocalVariableJS - const dummyParent = document.createElement('div'); - return dummyParent; - } else if (newContent.generatedByIdiomorph) { - // the template tag created by idiomorph parsing can serve as a dummy parent - return newContent; - } else if (newContent instanceof Node) { - // a single node is added as a child to a dummy parent - const dummyParent = document.createElement('div'); - dummyParent.append(newContent); - return dummyParent; - } else { - // all nodes in the array or HTMLElement collection are consolidated under - // a single dummy parent element - const dummyParent = document.createElement('div'); - for (const elt of [...newContent]) { - dummyParent.append(elt); - } - return dummyParent; - } - } - - function insertSiblings(previousSibling, morphedNode, nextSibling) { - let stack = []; - let added = []; - while (previousSibling != null) { - stack.push(previousSibling); - previousSibling = previousSibling.previousSibling; - } - while (stack.length > 0) { - let node = stack.pop(); - added.push(node); // push added preceding siblings on in order and insert - morphedNode.parentElement.insertBefore(node, morphedNode); - } - added.push(morphedNode); - while (nextSibling != null) { - stack.push(nextSibling); - added.push(nextSibling); // here we are going in order, so push on as we scan, rather than add - nextSibling = nextSibling.nextSibling; - } - while (stack.length > 0) { - morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling); - } - return added; - } - - function findBestNodeMatch(newContent, oldNode, ctx) { - let currentElement; - currentElement = newContent.firstChild; - let bestElement = currentElement; - let score = 0; - while (currentElement) { - let newScore = scoreElement(currentElement, oldNode, ctx); - if (newScore > score) { - bestElement = currentElement; - score = newScore; - } - currentElement = currentElement.nextSibling; - } - return bestElement; - } - - function scoreElement(node1, node2, ctx) { - if (isSoftMatch(node1, node2)) { - return .5 + getIdIntersectionCount(ctx, node1, node2); - } - return 0; - } - - function removeNode(tempNode, ctx) { - removeIdsFromConsideration(ctx, tempNode); - if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; - - tempNode.remove(); - ctx.callbacks.afterNodeRemoved(tempNode); - } - - //============================================================================= - // ID Set Functions - //============================================================================= - - function isIdInConsideration(ctx, id) { - return !ctx.deadIds.has(id); - } - - function idIsWithinNode(ctx, id, targetNode) { - let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; - return idSet.has(id); - } - - function removeIdsFromConsideration(ctx, node) { - let idSet = ctx.idMap.get(node) || EMPTY_SET; - for (const id of idSet) { - ctx.deadIds.add(id); - } - } - - function getIdIntersectionCount(ctx, node1, node2) { - let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; - let matchCount = 0; - for (const id of sourceSet) { - // a potential match is an id in the source and potentialIdsSet, but - // that has not already been merged into the DOM - if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) { - ++matchCount; - } - } - return matchCount; - } - - /** - * A bottom up algorithm that finds all elements with ids inside of the node - * argument and populates id sets for those nodes and all their parents, generating - * a set of ids contained within all nodes for the entire hierarchy in the DOM - * - * @param node {Element} - * @param {Map>} idMap - */ - function populateIdMapForNode(node, idMap) { - let nodeParent = node.parentElement; - // find all elements with an id property - let idElements = node.querySelectorAll('[id]'); - for (const elt of idElements) { - let current = elt; - // walk up the parent hierarchy of that element, adding the id - // of element to the parent's id set - while (current !== nodeParent && current != null) { - let idSet = idMap.get(current); - // if the id set doesn't exist, create it and insert it in the map - if (idSet == null) { - idSet = new Set(); - idMap.set(current, idSet); - } - idSet.add(elt.id); - current = current.parentElement; - } - } - } - - /** - * This function computes a map of nodes to all ids contained within that node (inclusive of the - * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows - * for a looser definition of "matching" than tradition id matching, and allows child nodes - * to contribute to a parent nodes matching. - * - * @param {Element} oldContent the old content that will be morphed - * @param {Element} newContent the new content to morph to - * @returns {Map>} a map of nodes to id sets for the - */ - function createIdMap(oldContent, newContent) { - let idMap = new Map(); - populateIdMapForNode(oldContent, idMap); - populateIdMapForNode(newContent, idMap); - return idMap; - } - - //============================================================================= - // This is what ends up becoming the Idiomorph global object - //============================================================================= - return { - morph, - defaults - } - })(); +//#endregion +//#region src/HookManager.ts +var HookManager_default = class { + hooks = /* @__PURE__ */ new Map(); + register(hookName, callback) { + const hooks = this.hooks.get(hookName) || []; + hooks.push(callback); + this.hooks.set(hookName, hooks); + } + unregister(hookName, callback) { + const hooks = this.hooks.get(hookName) || []; + const index = hooks.indexOf(callback); + if (index === -1) return; + hooks.splice(index, 1); + this.hooks.set(hookName, hooks); + } + triggerHook(hookName, ...args) { + const hooks = this.hooks.get(hookName) || []; + hooks.forEach((callback) => callback(...args)); + } +}; +//#endregion +//#region ../../../node_modules/.pnpm/idiomorph@0.3.0/node_modules/idiomorph/dist/idiomorph.esm.js +var Idiomorph = function() { + "use strict"; + let EMPTY_SET = /* @__PURE__ */ new Set(); + let defaults = { + morphStyle: "outerHTML", + callbacks: { + beforeNodeAdded: noOp, + afterNodeAdded: noOp, + beforeNodeMorphed: noOp, + afterNodeMorphed: noOp, + beforeNodeRemoved: noOp, + afterNodeRemoved: noOp, + beforeAttributeUpdated: noOp + }, + head: { + style: "merge", + shouldPreserve: function(elt) { + return elt.getAttribute("im-preserve") === "true"; + }, + shouldReAppend: function(elt) { + return elt.getAttribute("im-re-append") === "true"; + }, + shouldRemove: noOp, + afterHeadMorphed: noOp + } + }; + function morph(oldNode, newContent, config = {}) { + if (oldNode instanceof Document) oldNode = oldNode.documentElement; + if (typeof newContent === "string") newContent = parseContent(newContent); + let normalizedContent = normalizeContent(newContent); + let ctx = createMorphContext(oldNode, normalizedContent, config); + return morphNormalizedContent(oldNode, normalizedContent, ctx); + } + function morphNormalizedContent(oldNode, normalizedNewContent, ctx) { + if (ctx.head.block) { + let oldHead = oldNode.querySelector("head"); + let newHead = normalizedNewContent.querySelector("head"); + if (oldHead && newHead) { + let promises = handleHeadElement(newHead, oldHead, ctx); + Promise.all(promises).then(function() { + morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, { head: { + block: false, + ignore: true + } })); + }); + return; + } + } + if (ctx.morphStyle === "innerHTML") { + morphChildren(normalizedNewContent, oldNode, ctx); + return oldNode.children; + } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) { + let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx); + let previousSibling = bestMatch?.previousSibling; + let nextSibling = bestMatch?.nextSibling; + let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx); + if (bestMatch) return insertSiblings(previousSibling, morphedNode, nextSibling); + else return []; + } else throw "Do not understand how to morph style " + ctx.morphStyle; + } + /** + * @param possibleActiveElement + * @param ctx + * @returns {boolean} + */ + function ignoreValueOfActiveElement(possibleActiveElement, ctx) { + return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement; + } + /** + * @param oldNode root node to merge content into + * @param newContent new content to merge + * @param ctx the merge context + * @returns {Element} the element that ended up in the DOM + */ + function morphOldNodeTo(oldNode, newContent, ctx) { + if (ctx.ignoreActive && oldNode === document.activeElement) {} else if (newContent == null) { + if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; + oldNode.remove(); + ctx.callbacks.afterNodeRemoved(oldNode); + return null; + } else if (!isSoftMatch(oldNode, newContent)) { + if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode; + if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode; + oldNode.parentElement.replaceChild(newContent, oldNode); + ctx.callbacks.afterNodeAdded(newContent); + ctx.callbacks.afterNodeRemoved(oldNode); + return newContent; + } else { + if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode; + if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) {} else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") handleHeadElement(newContent, oldNode, ctx); + else { + syncNodeFrom(newContent, oldNode, ctx); + if (!ignoreValueOfActiveElement(oldNode, ctx)) morphChildren(newContent, oldNode, ctx); + } + ctx.callbacks.afterNodeMorphed(oldNode, newContent); + return oldNode; + } + } + /** + * This is the core algorithm for matching up children. The idea is to use id sets to try to match up + * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but + * by using id sets, we are able to better match up with content deeper in the DOM. + * + * Basic algorithm is, for each node in the new content: + * + * - if we have reached the end of the old parent, append the new content + * - if the new content has an id set match with the current insertion point, morph + * - search for an id set match + * - if id set match found, morph + * - otherwise search for a "soft" match + * - if a soft match is found, morph + * - otherwise, prepend the new node before the current insertion point + * + * The two search algorithms terminate if competing node matches appear to outweigh what can be achieved + * with the current node. See findIdSetMatch() and findSoftMatch() for details. + * + * @param {Element} newParent the parent element of the new content + * @param {Element } oldParent the old content that we are merging the new content into + * @param ctx the merge context + */ + function morphChildren(newParent, oldParent, ctx) { + let nextNewChild = newParent.firstChild; + let insertionPoint = oldParent.firstChild; + let newChild; + while (nextNewChild) { + newChild = nextNewChild; + nextNewChild = newChild.nextSibling; + if (insertionPoint == null) { + if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; + oldParent.appendChild(newChild); + ctx.callbacks.afterNodeAdded(newChild); + removeIdsFromConsideration(ctx, newChild); + continue; + } + if (isIdSetMatch(newChild, insertionPoint, ctx)) { + morphOldNodeTo(insertionPoint, newChild, ctx); + insertionPoint = insertionPoint.nextSibling; + removeIdsFromConsideration(ctx, newChild); + continue; + } + let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx); + if (idSetMatch) { + insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx); + morphOldNodeTo(idSetMatch, newChild, ctx); + removeIdsFromConsideration(ctx, newChild); + continue; + } + let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx); + if (softMatch) { + insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx); + morphOldNodeTo(softMatch, newChild, ctx); + removeIdsFromConsideration(ctx, newChild); + continue; + } + if (ctx.callbacks.beforeNodeAdded(newChild) === false) return; + oldParent.insertBefore(newChild, insertionPoint); + ctx.callbacks.afterNodeAdded(newChild); + removeIdsFromConsideration(ctx, newChild); + } + while (insertionPoint !== null) { + let tempNode = insertionPoint; + insertionPoint = insertionPoint.nextSibling; + removeNode(tempNode, ctx); + } + } + /** + * @param attr {String} the attribute to be mutated + * @param to {Element} the element that is going to be updated + * @param updateType {("update"|"remove")} + * @param ctx the merge context + * @returns {boolean} true if the attribute should be ignored, false otherwise + */ + function ignoreAttribute(attr, to, updateType, ctx) { + if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) return true; + return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false; + } + /** + * syncs a given node with another node, copying over all attributes and + * inner element state from the 'from' node to the 'to' node + * + * @param {Element} from the element to copy attributes & state from + * @param {Element} to the element to copy attributes & state to + * @param ctx the merge context + */ + function syncNodeFrom(from, to, ctx) { + let type = from.nodeType; + if (type === 1) { + const fromAttributes = from.attributes; + const toAttributes = to.attributes; + for (const fromAttribute of fromAttributes) { + if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) continue; + if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) to.setAttribute(fromAttribute.name, fromAttribute.value); + } + for (let i = toAttributes.length - 1; 0 <= i; i--) { + const toAttribute = toAttributes[i]; + if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) continue; + if (!from.hasAttribute(toAttribute.name)) to.removeAttribute(toAttribute.name); + } + } + if (type === 8 || type === 3) { + if (to.nodeValue !== from.nodeValue) to.nodeValue = from.nodeValue; + } + if (!ignoreValueOfActiveElement(to, ctx)) syncInputValue(from, to, ctx); + } + /** + * @param from {Element} element to sync the value from + * @param to {Element} element to sync the value to + * @param attributeName {String} the attribute name + * @param ctx the merge context + */ + function syncBooleanAttribute(from, to, attributeName, ctx) { + if (from[attributeName] !== to[attributeName]) { + let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx); + if (!ignoreUpdate) to[attributeName] = from[attributeName]; + if (from[attributeName]) { + if (!ignoreUpdate) to.setAttribute(attributeName, from[attributeName]); + } else if (!ignoreAttribute(attributeName, to, "remove", ctx)) to.removeAttribute(attributeName); + } + } + /** + * NB: many bothans died to bring us information: + * + * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js + * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113 + * + * @param from {Element} the element to sync the input value from + * @param to {Element} the element to sync the input value to + * @param ctx the merge context + */ + function syncInputValue(from, to, ctx) { + if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") { + let fromValue = from.value; + let toValue = to.value; + syncBooleanAttribute(from, to, "checked", ctx); + syncBooleanAttribute(from, to, "disabled", ctx); + if (!from.hasAttribute("value")) { + if (!ignoreAttribute("value", to, "remove", ctx)) { + to.value = ""; + to.removeAttribute("value"); + } + } else if (fromValue !== toValue) { + if (!ignoreAttribute("value", to, "update", ctx)) { + to.setAttribute("value", fromValue); + to.value = fromValue; + } + } + } else if (from instanceof HTMLOptionElement) syncBooleanAttribute(from, to, "selected", ctx); + else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) { + let fromValue = from.value; + let toValue = to.value; + if (ignoreAttribute("value", to, "update", ctx)) return; + if (fromValue !== toValue) to.value = fromValue; + if (to.firstChild && to.firstChild.nodeValue !== fromValue) to.firstChild.nodeValue = fromValue; + } + } + function handleHeadElement(newHeadTag, currentHead, ctx) { + let added = []; + let removed = []; + let preserved = []; + let nodesToAppend = []; + let headMergeStyle = ctx.head.style; + let srcToNewHeadNodes = /* @__PURE__ */ new Map(); + for (const newHeadChild of newHeadTag.children) srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild); + for (const currentHeadElt of currentHead.children) { + let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML); + let isReAppended = ctx.head.shouldReAppend(currentHeadElt); + let isPreserved = ctx.head.shouldPreserve(currentHeadElt); + if (inNewContent || isPreserved) if (isReAppended) removed.push(currentHeadElt); + else { + srcToNewHeadNodes.delete(currentHeadElt.outerHTML); + preserved.push(currentHeadElt); + } + else if (headMergeStyle === "append") { + if (isReAppended) { + removed.push(currentHeadElt); + nodesToAppend.push(currentHeadElt); + } + } else if (ctx.head.shouldRemove(currentHeadElt) !== false) removed.push(currentHeadElt); + } + nodesToAppend.push(...srcToNewHeadNodes.values()); + log("to append: ", nodesToAppend); + let promises = []; + for (const newNode of nodesToAppend) { + log("adding: ", newNode); + let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild; + log(newElt); + if (ctx.callbacks.beforeNodeAdded(newElt) !== false) { + if (newElt.href || newElt.src) { + let resolve = null; + let promise = new Promise(function(_resolve) { + resolve = _resolve; + }); + newElt.addEventListener("load", function() { + resolve(); + }); + promises.push(promise); + } + currentHead.appendChild(newElt); + ctx.callbacks.afterNodeAdded(newElt); + added.push(newElt); + } + } + for (const removedElement of removed) if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) { + currentHead.removeChild(removedElement); + ctx.callbacks.afterNodeRemoved(removedElement); + } + ctx.head.afterHeadMorphed(currentHead, { + added, + kept: preserved, + removed + }); + return promises; + } + function log() {} + function noOp() {} + function mergeDefaults(config) { + let finalConfig = {}; + Object.assign(finalConfig, defaults); + Object.assign(finalConfig, config); + finalConfig.callbacks = {}; + Object.assign(finalConfig.callbacks, defaults.callbacks); + Object.assign(finalConfig.callbacks, config.callbacks); + finalConfig.head = {}; + Object.assign(finalConfig.head, defaults.head); + Object.assign(finalConfig.head, config.head); + return finalConfig; + } + function createMorphContext(oldNode, newContent, config) { + config = mergeDefaults(config); + return { + target: oldNode, + newContent, + config, + morphStyle: config.morphStyle, + ignoreActive: config.ignoreActive, + ignoreActiveValue: config.ignoreActiveValue, + idMap: createIdMap(oldNode, newContent), + deadIds: /* @__PURE__ */ new Set(), + callbacks: config.callbacks, + head: config.head + }; + } + function isIdSetMatch(node1, node2, ctx) { + if (node1 == null || node2 == null) return false; + if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) if (node1.id !== "" && node1.id === node2.id) return true; + else return getIdIntersectionCount(ctx, node1, node2) > 0; + return false; + } + function isSoftMatch(node1, node2) { + if (node1 == null || node2 == null) return false; + return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName; + } + function removeNodesBetween(startInclusive, endExclusive, ctx) { + while (startInclusive !== endExclusive) { + let tempNode = startInclusive; + startInclusive = startInclusive.nextSibling; + removeNode(tempNode, ctx); + } + removeIdsFromConsideration(ctx, endExclusive); + return endExclusive.nextSibling; + } + function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) { + let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent); + let potentialMatch = null; + if (newChildPotentialIdCount > 0) { + let potentialMatch$1 = insertionPoint; + let otherMatchCount = 0; + while (potentialMatch$1 != null) { + if (isIdSetMatch(newChild, potentialMatch$1, ctx)) return potentialMatch$1; + otherMatchCount += getIdIntersectionCount(ctx, potentialMatch$1, newContent); + if (otherMatchCount > newChildPotentialIdCount) return null; + potentialMatch$1 = potentialMatch$1.nextSibling; + } + } + return potentialMatch; + } + function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) { + let potentialSoftMatch = insertionPoint; + let nextSibling = newChild.nextSibling; + let siblingSoftMatchCount = 0; + while (potentialSoftMatch != null) { + if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) return null; + if (isSoftMatch(newChild, potentialSoftMatch)) return potentialSoftMatch; + if (isSoftMatch(nextSibling, potentialSoftMatch)) { + siblingSoftMatchCount++; + nextSibling = nextSibling.nextSibling; + if (siblingSoftMatchCount >= 2) return null; + } + potentialSoftMatch = potentialSoftMatch.nextSibling; + } + return potentialSoftMatch; + } + function parseContent(newContent) { + let parser = new DOMParser(); + let contentWithSvgsRemoved = newContent.replace(/]*>|>)([\s\S]*?)<\/svg>/gim, ""); + if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) { + let content = parser.parseFromString(newContent, "text/html"); + if (contentWithSvgsRemoved.match(/<\/html>/)) { + content.generatedByIdiomorph = true; + return content; + } else { + let htmlElement = content.firstChild; + if (htmlElement) { + htmlElement.generatedByIdiomorph = true; + return htmlElement; + } else return null; + } + } else { + let responseDoc = parser.parseFromString("", "text/html"); + let content = responseDoc.body.querySelector("template").content; + content.generatedByIdiomorph = true; + return content; + } + } + function normalizeContent(newContent) { + if (newContent == null) { + const dummyParent = document.createElement("div"); + return dummyParent; + } else if (newContent.generatedByIdiomorph) return newContent; + else if (newContent instanceof Node) { + const dummyParent = document.createElement("div"); + dummyParent.append(newContent); + return dummyParent; + } else { + const dummyParent = document.createElement("div"); + for (const elt of [...newContent]) dummyParent.append(elt); + return dummyParent; + } + } + function insertSiblings(previousSibling, morphedNode, nextSibling) { + let stack = []; + let added = []; + while (previousSibling != null) { + stack.push(previousSibling); + previousSibling = previousSibling.previousSibling; + } + while (stack.length > 0) { + let node = stack.pop(); + added.push(node); + morphedNode.parentElement.insertBefore(node, morphedNode); + } + added.push(morphedNode); + while (nextSibling != null) { + stack.push(nextSibling); + added.push(nextSibling); + nextSibling = nextSibling.nextSibling; + } + while (stack.length > 0) morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling); + return added; + } + function findBestNodeMatch(newContent, oldNode, ctx) { + let currentElement; + currentElement = newContent.firstChild; + let bestElement = currentElement; + let score = 0; + while (currentElement) { + let newScore = scoreElement(currentElement, oldNode, ctx); + if (newScore > score) { + bestElement = currentElement; + score = newScore; + } + currentElement = currentElement.nextSibling; + } + return bestElement; + } + function scoreElement(node1, node2, ctx) { + if (isSoftMatch(node1, node2)) return .5 + getIdIntersectionCount(ctx, node1, node2); + return 0; + } + function removeNode(tempNode, ctx) { + removeIdsFromConsideration(ctx, tempNode); + if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return; + tempNode.remove(); + ctx.callbacks.afterNodeRemoved(tempNode); + } + function isIdInConsideration(ctx, id) { + return !ctx.deadIds.has(id); + } + function idIsWithinNode(ctx, id, targetNode) { + let idSet = ctx.idMap.get(targetNode) || EMPTY_SET; + return idSet.has(id); + } + function removeIdsFromConsideration(ctx, node) { + let idSet = ctx.idMap.get(node) || EMPTY_SET; + for (const id of idSet) ctx.deadIds.add(id); + } + function getIdIntersectionCount(ctx, node1, node2) { + let sourceSet = ctx.idMap.get(node1) || EMPTY_SET; + let matchCount = 0; + for (const id of sourceSet) if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) ++matchCount; + return matchCount; + } + /** + * A bottom up algorithm that finds all elements with ids inside of the node + * argument and populates id sets for those nodes and all their parents, generating + * a set of ids contained within all nodes for the entire hierarchy in the DOM + * + * @param node {Element} + * @param {Map>} idMap + */ + function populateIdMapForNode(node, idMap) { + let nodeParent = node.parentElement; + let idElements = node.querySelectorAll("[id]"); + for (const elt of idElements) { + let current = elt; + while (current !== nodeParent && current != null) { + let idSet = idMap.get(current); + if (idSet == null) { + idSet = /* @__PURE__ */ new Set(); + idMap.set(current, idSet); + } + idSet.add(elt.id); + current = current.parentElement; + } + } + } + /** + * This function computes a map of nodes to all ids contained within that node (inclusive of the + * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows + * for a looser definition of "matching" than tradition id matching, and allows child nodes + * to contribute to a parent nodes matching. + * + * @param {Element} oldContent the old content that will be morphed + * @param {Element} newContent the new content to morph to + * @returns {Map>} a map of nodes to id sets for the + */ + function createIdMap(oldContent, newContent) { + let idMap = /* @__PURE__ */ new Map(); + populateIdMapForNode(oldContent, idMap); + populateIdMapForNode(newContent, idMap); + return idMap; + } + return { + morph, + defaults + }; +}(); + +//#endregion +//#region src/normalize_attributes_for_comparison.ts +/** +* Updates an HTML node to represent its underlying data. +* +* For example, this finds the value property of each underlying node +* and sets that onto the value attribute. This is useful to compare +* if two nodes are identical. +*/ function normalizeAttributesForComparison(element) { - const isFileInput = element instanceof HTMLInputElement && element.type === 'file'; - if (!isFileInput) { - if ('value' in element) { - element.setAttribute('value', element.value); - } - else if (element.hasAttribute('value')) { - element.setAttribute('value', ''); - } - } - Array.from(element.children).forEach((child) => { - normalizeAttributesForComparison(child); - }); + const isFileInput = element instanceof HTMLInputElement && element.type === "file"; + if (!isFileInput) { + if ("value" in element) element.setAttribute("value", element.value); + else if (element.hasAttribute("value")) element.setAttribute("value", ""); + } + Array.from(element.children).forEach((child) => { + normalizeAttributesForComparison(child); + }); } +//#endregion +//#region src/morphdom.ts const syncAttributes = (fromEl, toEl) => { - for (let i = 0; i < fromEl.attributes.length; i++) { - const attr = fromEl.attributes[i]; - toEl.setAttribute(attr.name, attr.value); - } + for (let i = 0; i < fromEl.attributes.length; i++) { + const attr = fromEl.attributes[i]; + toEl.setAttribute(attr.name, attr.value); + } }; function executeMorphdom(rootFromElement, rootToElement, modifiedFieldElements, getElementValue, externalMutationTracker) { - const originalElementIdsToSwapAfter = []; - const originalElementsToPreserve = new Map(); - const markElementAsNeedingPostMorphSwap = (id, replaceWithClone) => { - const oldElement = originalElementsToPreserve.get(id); - if (!(oldElement instanceof HTMLElement)) { - throw new Error(`Original element with id ${id} not found`); - } - originalElementIdsToSwapAfter.push(id); - if (!replaceWithClone) { - return null; - } - const clonedOldElement = cloneHTMLElement(oldElement); - oldElement.replaceWith(clonedOldElement); - return clonedOldElement; - }; - rootToElement.querySelectorAll('[data-live-preserve]').forEach((newElement) => { - const id = newElement.id; - if (!id) { - throw new Error('The data-live-preserve attribute requires an id attribute to be set on the element'); - } - const oldElement = rootFromElement.querySelector(`#${id}`); - if (!(oldElement instanceof HTMLElement)) { - throw new Error(`The element with id "${id}" was not found in the original HTML`); - } - newElement.removeAttribute('data-live-preserve'); - originalElementsToPreserve.set(id, oldElement); - syncAttributes(newElement, oldElement); - }); - Idiomorph.morph(rootFromElement, rootToElement, { - callbacks: { - beforeNodeMorphed: (fromEl, toEl) => { - if (!(fromEl instanceof Element) || !(toEl instanceof Element)) { - return true; - } - if (fromEl === rootFromElement) { - return true; - } - if (fromEl.id && originalElementsToPreserve.has(fromEl.id)) { - if (fromEl.id === toEl.id) { - return false; - } - const clonedFromEl = markElementAsNeedingPostMorphSwap(fromEl.id, true); - if (!clonedFromEl) { - throw new Error('missing clone'); - } - Idiomorph.morph(clonedFromEl, toEl); - return false; - } - if (fromEl instanceof HTMLElement && toEl instanceof HTMLElement) { - if (typeof fromEl.__x !== 'undefined') { - if (!window.Alpine) { - throw new Error('Unable to access Alpine.js though the global window.Alpine variable. Please make sure Alpine.js is loaded before Symfony UX LiveComponent.'); - } - if (typeof window.Alpine.morph !== 'function') { - throw new Error('Unable to access Alpine.js morph function. Please make sure the Alpine.js Morph plugin is installed and loaded, see https://alpinejs.dev/plugins/morph for more information.'); - } - window.Alpine.morph(fromEl.__x, toEl); - } - if (externalMutationTracker.wasElementAdded(fromEl)) { - fromEl.insertAdjacentElement('afterend', toEl); - return false; - } - if (modifiedFieldElements.includes(fromEl)) { - setValueOnElement(toEl, getElementValue(fromEl)); - } - if (fromEl === document.activeElement && - fromEl !== document.body && - null !== getModelDirectiveFromElement(fromEl, false)) { - setValueOnElement(toEl, getElementValue(fromEl)); - } - const elementChanges = externalMutationTracker.getChangedElement(fromEl); - if (elementChanges) { - elementChanges.applyToElement(toEl); - } - if (fromEl.nodeName.toUpperCase() !== 'OPTION' && fromEl.isEqualNode(toEl)) { - const normalizedFromEl = cloneHTMLElement(fromEl); - normalizeAttributesForComparison(normalizedFromEl); - const normalizedToEl = cloneHTMLElement(toEl); - normalizeAttributesForComparison(normalizedToEl); - if (normalizedFromEl.isEqualNode(normalizedToEl)) { - return false; - } - } - } - if (fromEl.hasAttribute('data-skip-morph') || (fromEl.id && fromEl.id !== toEl.id)) { - fromEl.innerHTML = toEl.innerHTML; - return true; - } - if (fromEl.parentElement?.hasAttribute('data-skip-morph')) { - return false; - } - return !fromEl.hasAttribute('data-live-ignore'); - }, - beforeNodeRemoved(node) { - if (!(node instanceof HTMLElement)) { - return true; - } - if (node.id && originalElementsToPreserve.has(node.id)) { - markElementAsNeedingPostMorphSwap(node.id, false); - return true; - } - if (externalMutationTracker.wasElementAdded(node)) { - return false; - } - return !node.hasAttribute('data-live-ignore'); - }, - }, - }); - originalElementIdsToSwapAfter.forEach((id) => { - const newElement = rootFromElement.querySelector(`#${id}`); - const originalElement = originalElementsToPreserve.get(id); - if (!(newElement instanceof HTMLElement) || !(originalElement instanceof HTMLElement)) { - throw new Error('Missing elements.'); - } - newElement.replaceWith(originalElement); - }); + const originalElementIdsToSwapAfter = []; + const originalElementsToPreserve = /* @__PURE__ */ new Map(); + /** + * Called when a preserved element is about to be morphed. + * + * Instead of allowing the original to be morphed, a fake clone + * is created and morphed instead. The original is then marked + * to be replaced after the morph with wherever the final + * matching id element ends up. + */ + const markElementAsNeedingPostMorphSwap = (id, replaceWithClone) => { + const oldElement = originalElementsToPreserve.get(id); + if (!(oldElement instanceof HTMLElement)) throw new Error(`Original element with id ${id} not found`); + originalElementIdsToSwapAfter.push(id); + if (!replaceWithClone) return null; + const clonedOldElement = cloneHTMLElement(oldElement); + oldElement.replaceWith(clonedOldElement); + return clonedOldElement; + }; + rootToElement.querySelectorAll("[data-live-preserve]").forEach((newElement) => { + const id = newElement.id; + if (!id) throw new Error("The data-live-preserve attribute requires an id attribute to be set on the element"); + const oldElement = rootFromElement.querySelector(`#${id}`); + if (!(oldElement instanceof HTMLElement)) throw new Error(`The element with id "${id}" was not found in the original HTML`); + newElement.removeAttribute("data-live-preserve"); + originalElementsToPreserve.set(id, oldElement); + syncAttributes(newElement, oldElement); + }); + Idiomorph.morph(rootFromElement, rootToElement, { callbacks: { + beforeNodeMorphed: (fromEl, toEl) => { + if (!(fromEl instanceof Element) || !(toEl instanceof Element)) return true; + if (fromEl === rootFromElement) return true; + if (fromEl.id && originalElementsToPreserve.has(fromEl.id)) { + if (fromEl.id === toEl.id) return false; + const clonedFromEl = markElementAsNeedingPostMorphSwap(fromEl.id, true); + if (!clonedFromEl) throw new Error("missing clone"); + Idiomorph.morph(clonedFromEl, toEl); + return false; + } + if (fromEl instanceof HTMLElement && toEl instanceof HTMLElement) { + if (typeof fromEl.__x !== "undefined") { + if (!window.Alpine) throw new Error("Unable to access Alpine.js though the global window.Alpine variable. Please make sure Alpine.js is loaded before Symfony UX LiveComponent."); + if (typeof window.Alpine.morph !== "function") throw new Error("Unable to access Alpine.js morph function. Please make sure the Alpine.js Morph plugin is installed and loaded, see https://alpinejs.dev/plugins/morph for more information."); + window.Alpine.morph(fromEl.__x, toEl); + } + if (externalMutationTracker.wasElementAdded(fromEl)) { + fromEl.insertAdjacentElement("afterend", toEl); + return false; + } + if (modifiedFieldElements.includes(fromEl)) setValueOnElement(toEl, getElementValue(fromEl)); + if (fromEl === document.activeElement && fromEl !== document.body && null !== getModelDirectiveFromElement(fromEl, false)) setValueOnElement(toEl, getElementValue(fromEl)); + const elementChanges = externalMutationTracker.getChangedElement(fromEl); + if (elementChanges) elementChanges.applyToElement(toEl); + if (fromEl.nodeName.toUpperCase() !== "OPTION" && fromEl.isEqualNode(toEl)) { + const normalizedFromEl = cloneHTMLElement(fromEl); + normalizeAttributesForComparison(normalizedFromEl); + const normalizedToEl = cloneHTMLElement(toEl); + normalizeAttributesForComparison(normalizedToEl); + if (normalizedFromEl.isEqualNode(normalizedToEl)) return false; + } + } + if (fromEl.hasAttribute("data-skip-morph") || fromEl.id && fromEl.id !== toEl.id) { + fromEl.innerHTML = toEl.innerHTML; + return true; + } + if (fromEl.parentElement?.hasAttribute("data-skip-morph")) return false; + return !fromEl.hasAttribute("data-live-ignore"); + }, + beforeNodeRemoved(node) { + if (!(node instanceof HTMLElement)) return true; + if (node.id && originalElementsToPreserve.has(node.id)) { + markElementAsNeedingPostMorphSwap(node.id, false); + return true; + } + if (externalMutationTracker.wasElementAdded(node)) return false; + return !node.hasAttribute("data-live-ignore"); + } + } }); + originalElementIdsToSwapAfter.forEach((id) => { + const newElement = rootFromElement.querySelector(`#${id}`); + const originalElement = originalElementsToPreserve.get(id); + if (!(newElement instanceof HTMLElement) || !(originalElement instanceof HTMLElement)) throw new Error("Missing elements."); + newElement.replaceWith(originalElement); + }); } -class ChangingItemsTracker { - constructor() { - this.changedItems = new Map(); - this.removedItems = new Map(); - } - setItem(itemName, newValue, previousValue) { - if (this.removedItems.has(itemName)) { - const removedRecord = this.removedItems.get(itemName); - this.removedItems.delete(itemName); - if (removedRecord.original === newValue) { - return; - } - } - if (this.changedItems.has(itemName)) { - const originalRecord = this.changedItems.get(itemName); - if (originalRecord.original === newValue) { - this.changedItems.delete(itemName); - return; - } - this.changedItems.set(itemName, { original: originalRecord.original, new: newValue }); - return; - } - this.changedItems.set(itemName, { original: previousValue, new: newValue }); - } - removeItem(itemName, currentValue) { - let trueOriginalValue = currentValue; - if (this.changedItems.has(itemName)) { - const originalRecord = this.changedItems.get(itemName); - trueOriginalValue = originalRecord.original; - this.changedItems.delete(itemName); - if (trueOriginalValue === null) { - return; - } - } - if (!this.removedItems.has(itemName)) { - this.removedItems.set(itemName, { original: trueOriginalValue }); - } - } - getChangedItems() { - return Array.from(this.changedItems, ([name, { new: value }]) => ({ name, value })); - } - getRemovedItems() { - return Array.from(this.removedItems.keys()); - } - isEmpty() { - return this.changedItems.size === 0 && this.removedItems.size === 0; - } -} +//#endregion +//#region src/Rendering/ChangingItemsTracker.ts +/** +* Helps track added/changed styles or attributes. +*/ +var ChangingItemsTracker_default = class { + changedItems = /* @__PURE__ */ new Map(); + removedItems = /* @__PURE__ */ new Map(); + /** + * A "null" previousValue means the item was NOT previously present. + */ + setItem(itemName, newValue, previousValue) { + if (this.removedItems.has(itemName)) { + const removedRecord = this.removedItems.get(itemName); + this.removedItems.delete(itemName); + if (removedRecord.original === newValue) return; + } + if (this.changedItems.has(itemName)) { + const originalRecord = this.changedItems.get(itemName); + if (originalRecord.original === newValue) { + this.changedItems.delete(itemName); + return; + } + this.changedItems.set(itemName, { + original: originalRecord.original, + new: newValue + }); + return; + } + this.changedItems.set(itemName, { + original: previousValue, + new: newValue + }); + } + removeItem(itemName, currentValue) { + let trueOriginalValue = currentValue; + if (this.changedItems.has(itemName)) { + const originalRecord = this.changedItems.get(itemName); + trueOriginalValue = originalRecord.original; + this.changedItems.delete(itemName); + if (trueOriginalValue === null) return; + } + if (!this.removedItems.has(itemName)) this.removedItems.set(itemName, { original: trueOriginalValue }); + } + getChangedItems() { + return Array.from(this.changedItems, ([name, { new: value }]) => ({ + name, + value + })); + } + getRemovedItems() { + return Array.from(this.removedItems.keys()); + } + isEmpty() { + return this.changedItems.size === 0 && this.removedItems.size === 0; + } +}; -class ElementChanges { - constructor() { - this.addedClasses = new Set(); - this.removedClasses = new Set(); - this.styleChanges = new ChangingItemsTracker(); - this.attributeChanges = new ChangingItemsTracker(); - } - addClass(className) { - if (!this.removedClasses.delete(className)) { - this.addedClasses.add(className); - } - } - removeClass(className) { - if (!this.addedClasses.delete(className)) { - this.removedClasses.add(className); - } - } - addStyle(styleName, newValue, originalValue) { - this.styleChanges.setItem(styleName, newValue, originalValue); - } - removeStyle(styleName, originalValue) { - this.styleChanges.removeItem(styleName, originalValue); - } - addAttribute(attributeName, newValue, originalValue) { - this.attributeChanges.setItem(attributeName, newValue, originalValue); - } - removeAttribute(attributeName, originalValue) { - this.attributeChanges.removeItem(attributeName, originalValue); - } - getAddedClasses() { - return [...this.addedClasses]; - } - getRemovedClasses() { - return [...this.removedClasses]; - } - getChangedStyles() { - return this.styleChanges.getChangedItems(); - } - getRemovedStyles() { - return this.styleChanges.getRemovedItems(); - } - getChangedAttributes() { - return this.attributeChanges.getChangedItems(); - } - getRemovedAttributes() { - return this.attributeChanges.getRemovedItems(); - } - applyToElement(element) { - element.classList.add(...this.addedClasses); - element.classList.remove(...this.removedClasses); - this.styleChanges.getChangedItems().forEach((change) => { - element.style.setProperty(change.name, change.value); - return; - }); - this.styleChanges.getRemovedItems().forEach((styleName) => { - element.style.removeProperty(styleName); - }); - this.attributeChanges.getChangedItems().forEach((change) => { - element.setAttribute(change.name, change.value); - }); - this.attributeChanges.getRemovedItems().forEach((attributeName) => { - element.removeAttribute(attributeName); - }); - } - isEmpty() { - return (this.addedClasses.size === 0 && - this.removedClasses.size === 0 && - this.styleChanges.isEmpty() && - this.attributeChanges.isEmpty()); - } -} +//#endregion +//#region src/Rendering/ElementChanges.ts +/** +* Tracks attribute changes for a specific element. +*/ +var ElementChanges = class { + addedClasses = /* @__PURE__ */ new Set(); + removedClasses = /* @__PURE__ */ new Set(); + styleChanges = new ChangingItemsTracker_default(); + attributeChanges = new ChangingItemsTracker_default(); + addClass(className) { + if (!this.removedClasses.delete(className)) this.addedClasses.add(className); + } + removeClass(className) { + if (!this.addedClasses.delete(className)) this.removedClasses.add(className); + } + addStyle(styleName, newValue, originalValue) { + this.styleChanges.setItem(styleName, newValue, originalValue); + } + removeStyle(styleName, originalValue) { + this.styleChanges.removeItem(styleName, originalValue); + } + addAttribute(attributeName, newValue, originalValue) { + this.attributeChanges.setItem(attributeName, newValue, originalValue); + } + removeAttribute(attributeName, originalValue) { + this.attributeChanges.removeItem(attributeName, originalValue); + } + getAddedClasses() { + return [...this.addedClasses]; + } + getRemovedClasses() { + return [...this.removedClasses]; + } + getChangedStyles() { + return this.styleChanges.getChangedItems(); + } + getRemovedStyles() { + return this.styleChanges.getRemovedItems(); + } + getChangedAttributes() { + return this.attributeChanges.getChangedItems(); + } + getRemovedAttributes() { + return this.attributeChanges.getRemovedItems(); + } + applyToElement(element) { + element.classList.add(...this.addedClasses); + element.classList.remove(...this.removedClasses); + this.styleChanges.getChangedItems().forEach((change) => { + element.style.setProperty(change.name, change.value); + return; + }); + this.styleChanges.getRemovedItems().forEach((styleName) => { + element.style.removeProperty(styleName); + }); + this.attributeChanges.getChangedItems().forEach((change) => { + element.setAttribute(change.name, change.value); + }); + this.attributeChanges.getRemovedItems().forEach((attributeName) => { + element.removeAttribute(attributeName); + }); + } + isEmpty() { + return this.addedClasses.size === 0 && this.removedClasses.size === 0 && this.styleChanges.isEmpty() && this.attributeChanges.isEmpty(); + } +}; -class ExternalMutationTracker { - constructor(element, shouldTrackChangeCallback) { - this.changedElements = new WeakMap(); - this.changedElementsCount = 0; - this.addedElements = []; - this.removedElements = []; - this.isStarted = false; - this.element = element; - this.shouldTrackChangeCallback = shouldTrackChangeCallback; - this.mutationObserver = new MutationObserver(this.onMutations.bind(this)); - } - start() { - if (this.isStarted) { - return; - } - this.mutationObserver.observe(this.element, { - childList: true, - subtree: true, - attributes: true, - attributeOldValue: true, - }); - this.isStarted = true; - } - stop() { - if (this.isStarted) { - this.mutationObserver.disconnect(); - this.isStarted = false; - } - } - getChangedElement(element) { - return this.changedElements.has(element) ? this.changedElements.get(element) : null; - } - getAddedElements() { - return this.addedElements; - } - wasElementAdded(element) { - return this.addedElements.includes(element); - } - handlePendingChanges() { - this.onMutations(this.mutationObserver.takeRecords()); - } - onMutations(mutations) { - const handledAttributeMutations = new WeakMap(); - for (const mutation of mutations) { - const element = mutation.target; - if (!this.shouldTrackChangeCallback(element)) { - continue; - } - if (this.isElementAddedByTranslation(element)) { - continue; - } - let isChangeInAddedElement = false; - for (const addedElement of this.addedElements) { - if (addedElement.contains(element)) { - isChangeInAddedElement = true; - break; - } - } - if (isChangeInAddedElement) { - continue; - } - switch (mutation.type) { - case 'childList': - this.handleChildListMutation(mutation); - break; - case 'attributes': - if (!handledAttributeMutations.has(element)) { - handledAttributeMutations.set(element, []); - } - if (!handledAttributeMutations.get(element).includes(mutation.attributeName)) { - this.handleAttributeMutation(mutation); - handledAttributeMutations.set(element, [ - ...handledAttributeMutations.get(element), - mutation.attributeName, - ]); - } - break; - } - } - } - handleChildListMutation(mutation) { - mutation.addedNodes.forEach((node) => { - if (!(node instanceof Element)) { - return; - } - if (this.removedElements.includes(node)) { - this.removedElements.splice(this.removedElements.indexOf(node), 1); - return; - } - if (this.isElementAddedByTranslation(node)) { - return; - } - this.addedElements.push(node); - }); - mutation.removedNodes.forEach((node) => { - if (!(node instanceof Element)) { - return; - } - if (this.addedElements.includes(node)) { - this.addedElements.splice(this.addedElements.indexOf(node), 1); - return; - } - this.removedElements.push(node); - }); - } - handleAttributeMutation(mutation) { - const element = mutation.target; - if (!this.changedElements.has(element)) { - this.changedElements.set(element, new ElementChanges()); - this.changedElementsCount++; - } - const changedElement = this.changedElements.get(element); - switch (mutation.attributeName) { - case 'class': - this.handleClassAttributeMutation(mutation, changedElement); - break; - case 'style': - this.handleStyleAttributeMutation(mutation, changedElement); - break; - default: - this.handleGenericAttributeMutation(mutation, changedElement); - } - if (changedElement.isEmpty()) { - this.changedElements.delete(element); - this.changedElementsCount--; - } - } - handleClassAttributeMutation(mutation, elementChanges) { - const element = mutation.target; - const previousValue = mutation.oldValue || ''; - const previousValues = previousValue.match(/(\S+)/gu) || []; - const newValues = [].slice.call(element.classList); - const addedValues = newValues.filter((value) => !previousValues.includes(value)); - const removedValues = previousValues.filter((value) => !newValues.includes(value)); - addedValues.forEach((value) => { - elementChanges.addClass(value); - }); - removedValues.forEach((value) => { - elementChanges.removeClass(value); - }); - } - handleStyleAttributeMutation(mutation, elementChanges) { - const element = mutation.target; - const previousValue = mutation.oldValue || ''; - const previousStyles = this.extractStyles(previousValue); - const newValue = element.getAttribute('style') || ''; - const newStyles = this.extractStyles(newValue); - const addedOrChangedStyles = Object.keys(newStyles).filter((key) => previousStyles[key] === undefined || previousStyles[key] !== newStyles[key]); - const removedStyles = Object.keys(previousStyles).filter((key) => !newStyles[key]); - addedOrChangedStyles.forEach((style) => { - elementChanges.addStyle(style, newStyles[style], previousStyles[style] === undefined ? null : previousStyles[style]); - }); - removedStyles.forEach((style) => { - elementChanges.removeStyle(style, previousStyles[style]); - }); - } - handleGenericAttributeMutation(mutation, elementChanges) { - const attributeName = mutation.attributeName; - const element = mutation.target; - let oldValue = mutation.oldValue; - let newValue = element.getAttribute(attributeName); - if (oldValue === attributeName) { - oldValue = ''; - } - if (newValue === attributeName) { - newValue = ''; - } - if (!element.hasAttribute(attributeName)) { - if (oldValue === null) { - return; - } - elementChanges.removeAttribute(attributeName, mutation.oldValue); - return; - } - if (newValue === oldValue) { - return; - } - elementChanges.addAttribute(attributeName, element.getAttribute(attributeName), mutation.oldValue); - } - extractStyles(styles) { - const styleObject = {}; - styles.split(';').forEach((style) => { - const parts = style.split(':'); - if (parts.length === 1) { - return; - } - const property = parts[0].trim(); - styleObject[property] = parts.slice(1).join(':').trim(); - }); - return styleObject; - } - isElementAddedByTranslation(element) { - return element.tagName === 'FONT' && element.getAttribute('style') === 'vertical-align: inherit;'; - } -} +//#endregion +//#region src/Rendering/ExternalMutationTracker.ts +/** +* Uses MutationObserver to track changes to the DOM inside a component. +* +* This is meant to track changes that are made by external code - i.e. not +* a change from a component re-render. +*/ +var ExternalMutationTracker_default = class { + element; + shouldTrackChangeCallback; + mutationObserver; + changedElements = /* @__PURE__ */ new WeakMap(); + /** For testing */ + changedElementsCount = 0; + addedElements = []; + removedElements = []; + isStarted = false; + constructor(element, shouldTrackChangeCallback) { + this.element = element; + this.shouldTrackChangeCallback = shouldTrackChangeCallback; + this.mutationObserver = new MutationObserver(this.onMutations.bind(this)); + } + start() { + if (this.isStarted) return; + this.mutationObserver.observe(this.element, { + childList: true, + subtree: true, + attributes: true, + attributeOldValue: true + }); + this.isStarted = true; + } + stop() { + if (this.isStarted) { + this.mutationObserver.disconnect(); + this.isStarted = false; + } + } + getChangedElement(element) { + return this.changedElements.has(element) ? this.changedElements.get(element) : null; + } + getAddedElements() { + return this.addedElements; + } + wasElementAdded(element) { + return this.addedElements.includes(element); + } + /** + * Forces any pending mutations to be handled immediately, then clears the queue. + */ + handlePendingChanges() { + this.onMutations(this.mutationObserver.takeRecords()); + } + onMutations(mutations) { + const handledAttributeMutations = /* @__PURE__ */ new WeakMap(); + for (const mutation of mutations) { + const element = mutation.target; + if (!this.shouldTrackChangeCallback(element)) continue; + if (this.isElementAddedByTranslation(element)) continue; + let isChangeInAddedElement = false; + for (const addedElement of this.addedElements) if (addedElement.contains(element)) { + isChangeInAddedElement = true; + break; + } + if (isChangeInAddedElement) continue; + switch (mutation.type) { + case "childList": + this.handleChildListMutation(mutation); + break; + case "attributes": + if (!handledAttributeMutations.has(element)) handledAttributeMutations.set(element, []); + if (!handledAttributeMutations.get(element).includes(mutation.attributeName)) { + this.handleAttributeMutation(mutation); + handledAttributeMutations.set(element, [...handledAttributeMutations.get(element), mutation.attributeName]); + } + break; + } + } + } + handleChildListMutation(mutation) { + mutation.addedNodes.forEach((node) => { + if (!(node instanceof Element)) return; + if (this.removedElements.includes(node)) { + this.removedElements.splice(this.removedElements.indexOf(node), 1); + return; + } + if (this.isElementAddedByTranslation(node)) return; + this.addedElements.push(node); + }); + mutation.removedNodes.forEach((node) => { + if (!(node instanceof Element)) return; + if (this.addedElements.includes(node)) { + this.addedElements.splice(this.addedElements.indexOf(node), 1); + return; + } + this.removedElements.push(node); + }); + } + handleAttributeMutation(mutation) { + const element = mutation.target; + if (!this.changedElements.has(element)) { + this.changedElements.set(element, new ElementChanges()); + this.changedElementsCount++; + } + const changedElement = this.changedElements.get(element); + switch (mutation.attributeName) { + case "class": + this.handleClassAttributeMutation(mutation, changedElement); + break; + case "style": + this.handleStyleAttributeMutation(mutation, changedElement); + break; + default: this.handleGenericAttributeMutation(mutation, changedElement); + } + if (changedElement.isEmpty()) { + this.changedElements.delete(element); + this.changedElementsCount--; + } + } + handleClassAttributeMutation(mutation, elementChanges) { + const element = mutation.target; + const previousValue = mutation.oldValue || ""; + const previousValues = previousValue.match(/(\S+)/gu) || []; + const newValues = [].slice.call(element.classList); + const addedValues = newValues.filter((value) => !previousValues.includes(value)); + const removedValues = previousValues.filter((value) => !newValues.includes(value)); + addedValues.forEach((value) => { + elementChanges.addClass(value); + }); + removedValues.forEach((value) => { + elementChanges.removeClass(value); + }); + } + handleStyleAttributeMutation(mutation, elementChanges) { + const element = mutation.target; + const previousValue = mutation.oldValue || ""; + const previousStyles = this.extractStyles(previousValue); + const newValue = element.getAttribute("style") || ""; + const newStyles = this.extractStyles(newValue); + const addedOrChangedStyles = Object.keys(newStyles).filter((key) => previousStyles[key] === void 0 || previousStyles[key] !== newStyles[key]); + const removedStyles = Object.keys(previousStyles).filter((key) => !newStyles[key]); + addedOrChangedStyles.forEach((style) => { + elementChanges.addStyle(style, newStyles[style], previousStyles[style] === void 0 ? null : previousStyles[style]); + }); + removedStyles.forEach((style) => { + elementChanges.removeStyle(style, previousStyles[style]); + }); + } + handleGenericAttributeMutation(mutation, elementChanges) { + const attributeName = mutation.attributeName; + const element = mutation.target; + let oldValue = mutation.oldValue; + let newValue = element.getAttribute(attributeName); + if (oldValue === attributeName) oldValue = ""; + if (newValue === attributeName) newValue = ""; + if (!element.hasAttribute(attributeName)) { + if (oldValue === null) return; + elementChanges.removeAttribute(attributeName, mutation.oldValue); + return; + } + if (newValue === oldValue) return; + elementChanges.addAttribute(attributeName, element.getAttribute(attributeName), mutation.oldValue); + } + extractStyles(styles) { + const styleObject = {}; + styles.split(";").forEach((style) => { + const parts = style.split(":"); + if (parts.length === 1) return; + const property = parts[0].trim(); + styleObject[property] = parts.slice(1).join(":").trim(); + }); + return styleObject; + } + /** + * Helps avoid tracking changes by Chrome's translation feature. + * + * When Chrome translates, it mutates the dom in a way that triggers MutationObserver. + * This includes adding new elements wrapped in a tag. This causes live + * components to incorrectly think that these new elements should persist through + * re-renders, causing duplicate text. + */ + isElementAddedByTranslation(element) { + return element.tagName === "FONT" && element.getAttribute("style") === "vertical-align: inherit;"; + } +}; -class UnsyncedInputsTracker { - constructor(component, modelElementResolver) { - this.elementEventListeners = [ - { event: 'input', callback: (event) => this.handleInputEvent(event) }, - ]; - this.component = component; - this.modelElementResolver = modelElementResolver; - this.unsyncedInputs = new UnsyncedInputContainer(); - } - activate() { - this.elementEventListeners.forEach(({ event, callback }) => { - this.component.element.addEventListener(event, callback); - }); - } - deactivate() { - this.elementEventListeners.forEach(({ event, callback }) => { - this.component.element.removeEventListener(event, callback); - }); - } - markModelAsSynced(modelName) { - this.unsyncedInputs.markModelAsSynced(modelName); - } - handleInputEvent(event) { - const target = event.target; - if (!target) { - return; - } - this.updateModelFromElement(target); - } - updateModelFromElement(element) { - if (!elementBelongsToThisComponent(element, this.component)) { - return; - } - if (!(element instanceof HTMLElement)) { - throw new Error('Could not update model for non HTMLElement'); - } - const modelName = this.modelElementResolver.getModelName(element); - this.unsyncedInputs.add(element, modelName); - } - getUnsyncedInputs() { - return this.unsyncedInputs.allUnsyncedInputs(); - } - getUnsyncedModels() { - return Array.from(this.unsyncedInputs.getUnsyncedModelNames()); - } - resetUnsyncedFields() { - this.unsyncedInputs.resetUnsyncedFields(); - } -} -class UnsyncedInputContainer { - constructor() { - this.unsyncedNonModelFields = []; - this.unsyncedModelNames = []; - this.unsyncedModelFields = new Map(); - } - add(element, modelName = null) { - if (modelName) { - this.unsyncedModelFields.set(modelName, element); - if (!this.unsyncedModelNames.includes(modelName)) { - this.unsyncedModelNames.push(modelName); - } - return; - } - this.unsyncedNonModelFields.push(element); - } - resetUnsyncedFields() { - this.unsyncedModelFields.forEach((value, key) => { - if (!this.unsyncedModelNames.includes(key)) { - this.unsyncedModelFields.delete(key); - } - }); - } - allUnsyncedInputs() { - return [...this.unsyncedNonModelFields, ...this.unsyncedModelFields.values()]; - } - markModelAsSynced(modelName) { - const index = this.unsyncedModelNames.indexOf(modelName); - if (index !== -1) { - this.unsyncedModelNames.splice(index, 1); - } - } - getUnsyncedModelNames() { - return this.unsyncedModelNames; - } -} +//#endregion +//#region src/Component/UnsyncedInputsTracker.ts +var UnsyncedInputsTracker_default = class { + component; + modelElementResolver; + /** Fields that have changed, but whose value is not set back onto the value store */ + unsyncedInputs; + elementEventListeners = [{ + event: "input", + callback: (event) => this.handleInputEvent(event) + }]; + constructor(component, modelElementResolver) { + this.component = component; + this.modelElementResolver = modelElementResolver; + this.unsyncedInputs = new UnsyncedInputContainer(); + } + activate() { + this.elementEventListeners.forEach(({ event, callback }) => { + this.component.element.addEventListener(event, callback); + }); + } + deactivate() { + this.elementEventListeners.forEach(({ event, callback }) => { + this.component.element.removeEventListener(event, callback); + }); + } + markModelAsSynced(modelName) { + this.unsyncedInputs.markModelAsSynced(modelName); + } + handleInputEvent(event) { + const target = event.target; + if (!target) return; + this.updateModelFromElement(target); + } + updateModelFromElement(element) { + if (!elementBelongsToThisComponent(element, this.component)) return; + if (!(element instanceof HTMLElement)) throw new Error("Could not update model for non HTMLElement"); + const modelName = this.modelElementResolver.getModelName(element); + this.unsyncedInputs.add(element, modelName); + } + getUnsyncedInputs() { + return this.unsyncedInputs.allUnsyncedInputs(); + } + getUnsyncedModels() { + return Array.from(this.unsyncedInputs.getUnsyncedModelNames()); + } + resetUnsyncedFields() { + this.unsyncedInputs.resetUnsyncedFields(); + } +}; +/** +* Tracks field & models whose values are "unsynced". +* +* For a model, unsynced means that the value has been updated inside +* a field (e.g. an input), but that this new value hasn't +* yet been set onto the actual model data. It is "unsynced" +* from the underlying model data. +* +* For a field, unsynced means that it is "modified on the client side". In +* other words, the field's value in the browser would be different than the +* one returned from the server. This can happen because a field has no model +* (and so it is permanently unsynced once changed) or the field has been changed +* and the corresponding model has not yet been sent to the server. +* +* Note: a "model" can become synced when that value is set back +* onto the data store. But the corresponding field will +* remain unsynced until the next Ajax call starts. +*/ +var UnsyncedInputContainer = class { + unsyncedModelFields; + unsyncedNonModelFields = []; + unsyncedModelNames = []; + constructor() { + this.unsyncedModelFields = /* @__PURE__ */ new Map(); + } + add(element, modelName = null) { + if (modelName) { + this.unsyncedModelFields.set(modelName, element); + if (!this.unsyncedModelNames.includes(modelName)) this.unsyncedModelNames.push(modelName); + return; + } + this.unsyncedNonModelFields.push(element); + } + /** + * Mark all fields as synced, except for those not bound to a model or whose + * values are still dirty. + */ + resetUnsyncedFields() { + this.unsyncedModelFields.forEach((value, key) => { + if (!this.unsyncedModelNames.includes(key)) this.unsyncedModelFields.delete(key); + }); + } + allUnsyncedInputs() { + return [...this.unsyncedNonModelFields, ...this.unsyncedModelFields.values()]; + } + markModelAsSynced(modelName) { + const index = this.unsyncedModelNames.indexOf(modelName); + if (index !== -1) this.unsyncedModelNames.splice(index, 1); + } + /** + * Returns a list of models whose fields have been modified, but whose values + * have not yet been set onto the data store. + */ + getUnsyncedModelNames() { + return this.unsyncedModelNames; + } +}; +//#endregion +//#region src/data_manipulation_utils.ts function getDeepData(data, propertyPath) { - const { currentLevelData, finalKey } = parseDeepData(data, propertyPath); - if (currentLevelData === undefined) { - return undefined; - } - return currentLevelData[finalKey]; + const { currentLevelData, finalKey } = parseDeepData(data, propertyPath); + if (currentLevelData === void 0) return void 0; + return currentLevelData[finalKey]; } const parseDeepData = (data, propertyPath) => { - const finalData = JSON.parse(JSON.stringify(data)); - let currentLevelData = finalData; - const parts = propertyPath.split('.'); - for (let i = 0; i < parts.length - 1; i++) { - currentLevelData = currentLevelData[parts[i]]; - } - const finalKey = parts[parts.length - 1]; - return { - currentLevelData, - finalData, - finalKey, - parts, - }; + const finalData = JSON.parse(JSON.stringify(data)); + let currentLevelData = finalData; + const parts = propertyPath.split("."); + for (let i = 0; i < parts.length - 1; i++) currentLevelData = currentLevelData[parts[i]]; + const finalKey = parts[parts.length - 1]; + return { + currentLevelData, + finalData, + finalKey, + parts + }; }; -class ValueStore { - constructor(props) { - this.props = {}; - this.dirtyProps = {}; - this.pendingProps = {}; - this.updatedPropsFromParent = {}; - this.props = props; - } - get(name) { - const normalizedName = normalizeModelName(name); - if (this.dirtyProps[normalizedName] !== undefined) { - return this.dirtyProps[normalizedName]; - } - if (this.pendingProps[normalizedName] !== undefined) { - return this.pendingProps[normalizedName]; - } - if (this.props[normalizedName] !== undefined) { - return this.props[normalizedName]; - } - return getDeepData(this.props, normalizedName); - } - has(name) { - return this.get(name) !== undefined; - } - set(name, value) { - const normalizedName = normalizeModelName(name); - if (this.get(normalizedName) === value) { - return false; - } - this.dirtyProps[normalizedName] = value; - return true; - } - getOriginalProps() { - return { ...this.props }; - } - getDirtyProps() { - return { ...this.dirtyProps }; - } - getUpdatedPropsFromParent() { - return { ...this.updatedPropsFromParent }; - } - flushDirtyPropsToPending() { - this.pendingProps = { ...this.dirtyProps }; - this.dirtyProps = {}; - } - reinitializeAllProps(props) { - this.props = props; - this.updatedPropsFromParent = {}; - this.pendingProps = {}; - } - pushPendingPropsBackToDirty() { - this.dirtyProps = { ...this.pendingProps, ...this.dirtyProps }; - this.pendingProps = {}; - } - storeNewPropsFromParent(props) { - let changed = false; - for (const [key, value] of Object.entries(props)) { - const currentValue = this.get(key); - if (currentValue !== value) { - changed = true; - } - } - if (changed) { - this.updatedPropsFromParent = props; - } - return changed; - } -} +//#endregion +//#region src/Component/ValueStore.ts +var ValueStore_default = class { + /** + * Original, read-only props that represent the original component state. + * + * @private + */ + props = {}; + /** + * A list of props that have been "dirty" (changed) since the last request to the server. + */ + dirtyProps = {}; + /** + * A list of dirty props that were sent to the server, but the response has + * not yet been received. + */ + pendingProps = {}; + /** + * A list of props that the parent wants us to update. + * + * These will be sent on the next request to the server. + */ + updatedPropsFromParent = {}; + constructor(props) { + this.props = props; + } + /** + * Returns the props with the given name. + * + * This allows for non-normalized model names - e.g. + * user[firstName] -> user.firstName and also will fetch + * deeply (fetching the "firstName" sub-key from the "user" key). + */ + get(name) { + const normalizedName = normalizeModelName(name); + if (this.dirtyProps[normalizedName] !== void 0) return this.dirtyProps[normalizedName]; + if (this.pendingProps[normalizedName] !== void 0) return this.pendingProps[normalizedName]; + if (this.props[normalizedName] !== void 0) return this.props[normalizedName]; + return getDeepData(this.props, normalizedName); + } + has(name) { + return this.get(name) !== void 0; + } + /** + * Sets data back onto the value store. + * + * The name can be in the non-normalized format. + * + * Returns true if the new value is different from the existing value. + */ + set(name, value) { + const normalizedName = normalizeModelName(name); + if (this.get(normalizedName) === value) return false; + this.dirtyProps[normalizedName] = value; + return true; + } + getOriginalProps() { + return { ...this.props }; + } + getDirtyProps() { + return { ...this.dirtyProps }; + } + getUpdatedPropsFromParent() { + return { ...this.updatedPropsFromParent }; + } + /** + * Called when an update request begins. + */ + flushDirtyPropsToPending() { + this.pendingProps = { ...this.dirtyProps }; + this.dirtyProps = {}; + } + /** + * Called when an update request finishes successfully. + */ + reinitializeAllProps(props) { + this.props = props; + this.updatedPropsFromParent = {}; + this.pendingProps = {}; + } + /** + * Called after an update request failed. + */ + pushPendingPropsBackToDirty() { + this.dirtyProps = { + ...this.pendingProps, + ...this.dirtyProps + }; + this.pendingProps = {}; + } + /** + * This is used when a parent component is rendering, and it includes + * a fresh set of props that should be updated on the child component. + * + * The server manages returning only the props that should be updated onto + * the child, so we don't need to worry about that. + * + * The props are stored in a different place, because the existing props + * have their own checksum and these new props have *their* own checksum. + * So, on the next render, both need to be sent independently. + * + * Returns true if any of the props are different. + */ + storeNewPropsFromParent(props) { + let changed = false; + for (const [key, value] of Object.entries(props)) { + const currentValue = this.get(key); + if (currentValue !== value) changed = true; + } + if (changed) this.updatedPropsFromParent = props; + return changed; + } +}; -class Component { - constructor(element, name, props, listeners, id, backend, elementDriver) { - this.fingerprint = ''; - this.defaultDebounce = 150; - this.backendRequest = null; - this.pendingActions = []; - this.pendingFiles = {}; - this.isRequestPending = false; - this.requestDebounceTimeout = null; - this.element = element; - this.name = name; - this.backend = backend; - this.elementDriver = elementDriver; - this.id = id; - this.listeners = new Map(); - listeners.forEach((listener) => { - if (!this.listeners.has(listener.event)) { - this.listeners.set(listener.event, []); - } - this.listeners.get(listener.event)?.push(listener.action); - }); - this.valueStore = new ValueStore(props); - this.unsyncedInputsTracker = new UnsyncedInputsTracker(this, elementDriver); - this.hooks = new HookManager(); - this.resetPromise(); - this.externalMutationTracker = new ExternalMutationTracker(this.element, (element) => elementBelongsToThisComponent(element, this)); - this.externalMutationTracker.start(); - } - addPlugin(plugin) { - plugin.attachToComponent(this); - } - connect() { - registerComponent(this); - this.hooks.triggerHook('connect', this); - this.unsyncedInputsTracker.activate(); - this.externalMutationTracker.start(); - } - disconnect() { - unregisterComponent(this); - this.hooks.triggerHook('disconnect', this); - this.clearRequestDebounceTimeout(); - this.unsyncedInputsTracker.deactivate(); - this.externalMutationTracker.stop(); - } - on(hookName, callback) { - this.hooks.register(hookName, callback); - } - off(hookName, callback) { - this.hooks.unregister(hookName, callback); - } - set(model, value, reRender = false, debounce = false) { - const promise = this.nextRequestPromise; - const modelName = normalizeModelName(model); - if (!this.valueStore.has(modelName)) { - throw new Error(`Invalid model name "${model}".`); - } - const isChanged = this.valueStore.set(modelName, value); - this.hooks.triggerHook('model:set', model, value, this); - this.unsyncedInputsTracker.markModelAsSynced(modelName); - if (reRender && isChanged) { - this.debouncedStartRequest(debounce); - } - return promise; - } - getData(model) { - const modelName = normalizeModelName(model); - if (!this.valueStore.has(modelName)) { - throw new Error(`Invalid model "${model}".`); - } - return this.valueStore.get(modelName); - } - action(name, args = {}, debounce = false) { - const promise = this.nextRequestPromise; - this.pendingActions.push({ - name, - args, - }); - this.debouncedStartRequest(debounce); - return promise; - } - files(key, input) { - this.pendingFiles[key] = input; - } - render() { - const promise = this.nextRequestPromise; - this.tryStartingRequest(); - return promise; - } - getUnsyncedModels() { - return this.unsyncedInputsTracker.getUnsyncedModels(); - } - emit(name, data, onlyMatchingComponentsNamed = null) { - this.performEmit(name, data, false, onlyMatchingComponentsNamed); - } - emitUp(name, data, onlyMatchingComponentsNamed = null) { - this.performEmit(name, data, true, onlyMatchingComponentsNamed); - } - emitSelf(name, data) { - this.doEmit(name, data); - } - performEmit(name, data, emitUp, matchingName) { - const components = findComponents(this, emitUp, matchingName); - components.forEach((component) => { - component.doEmit(name, data); - }); - } - doEmit(name, data) { - if (!this.listeners.has(name)) { - return; - } - const actions = this.listeners.get(name) || []; - actions.forEach((action) => { - this.action(action, data, 1); - }); - } - isTurboEnabled() { - return typeof Turbo !== 'undefined' && !this.element.closest('[data-turbo="false"]'); - } - tryStartingRequest() { - if (!this.backendRequest) { - this.performRequest(); - return; - } - this.isRequestPending = true; - } - performRequest() { - const thisPromiseResolve = this.nextRequestPromiseResolve; - this.resetPromise(); - this.unsyncedInputsTracker.resetUnsyncedFields(); - const filesToSend = {}; - for (const [key, value] of Object.entries(this.pendingFiles)) { - if (value.files) { - filesToSend[key] = value.files; - } - } - const requestConfig = { - props: this.valueStore.getOriginalProps(), - actions: this.pendingActions, - updated: this.valueStore.getDirtyProps(), - children: {}, - updatedPropsFromParent: this.valueStore.getUpdatedPropsFromParent(), - files: filesToSend, - }; - this.hooks.triggerHook('request:started', requestConfig); - this.backendRequest = this.backend.makeRequest(requestConfig.props, requestConfig.actions, requestConfig.updated, requestConfig.children, requestConfig.updatedPropsFromParent, requestConfig.files); - this.hooks.triggerHook('loading.state:started', this.element, this.backendRequest); - this.pendingActions = []; - this.valueStore.flushDirtyPropsToPending(); - this.isRequestPending = false; - this.backendRequest.promise.then(async (response) => { - const backendResponse = new BackendResponse(response); - const html = await backendResponse.getBody(); - for (const input of Object.values(this.pendingFiles)) { - input.value = ''; - } - const headers = backendResponse.response.headers; - if (!headers.get('Content-Type')?.includes('application/vnd.live-component+html') && - !headers.get('X-Live-Redirect')) { - const controls = { displayError: true }; - this.valueStore.pushPendingPropsBackToDirty(); - this.hooks.triggerHook('response:error', backendResponse, controls); - if (controls.displayError) { - this.renderError(html); - } - this.backendRequest = null; - thisPromiseResolve(backendResponse); - return response; - } - this.processRerender(html, backendResponse); - this.backendRequest = null; - thisPromiseResolve(backendResponse); - if (this.isRequestPending) { - this.isRequestPending = false; - this.performRequest(); - } - return response; - }); - } - processRerender(html, backendResponse) { - const controls = { shouldRender: true }; - this.hooks.triggerHook('render:started', html, backendResponse, controls); - if (!controls.shouldRender) { - return; - } - if (backendResponse.response.headers.get('Location')) { - if (this.isTurboEnabled()) { - Turbo.visit(backendResponse.response.headers.get('Location')); - } - else { - window.location.href = backendResponse.response.headers.get('Location') || ''; - } - return; - } - this.hooks.triggerHook('loading.state:finished', this.element); - const modifiedModelValues = {}; - Object.keys(this.valueStore.getDirtyProps()).forEach((modelName) => { - modifiedModelValues[modelName] = this.valueStore.get(modelName); - }); - let newElement; - try { - newElement = htmlToElement(html); - if (!newElement.matches('[data-controller~=live]')) { - throw new Error('A live component template must contain a single root controller element.'); - } - } - catch (error) { - console.error(`There was a problem with the '${this.name}' component HTML returned:`, { - id: this.id, - }); - throw error; - } - this.externalMutationTracker.handlePendingChanges(); - this.externalMutationTracker.stop(); - executeMorphdom(this.element, newElement, this.unsyncedInputsTracker.getUnsyncedInputs(), (element) => getValueFromElement(element, this.valueStore), this.externalMutationTracker); - this.externalMutationTracker.start(); - const newProps = this.elementDriver.getComponentProps(); - this.valueStore.reinitializeAllProps(newProps); - const eventsToEmit = this.elementDriver.getEventsToEmit(); - const browserEventsToDispatch = this.elementDriver.getBrowserEventsToDispatch(); - Object.keys(modifiedModelValues).forEach((modelName) => { - this.valueStore.set(modelName, modifiedModelValues[modelName]); - }); - eventsToEmit.forEach(({ event, data, target, componentName }) => { - if (target === 'up') { - this.emitUp(event, data, componentName); - return; - } - if (target === 'self') { - this.emitSelf(event, data); - return; - } - this.emit(event, data, componentName); - }); - browserEventsToDispatch.forEach(({ event, payload }) => { - this.element.dispatchEvent(new CustomEvent(event, { - detail: payload, - bubbles: true, - })); - }); - this.hooks.triggerHook('render:finished', this); - } - calculateDebounce(debounce) { - if (debounce === true) { - return this.defaultDebounce; - } - if (debounce === false) { - return 0; - } - return debounce; - } - clearRequestDebounceTimeout() { - if (this.requestDebounceTimeout) { - clearTimeout(this.requestDebounceTimeout); - this.requestDebounceTimeout = null; - } - } - debouncedStartRequest(debounce) { - this.clearRequestDebounceTimeout(); - this.requestDebounceTimeout = window.setTimeout(() => { - this.render(); - }, this.calculateDebounce(debounce)); - } - renderError(html) { - let modal = document.getElementById('live-component-error'); - if (modal) { - modal.innerHTML = ''; - } - else { - modal = document.createElement('div'); - modal.id = 'live-component-error'; - modal.style.padding = '50px'; - modal.style.backgroundColor = 'rgba(0, 0, 0, .5)'; - modal.style.zIndex = '100000'; - modal.style.position = 'fixed'; - modal.style.top = '0px'; - modal.style.bottom = '0px'; - modal.style.left = '0px'; - modal.style.right = '0px'; - modal.style.display = 'flex'; - modal.style.flexDirection = 'column'; - } - const iframe = document.createElement('iframe'); - iframe.style.borderRadius = '5px'; - iframe.style.flexGrow = '1'; - modal.appendChild(iframe); - document.body.prepend(modal); - document.body.style.overflow = 'hidden'; - if (iframe.contentWindow) { - iframe.contentWindow.document.open(); - iframe.contentWindow.document.write(html); - iframe.contentWindow.document.close(); - } - const closeModal = (modal) => { - if (modal) { - modal.outerHTML = ''; - } - document.body.style.overflow = 'visible'; - }; - modal.addEventListener('click', () => closeModal(modal)); - modal.setAttribute('tabindex', '0'); - modal.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - closeModal(modal); - } - }); - modal.focus(); - } - resetPromise() { - this.nextRequestPromise = new Promise((resolve) => { - this.nextRequestPromiseResolve = resolve; - }); - } - _updateFromParentProps(props) { - const isChanged = this.valueStore.storeNewPropsFromParent(props); - if (isChanged) { - this.render(); - } - } -} +//#endregion +//#region src/Component/index.ts +var Component = class { + element; + name; + listeners; + backend; + elementDriver; + id; + /** + * A fingerprint that identifies the props/input that was used on + * the server to create this component, especially if it was a + * child component. This is sent back to the server and can be used + * to determine if any "input" to the child component changed and thus, + * if the child component needs to be re-rendered. + */ + fingerprint = ""; + valueStore; + unsyncedInputsTracker; + hooks; + defaultDebounce = 150; + backendRequest = null; + /** Actions that are waiting to be executed */ + pendingActions = []; + /** Files that are waiting to be sent */ + pendingFiles = {}; + /** Is a request waiting to be made? */ + isRequestPending = false; + /** Current "timeout" before the pending request should be sent. */ + requestDebounceTimeout = null; + nextRequestPromise; + nextRequestPromiseResolve; + externalMutationTracker; + /** + * @param element The root element + * @param name The name of the component + * @param props Readonly component props + * @param listeners Array of event -> action listeners + * @param id Some unique id to identify this component. Needed to be a child component + * @param backend Backend instance for updating + * @param elementDriver Class to get "model" name from any element. + */ + constructor(element, name, props, listeners, id, backend, elementDriver) { + this.element = element; + this.name = name; + this.backend = backend; + this.elementDriver = elementDriver; + this.id = id; + this.listeners = /* @__PURE__ */ new Map(); + listeners.forEach((listener) => { + if (!this.listeners.has(listener.event)) this.listeners.set(listener.event, []); + this.listeners.get(listener.event)?.push(listener.action); + }); + this.valueStore = new ValueStore_default(props); + this.unsyncedInputsTracker = new UnsyncedInputsTracker_default(this, elementDriver); + this.hooks = new HookManager_default(); + this.resetPromise(); + this.externalMutationTracker = new ExternalMutationTracker_default(this.element, (element$1) => elementBelongsToThisComponent(element$1, this)); + this.externalMutationTracker.start(); + } + addPlugin(plugin) { + plugin.attachToComponent(this); + } + connect() { + registerComponent(this); + this.hooks.triggerHook("connect", this); + this.unsyncedInputsTracker.activate(); + this.externalMutationTracker.start(); + } + disconnect() { + unregisterComponent(this); + this.hooks.triggerHook("disconnect", this); + this.clearRequestDebounceTimeout(); + this.unsyncedInputsTracker.deactivate(); + this.externalMutationTracker.stop(); + } + on(hookName, callback) { + this.hooks.register(hookName, callback); + } + off(hookName, callback) { + this.hooks.unregister(hookName, callback); + } + set(model, value, reRender = false, debounce = false) { + const promise = this.nextRequestPromise; + const modelName = normalizeModelName(model); + if (!this.valueStore.has(modelName)) throw new Error(`Invalid model name "${model}".`); + const isChanged = this.valueStore.set(modelName, value); + this.hooks.triggerHook("model:set", model, value, this); + this.unsyncedInputsTracker.markModelAsSynced(modelName); + if (reRender && isChanged) this.debouncedStartRequest(debounce); + return promise; + } + getData(model) { + const modelName = normalizeModelName(model); + if (!this.valueStore.has(modelName)) throw new Error(`Invalid model "${model}".`); + return this.valueStore.get(modelName); + } + action(name, args = {}, debounce = false) { + const promise = this.nextRequestPromise; + this.pendingActions.push({ + name, + args + }); + this.debouncedStartRequest(debounce); + return promise; + } + files(key, input) { + this.pendingFiles[key] = input; + } + render() { + const promise = this.nextRequestPromise; + this.tryStartingRequest(); + return promise; + } + /** + * Returns an array of models the user has modified, but whose model has not + * yet been updated. + */ + getUnsyncedModels() { + return this.unsyncedInputsTracker.getUnsyncedModels(); + } + emit(name, data, onlyMatchingComponentsNamed = null) { + this.performEmit(name, data, false, onlyMatchingComponentsNamed); + } + emitUp(name, data, onlyMatchingComponentsNamed = null) { + this.performEmit(name, data, true, onlyMatchingComponentsNamed); + } + emitSelf(name, data) { + this.doEmit(name, data); + } + performEmit(name, data, emitUp, matchingName) { + const components = findComponents(this, emitUp, matchingName); + components.forEach((component) => { + component.doEmit(name, data); + }); + } + doEmit(name, data) { + if (!this.listeners.has(name)) return; + const actions = this.listeners.get(name) || []; + actions.forEach((action) => { + this.action(action, data, 1); + }); + } + isTurboEnabled() { + return typeof Turbo !== "undefined" && !this.element.closest("[data-turbo=\"false\"]"); + } + tryStartingRequest() { + if (!this.backendRequest) { + this.performRequest(); + return; + } + this.isRequestPending = true; + } + performRequest() { + const thisPromiseResolve = this.nextRequestPromiseResolve; + this.resetPromise(); + this.unsyncedInputsTracker.resetUnsyncedFields(); + const filesToSend = {}; + for (const [key, value] of Object.entries(this.pendingFiles)) if (value.files) filesToSend[key] = value.files; + const requestConfig = { + props: this.valueStore.getOriginalProps(), + actions: this.pendingActions, + updated: this.valueStore.getDirtyProps(), + children: {}, + updatedPropsFromParent: this.valueStore.getUpdatedPropsFromParent(), + files: filesToSend + }; + this.hooks.triggerHook("request:started", requestConfig); + this.backendRequest = this.backend.makeRequest(requestConfig.props, requestConfig.actions, requestConfig.updated, requestConfig.children, requestConfig.updatedPropsFromParent, requestConfig.files); + this.hooks.triggerHook("loading.state:started", this.element, this.backendRequest); + this.pendingActions = []; + this.valueStore.flushDirtyPropsToPending(); + this.isRequestPending = false; + this.backendRequest.promise.then(async (response) => { + const backendResponse = new BackendResponse_default(response); + const html = await backendResponse.getBody(); + for (const input of Object.values(this.pendingFiles)) input.value = ""; + const headers = backendResponse.response.headers; + if (!headers.get("Content-Type")?.includes("application/vnd.live-component+html") && !headers.get("X-Live-Redirect")) { + const controls = { displayError: true }; + this.valueStore.pushPendingPropsBackToDirty(); + this.hooks.triggerHook("response:error", backendResponse, controls); + if (controls.displayError) this.renderError(html); + this.backendRequest = null; + thisPromiseResolve(backendResponse); + return response; + } + this.processRerender(html, backendResponse); + this.backendRequest = null; + thisPromiseResolve(backendResponse); + if (this.isRequestPending) { + this.isRequestPending = false; + this.performRequest(); + } + return response; + }); + } + processRerender(html, backendResponse) { + const controls = { shouldRender: true }; + this.hooks.triggerHook("render:started", html, backendResponse, controls); + if (!controls.shouldRender) return; + if (backendResponse.response.headers.get("Location")) { + if (this.isTurboEnabled()) Turbo.visit(backendResponse.response.headers.get("Location")); + else window.location.href = backendResponse.response.headers.get("Location") || ""; + return; + } + this.hooks.triggerHook("loading.state:finished", this.element); + /** + * For any models modified since the last request started, grab + * their value now: we will re-set them after the new data from + * the server has been processed. + */ + const modifiedModelValues = {}; + Object.keys(this.valueStore.getDirtyProps()).forEach((modelName) => { + modifiedModelValues[modelName] = this.valueStore.get(modelName); + }); + let newElement; + try { + newElement = htmlToElement(html); + if (!newElement.matches("[data-controller~=live]")) throw new Error("A live component template must contain a single root controller element."); + } catch (error) { + console.error(`There was a problem with the '${this.name}' component HTML returned:`, { id: this.id }); + throw error; + } + this.externalMutationTracker.handlePendingChanges(); + this.externalMutationTracker.stop(); + executeMorphdom(this.element, newElement, this.unsyncedInputsTracker.getUnsyncedInputs(), (element) => getValueFromElement(element, this.valueStore), this.externalMutationTracker); + this.externalMutationTracker.start(); + const newProps = this.elementDriver.getComponentProps(); + this.valueStore.reinitializeAllProps(newProps); + const eventsToEmit = this.elementDriver.getEventsToEmit(); + const browserEventsToDispatch = this.elementDriver.getBrowserEventsToDispatch(); + Object.keys(modifiedModelValues).forEach((modelName) => { + this.valueStore.set(modelName, modifiedModelValues[modelName]); + }); + eventsToEmit.forEach(({ event, data, target, componentName }) => { + if (target === "up") { + this.emitUp(event, data, componentName); + return; + } + if (target === "self") { + this.emitSelf(event, data); + return; + } + this.emit(event, data, componentName); + }); + browserEventsToDispatch.forEach(({ event, payload }) => { + this.element.dispatchEvent(new CustomEvent(event, { + detail: payload, + bubbles: true + })); + }); + this.hooks.triggerHook("render:finished", this); + } + calculateDebounce(debounce) { + if (debounce === true) return this.defaultDebounce; + if (debounce === false) return 0; + return debounce; + } + clearRequestDebounceTimeout() { + if (this.requestDebounceTimeout) { + clearTimeout(this.requestDebounceTimeout); + this.requestDebounceTimeout = null; + } + } + debouncedStartRequest(debounce) { + this.clearRequestDebounceTimeout(); + this.requestDebounceTimeout = window.setTimeout(() => { + this.render(); + }, this.calculateDebounce(debounce)); + } + renderError(html) { + let modal = document.getElementById("live-component-error"); + if (modal) modal.innerHTML = ""; + else { + modal = document.createElement("div"); + modal.id = "live-component-error"; + modal.style.padding = "50px"; + modal.style.backgroundColor = "rgba(0, 0, 0, .5)"; + modal.style.zIndex = "100000"; + modal.style.position = "fixed"; + modal.style.top = "0px"; + modal.style.bottom = "0px"; + modal.style.left = "0px"; + modal.style.right = "0px"; + modal.style.display = "flex"; + modal.style.flexDirection = "column"; + } + const iframe = document.createElement("iframe"); + iframe.style.borderRadius = "5px"; + iframe.style.flexGrow = "1"; + modal.appendChild(iframe); + document.body.prepend(modal); + document.body.style.overflow = "hidden"; + if (iframe.contentWindow) { + iframe.contentWindow.document.open(); + iframe.contentWindow.document.write(html); + iframe.contentWindow.document.close(); + } + const closeModal = (modal$1) => { + if (modal$1) modal$1.outerHTML = ""; + document.body.style.overflow = "visible"; + }; + modal.addEventListener("click", () => closeModal(modal)); + modal.setAttribute("tabindex", "0"); + modal.addEventListener("keydown", (e) => { + if (e.key === "Escape") closeModal(modal); + }); + modal.focus(); + } + resetPromise() { + this.nextRequestPromise = new Promise((resolve) => { + this.nextRequestPromiseResolve = resolve; + }); + } + /** + * Called on a child component after the parent component render has requested + * that the child component update its props & re-render if necessary. + */ + _updateFromParentProps(props) { + const isChanged = this.valueStore.storeNewPropsFromParent(props); + if (isChanged) this.render(); + } +}; +/** +* Makes the Component feel more like a JS-version of the PHP component: +* +* // set model like properties +* component.firstName = 'Ryan'; +* +* // call a live action called "saveStatus" with a "status" arg +* component.saveStatus({ status: 'published' }); +*/ function proxifyComponent(component) { - return new Proxy(component, { - get(component, prop) { - if (prop in component || typeof prop !== 'string') { - if (typeof component[prop] === 'function') { - const callable = component[prop]; - return (...args) => { - return callable.apply(component, args); - }; - } - return Reflect.get(component, prop); - } - if (component.valueStore.has(prop)) { - return component.getData(prop); - } - return (args) => { - return component.action.apply(component, [prop, args]); - }; - }, - set(target, property, value) { - if (property in target) { - target[property] = value; - return true; - } - target.set(property, value); - return true; - }, - }); + return new Proxy(component, { + get(component$1, prop) { + if (prop in component$1 || typeof prop !== "string") { + if (typeof component$1[prop] === "function") { + const callable = component$1[prop]; + return (...args) => { + return callable.apply(component$1, args); + }; + } + return Reflect.get(component$1, prop); + } + if (component$1.valueStore.has(prop)) return component$1.getData(prop); + return (args) => { + return component$1.action.apply(component$1, [prop, args]); + }; + }, + set(target, property, value) { + if (property in target) { + target[property] = value; + return true; + } + target.set(property, value); + return true; + } + }); } -class StimulusElementDriver { - constructor(controller) { - this.controller = controller; - } - getModelName(element) { - const modelDirective = getModelDirectiveFromElement(element, false); - if (!modelDirective) { - return null; - } - return modelDirective.action; - } - getComponentProps() { - return this.controller.propsValue; - } - getEventsToEmit() { - return this.controller.eventsToEmitValue; - } - getBrowserEventsToDispatch() { - return this.controller.eventsToDispatchValue; - } -} +//#endregion +//#region src/Component/ElementDriver.ts +var StimulusElementDriver = class { + controller; + constructor(controller) { + this.controller = controller; + } + getModelName(element) { + const modelDirective = getModelDirectiveFromElement(element, false); + if (!modelDirective) return null; + return modelDirective.action; + } + getComponentProps() { + return this.controller.propsValue; + } + getEventsToEmit() { + return this.controller.eventsToEmitValue; + } + getBrowserEventsToDispatch() { + return this.controller.eventsToDispatchValue; + } +}; -function getModelBinding (modelDirective) { - let shouldRender = true; - let targetEventName = null; - let debounce = false; - let minLength = null; - let maxLength = null; - let minValue = null; - let maxValue = null; - modelDirective.modifiers.forEach((modifier) => { - switch (modifier.name) { - case 'on': - if (!modifier.value) { - throw new Error(`The "on" modifier in ${modelDirective.getString()} requires a value - e.g. on(change).`); - } - if (!['input', 'change'].includes(modifier.value)) { - throw new Error(`The "on" modifier in ${modelDirective.getString()} only accepts the arguments "input" or "change".`); - } - targetEventName = modifier.value; - break; - case 'norender': - shouldRender = false; - break; - case 'debounce': - debounce = modifier.value ? Number.parseInt(modifier.value) : true; - break; - case 'min_length': - minLength = modifier.value ? Number.parseInt(modifier.value) : null; - break; - case 'max_length': - maxLength = modifier.value ? Number.parseInt(modifier.value) : null; - break; - case 'min_value': - minValue = modifier.value ? Number.parseFloat(modifier.value) : null; - break; - case 'max_value': - maxValue = modifier.value ? Number.parseFloat(modifier.value) : null; - break; - default: - throw new Error(`Unknown modifier "${modifier.name}" in data-model="${modelDirective.getString()}".`); - } - }); - const [modelName, innerModelName] = modelDirective.action.split(':'); - return { - modelName, - innerModelName: innerModelName || null, - shouldRender, - debounce, - targetEventName, - minLength, - maxLength, - minValue, - maxValue, - }; +//#endregion +//#region src/Directive/get_model_binding.ts +function get_model_binding_default(modelDirective) { + let shouldRender = true; + let targetEventName = null; + let debounce = false; + let minLength = null; + let maxLength = null; + let minValue = null; + let maxValue = null; + modelDirective.modifiers.forEach((modifier) => { + switch (modifier.name) { + case "on": + if (!modifier.value) throw new Error(`The "on" modifier in ${modelDirective.getString()} requires a value - e.g. on(change).`); + if (!["input", "change"].includes(modifier.value)) throw new Error(`The "on" modifier in ${modelDirective.getString()} only accepts the arguments "input" or "change".`); + targetEventName = modifier.value; + break; + case "norender": + shouldRender = false; + break; + case "debounce": + debounce = modifier.value ? Number.parseInt(modifier.value) : true; + break; + case "min_length": + minLength = modifier.value ? Number.parseInt(modifier.value) : null; + break; + case "max_length": + maxLength = modifier.value ? Number.parseInt(modifier.value) : null; + break; + case "min_value": + minValue = modifier.value ? Number.parseFloat(modifier.value) : null; + break; + case "max_value": + maxValue = modifier.value ? Number.parseFloat(modifier.value) : null; + break; + default: throw new Error(`Unknown modifier "${modifier.name}" in data-model="${modelDirective.getString()}".`); + } + }); + const [modelName, innerModelName] = modelDirective.action.split(":"); + return { + modelName, + innerModelName: innerModelName || null, + shouldRender, + debounce, + targetEventName, + minLength, + maxLength, + minValue, + maxValue + }; } -class ChildComponentPlugin { - constructor(component) { - this.parentModelBindings = []; - this.component = component; - const modelDirectives = getAllModelDirectiveFromElements(this.component.element); - this.parentModelBindings = modelDirectives.map(getModelBinding); - } - attachToComponent(component) { - component.on('request:started', (requestData) => { - requestData.children = this.getChildrenFingerprints(); - }); - component.on('model:set', (model, value) => { - this.notifyParentModelChange(model, value); - }); - } - getChildrenFingerprints() { - const fingerprints = {}; - this.getChildren().forEach((child) => { - if (!child.id) { - throw new Error('missing id'); - } - fingerprints[child.id] = { - fingerprint: child.fingerprint, - tag: child.element.tagName.toLowerCase(), - }; - }); - return fingerprints; - } - notifyParentModelChange(modelName, value) { - const parentComponent = findParent(this.component); - if (!parentComponent) { - return; - } - this.parentModelBindings.forEach((modelBinding) => { - const childModelName = modelBinding.innerModelName || 'value'; - if (childModelName !== modelName) { - return; - } - parentComponent.set(modelBinding.modelName, value, modelBinding.shouldRender, modelBinding.debounce); - }); - } - getChildren() { - return findChildren(this.component); - } -} +//#endregion +//#region src/Component/plugins/ChildComponentPlugin.ts +/** +* Handles all interactions for child components of a component. +* +* A) This parent component handling its children: +* * Sending children fingerprints to the server. +* +* B) This child component handling its parent: +* * Notifying the parent of a model change. +*/ +var ChildComponentPlugin_default = class { + component; + parentModelBindings = []; + constructor(component) { + this.component = component; + const modelDirectives = getAllModelDirectiveFromElements(this.component.element); + this.parentModelBindings = modelDirectives.map(get_model_binding_default); + } + attachToComponent(component) { + component.on("request:started", (requestData) => { + requestData.children = this.getChildrenFingerprints(); + }); + component.on("model:set", (model, value) => { + this.notifyParentModelChange(model, value); + }); + } + getChildrenFingerprints() { + const fingerprints = {}; + this.getChildren().forEach((child) => { + if (!child.id) throw new Error("missing id"); + fingerprints[child.id] = { + fingerprint: child.fingerprint, + tag: child.element.tagName.toLowerCase() + }; + }); + return fingerprints; + } + /** + * Notifies parent of a model change if desired. + * + * This makes the child "behave" like it's a normal `` element, + * where, when its value changes, the parent is notified. + */ + notifyParentModelChange(modelName, value) { + const parentComponent = findParent(this.component); + if (!parentComponent) return; + this.parentModelBindings.forEach((modelBinding) => { + const childModelName = modelBinding.innerModelName || "value"; + if (childModelName !== modelName) return; + parentComponent.set(modelBinding.modelName, value, modelBinding.shouldRender, modelBinding.debounce); + }); + } + getChildren() { + return findChildren(this.component); + } +}; -class LazyPlugin { - constructor() { - this.intersectionObserver = null; - } - attachToComponent(component) { - if ('lazy' !== component.element.attributes.getNamedItem('loading')?.value) { - return; - } - component.on('connect', () => { - this.getObserver().observe(component.element); - }); - component.on('disconnect', () => { - this.intersectionObserver?.unobserve(component.element); - }); - } - getObserver() { - if (!this.intersectionObserver) { - this.intersectionObserver = new IntersectionObserver((entries, observer) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - entry.target.dispatchEvent(new CustomEvent('live:appear')); - observer.unobserve(entry.target); - } - }); - }); - } - return this.intersectionObserver; - } -} +//#endregion +//#region src/Component/plugins/LazyPlugin.ts +var LazyPlugin_default = class { + intersectionObserver = null; + attachToComponent(component) { + if ("lazy" !== component.element.attributes.getNamedItem("loading")?.value) return; + component.on("connect", () => { + this.getObserver().observe(component.element); + }); + component.on("disconnect", () => { + this.intersectionObserver?.unobserve(component.element); + }); + } + getObserver() { + if (!this.intersectionObserver) this.intersectionObserver = new IntersectionObserver((entries, observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.dispatchEvent(new CustomEvent("live:appear")); + observer.unobserve(entry.target); + } + }); + }); + return this.intersectionObserver; + } +}; -class LoadingPlugin { - attachToComponent(component) { - component.on('loading.state:started', (element, request) => { - this.startLoading(component, element, request); - }); - component.on('loading.state:finished', (element) => { - this.finishLoading(component, element); - }); - this.finishLoading(component, component.element); - } - startLoading(component, targetElement, backendRequest) { - this.handleLoadingToggle(component, true, targetElement, backendRequest); - } - finishLoading(component, targetElement) { - this.handleLoadingToggle(component, false, targetElement, null); - } - handleLoadingToggle(component, isLoading, targetElement, backendRequest) { - if (isLoading) { - this.addAttributes(targetElement, ['busy']); - } - else { - this.removeAttributes(targetElement, ['busy']); - } - this.getLoadingDirectives(component, targetElement).forEach(({ element, directives }) => { - if (isLoading) { - this.addAttributes(element, ['data-live-is-loading']); - } - else { - this.removeAttributes(element, ['data-live-is-loading']); - } - directives.forEach((directive) => { - this.handleLoadingDirective(element, isLoading, directive, backendRequest); - }); - }); - } - handleLoadingDirective(element, isLoading, directive, backendRequest) { - const finalAction = parseLoadingAction(directive.action, isLoading); - const targetedActions = []; - const targetedModels = []; - let delay = 0; - const validModifiers = new Map(); - validModifiers.set('delay', (modifier) => { - if (!isLoading) { - return; - } - delay = modifier.value ? Number.parseInt(modifier.value) : 200; - }); - validModifiers.set('action', (modifier) => { - if (!modifier.value) { - throw new Error(`The "action" in data-loading must have an action name - e.g. action(foo). It's missing for "${directive.getString()}"`); - } - targetedActions.push(modifier.value); - }); - validModifiers.set('model', (modifier) => { - if (!modifier.value) { - throw new Error(`The "model" in data-loading must have an action name - e.g. model(foo). It's missing for "${directive.getString()}"`); - } - targetedModels.push(modifier.value); - }); - directive.modifiers.forEach((modifier) => { - if (validModifiers.has(modifier.name)) { - const callable = validModifiers.get(modifier.name) ?? (() => { }); - callable(modifier); - return; - } - throw new Error(`Unknown modifier "${modifier.name}" used in data-loading="${directive.getString()}". Available modifiers are: ${Array.from(validModifiers.keys()).join(', ')}.`); - }); - if (isLoading && - targetedActions.length > 0 && - backendRequest && - !backendRequest.containsOneOfActions(targetedActions)) { - return; - } - if (isLoading && - targetedModels.length > 0 && - backendRequest && - !backendRequest.areAnyModelsUpdated(targetedModels)) { - return; - } - let loadingDirective; - switch (finalAction) { - case 'show': - loadingDirective = () => this.showElement(element); - break; - case 'hide': - loadingDirective = () => this.hideElement(element); - break; - case 'addClass': - loadingDirective = () => this.addClass(element, directive.args); - break; - case 'removeClass': - loadingDirective = () => this.removeClass(element, directive.args); - break; - case 'addAttribute': - loadingDirective = () => this.addAttributes(element, directive.args); - break; - case 'removeAttribute': - loadingDirective = () => this.removeAttributes(element, directive.args); - break; - default: - throw new Error(`Unknown data-loading action "${finalAction}"`); - } - if (delay) { - window.setTimeout(() => { - if (backendRequest && !backendRequest.isResolved) { - loadingDirective(); - } - }, delay); - return; - } - loadingDirective(); - } - getLoadingDirectives(component, element) { - const loadingDirectives = []; - let matchingElements = [...Array.from(element.querySelectorAll('[data-loading]'))]; - matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component)); - if (element.hasAttribute('data-loading')) { - matchingElements = [element, ...matchingElements]; - } - matchingElements.forEach((element) => { - if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) { - throw new Error('Invalid Element Type'); - } - const directives = parseDirectives(element.dataset.loading || 'show'); - loadingDirectives.push({ - element, - directives, - }); - }); - return loadingDirectives; - } - showElement(element) { - element.style.display = 'revert'; - } - hideElement(element) { - element.style.display = 'none'; - } - addClass(element, classes) { - element.classList.add(...combineSpacedArray(classes)); - } - removeClass(element, classes) { - element.classList.remove(...combineSpacedArray(classes)); - if (element.classList.length === 0) { - element.removeAttribute('class'); - } - } - addAttributes(element, attributes) { - attributes.forEach((attribute) => { - element.setAttribute(attribute, ''); - }); - } - removeAttributes(element, attributes) { - attributes.forEach((attribute) => { - element.removeAttribute(attribute); - }); - } -} +//#endregion +//#region src/Component/plugins/LoadingPlugin.ts +var LoadingPlugin_default = class { + attachToComponent(component) { + component.on("loading.state:started", (element, request) => { + this.startLoading(component, element, request); + }); + component.on("loading.state:finished", (element) => { + this.finishLoading(component, element); + }); + this.finishLoading(component, component.element); + } + startLoading(component, targetElement, backendRequest) { + this.handleLoadingToggle(component, true, targetElement, backendRequest); + } + finishLoading(component, targetElement) { + this.handleLoadingToggle(component, false, targetElement, null); + } + handleLoadingToggle(component, isLoading, targetElement, backendRequest) { + if (isLoading) this.addAttributes(targetElement, ["busy"]); + else this.removeAttributes(targetElement, ["busy"]); + this.getLoadingDirectives(component, targetElement).forEach(({ element, directives }) => { + if (isLoading) this.addAttributes(element, ["data-live-is-loading"]); + else this.removeAttributes(element, ["data-live-is-loading"]); + directives.forEach((directive) => { + this.handleLoadingDirective(element, isLoading, directive, backendRequest); + }); + }); + } + handleLoadingDirective(element, isLoading, directive, backendRequest) { + const finalAction = parseLoadingAction(directive.action, isLoading); + const targetedActions = []; + const targetedModels = []; + let delay = 0; + const validModifiers = /* @__PURE__ */ new Map(); + validModifiers.set("delay", (modifier) => { + if (!isLoading) return; + delay = modifier.value ? Number.parseInt(modifier.value) : 200; + }); + validModifiers.set("action", (modifier) => { + if (!modifier.value) throw new Error(`The "action" in data-loading must have an action name - e.g. action(foo). It's missing for "${directive.getString()}"`); + targetedActions.push(modifier.value); + }); + validModifiers.set("model", (modifier) => { + if (!modifier.value) throw new Error(`The "model" in data-loading must have an action name - e.g. model(foo). It's missing for "${directive.getString()}"`); + targetedModels.push(modifier.value); + }); + directive.modifiers.forEach((modifier) => { + if (validModifiers.has(modifier.name)) { + const callable = validModifiers.get(modifier.name) ?? (() => {}); + callable(modifier); + return; + } + throw new Error(`Unknown modifier "${modifier.name}" used in data-loading="${directive.getString()}". Available modifiers are: ${Array.from(validModifiers.keys()).join(", ")}.`); + }); + if (isLoading && targetedActions.length > 0 && backendRequest && !backendRequest.containsOneOfActions(targetedActions)) return; + if (isLoading && targetedModels.length > 0 && backendRequest && !backendRequest.areAnyModelsUpdated(targetedModels)) return; + let loadingDirective; + switch (finalAction) { + case "show": + loadingDirective = () => this.showElement(element); + break; + case "hide": + loadingDirective = () => this.hideElement(element); + break; + case "addClass": + loadingDirective = () => this.addClass(element, directive.args); + break; + case "removeClass": + loadingDirective = () => this.removeClass(element, directive.args); + break; + case "addAttribute": + loadingDirective = () => this.addAttributes(element, directive.args); + break; + case "removeAttribute": + loadingDirective = () => this.removeAttributes(element, directive.args); + break; + default: throw new Error(`Unknown data-loading action "${finalAction}"`); + } + if (delay) { + window.setTimeout(() => { + if (backendRequest && !backendRequest.isResolved) loadingDirective(); + }, delay); + return; + } + loadingDirective(); + } + getLoadingDirectives(component, element) { + const loadingDirectives = []; + let matchingElements = [...Array.from(element.querySelectorAll("[data-loading]"))]; + matchingElements = matchingElements.filter((elt) => elementBelongsToThisComponent(elt, component)); + if (element.hasAttribute("data-loading")) matchingElements = [element, ...matchingElements]; + matchingElements.forEach((element$1) => { + if (!(element$1 instanceof HTMLElement) && !(element$1 instanceof SVGElement)) throw new Error("Invalid Element Type"); + const directives = parseDirectives(element$1.dataset.loading || "show"); + loadingDirectives.push({ + element: element$1, + directives + }); + }); + return loadingDirectives; + } + showElement(element) { + element.style.display = "revert"; + } + hideElement(element) { + element.style.display = "none"; + } + addClass(element, classes) { + element.classList.add(...combineSpacedArray(classes)); + } + removeClass(element, classes) { + element.classList.remove(...combineSpacedArray(classes)); + if (element.classList.length === 0) element.removeAttribute("class"); + } + addAttributes(element, attributes) { + attributes.forEach((attribute) => { + element.setAttribute(attribute, ""); + }); + } + removeAttributes(element, attributes) { + attributes.forEach((attribute) => { + element.removeAttribute(attribute); + }); + } +}; const parseLoadingAction = (action, isLoading) => { - switch (action) { - case 'show': - return isLoading ? 'show' : 'hide'; - case 'hide': - return isLoading ? 'hide' : 'show'; - case 'addClass': - return isLoading ? 'addClass' : 'removeClass'; - case 'removeClass': - return isLoading ? 'removeClass' : 'addClass'; - case 'addAttribute': - return isLoading ? 'addAttribute' : 'removeAttribute'; - case 'removeAttribute': - return isLoading ? 'removeAttribute' : 'addAttribute'; - } - throw new Error(`Unknown data-loading action "${action}"`); + switch (action) { + case "show": return isLoading ? "show" : "hide"; + case "hide": return isLoading ? "hide" : "show"; + case "addClass": return isLoading ? "addClass" : "removeClass"; + case "removeClass": return isLoading ? "removeClass" : "addClass"; + case "addAttribute": return isLoading ? "addAttribute" : "removeAttribute"; + case "removeAttribute": return isLoading ? "removeAttribute" : "addAttribute"; + } + throw new Error(`Unknown data-loading action "${action}"`); }; -class PageUnloadingPlugin { - constructor() { - this.isConnected = false; - } - attachToComponent(component) { - component.on('render:started', (html, response, controls) => { - if (!this.isConnected) { - controls.shouldRender = false; - } - }); - component.on('connect', () => { - this.isConnected = true; - }); - component.on('disconnect', () => { - this.isConnected = false; - }); - } -} +//#endregion +//#region src/Component/plugins/PageUnloadingPlugin.ts +var PageUnloadingPlugin_default = class { + isConnected = false; + attachToComponent(component) { + component.on("render:started", (html, response, controls) => { + if (!this.isConnected) controls.shouldRender = false; + }); + component.on("connect", () => { + this.isConnected = true; + }); + component.on("disconnect", () => { + this.isConnected = false; + }); + } +}; -class PollingDirector { - constructor(component) { - this.isPollingActive = true; - this.pollingIntervals = []; - this.component = component; - } - addPoll(actionName, duration) { - this.polls.push({ actionName, duration }); - if (this.isPollingActive) { - this.initiatePoll(actionName, duration); - } - } - startAllPolling() { - if (this.isPollingActive) { - return; - } - this.isPollingActive = true; - this.polls.forEach(({ actionName, duration }) => { - this.initiatePoll(actionName, duration); - }); - } - stopAllPolling() { - this.isPollingActive = false; - this.pollingIntervals.forEach((interval) => { - clearInterval(interval); - }); - } - clearPolling() { - this.stopAllPolling(); - this.polls = []; - this.startAllPolling(); - } - initiatePoll(actionName, duration) { - let callback; - if (actionName === '$render') { - callback = () => { - this.component.render(); - }; - } - else { - callback = () => { - this.component.action(actionName, {}, 0); - }; - } - const timer = window.setInterval(() => { - callback(); - }, duration); - this.pollingIntervals.push(timer); - } -} +//#endregion +//#region src/PollingDirector.ts +var PollingDirector_default = class { + component; + isPollingActive = true; + polls; + pollingIntervals = []; + constructor(component) { + this.component = component; + } + addPoll(actionName, duration) { + this.polls.push({ + actionName, + duration + }); + if (this.isPollingActive) this.initiatePoll(actionName, duration); + } + startAllPolling() { + if (this.isPollingActive) return; + this.isPollingActive = true; + this.polls.forEach(({ actionName, duration }) => { + this.initiatePoll(actionName, duration); + }); + } + stopAllPolling() { + this.isPollingActive = false; + this.pollingIntervals.forEach((interval) => { + clearInterval(interval); + }); + } + clearPolling() { + this.stopAllPolling(); + this.polls = []; + this.startAllPolling(); + } + initiatePoll(actionName, duration) { + let callback; + if (actionName === "$render") callback = () => { + this.component.render(); + }; + else callback = () => { + this.component.action(actionName, {}, 0); + }; + const timer = window.setInterval(() => { + callback(); + }, duration); + this.pollingIntervals.push(timer); + } +}; -class PollingPlugin { - attachToComponent(component) { - this.element = component.element; - this.pollingDirector = new PollingDirector(component); - this.initializePolling(); - component.on('connect', () => { - this.pollingDirector.startAllPolling(); - }); - component.on('disconnect', () => { - this.pollingDirector.stopAllPolling(); - }); - component.on('render:finished', () => { - this.initializePolling(); - }); - } - addPoll(actionName, duration) { - this.pollingDirector.addPoll(actionName, duration); - } - clearPolling() { - this.pollingDirector.clearPolling(); - } - initializePolling() { - this.clearPolling(); - if (this.element.dataset.poll === undefined) { - return; - } - const rawPollConfig = this.element.dataset.poll; - const directives = parseDirectives(rawPollConfig || '$render'); - directives.forEach((directive) => { - let duration = 2000; - directive.modifiers.forEach((modifier) => { - switch (modifier.name) { - case 'delay': - if (modifier.value) { - duration = Number.parseInt(modifier.value); - } - break; - default: - console.warn(`Unknown modifier "${modifier.name}" in data-poll "${rawPollConfig}".`); - } - }); - this.addPoll(directive.action, duration); - }); - } -} +//#endregion +//#region src/Component/plugins/PollingPlugin.ts +var PollingPlugin_default = class { + element; + pollingDirector; + attachToComponent(component) { + this.element = component.element; + this.pollingDirector = new PollingDirector_default(component); + this.initializePolling(); + component.on("connect", () => { + this.pollingDirector.startAllPolling(); + }); + component.on("disconnect", () => { + this.pollingDirector.stopAllPolling(); + }); + component.on("render:finished", () => { + this.initializePolling(); + }); + } + addPoll(actionName, duration) { + this.pollingDirector.addPoll(actionName, duration); + } + clearPolling() { + this.pollingDirector.clearPolling(); + } + initializePolling() { + this.clearPolling(); + if (this.element.dataset.poll === void 0) return; + const rawPollConfig = this.element.dataset.poll; + const directives = parseDirectives(rawPollConfig || "$render"); + directives.forEach((directive) => { + let duration = 2e3; + directive.modifiers.forEach((modifier) => { + switch (modifier.name) { + case "delay": + if (modifier.value) duration = Number.parseInt(modifier.value); + break; + default: console.warn(`Unknown modifier "${modifier.name}" in data-poll "${rawPollConfig}".`); + } + }); + this.addPoll(directive.action, duration); + }); + } +}; +//#endregion +//#region src/url_utils.ts +/** +* Adapted from Livewire's history plugin. +* +* @see https://github.com/livewire/livewire/blob/d4839e3b2c23fc71e615e68bc29ff4de95751810/js/plugins/history/index.js +*/ +/** +* Check if a value is empty. +* +* Empty values are: +* - `null` and `undefined` +* - Empty strings +* - Empty arrays +* - Deeply empty objects +*/ function isValueEmpty(value) { - if (null === value || value === '' || undefined === value || (Array.isArray(value) && value.length === 0)) { - return true; - } - if (typeof value !== 'object') { - return false; - } - for (const key of Object.keys(value)) { - if (!isValueEmpty(value[key])) { - return false; - } - } - return true; + if (null === value || value === "" || void 0 === value || Array.isArray(value) && value.length === 0) return true; + if (typeof value !== "object") return false; + for (const key of Object.keys(value)) if (!isValueEmpty(value[key])) return false; + return true; } +/** +* Converts JavaScript data to bracketed query string notation. +* +* Input: `{ items: [['foo']] }` +* +* Output: `"items[0][0]=foo"` +*/ function toQueryString(data) { - const buildQueryStringEntries = (data, entries = {}, baseKey = '') => { - Object.entries(data).forEach(([iKey, iValue]) => { - const key = baseKey === '' ? iKey : `${baseKey}[${iKey}]`; - if ('' === baseKey && isValueEmpty(iValue)) { - entries[key] = ''; - } - else if (null !== iValue) { - if (typeof iValue === 'object') { - entries = { ...entries, ...buildQueryStringEntries(iValue, entries, key) }; - } - else { - entries[key] = encodeURIComponent(iValue) - .replace(/%20/g, '+') - .replace(/%2C/g, ','); - } - } - }); - return entries; - }; - const entries = buildQueryStringEntries(data); - return Object.entries(entries) - .map(([key, value]) => `${key}=${value}`) - .join('&'); + const buildQueryStringEntries = (data$1, entries$1 = {}, baseKey = "") => { + Object.entries(data$1).forEach(([iKey, iValue]) => { + const key = baseKey === "" ? iKey : `${baseKey}[${iKey}]`; + if ("" === baseKey && isValueEmpty(iValue)) entries$1[key] = ""; + else if (null !== iValue) if (typeof iValue === "object") entries$1 = { + ...entries$1, + ...buildQueryStringEntries(iValue, entries$1, key) + }; + else entries$1[key] = encodeURIComponent(iValue).replace(/%20/g, "+").replace(/%2C/g, ","); + }); + return entries$1; + }; + const entries = buildQueryStringEntries(data); + return Object.entries(entries).map(([key, value]) => `${key}=${value}`).join("&"); } +/** +* Converts bracketed query string notation to JavaScript data. +* +* Input: `"items[0][0]=foo"` +* +* Output: `{ items: [['foo']] }` +*/ function fromQueryString(search) { - search = search.replace('?', ''); - if (search === '') - return {}; - const insertDotNotatedValueIntoData = (key, value, data) => { - const [first, second, ...rest] = key.split('.'); - if (!second) { - data[key] = value; - return value; - } - if (data[first] === undefined) { - data[first] = Number.isNaN(Number.parseInt(second)) ? {} : []; - } - insertDotNotatedValueIntoData([second, ...rest].join('.'), value, data[first]); - }; - const entries = search.split('&').map((i) => i.split('=')); - const data = {}; - entries.forEach(([key, value]) => { - value = decodeURIComponent(String(value || '').replace(/\+/g, '%20')); - if (!key.includes('[')) { - data[key] = value; - } - else { - if ('' === value) - return; - const dotNotatedKey = key.replace(/\[/g, '.').replace(/]/g, ''); - insertDotNotatedValueIntoData(dotNotatedKey, value, data); - } - }); - return data; -} -class UrlUtils extends URL { - has(key) { - const data = this.getData(); - return Object.keys(data).includes(key); - } - set(key, value) { - const data = this.getData(); - data[key] = value; - this.setData(data); - } - get(key) { - return this.getData()[key]; - } - remove(key) { - const data = this.getData(); - delete data[key]; - this.setData(data); - } - getData() { - if (!this.search) { - return {}; - } - return fromQueryString(this.search); - } - setData(data) { - this.search = toQueryString(data); - } -} -class HistoryStrategy { - static replace(url) { - history.replaceState(history.state, '', url); - } + search = search.replace("?", ""); + if (search === "") return {}; + const insertDotNotatedValueIntoData = (key, value, data$1) => { + const [first, second, ...rest] = key.split("."); + if (!second) { + data$1[key] = value; + return value; + } + if (data$1[first] === void 0) data$1[first] = Number.isNaN(Number.parseInt(second)) ? {} : []; + insertDotNotatedValueIntoData([second, ...rest].join("."), value, data$1[first]); + }; + const entries = search.split("&").map((i) => i.split("=")); + const data = {}; + entries.forEach(([key, value]) => { + value = decodeURIComponent(String(value || "").replace(/\+/g, "%20")); + if (!key.includes("[")) data[key] = value; + else { + if ("" === value) return; + const dotNotatedKey = key.replace(/\[/g, ".").replace(/]/g, ""); + insertDotNotatedValueIntoData(dotNotatedKey, value, data); + } + }); + return data; } +/** +* Wraps a URL to manage search parameters with common map functions. +*/ +var UrlUtils = class extends URL { + has(key) { + const data = this.getData(); + return Object.keys(data).includes(key); + } + set(key, value) { + const data = this.getData(); + data[key] = value; + this.setData(data); + } + get(key) { + return this.getData()[key]; + } + remove(key) { + const data = this.getData(); + delete data[key]; + this.setData(data); + } + getData() { + if (!this.search) return {}; + return fromQueryString(this.search); + } + setData(data) { + this.search = toQueryString(data); + } +}; +var HistoryStrategy = class { + static replace(url) { + history.replaceState(history.state, "", url); + } +}; -class QueryStringPlugin { - constructor(mapping) { - this.mapping = mapping; - } - attachToComponent(component) { - component.on('render:finished', (component) => { - const urlUtils = new UrlUtils(window.location.href); - const currentUrl = urlUtils.toString(); - Object.entries(this.mapping).forEach(([prop, mapping]) => { - const value = component.valueStore.get(prop); - urlUtils.set(mapping.name, value); - }); - if (currentUrl !== urlUtils.toString()) { - HistoryStrategy.replace(urlUtils); - } - }); - } -} +//#endregion +//#region src/Component/plugins/QueryStringPlugin.ts +var QueryStringPlugin_default = class { + constructor(mapping) { + this.mapping = mapping; + } + attachToComponent(component) { + component.on("render:finished", (component$1) => { + const urlUtils = new UrlUtils(window.location.href); + const currentUrl = urlUtils.toString(); + Object.entries(this.mapping).forEach(([prop, mapping]) => { + const value = component$1.valueStore.get(prop); + urlUtils.set(mapping.name, value); + }); + if (currentUrl !== urlUtils.toString()) HistoryStrategy.replace(urlUtils); + }); + } +}; -class SetValueOntoModelFieldsPlugin { - attachToComponent(component) { - this.synchronizeValueOfModelFields(component); - component.on('render:finished', () => { - this.synchronizeValueOfModelFields(component); - }); - } - synchronizeValueOfModelFields(component) { - component.element.querySelectorAll('[data-model]').forEach((element) => { - if (!(element instanceof HTMLElement)) { - throw new Error('Invalid element using data-model.'); - } - if (element instanceof HTMLFormElement) { - return; - } - if (!elementBelongsToThisComponent(element, component)) { - return; - } - const modelDirective = getModelDirectiveFromElement(element); - if (!modelDirective) { - return; - } - const modelName = modelDirective.action; - if (component.getUnsyncedModels().includes(modelName)) { - return; - } - if (component.valueStore.has(modelName)) { - setValueOnElement(element, component.valueStore.get(modelName)); - } - if (element instanceof HTMLSelectElement && !element.multiple) { - component.valueStore.set(modelName, getValueFromElement(element, component.valueStore)); - } - }); - } -} +//#endregion +//#region src/Component/plugins/SetValueOntoModelFieldsPlugin.ts +/** +* Handles setting the "value" onto data-model fields automatically from the data store. +*/ +var SetValueOntoModelFieldsPlugin_default = class { + attachToComponent(component) { + this.synchronizeValueOfModelFields(component); + component.on("render:finished", () => { + this.synchronizeValueOfModelFields(component); + }); + } + /** + * Sets the "value" of all model fields to the component data. + * + * This is called when the component initializes and after re-render. + * Take the following element: + * + * + * + * This method will set the "value" of that element to the value of + * the "firstName" model. + */ + synchronizeValueOfModelFields(component) { + component.element.querySelectorAll("[data-model]").forEach((element) => { + if (!(element instanceof HTMLElement)) throw new Error("Invalid element using data-model."); + if (element instanceof HTMLFormElement) return; + if (!elementBelongsToThisComponent(element, component)) return; + const modelDirective = getModelDirectiveFromElement(element); + if (!modelDirective) return; + const modelName = modelDirective.action; + if (component.getUnsyncedModels().includes(modelName)) return; + if (component.valueStore.has(modelName)) setValueOnElement(element, component.valueStore.get(modelName)); + if (element instanceof HTMLSelectElement && !element.multiple) component.valueStore.set(modelName, getValueFromElement(element, component.valueStore)); + }); + } +}; -class ValidatedFieldsPlugin { - attachToComponent(component) { - component.on('model:set', (modelName) => { - this.handleModelSet(modelName, component.valueStore); - }); - } - handleModelSet(modelName, valueStore) { - if (valueStore.has('validatedFields')) { - const validatedFields = [...valueStore.get('validatedFields')]; - if (!validatedFields.includes(modelName)) { - validatedFields.push(modelName); - } - valueStore.set('validatedFields', validatedFields); - } - } -} +//#endregion +//#region src/Component/plugins/ValidatedFieldsPlugin.ts +var ValidatedFieldsPlugin_default = class { + attachToComponent(component) { + component.on("model:set", (modelName) => { + this.handleModelSet(modelName, component.valueStore); + }); + } + handleModelSet(modelName, valueStore) { + if (valueStore.has("validatedFields")) { + const validatedFields = [...valueStore.get("validatedFields")]; + if (!validatedFields.includes(modelName)) validatedFields.push(modelName); + valueStore.set("validatedFields", validatedFields); + } + } +}; -class LiveControllerDefault extends Controller { - constructor() { - super(...arguments); - this.pendingActionTriggerModelElement = null; - this.elementEventListeners = [ - { event: 'input', callback: (event) => this.handleInputEvent(event) }, - { event: 'change', callback: (event) => this.handleChangeEvent(event) }, - ]; - this.pendingFiles = {}; - } - initialize() { - this.mutationObserver = new MutationObserver(this.onMutations.bind(this)); - this.createComponent(); - } - connect() { - this.connectComponent(); - this.mutationObserver.observe(this.element, { - attributes: true, - }); - } - disconnect() { - this.disconnectComponent(); - this.mutationObserver.disconnect(); - } - update(event) { - if (event.type === 'input' || event.type === 'change') { - throw new Error(`Since LiveComponents 2.3, you no longer need data-action="live#update" on form elements. Found on element: ${getElementAsTagText(event.currentTarget)}`); - } - this.updateModelFromElementEvent(event.currentTarget, null); - } - action(event) { - const params = event.params; - if (!params.action) { - throw new Error(`No action name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-action-param" attribute?`); - } - const rawAction = params.action; - const actionArgs = { ...params }; - delete actionArgs.action; - const directives = parseDirectives(rawAction); - let debounce = false; - directives.forEach((directive) => { - let pendingFiles = {}; - const validModifiers = new Map(); - validModifiers.set('stop', () => { - event.stopPropagation(); - }); - validModifiers.set('self', () => { - if (event.target !== event.currentTarget) { - return; - } - }); - validModifiers.set('debounce', (modifier) => { - debounce = modifier.value ? Number.parseInt(modifier.value) : true; - }); - validModifiers.set('files', (modifier) => { - if (!modifier.value) { - pendingFiles = this.pendingFiles; - } - else if (this.pendingFiles[modifier.value]) { - pendingFiles[modifier.value] = this.pendingFiles[modifier.value]; - } - }); - directive.modifiers.forEach((modifier) => { - if (validModifiers.has(modifier.name)) { - const callable = validModifiers.get(modifier.name) ?? (() => { }); - callable(modifier); - return; - } - console.warn(`Unknown modifier ${modifier.name} in action "${rawAction}". Available modifiers are: ${Array.from(validModifiers.keys()).join(', ')}.`); - }); - for (const [key, input] of Object.entries(pendingFiles)) { - if (input.files) { - this.component.files(key, input); - } - delete this.pendingFiles[key]; - } - this.component.action(directive.action, actionArgs, debounce); - if (getModelDirectiveFromElement(event.currentTarget, false)) { - this.pendingActionTriggerModelElement = event.currentTarget; - } - }); - } - $render() { - return this.component.render(); - } - emit(event) { - this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => { - this.component.emit(name, data, nameMatch); - }); - } - emitUp(event) { - this.getEmitDirectives(event).forEach(({ name, data, nameMatch }) => { - this.component.emitUp(name, data, nameMatch); - }); - } - emitSelf(event) { - this.getEmitDirectives(event).forEach(({ name, data }) => { - this.component.emitSelf(name, data); - }); - } - $updateModel(model, value, shouldRender = true, debounce = true) { - return this.component.set(model, value, shouldRender, debounce); - } - propsUpdatedFromParentValueChanged() { - this.component._updateFromParentProps(this.propsUpdatedFromParentValue); - } - fingerprintValueChanged() { - this.component.fingerprint = this.fingerprintValue; - } - getEmitDirectives(event) { - const params = event.params; - if (!params.event) { - throw new Error(`No event name provided on element: ${getElementAsTagText(event.currentTarget)}. Did you forget to add the "data-live-event-param" attribute?`); - } - const eventInfo = params.event; - const eventArgs = { ...params }; - delete eventArgs.event; - const directives = parseDirectives(eventInfo); - const emits = []; - directives.forEach((directive) => { - let nameMatch = null; - directive.modifiers.forEach((modifier) => { - switch (modifier.name) { - case 'name': - nameMatch = modifier.value; - break; - default: - throw new Error(`Unknown modifier ${modifier.name} in event "${eventInfo}".`); - } - }); - emits.push({ - name: directive.action, - data: eventArgs, - nameMatch, - }); - }); - return emits; - } - createComponent() { - const id = this.element.id || null; - this.component = new Component(this.element, this.nameValue, this.propsValue, this.listenersValue, id, LiveControllerDefault.backendFactory(this), new StimulusElementDriver(this)); - this.proxiedComponent = proxifyComponent(this.component); - Object.defineProperty(this.element, '__component', { - value: this.proxiedComponent, - writable: true, - }); - if (this.hasDebounceValue) { - this.component.defaultDebounce = this.debounceValue; - } - const plugins = [ - new LoadingPlugin(), - new LazyPlugin(), - new ValidatedFieldsPlugin(), - new PageUnloadingPlugin(), - new PollingPlugin(), - new SetValueOntoModelFieldsPlugin(), - new QueryStringPlugin(this.queryMappingValue), - new ChildComponentPlugin(this.component), - ]; - plugins.forEach((plugin) => { - this.component.addPlugin(plugin); - }); - } - connectComponent() { - this.component.connect(); - this.mutationObserver.observe(this.element, { - attributes: true, - }); - this.elementEventListeners.forEach(({ event, callback }) => { - this.component.element.addEventListener(event, callback); - }); - this.dispatchEvent('connect'); - } - disconnectComponent() { - this.component.disconnect(); - this.elementEventListeners.forEach(({ event, callback }) => { - this.component.element.removeEventListener(event, callback); - }); - this.dispatchEvent('disconnect'); - } - handleInputEvent(event) { - const target = event.target; - if (!target) { - return; - } - this.updateModelFromElementEvent(target, 'input'); - } - handleChangeEvent(event) { - const target = event.target; - if (!target) { - return; - } - this.updateModelFromElementEvent(target, 'change'); - } - updateModelFromElementEvent(element, eventName) { - if (!elementBelongsToThisComponent(element, this.component)) { - return; - } - if (!(element instanceof HTMLElement)) { - throw new Error('Could not update model for non HTMLElement'); - } - if (element instanceof HTMLInputElement && element.type === 'file') { - const key = element.name; - if (element.files?.length) { - this.pendingFiles[key] = element; - } - else if (this.pendingFiles[key]) { - delete this.pendingFiles[key]; - } - } - const modelDirective = getModelDirectiveFromElement(element, false); - if (!modelDirective) { - return; - } - const modelBinding = getModelBinding(modelDirective); - if (!modelBinding.targetEventName) { - modelBinding.targetEventName = 'input'; - } - if (this.pendingActionTriggerModelElement === element) { - modelBinding.shouldRender = false; - } - if (eventName === 'change' && modelBinding.targetEventName === 'input') { - modelBinding.targetEventName = 'change'; - } - if (eventName && modelBinding.targetEventName !== eventName) { - return; - } - if (false === modelBinding.debounce) { - if (modelBinding.targetEventName === 'input') { - modelBinding.debounce = true; - } - else { - modelBinding.debounce = 0; - } - } - const finalValue = getValueFromElement(element, this.component.valueStore); - if (isTextualInputElement(element) || isTextareaElement(element)) { - if (modelBinding.minLength !== null && - typeof finalValue === 'string' && - finalValue.length < modelBinding.minLength) { - return; - } - if (modelBinding.maxLength !== null && - typeof finalValue === 'string' && - finalValue.length > modelBinding.maxLength) { - return; - } - } - if (isNumericalInputElement(element)) { - const numericValue = Number(finalValue); - if (modelBinding.minValue !== null && numericValue < modelBinding.minValue) { - return; - } - if (modelBinding.maxValue !== null && numericValue > modelBinding.maxValue) { - return; - } - } - this.component.set(modelBinding.modelName, finalValue, modelBinding.shouldRender, modelBinding.debounce); - } - dispatchEvent(name, detail = {}, canBubble = true, cancelable = false) { - detail.controller = this; - detail.component = this.proxiedComponent; - this.dispatch(name, { detail, prefix: 'live', cancelable, bubbles: canBubble }); - } - onMutations(mutations) { - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && - mutation.attributeName === 'id' && - this.element.id !== this.component.id) { - this.disconnectComponent(); - this.createComponent(); - this.connectComponent(); - } - }); - } -} -LiveControllerDefault.values = { - name: String, - url: String, - props: { type: Object, default: {} }, - propsUpdatedFromParent: { type: Object, default: {} }, - listeners: { type: Array, default: [] }, - eventsToEmit: { type: Array, default: [] }, - eventsToDispatch: { type: Array, default: [] }, - debounce: { type: Number, default: 150 }, - fingerprint: { type: String, default: '' }, - requestMethod: { type: String, default: 'post' }, - queryMapping: { type: Object, default: {} }, +//#endregion +//#region src/live_controller.ts +var LiveControllerDefault = class LiveControllerDefault extends Controller { + static values = { + name: String, + url: String, + props: { + type: Object, + default: {} + }, + propsUpdatedFromParent: { + type: Object, + default: {} + }, + listeners: { + type: Array, + default: [] + }, + eventsToEmit: { + type: Array, + default: [] + }, + eventsToDispatch: { + type: Array, + default: [] + }, + debounce: { + type: Number, + default: 150 + }, + fingerprint: { + type: String, + default: "" + }, + requestMethod: { + type: String, + default: "post" + }, + queryMapping: { + type: Object, + default: {} + } + }; + /** The component, wrapped in the convenience Proxy */ + proxiedComponent; + mutationObserver; + /** The raw Component object */ + component; + pendingActionTriggerModelElement = null; + elementEventListeners = [{ + event: "input", + callback: (event) => this.handleInputEvent(event) + }, { + event: "change", + callback: (event) => this.handleChangeEvent(event) + }]; + pendingFiles = {}; + static backendFactory = (controller) => new Backend_default(controller.urlValue, controller.requestMethodValue); + initialize() { + this.mutationObserver = new MutationObserver(this.onMutations.bind(this)); + this.createComponent(); + } + connect() { + this.connectComponent(); + this.mutationObserver.observe(this.element, { attributes: true }); + } + disconnect() { + this.disconnectComponent(); + this.mutationObserver.disconnect(); + } + /** + * Called to update one piece of the model. + * + *