@nutin/cli 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +322 -0
- package/README.md +14 -6
- package/bin/add-feature.mjs +1 -1
- package/bin/cli.mjs +1 -1
- package/bin/create-app.mjs +1 -1
- package/bin/update-app.mjs +1 -1
- package/lib/index.mjs +6 -2
- package/lib/src/add-feature/base-template-patcher.mjs +139 -0
- package/lib/src/add-feature/feature-adder.mjs +20 -9
- package/lib/src/add-feature/feature-context-builder.mjs +1 -0
- package/lib/src/add-feature/nutin-config-updater.mjs +34 -8
- package/lib/src/add-feature/package-json-updater.mjs +7 -9
- package/lib/src/common/feature-registry.mjs +4 -0
- package/lib/src/common/file-generator.mjs +25 -3
- package/lib/src/common/git.mjs +31 -0
- package/lib/src/common/meta-bootstrap-prompt.mjs +28 -2
- package/lib/src/common/package-json-helper.mjs +14 -4
- package/lib/src/common/project-meta.mjs +2 -2
- package/lib/src/common/template-compiler.mjs +2 -0
- package/lib/src/common/transpile-ts.mjs +94 -0
- package/lib/src/create-app/context-builder.mjs +1 -0
- package/lib/src/create-app/git-manager.mjs +12 -2
- package/lib/src/create-app/json-manager.mjs +21 -18
- package/lib/src/create-app/new-app-prompt.mjs +4 -2
- package/lib/src/create-app/success-message.mjs +1 -1
- package/lib/src/update-app/conflict-report-writer.mjs +33 -21
- package/lib/src/update-app/json-drift.mjs +54 -0
- package/lib/src/update-app/project-updater.mjs +123 -14
- package/lib/src/update-app/template-differ.mjs +5 -5
- package/lib/src/update-app/three-way-merge.mjs +42 -0
- package/lib/src/update-app/update-context-builder.mjs +1 -0
- package/lib/src/update-app/update-summary-printer.mjs +10 -2
- package/package.json +12 -11
- package/templates/base/.gitignore.hbs +1 -0
- package/templates/base/AGENTS.md.hbs +45 -43
- package/templates/base/GETTING_STARTED.md.hbs +32 -23
- package/templates/base/README.md.hbs +7 -4
- package/templates/base/nutin.config.js.hbs +1 -1
- package/templates/base/src/app/components/.gitkeep +0 -0
- package/templates/base/src/app/globals.d.ts.hbs +6 -3
- package/templates/base/src/app/guards.ts.hbs +1 -3
- package/templates/base/src/app/main.ts.hbs +2 -9
- package/templates/base/src/app/routes.ts.hbs +9 -3
- package/templates/base/src/app/services/.gitkeep +0 -0
- package/templates/base/src/app/views/home/home.view.html.hbs +1 -1
- package/templates/base/src/app/views/home/home.view.ts.hbs +2 -2
- package/templates/base/src/app/views/not-found/not-found.view.ts.hbs +2 -2
- package/templates/base/src/core/base-classes/base-component/base-component.test.js.hbs +461 -1
- package/templates/base/src/core/base-classes/base-component/base-component.ts.hbs +39 -48
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.test.js.hbs +36 -1
- package/templates/base/src/core/base-classes/base-component/helpers/catalog.helper.ts.hbs +47 -77
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.test.js.hbs +307 -21
- package/templates/base/src/core/base-classes/base-component/helpers/children.helper.ts.hbs +144 -26
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.test.js.hbs +105 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +136 -62
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs +85 -4
- package/templates/base/src/core/base-classes/base-component/helpers/event.helper.ts.hbs +101 -51
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.test.js.hbs +404 -0
- package/templates/base/src/core/base-classes/base-component/helpers/html.helper.ts.hbs +242 -0
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.test.js.hbs +70 -1
- package/templates/base/src/core/base-classes/base-component/helpers/i18n.helper.ts.hbs +54 -15
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.test.js.hbs +12 -1
- package/templates/base/src/core/base-classes/base-component/helpers/pipe.helper.ts.hbs +39 -36
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs +179 -0
- package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs +93 -0
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.test.js.hbs +135 -17
- package/templates/base/src/core/base-classes/base-component/helpers/security.helper.ts.hbs +199 -81
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.test.js.hbs +40 -23
- package/templates/base/src/core/base-classes/base-component/helpers/token.helper.ts.hbs +67 -65
- package/templates/base/src/core/base-classes/component/component.test.js.hbs +17 -17
- package/templates/base/src/core/base-classes/component/component.ts.hbs +8 -41
- package/templates/base/src/core/base-classes/component/helpers/config.helper.test.js.hbs +1 -1
- package/templates/base/src/core/base-classes/component/helpers/config.helper.ts.hbs +37 -39
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.test.js.hbs +14 -1
- package/templates/base/src/core/base-classes/component/helpers/data-binding.helper.ts.hbs +33 -31
- package/templates/base/src/core/base-classes/service/service.test.js.hbs +32 -0
- package/templates/base/src/core/base-classes/service/service.ts.hbs +21 -13
- package/templates/base/src/core/base-classes/view/view.test.js.hbs +4 -0
- package/templates/base/src/core/base-classes/view/view.ts.hbs +11 -8
- package/templates/base/src/core/index.ts.hbs +4 -2
- package/templates/base/src/core/internals.d.ts.hbs +199 -20
- package/templates/base/src/core/services/event-bus/event-bus.test.js.hbs +18 -0
- package/templates/base/src/core/services/event-bus/event-bus.ts.hbs +26 -25
- package/templates/base/src/core/services/event-bus/lifecycle.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/event-bus/navigation.facade.ts.hbs +3 -3
- package/templates/base/src/core/services/globals/globals.ts.hbs +7 -32
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.test.js.hbs +133 -50
- package/templates/base/src/core/services/http-client/helpers/http-builder.helper.ts.hbs +147 -39
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.test.js.hbs +75 -19
- package/templates/base/src/core/services/http-client/helpers/http-manager.helper.ts.hbs +73 -47
- package/templates/base/src/core/services/http-client/http-client.test.js.hbs +339 -232
- package/templates/base/src/core/services/http-client/http-client.ts.hbs +37 -25
- package/templates/base/src/core/services/i18n/i18n.test.js.hbs +57 -16
- package/templates/base/src/core/services/i18n/i18n.ts.hbs +42 -42
- package/templates/base/src/core/services/i18n/languages.ts.hbs +0 -2
- package/templates/base/src/core/services/pipe-registry/pipe-registry.ts.hbs +4 -6
- package/templates/base/src/core/services/pipe-registry/pipes.test.js.hbs +10 -0
- package/templates/base/src/core/services/pipe-registry/pipes.ts.hbs +4 -2
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.test.js.hbs +217 -4
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +210 -65
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.test.js.hbs +26 -27
- package/templates/base/src/core/services/router/helpers/route-guard-manager.helper.ts.hbs +38 -60
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.test.js.hbs +4 -4
- package/templates/base/src/core/services/router/helpers/view-render-manager.helper.ts.hbs +30 -30
- package/templates/base/src/core/services/router/router.test.js.hbs +321 -0
- package/templates/base/src/core/services/router/router.ts.hbs +122 -54
- package/templates/base/src/index.html.hbs +1 -0
- package/templates/base/src/styles/_styles.scss.hbs +1 -113
- package/templates/base/src/styles/main.scss.hbs +1 -1
- package/templates/base/tools/builder/builder.config.js.hbs +6 -0
- package/templates/base/tools/builder/builder.js.hbs +26 -8
- package/templates/base/tools/builder/core/app/compile-ts.js.hbs +81 -6
- package/templates/base/tools/builder/core/app/copy-static.js.hbs +12 -1
- package/templates/base/tools/builder/core/app/handler-refs.js.hbs +114 -0
- package/templates/base/tools/builder/core/app/handler-refs.test.js.hbs +70 -0
- package/templates/base/tools/builder/core/app/paths.js.hbs +17 -0
- package/templates/base/tools/builder/core/app/route-keys.js.hbs +47 -0
- package/templates/base/tools/builder/core/app/route-keys.test.js.hbs +97 -0
- package/templates/base/tools/builder/core/app/ts-errors.js.hbs +58 -0
- package/templates/base/tools/builder/core/app/ts-errors.test.js.hbs +84 -0
- package/templates/base/tools/builder/core/app/validate-routes.js.hbs +21 -55
- package/templates/base/tools/builder/core/finalize-build.js.hbs +6 -0
- package/templates/base/tools/builder/core/html-index/add-tags.js.hbs +1 -4
- package/templates/base/tools/builder/core/html-index/add-tags.test.js.hbs +3 -3
- package/templates/base/tools/builder/core/html-index/validate-html.js.hbs +10 -2
- package/templates/base/tools/builder/core/html-templates/merge-templates.js.hbs +111 -23
- package/templates/base/tools/builder/core/html-templates/minify-templates.js.hbs +8 -0
- package/templates/base/tools/builder/core/html-templates/process-html-templates.js.hbs +5 -3
- package/templates/base/tools/builder/core/html-templates/template-literal.js.hbs +193 -0
- package/templates/base/tools/builder/core/html-templates/template-literal.test.js.hbs +69 -0
- package/templates/base/tools/builder/core/i18n/build-i18n.js.hbs +14 -6
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.js.hbs +82 -0
- package/templates/base/tools/builder/core/prod-bundle/bundle-options.test.js.hbs +65 -0
- package/templates/base/tools/builder/core/prod-bundle/compress-files.js.hbs +6 -6
- package/templates/base/tools/builder/core/prod-bundle/esbuild.js.hbs +25 -8
- package/templates/base/tools/builder/core/prod-bundle/hash-files.js.hbs +4 -4
- package/templates/base/tools/builder/core/seo/generate-robots-txt.js.hbs +31 -13
- package/templates/base/tools/builder/core/seo/generate-robots-txt.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/generate-seo-files.js.hbs +3 -3
- package/templates/base/tools/builder/core/seo/generate-seo-html.js.hbs +61 -15
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.js.hbs +22 -7
- package/templates/base/tools/builder/core/seo/generate-sitemap-xml.test.js.hbs +30 -0
- package/templates/base/tools/builder/core/seo/html-substitutions.js.hbs +48 -18
- package/templates/base/tools/builder/core/seo/html-substitutions.test.js.hbs +45 -0
- package/templates/base/tools/builder/core/seo/route-output.js.hbs +26 -8
- package/templates/base/tools/builder/core/seo/route-output.test.js.hbs +31 -0
- package/templates/base/tools/builder/core/seo/seo-routes.js.hbs +29 -0
- package/templates/base/tools/builder/core/seo/seo-routes.test.js.hbs +21 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-bundle.js.hbs +6 -7
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.js.hbs +34 -5
- package/templates/base/tools/builder/core/seo/ssr/ssr-polyfills.test.js.hbs +48 -0
- package/templates/base/tools/builder/core/seo/ssr/ssr-render.js.hbs +26 -9
- package/templates/base/tools/builder/core/styles/sass.js.hbs +7 -5
- package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
- package/templates/base/tools/dev/dev-serve.js.hbs +71 -7
- package/templates/base/tools/dev/serve.js.hbs +152 -22
- package/templates/base/tools/dev/watcher.js.hbs +75 -28
- package/templates/base/tools/generator/generator.js.hbs +23 -18
- package/templates/base/tools/generator/handle-file.js.hbs +4 -24
- package/templates/base/tools/generator/handle-file.test.js.hbs +4 -23
- package/templates/base/tools/generator/templates/component/component.template.js.hbs +3 -3
- package/templates/base/tools/generator/templates/component/component.template.test.js.hbs +3 -3
- package/templates/base/tools/generator/templates/service/service.template.js.hbs +1 -1
- package/templates/base/tools/generator/templates/service/service.template.test.js.hbs +1 -1
- package/templates/base/tools/generator/templates/view/view.template.js.hbs +2 -2
- package/templates/base/tools/generator/templates/view/view.template.test.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/coverage/write-uncovered-report.js.hbs +1 -1
- package/templates/base/tools/testin-nutin/core/globals/assertion-lib.js.hbs +2 -2
- package/templates/base/tools/testin-nutin/core/{tests/assertions.test.js.hbs → globals/assertion-lib.test.js.hbs} +6 -0
- package/templates/base/tools/testin-nutin/core/globals/register-test-globals.js.hbs +6 -7
- package/templates/base/tools/testin-nutin/core/{tests/globals-each-all.test.js.hbs → globals/register-test-globals.test.js.hbs} +12 -0
- package/templates/base/tools/testin-nutin/core/printer.js.hbs +10 -4
- package/templates/base/tools/testin-nutin/core/queue/test-queue.js.hbs +8 -8
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.js.hbs +27 -25
- package/templates/base/tools/testin-nutin/mocks/mock-event-bus.test.js.hbs +30 -22
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.js.hbs +7 -10
- package/templates/base/tools/testin-nutin/mocks/mock-http-client.test.js.hbs +12 -0
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.js.hbs +47 -22
- package/templates/base/tools/testin-nutin/mocks/mock-i18n.test.js.hbs +47 -26
- package/templates/base/tools/testin-nutin/mocks/mock-router.js.hbs +19 -11
- package/templates/base/tools/testin-nutin/mocks/mock-router.test.js.hbs +24 -7
- package/templates/base/tools/testin-nutin/runner.js.hbs +12 -3
- package/templates/base/tools/testin-nutin/watch-tests.js.hbs +45 -20
- package/templates/base/tools/utils/build-lock.js.hbs +97 -0
- package/templates/base/tools/utils/ensure-deps.js.hbs +72 -0
- package/templates/base/tools/utils/ensure-deps.test.js.hbs +17 -0
- package/templates/base/tools/utils/format.js.hbs +1 -1
- package/templates/base/tools/utils/index.js.hbs +3 -1
- package/templates/base/tools/utils/print.js.hbs +2 -2
- package/templates/base/tools/utils/resolve-port.js.hbs +30 -0
- package/templates/base/tools/utils/resolve-port.test.js.hbs +47 -0
- package/templates/base/tools/utils/run-command.js.hbs +30 -1
- package/templates/base/tools/utils/run-command.test.js.hbs +12 -1
- package/templates/base/tools/utils/run-script.js.hbs +2 -2
- package/templates/features/docker/tools/docker/Dockerfile.template.hbs +5 -2
- package/templates/features/docker/tools/docker/nginx.conf.template +19 -2
- package/templates/features/docker/tools/docker/scripts/docker-ports.js.hbs +1 -1
- package/templates/features/docker/tools/docker/scripts/docker-ports.test.js.hbs +2 -2
- package/templates/features/docker/tools/docker/scripts/locale-redirects.js.hbs +28 -0
- package/templates/features/docker/tools/docker/scripts/locale-redirects.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.js.hbs +25 -0
- package/templates/features/docker/tools/docker/scripts/markdown-sources-copy.test.js.hbs +34 -0
- package/templates/features/docker/tools/docker/scripts/render-docker-template.js.hbs +3 -1
- package/templates/features/docker/tools/docker/scripts/render-docker-template.test.js.hbs +12 -0
- package/templates/features/docker/tools/docker/scripts/validate-docker.js.hbs +29 -0
- package/templates/features/markdown/markdown-content/getting-started.md.hbs +28 -0
- package/templates/features/markdown/markdown-content/writing-pages.md.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.scss.hbs +75 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.test.js.hbs +40 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-content/markdown-content.component.ts.hbs +65 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.test.js.hbs +67 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-landing/markdown-landing.component.ts.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.scss.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.test.js.hbs +59 -0
- package/templates/features/markdown/src/app/markdown/components/markdown-nav/markdown-nav.component.ts.hbs +72 -0
- package/templates/features/markdown/src/app/markdown/locales/en.json.hbs +6 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.test.js.hbs +63 -0
- package/templates/features/markdown/src/app/markdown/markdown-guards.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.test.js.hbs +78 -0
- package/templates/features/markdown/src/app/markdown/markdown-i18n.ts.hbs +31 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.test.js.hbs +98 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifest.ts.hbs +157 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.test.js.hbs +87 -0
- package/templates/features/markdown/src/app/markdown/markdown-manifests.service.ts.hbs +50 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.test.js.hbs +74 -0
- package/templates/features/markdown/src/app/markdown/markdown-routes.ts.hbs +26 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.test.js.hbs +22 -0
- package/templates/features/markdown/src/app/markdown/markdown-sources.ts.hbs +42 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.html.hbs +4 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.scss.hbs +19 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.test.js.hbs +125 -0
- package/templates/features/markdown/src/app/markdown/view/markdown.view.ts.hbs +146 -0
- package/templates/features/markdown/tools/builder/core/markdown/generate-markdown-pages.js.hbs +71 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.js.hbs +582 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-compiler.test.js.hbs +390 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.js.hbs +155 -0
- package/templates/features/markdown/tools/builder/core/markdown/markdown-seo-routes.test.js.hbs +112 -0
- package/templates/base/src/app/components/index.ts.hbs +0 -1
- package/templates/base/src/app/services/index.ts.hbs +0 -1
- package/templates/base/src/app/views/index.ts.hbs +0 -3
- /package/templates/base/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as DomHelper from '#root/dist/src/core/base-classes/base-component/helpers/dom.helper.js';
|
|
2
2
|
|
|
3
3
|
describe('DomHelper', () => {
|
|
4
4
|
let container;
|
|
@@ -20,10 +20,19 @@ describe('DomHelper', () => {
|
|
|
20
20
|
expect(container.contains(el)).toBe(true);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it('mountElement
|
|
23
|
+
it('mountElement mounts nothing and logs an error when the target is not found', () => {
|
|
24
24
|
const el = document.createElement('span');
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const errorSpy = spyOn(console, 'error').andCallFake(() => {});
|
|
26
|
+
try {
|
|
27
|
+
DomHelper.mountElement(el, '#does-not-exist');
|
|
28
|
+
DomHelper.mountElement(document.createElement('div'), null);
|
|
29
|
+
expect(el.parentElement).toBe(null);
|
|
30
|
+
expect(errorSpy.callCount).toBe(2);
|
|
31
|
+
expect(errorSpy.calls[0][0]).toBe('Nothing to mount <span> into: "#does-not-exist" isn\'t in the page.');
|
|
32
|
+
expect(errorSpy.calls[1][0]).toContain('its mount target is null');
|
|
33
|
+
} finally {
|
|
34
|
+
errorSpy.restore();
|
|
35
|
+
}
|
|
27
36
|
});
|
|
28
37
|
|
|
29
38
|
it('mountElement replaces an HTMLElement target with the element', () => {
|
|
@@ -39,6 +48,45 @@ describe('DomHelper', () => {
|
|
|
39
48
|
expect(container.contains(el)).toBe(true);
|
|
40
49
|
});
|
|
41
50
|
|
|
51
|
+
it('mountElement carries the placeholder attributes onto the element, merging classes', () => {
|
|
52
|
+
const placeholder = document.createElement('div');
|
|
53
|
+
placeholder.className = 'my-class other-class';
|
|
54
|
+
placeholder.id = 'foo';
|
|
55
|
+
placeholder.setAttribute('aria-label', 'Label');
|
|
56
|
+
placeholder.setAttribute('data-extra', 'bar');
|
|
57
|
+
container.appendChild(placeholder);
|
|
58
|
+
|
|
59
|
+
const el = document.createElement('span');
|
|
60
|
+
el.className = 'own-class';
|
|
61
|
+
DomHelper.mountElement(el, placeholder);
|
|
62
|
+
|
|
63
|
+
expect(el.classList.contains('own-class')).toBe(true);
|
|
64
|
+
expect(el.classList.contains('my-class')).toBe(true);
|
|
65
|
+
expect(el.classList.contains('other-class')).toBe(true);
|
|
66
|
+
expect(el.id).toBe('foo');
|
|
67
|
+
expect(el.getAttribute('aria-label')).toBe('Label');
|
|
68
|
+
expect(el.getAttribute('data-extra')).toBe('bar');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('mountElement does not carry data-component over from the placeholder', () => {
|
|
72
|
+
const placeholder = document.createElement('div');
|
|
73
|
+
placeholder.setAttribute('data-component', 'widget');
|
|
74
|
+
container.appendChild(placeholder);
|
|
75
|
+
|
|
76
|
+
const el = document.createElement('span');
|
|
77
|
+
DomHelper.mountElement(el, placeholder);
|
|
78
|
+
|
|
79
|
+
expect(el.hasAttribute('data-component')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('mountElement leaves the element attributes untouched in append mode', () => {
|
|
83
|
+
container.className = 'container-class';
|
|
84
|
+
const el = document.createElement('span');
|
|
85
|
+
DomHelper.mountElement(el, '#mount-target');
|
|
86
|
+
|
|
87
|
+
expect(el.attributes.length).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
42
90
|
it('createElement creates an element with the given tag name', () => {
|
|
43
91
|
const el = DomHelper.createElement('section');
|
|
44
92
|
expect(el.tagName).toBe('SECTION');
|
|
@@ -142,10 +190,63 @@ describe('DomHelper', () => {
|
|
|
142
190
|
expect(container.querySelectorAll('span').length).toBe(0);
|
|
143
191
|
});
|
|
144
192
|
|
|
193
|
+
it('cleanupOptionalContent only touches the given root, attached or not', () => {
|
|
194
|
+
container.innerHTML = '<p class="outside" data-optional></p>';
|
|
195
|
+
const detached = document.createElement('div');
|
|
196
|
+
detached.innerHTML = '<p class="inside" data-optional></p>';
|
|
197
|
+
|
|
198
|
+
DomHelper.cleanupOptionalContent(detached);
|
|
199
|
+
|
|
200
|
+
expect(detached.querySelector('.inside')).toBe(null);
|
|
201
|
+
expect(container.querySelector('.outside')).not.toBe(null);
|
|
202
|
+
});
|
|
203
|
+
|
|
145
204
|
it('cleanupOptionalContent always strips the data-optional attribute from surviving elements', () => {
|
|
146
205
|
container.innerHTML = '<span data-optional>hello</span>';
|
|
147
206
|
DomHelper.cleanupOptionalContent();
|
|
148
207
|
const span = container.querySelector('span');
|
|
149
208
|
expect(span.hasAttribute('data-optional')).toBe(false);
|
|
150
209
|
});
|
|
210
|
+
|
|
211
|
+
it('isInsideNestedComponent is true only below another component root inside the given root', () => {
|
|
212
|
+
container.innerHTML = '<p class="own"></p><div class="child"><p class="nested"></p></div>';
|
|
213
|
+
const child = container.querySelector('.child');
|
|
214
|
+
DomHelper.markComponentRoot(child);
|
|
215
|
+
|
|
216
|
+
expect(DomHelper.isInsideNestedComponent(container.querySelector('.own'), container)).toBe(false);
|
|
217
|
+
expect(DomHelper.isInsideNestedComponent(child, container)).toBe(false);
|
|
218
|
+
expect(DomHelper.isInsideNestedComponent(container.querySelector('.nested'), container)).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('replacePlaceholder swaps the placeholder for the element', () => {
|
|
222
|
+
container.innerHTML = '<div class="placeholder"></div>';
|
|
223
|
+
const el = document.createElement('section');
|
|
224
|
+
|
|
225
|
+
DomHelper.replacePlaceholder(el, container.querySelector('.placeholder'));
|
|
226
|
+
|
|
227
|
+
expect(container.firstElementChild).toBe(el);
|
|
228
|
+
expect(container.querySelector('.placeholder')).toBe(null);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('replacePlaceholder uses moveBefore when the element is connected and the browser supports it', () => {
|
|
232
|
+
const moved = [];
|
|
233
|
+
Element.prototype.moveBefore = function (node, child) {
|
|
234
|
+
moved.push(node);
|
|
235
|
+
this.insertBefore(node, child);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
container.innerHTML = '<div class="placeholder"></div>';
|
|
240
|
+
const el = document.createElement('section');
|
|
241
|
+
document.body.appendChild(el);
|
|
242
|
+
|
|
243
|
+
DomHelper.replacePlaceholder(el, container.querySelector('.placeholder'));
|
|
244
|
+
|
|
245
|
+
expect(moved).toEqual([el]);
|
|
246
|
+
expect(container.firstElementChild).toBe(el);
|
|
247
|
+
expect(container.querySelector('.placeholder')).toBe(null);
|
|
248
|
+
} finally {
|
|
249
|
+
delete Element.prototype.moveBefore;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
151
252
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as SecurityHelper from "./security.helper.js";
|
|
2
2
|
|
|
3
3
|
interface DomElementConfig {
|
|
4
4
|
target: Element | null;
|
|
@@ -6,76 +6,150 @@ interface DomElementConfig {
|
|
|
6
6
|
element: HTMLElement
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const target = typeof mountTarget === 'string'
|
|
12
|
-
? document.querySelector(mountTarget)
|
|
13
|
-
: mountTarget;
|
|
9
|
+
// Element.moveBefore() isn't in TypeScript's DOM lib yet.
|
|
10
|
+
type MovableParent = ParentNode & { moveBefore?: (node: Node, child: Node | null) => void };
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
// Root element of every component instance, so a parent can tell its own
|
|
13
|
+
// nodes apart from the ones nested components own.
|
|
14
|
+
const componentRoots = new WeakSet<Element>();
|
|
15
|
+
|
|
16
|
+
export function markComponentRoot(element: HTMLElement): void {
|
|
17
|
+
componentRoots.add(element);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// True when `el` lives inside another component's root below `root`.
|
|
21
|
+
export function isInsideNestedComponent(el: Element, root: Element): boolean {
|
|
22
|
+
let current = el.parentElement;
|
|
23
|
+
while (current && current !== root) {
|
|
24
|
+
if (componentRoots.has(current)) return true;
|
|
25
|
+
current = current.parentElement;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// State-preserving move (focus, iframes, animations, custom elements), only
|
|
31
|
+
// possible when both the node and the target parent are in the document.
|
|
32
|
+
export function canMoveBefore(parent: ParentNode, node: Node): boolean {
|
|
33
|
+
return typeof (parent as MovableParent).moveBefore === 'function'
|
|
34
|
+
&& parent.isConnected
|
|
35
|
+
&& node.isConnected;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function moveBefore(parent: ParentNode, node: Node, child: Node | null): void {
|
|
39
|
+
(parent as MovableParent).moveBefore!(node, child);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Chrome restarts the CSS animations of a subtree moved into a parent whose style
|
|
43
|
+
// hasn't been computed yet (e.g. created in the same task). Computing it first,
|
|
44
|
+
// before any move dirties it again, keeps them running.
|
|
45
|
+
export function prepareMoveTarget(parent: ParentNode | null, node: Node): void {
|
|
46
|
+
if (parent instanceof Element && canMoveBefore(parent, node)) {
|
|
47
|
+
void getComputedStyle(parent).display;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Puts an already-mounted element where a placeholder is, moving it without
|
|
52
|
+
// detaching it when the browser supports it.
|
|
53
|
+
export function replacePlaceholder(element: HTMLElement, placeholder: HTMLElement): void {
|
|
54
|
+
const parent = placeholder.parentNode;
|
|
55
|
+
if (parent && canMoveBefore(parent, element)) {
|
|
56
|
+
moveBefore(parent, element, placeholder);
|
|
57
|
+
placeholder.remove();
|
|
58
|
+
} else {
|
|
59
|
+
placeholder.replaceWith(element);
|
|
16
60
|
}
|
|
61
|
+
}
|
|
17
62
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
63
|
+
export function mountElement(element: HTMLElement, mountTarget: string | HTMLElement): void {
|
|
64
|
+
const target = typeof mountTarget === 'string'
|
|
65
|
+
? document.querySelector(mountTarget)
|
|
66
|
+
: mountTarget;
|
|
67
|
+
|
|
68
|
+
// Nothing would show up, and nothing else says why (e.g. a misspelled selector, or no #app).
|
|
69
|
+
if (!(target instanceof HTMLElement)) {
|
|
70
|
+
const where = typeof mountTarget === 'string' ? `"${mountTarget}" isn't in the page` : `its mount target is ${mountTarget}`;
|
|
71
|
+
console.error(`Nothing to mount <${element.tagName.toLowerCase()}> into: ${where}.`);
|
|
26
72
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
value.trim() === '' ||
|
|
57
|
-
value === "undefined" ||
|
|
58
|
-
value === "null"
|
|
59
|
-
);
|
|
73
|
+
|
|
74
|
+
appendOrReplace({ target, element, mountTarget });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createElement<T extends HTMLElement>(
|
|
78
|
+
tagName: keyof HTMLElementTagNameMap,
|
|
79
|
+
template: string = '',
|
|
80
|
+
trustLevel?: Nutin.TrustLevel
|
|
81
|
+
): T {
|
|
82
|
+
const element = document.createElement(tagName) as T;
|
|
83
|
+
element.replaceChildren(SecurityHelper.sanitizeToFragment(template, trustLevel));
|
|
84
|
+
return element;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Scoped to the rendering component (a detached one included); the document-wide
|
|
88
|
+
// default only serves direct callers.
|
|
89
|
+
export function cleanupOptionalContent(root: ParentNode = document): void {
|
|
90
|
+
const isEmpty = (el: HTMLElement): boolean => {
|
|
91
|
+
if (el instanceof HTMLImageElement) {
|
|
92
|
+
return !el.src || el.src.trim() === "";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
96
|
+
return !el.value?.trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (el instanceof HTMLMediaElement || el instanceof HTMLSourceElement) {
|
|
100
|
+
return !el.getAttribute("src");
|
|
60
101
|
}
|
|
61
102
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
103
|
+
const content = el.textContent?.trim();
|
|
104
|
+
return !content || content === "undefined" || content === "null";
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const isValueUndefined = (el: HTMLElement): boolean => {
|
|
108
|
+
const value = el.getAttribute("data-optional");
|
|
109
|
+
/*
|
|
110
|
+
A bare/empty data-optional (getAttribute returns "") is a plain marker and
|
|
111
|
+
defers entirely to isEmpty(el) below rather than forcing removal itself.
|
|
112
|
+
Only a present-but-blank value (whitespace) or the literal string produced
|
|
113
|
+
by interpolating an undefined/null expression counts as "value undefined".
|
|
114
|
+
*/
|
|
115
|
+
if (!value) return false;
|
|
116
|
+
return (
|
|
117
|
+
value.trim() === '' ||
|
|
118
|
+
value === "undefined" ||
|
|
119
|
+
value === "null"
|
|
120
|
+
);
|
|
66
121
|
}
|
|
67
122
|
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
123
|
+
root.querySelectorAll<HTMLElement>("[data-optional]").forEach(el => {
|
|
124
|
+
if (isValueUndefined(el) || isEmpty(el)) el.remove();
|
|
125
|
+
el.removeAttribute("data-optional");
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function appendOrReplace(config: DomElementConfig) {
|
|
130
|
+
if (config.target instanceof HTMLElement) {
|
|
131
|
+
if (typeof config.mountTarget === 'string') {
|
|
132
|
+
// Append mode
|
|
133
|
+
config.target.appendChild(config.element);
|
|
77
134
|
} else {
|
|
78
|
-
|
|
135
|
+
// Replace placeholder mode
|
|
136
|
+
copyAttributes(config.target, config.element);
|
|
137
|
+
config.target.replaceWith(config.element);
|
|
79
138
|
}
|
|
139
|
+
} else {
|
|
140
|
+
return;
|
|
80
141
|
}
|
|
81
142
|
}
|
|
143
|
+
|
|
144
|
+
// Carries placeholder attributes (class, id, aria-*, data-*...) onto the mounted
|
|
145
|
+
// element. data-component is skipped so an ancestor's child lookup can't re-match it.
|
|
146
|
+
function copyAttributes(from: HTMLElement, to: HTMLElement): void {
|
|
147
|
+
Array.from(from.attributes).forEach(attr => {
|
|
148
|
+
if (attr.name === 'data-component') return;
|
|
149
|
+
if (attr.name === 'class') {
|
|
150
|
+
to.classList.add(...Array.from(from.classList));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
to.setAttribute(attr.name, attr.value);
|
|
154
|
+
});
|
|
155
|
+
}
|
package/templates/base/src/core/base-classes/base-component/helpers/event.helper.test.js.hbs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as EventHelper from '#root/dist/src/core/base-classes/base-component/helpers/event.helper.js';
|
|
2
|
+
import * as DomHelper from '#root/dist/src/core/base-classes/base-component/helpers/dom.helper.js';
|
|
2
3
|
|
|
3
4
|
describe('EventHelper', () => {
|
|
4
5
|
let container;
|
|
@@ -56,11 +57,33 @@ describe('EventHelper', () => {
|
|
|
56
57
|
expect(eventListeners.length).toBe(0);
|
|
57
58
|
});
|
|
58
59
|
|
|
59
|
-
it('
|
|
60
|
-
container.innerHTML = '<button data-event="click:missingHandler"></button>';
|
|
60
|
+
it('binds nothing and warns when the referenced handler does not exist or is not a function', () => {
|
|
61
|
+
container.innerHTML = '<button data-event="click:missingHandler"></button><a data-event="click:typo"></a>';
|
|
61
62
|
const component = { missingHandler: 'not-a-function' };
|
|
63
|
+
const warnSpy = spyOn(console, 'warn').andCallFake(() => {});
|
|
64
|
+
try {
|
|
65
|
+
EventHelper.bindEvents(component, container, eventListeners);
|
|
66
|
+
expect(eventListeners.length).toBe(0);
|
|
67
|
+
expect(warnSpy.callCount).toBe(2);
|
|
68
|
+
expect(warnSpy.calls[1][0]).toContain('has no method "typo"');
|
|
69
|
+
} finally {
|
|
70
|
+
warnSpy.restore();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('keeps commas inside quoted literal args', () => {
|
|
75
|
+
container.innerHTML = `<button data-event="click:handleClick:'a,b',2,"c,d""></button>`;
|
|
76
|
+
const button = container.querySelector('button');
|
|
77
|
+
|
|
78
|
+
let received = null;
|
|
79
|
+
const component = {
|
|
80
|
+
handleClick(...args) { received = args; },
|
|
81
|
+
};
|
|
82
|
+
|
|
62
83
|
EventHelper.bindEvents(component, container, eventListeners);
|
|
63
|
-
|
|
84
|
+
click(button);
|
|
85
|
+
|
|
86
|
+
expect(received).toEqual(['a,b', 2, 'c,d']);
|
|
64
87
|
});
|
|
65
88
|
|
|
66
89
|
it('tracks each bound listener in the eventListeners array', () => {
|
|
@@ -73,6 +96,64 @@ describe('EventHelper', () => {
|
|
|
73
96
|
expect(eventListeners[0][1]).toBe('click');
|
|
74
97
|
});
|
|
75
98
|
|
|
99
|
+
it('skips data-event elements inside a nested component root, which binds its own', () => {
|
|
100
|
+
container.innerHTML = '<button class="own" data-event="click:toggle"></button><div class="child"><button class="nested" data-event="click:toggle"></button></div>';
|
|
101
|
+
DomHelper.markComponentRoot(container.querySelector('.child'));
|
|
102
|
+
let calls = 0;
|
|
103
|
+
const component = { toggle() { calls++; } };
|
|
104
|
+
|
|
105
|
+
EventHelper.bindEvents(component, container, eventListeners);
|
|
106
|
+
click(container.querySelector('.nested'));
|
|
107
|
+
expect(calls).toBe(0);
|
|
108
|
+
|
|
109
|
+
click(container.querySelector('.own'));
|
|
110
|
+
expect(calls).toBe(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('cancels link navigation, with or without args', () => {
|
|
114
|
+
container.innerHTML = '<a href="/x" data-event="click:go"></a><a href="/y" data-event="click:go:1"></a>';
|
|
115
|
+
const component = { go() {} };
|
|
116
|
+
EventHelper.bindEvents(component, container, eventListeners);
|
|
117
|
+
|
|
118
|
+
container.querySelectorAll('a').forEach((a) => {
|
|
119
|
+
const ev = new window.MouseEvent('click', { bubbles: true, cancelable: true });
|
|
120
|
+
a.dispatchEvent(ev);
|
|
121
|
+
expect(ev.defaultPrevented).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('cancels form submission from a submit event or a submit button click', () => {
|
|
126
|
+
container.innerHTML = '<form data-event="submit:save"><button type="submit" data-event="click:save"></button></form>';
|
|
127
|
+
const component = { save() {} };
|
|
128
|
+
EventHelper.bindEvents(component, container, eventListeners);
|
|
129
|
+
|
|
130
|
+
const submit = new window.Event('submit', { bubbles: true, cancelable: true });
|
|
131
|
+
container.querySelector('form').dispatchEvent(submit);
|
|
132
|
+
const buttonClick = new window.MouseEvent('click', { bubbles: true, cancelable: true });
|
|
133
|
+
container.querySelector('button').dispatchEvent(buttonClick);
|
|
134
|
+
|
|
135
|
+
expect(submit.defaultPrevented).toBe(true);
|
|
136
|
+
expect(buttonClick.defaultPrevented).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('keeps the default of other events, args or not (typing, checking a box)', () => {
|
|
140
|
+
container.innerHTML = '<input class="text" data-event="keydown:onKey:@key"><input class="box" type="checkbox" data-event="click:onToggle:@checked">';
|
|
141
|
+
let checked = null;
|
|
142
|
+
const component = { onKey() {}, onToggle(value) { checked = value; } };
|
|
143
|
+
EventHelper.bindEvents(component, container, eventListeners);
|
|
144
|
+
|
|
145
|
+
const keydown = new window.KeyboardEvent('keydown', { key: 'a', bubbles: true, cancelable: true });
|
|
146
|
+
container.querySelector('.text').dispatchEvent(keydown);
|
|
147
|
+
const box = container.querySelector('.box');
|
|
148
|
+
const boxClick = new window.MouseEvent('click', { bubbles: true, cancelable: true });
|
|
149
|
+
box.dispatchEvent(boxClick);
|
|
150
|
+
|
|
151
|
+
expect(keydown.defaultPrevented).toBe(false);
|
|
152
|
+
expect(boxClick.defaultPrevented).toBe(false);
|
|
153
|
+
expect(checked).toBe(true);
|
|
154
|
+
expect(box.checked).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
76
157
|
it('destroyEvents removes every tracked listener', () => {
|
|
77
158
|
container.innerHTML = '<button data-event="click:handleClick"></button>';
|
|
78
159
|
const button = container.querySelector('button');
|
|
@@ -1,56 +1,106 @@
|
|
|
1
|
-
import { BaseComponent } from '../base-component.js';
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
export class EventHelper {
|
|
5
|
-
public static bindEvents(
|
|
6
|
-
component: BaseComponent,
|
|
7
|
-
element: HTMLElement,
|
|
8
|
-
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
9
|
-
): void {
|
|
10
|
-
element.querySelectorAll('[data-event]').forEach(el => {
|
|
11
|
-
const [eventName, handlerName, ...argParts] = el.getAttribute('data-event')!.split(':');
|
|
12
|
-
const argsString = argParts.length ? argParts.join(':') : '';
|
|
13
|
-
|
|
14
|
-
if (!handlerName || !eventName) return;
|
|
15
|
-
|
|
16
|
-
const handler = (component as any)[handlerName];
|
|
17
|
-
|
|
18
|
-
if (typeof handler === 'function') {
|
|
19
|
-
const rawArgs = argsString ? argsString.split(',') : [];
|
|
20
|
-
const boundHandler = this.createBoundHandler(el, component, handler, rawArgs);
|
|
21
|
-
this.addEvent(el, eventName, boundHandler, eventListeners);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
}
|
|
1
|
+
import type { BaseComponent } from '../base-component.js';
|
|
2
|
+
import * as DomHelper from './dom.helper.js';
|
|
3
|
+
import * as TokenHelper from './token.helper.js';
|
|
25
4
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
5
|
+
export function bindEvents(
|
|
6
|
+
component: BaseComponent,
|
|
7
|
+
element: HTMLElement,
|
|
8
|
+
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
9
|
+
): void {
|
|
10
|
+
element.querySelectorAll('[data-event]').forEach(el => {
|
|
11
|
+
// A nested component binds its own data-event elements to its own methods.
|
|
12
|
+
if (DomHelper.isInsideNestedComponent(el, element)) return;
|
|
33
13
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
14
|
+
const [eventName, handlerName, ...argParts] = el.getAttribute('data-event')!.split(':');
|
|
15
|
+
const argsString = argParts.length ? argParts.join(':') : '';
|
|
16
|
+
|
|
17
|
+
if (!handlerName || !eventName) return;
|
|
18
|
+
|
|
19
|
+
const handler = (component as any)[handlerName];
|
|
20
|
+
|
|
21
|
+
if (typeof handler !== 'function') {
|
|
22
|
+
// TypeScript builds already fail on a handler the class doesn't have (see handler-refs.js).
|
|
23
|
+
console.warn(`data-event="${el.getAttribute('data-event')}": ${component.constructor.name} has no method "${handlerName}".`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
46
26
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
27
|
+
const rawArgs = argsString ? splitArgs(argsString) : [];
|
|
28
|
+
const boundHandler = createBoundHandler(el, component, handler, rawArgs);
|
|
29
|
+
addEvent(el, eventName, boundHandler, eventListeners);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Splits data-event args on commas, except inside a quoted literal ('a,b' or "a,b").
|
|
34
|
+
function splitArgs(argsString: string): string[] {
|
|
35
|
+
const args: string[] = [];
|
|
36
|
+
let current = '';
|
|
37
|
+
let quote: string | null = null;
|
|
38
|
+
for (const ch of argsString) {
|
|
39
|
+
if (quote) {
|
|
40
|
+
if (ch === quote) quote = null;
|
|
41
|
+
} else if (ch === '"' || ch === "'") {
|
|
42
|
+
quote = ch;
|
|
43
|
+
} else if (ch === ',') {
|
|
44
|
+
args.push(current);
|
|
45
|
+
current = '';
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
current += ch;
|
|
55
49
|
}
|
|
50
|
+
args.push(current);
|
|
51
|
+
return args;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function destroyEvents(
|
|
55
|
+
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
56
|
+
): void {
|
|
57
|
+
eventListeners.forEach(([target, event, listener]) => {
|
|
58
|
+
target.removeEventListener(event, listener);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createBoundHandler(
|
|
63
|
+
el: Element,
|
|
64
|
+
component: BaseComponent,
|
|
65
|
+
handler: (...args: any[]) => void,
|
|
66
|
+
rawArgs: string[]
|
|
67
|
+
): EventListener {
|
|
68
|
+
return (event: Event) => {
|
|
69
|
+
if (isModifiedAnchorClick(el, event)) return;
|
|
70
|
+
if (isNavigationDefault(el, event)) event.preventDefault();
|
|
71
|
+
|
|
72
|
+
const resolvedArgs = rawArgs.map(arg => TokenHelper.resolve(arg.trim(), el, event));
|
|
73
|
+
|
|
74
|
+
handler.call(component, ...resolvedArgs);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Lets a real <a href> fall through to native browser behavior (open in new tab, etc.)
|
|
79
|
+
// on a modified or non-primary click, instead of always intercepting via preventDefault.
|
|
80
|
+
function isModifiedAnchorClick(el: Element, event: Event): boolean {
|
|
81
|
+
if (event.type !== 'click' || el.tagName !== 'A' || !el.getAttribute('href')) return false;
|
|
82
|
+
|
|
83
|
+
const mouseEvent = event as MouseEvent;
|
|
84
|
+
return mouseEvent.ctrlKey || mouseEvent.metaKey || mouseEvent.shiftKey || mouseEvent.altKey || mouseEvent.button !== 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Only the defaults that would leave the page are cancelled: following a link and
|
|
88
|
+
// submitting a form. Everything else (typing, checking a box, ...) keeps its default.
|
|
89
|
+
function isNavigationDefault(el: Element, event: Event): boolean {
|
|
90
|
+
if (event.type === 'submit') return true;
|
|
91
|
+
if (event.type !== 'click') return false;
|
|
92
|
+
if (el.tagName === 'A') return el.hasAttribute('href');
|
|
93
|
+
return (el instanceof HTMLButtonElement || el instanceof HTMLInputElement)
|
|
94
|
+
&& el.type === 'submit'
|
|
95
|
+
&& el.form !== null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function addEvent(
|
|
99
|
+
target: EventTarget,
|
|
100
|
+
event: string,
|
|
101
|
+
listener: EventListener,
|
|
102
|
+
eventListeners: Array<[EventTarget, string, EventListener]>
|
|
103
|
+
): void {
|
|
104
|
+
target.addEventListener(event, listener);
|
|
105
|
+
eventListeners.push([target, event, listener]);
|
|
56
106
|
}
|