@nutin/cli 2.1.1 → 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 +268 -0
- package/README.md +13 -5
- 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 +9 -8
- package/templates/base/AGENTS.md.hbs +42 -41
- package/templates/base/GETTING_STARTED.md.hbs +26 -20
- package/templates/base/README.md.hbs +7 -4
- 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 +7 -2
- 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 +286 -24
- 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 +66 -4
- package/templates/base/src/core/base-classes/base-component/helpers/dom.helper.ts.hbs +135 -75
- 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 +93 -54
- 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 +183 -4
- package/templates/base/src/core/services/router/helpers/navigation-manager.helper.ts.hbs +207 -84
- 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 +306 -0
- package/templates/base/src/core/services/router/router.ts.hbs +113 -53
- package/templates/base/tools/builder/builder.config.js.hbs +6 -0
- package/templates/base/tools/builder/builder.js.hbs +18 -6
- 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/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 +15 -7
- 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 +43 -13
- 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 +28 -21
- 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 +24 -6
- 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 +24 -11
- package/templates/base/tools/builder/core/styles/sass.js.hbs +3 -2
- package/templates/base/tools/builder/core/styles/tailwind.js.hbs +19 -59
- package/templates/base/tools/dev/dev-serve.js.hbs +30 -5
- package/templates/base/tools/dev/serve.js.hbs +152 -22
- package/templates/base/tools/dev/watcher.js.hbs +34 -3
- package/templates/base/tools/generator/generator.js.hbs +1 -1
- package/templates/base/tools/generator/handle-file.js.hbs +4 -2
- package/templates/base/tools/generator/handle-file.test.js.hbs +3 -3
- 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 +30 -2
- 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 +2 -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 +3 -0
- package/templates/features/docker/tools/docker/nginx.conf.template +6 -4
- 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 +21 -4
- 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/tools/testin-nutin/core/{tests → globals}/clock.test.js.hbs +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as I18nHelper from '#root/dist/src/core/base-classes/base-component/helpers/i18n.helper.js';
|
|
2
2
|
import { I18nService } from '#root/dist/src/core/services/index.js';
|
|
3
|
+
import { CONFIG } from '#root/dist/src/core/config.js';
|
|
4
|
+
import * as DomHelper from '#root/dist/src/core/base-classes/base-component/helpers/dom.helper.js';
|
|
3
5
|
|
|
4
6
|
describe('I18nHelper', () => {
|
|
5
7
|
let container;
|
|
@@ -9,9 +11,11 @@ describe('I18nHelper', () => {
|
|
|
9
11
|
container = document.createElement('div');
|
|
10
12
|
document.body.appendChild(container);
|
|
11
13
|
originalTranslate = I18nService.translate;
|
|
14
|
+
CONFIG.i18n = true;
|
|
12
15
|
});
|
|
13
16
|
|
|
14
17
|
afterEach(() => {
|
|
18
|
+
CONFIG.i18n = false;
|
|
15
19
|
I18nService.translate = originalTranslate;
|
|
16
20
|
container.remove();
|
|
17
21
|
container = null;
|
|
@@ -59,4 +63,69 @@ describe('I18nHelper', () => {
|
|
|
59
63
|
expect(spans[0].textContent).toBe('[a]');
|
|
60
64
|
expect(spans[1].textContent).toBe('[b]');
|
|
61
65
|
});
|
|
66
|
+
|
|
67
|
+
it('keeps the element text, or falls back to the key, without calling I18nService when i18n is disabled', () => {
|
|
68
|
+
CONFIG.i18n = false;
|
|
69
|
+
let called = false;
|
|
70
|
+
I18nService.translate = () => { called = true; return 'translated'; };
|
|
71
|
+
container.innerHTML = '<span data-i18n="a">kept</span><span data-i18n="b"></span><input data-i18n="c">';
|
|
72
|
+
|
|
73
|
+
I18nHelper.parseI18nAttributes(container);
|
|
74
|
+
|
|
75
|
+
const spans = container.querySelectorAll('span');
|
|
76
|
+
expect(called).toBeFalsy();
|
|
77
|
+
expect(spans[0].textContent).toBe('kept');
|
|
78
|
+
expect(spans[1].textContent).toBe('b');
|
|
79
|
+
expect(container.querySelector('input').placeholder).toBe('c');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('skips elements inside a nested component root, which translates its own content', () => {
|
|
83
|
+
I18nService.translate = (key) => `[${key}]`;
|
|
84
|
+
container.innerHTML = '<span class="own" data-i18n="a"></span><div class="child"><span class="nested" data-i18n="b">b</span></div>';
|
|
85
|
+
DomHelper.markComponentRoot(container.querySelector('.child'));
|
|
86
|
+
|
|
87
|
+
I18nHelper.parseI18nAttributes(container);
|
|
88
|
+
|
|
89
|
+
expect(container.querySelector('.own').textContent).toBe('[a]');
|
|
90
|
+
expect(container.querySelector('.nested').textContent).toBe('b');
|
|
91
|
+
});
|
|
92
|
+
it('prefixes root-relative links with the current language and a trailing slash, keeping query and hash', () => {
|
|
93
|
+
const lang = I18nService.currentLanguage;
|
|
94
|
+
container.innerHTML = '<a class="a" href="/docs"></a><a class="b" href="/"></a><a class="c" href="/docs/api?x=1#h"></a><a class="d" href="/docs/#h"></a>';
|
|
95
|
+
|
|
96
|
+
I18nHelper.parseI18nAttributes(container);
|
|
97
|
+
|
|
98
|
+
expect(container.querySelector('.a').getAttribute('href')).toBe(`/${lang}/docs/`);
|
|
99
|
+
expect(container.querySelector('.b').getAttribute('href')).toBe(`/${lang}/`);
|
|
100
|
+
expect(container.querySelector('.c').getAttribute('href')).toBe(`/${lang}/docs/api/?x=1#h`);
|
|
101
|
+
expect(container.querySelector('.d').getAttribute('href')).toBe(`/${lang}/docs/#h`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('leaves localized, external, protocol-relative, relative, hash, mailto and file links untouched', () => {
|
|
105
|
+
const lang = I18nService.currentLanguage;
|
|
106
|
+
const hrefs = [`/${lang}/docs`, `/${lang}`, 'https://example.com/docs', '//example.com/docs', 'docs', '#top', 'mailto:a@b.c', '/favicon.ico', '/files/guide.pdf?v=2'];
|
|
107
|
+
container.innerHTML = hrefs.map((href) => `<a href="${href}"></a>`).join('');
|
|
108
|
+
|
|
109
|
+
I18nHelper.parseI18nAttributes(container);
|
|
110
|
+
|
|
111
|
+
expect([...container.querySelectorAll('a')].map((a) => a.getAttribute('href'))).toEqual(hrefs);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('leaves links inside a nested component root, which localizes its own', () => {
|
|
115
|
+
container.innerHTML = '<div class="child"><a href="/docs"></a></div>';
|
|
116
|
+
DomHelper.markComponentRoot(container.querySelector('.child'));
|
|
117
|
+
|
|
118
|
+
I18nHelper.parseI18nAttributes(container);
|
|
119
|
+
|
|
120
|
+
expect(container.querySelector('a').getAttribute('href')).toBe('/docs');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('leaves links untouched when i18n is disabled', () => {
|
|
124
|
+
CONFIG.i18n = false;
|
|
125
|
+
container.innerHTML = '<a href="/docs"></a>';
|
|
126
|
+
|
|
127
|
+
I18nHelper.parseI18nAttributes(container);
|
|
128
|
+
|
|
129
|
+
expect(container.querySelector('a').getAttribute('href')).toBe('/docs');
|
|
130
|
+
});
|
|
62
131
|
});
|
|
@@ -1,21 +1,60 @@
|
|
|
1
|
-
import { I18nService } from '../../../
|
|
1
|
+
import { I18nService } from '../../../services/i18n/i18n.js';
|
|
2
|
+
import { CONFIG } from '../../../config.js';
|
|
3
|
+
import * as DomHelper from './dom.helper.js';
|
|
2
4
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export function parseI18nAttributes(element: HTMLElement): void {
|
|
6
|
+
element.querySelectorAll('[data-i18n]').forEach(el => {
|
|
7
|
+
// Nested components translate their own content.
|
|
8
|
+
if (DomHelper.isInsideNestedComponent(el, element)) return;
|
|
9
|
+
const key = el.getAttribute('data-i18n')!;
|
|
10
|
+
setTranslatedContent(el, key);
|
|
11
|
+
});
|
|
12
|
+
localizeLinks(element);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// With i18n, every page lives under a language prefix: a root-relative link ("/docs") is
|
|
16
|
+
// rewritten to the page's canonical URL ("/en/docs/") so prerendered HTML and copied links
|
|
17
|
+
// don't point at an unlocalized duplicate. The router strips the prefix again on navigation.
|
|
18
|
+
function localizeLinks(element: HTMLElement): void {
|
|
19
|
+
if (globalThis.__NUTIN_I18N__ ?? CONFIG.i18n) {
|
|
20
|
+
element.querySelectorAll('a[href]').forEach(el => {
|
|
21
|
+
// Nested components localize their own links.
|
|
22
|
+
if (DomHelper.isInsideNestedComponent(el, element)) return;
|
|
23
|
+
const localized = localizedHref(el.getAttribute('href')!);
|
|
24
|
+
if (localized !== null) el.setAttribute('href', localized);
|
|
8
25
|
});
|
|
9
26
|
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// null when the href isn't an unprefixed app path: external, protocol-relative, relative,
|
|
30
|
+
// a file (has an extension), or already localized.
|
|
31
|
+
function localizedHref(href: string): string | null {
|
|
32
|
+
if (!href.startsWith('/') || href.startsWith('//')) return null;
|
|
33
|
+
|
|
34
|
+
const end = href.search(/[?#]/);
|
|
35
|
+
const path = end === -1 ? href : href.slice(0, end);
|
|
36
|
+
const rest = end === -1 ? '' : href.slice(end);
|
|
37
|
+
const segments = path.split('/').filter(Boolean);
|
|
38
|
+
if (segments[segments.length - 1]?.includes('.')) return null;
|
|
39
|
+
if (segments[0] && I18nService.languages.includes(segments[0] as Nutin.Language)) return null;
|
|
40
|
+
|
|
41
|
+
const suffix = segments.length ? `/${segments.join('/')}` : '';
|
|
42
|
+
return `/${I18nService.currentLanguage}${suffix}/${rest}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function setTranslatedContent(
|
|
46
|
+
el: Element,
|
|
47
|
+
key: string
|
|
48
|
+
): void {
|
|
49
|
+
// With i18n off, apply translate()'s own no-translation fallback without
|
|
50
|
+
// referencing I18nService, so prod builds can drop the service entirely.
|
|
51
|
+
const text = (globalThis.__NUTIN_I18N__ ?? CONFIG.i18n)
|
|
52
|
+
? I18nService.translate(key, el.textContent)
|
|
53
|
+
: el.textContent || key;
|
|
10
54
|
|
|
11
|
-
|
|
12
|
-
el
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (el instanceof HTMLInputElement) {
|
|
16
|
-
el.placeholder = I18nService.translate(key, el.textContent);
|
|
17
|
-
} else {
|
|
18
|
-
el.textContent = I18nService.translate(key, el.textContent);
|
|
19
|
-
}
|
|
55
|
+
if (el instanceof HTMLInputElement) {
|
|
56
|
+
el.placeholder = text;
|
|
57
|
+
} else {
|
|
58
|
+
el.textContent = text;
|
|
20
59
|
}
|
|
21
60
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as PipeHelper from '#root/dist/src/core/base-classes/base-component/helpers/pipe.helper.js';
|
|
2
2
|
import { AppPipeRegistry } from '#root/dist/src/core/services/index.js';
|
|
3
|
+
import * as DomHelper from '#root/dist/src/core/base-classes/base-component/helpers/dom.helper.js';
|
|
3
4
|
|
|
4
5
|
describe('PipeHelper', () => {
|
|
5
6
|
let container;
|
|
@@ -86,4 +87,14 @@ describe('PipeHelper', () => {
|
|
|
86
87
|
|
|
87
88
|
warnSpy.restore();
|
|
88
89
|
});
|
|
90
|
+
|
|
91
|
+
it('skips elements inside a nested component root, which pipes its own content', () => {
|
|
92
|
+
container.innerHTML = '<span class="own" data-pipe="upper">a</span><div class="child"><span class="nested" data-pipe="upper">b</span></div>';
|
|
93
|
+
DomHelper.markComponentRoot(container.querySelector('.child'));
|
|
94
|
+
|
|
95
|
+
PipeHelper.parsePipeAttributes(container);
|
|
96
|
+
|
|
97
|
+
expect(container.querySelector('.own').textContent).toBe('A');
|
|
98
|
+
expect(container.querySelector('.nested').textContent).toBe('b');
|
|
99
|
+
});
|
|
89
100
|
});
|
|
@@ -1,47 +1,50 @@
|
|
|
1
|
-
import { AppPipeRegistry } from '../../../
|
|
1
|
+
import { AppPipeRegistry } from '../../../services/pipe-registry/pipe-registry.js';
|
|
2
|
+
import * as DomHelper from './dom.helper.js';
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
export function parsePipeAttributes(element: HTMLElement): void {
|
|
5
|
+
element.querySelectorAll('[data-pipe]').forEach(el => {
|
|
6
|
+
// Nested components pipe their own content; piping it again here would
|
|
7
|
+
// apply every pipe twice.
|
|
8
|
+
if (DomHelper.isInsideNestedComponent(el, element)) return;
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const pipeRaw = el.getAttribute('data-pipe');
|
|
11
|
+
if (!pipeRaw) return;
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
13
|
+
const pipes = pipeRaw.split('|').map(s => s.trim());
|
|
14
|
+
const sourceAttr = el.getAttribute('data-pipe-source');
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
sourceAttr: string | null
|
|
20
|
-
): void {
|
|
21
|
-
let value: string;
|
|
22
|
-
|
|
23
|
-
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
24
|
-
value = sourceAttr !== null ? sourceAttr : el.value;
|
|
25
|
-
} else {
|
|
26
|
-
value = sourceAttr !== null ? sourceAttr : el.textContent || '';
|
|
27
|
-
}
|
|
16
|
+
processPipes(el, pipes, sourceAttr);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
function processPipes(
|
|
21
|
+
el: Element,
|
|
22
|
+
pipes: string[],
|
|
23
|
+
sourceAttr: string | null
|
|
24
|
+
): void {
|
|
25
|
+
let value: string;
|
|
26
|
+
|
|
27
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
28
|
+
value = sourceAttr !== null ? sourceAttr : el.value;
|
|
29
|
+
} else {
|
|
30
|
+
value = sourceAttr !== null ? sourceAttr : el.textContent || '';
|
|
31
|
+
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
33
|
+
for (const pipe of pipes) {
|
|
34
|
+
const [pipeName, ...argParts] = pipe.split(':');
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (!pipeName) {
|
|
37
|
+
console.warn(`Empty pipe name in segment "${pipe}" - skipping.`);
|
|
38
|
+
continue;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const args = argParts.length ? argParts.join(':').split(',') : [];
|
|
42
|
+
value = AppPipeRegistry.apply(pipeName.trim(), value, args.map(a => a.trim()));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
46
|
+
el.value = value;
|
|
47
|
+
} else {
|
|
48
|
+
el.textContent = value;
|
|
46
49
|
}
|
|
47
50
|
}
|
package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.test.js.hbs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as PreservationHelper from '#root/dist/src/core/base-classes/base-component/helpers/preservation.helper.js';
|
|
2
|
+
|
|
3
|
+
describe('PreservationHelper', () => {
|
|
4
|
+
let container;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
container = document.createElement('div');
|
|
8
|
+
document.body.appendChild(container);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
container.remove();
|
|
13
|
+
container = null;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('restores focus and caret on an element that was detached and reattached', () => {
|
|
17
|
+
const child = document.createElement('div');
|
|
18
|
+
child.innerHTML = '<input value="hello">';
|
|
19
|
+
container.appendChild(child);
|
|
20
|
+
const input = child.querySelector('input');
|
|
21
|
+
input.focus();
|
|
22
|
+
input.setSelectionRange(1, 3);
|
|
23
|
+
|
|
24
|
+
const state = PreservationHelper.preserve([child]);
|
|
25
|
+
child.remove();
|
|
26
|
+
container.appendChild(child);
|
|
27
|
+
input.blur();
|
|
28
|
+
PreservationHelper.restore(state);
|
|
29
|
+
|
|
30
|
+
expect(document.activeElement).toBe(input);
|
|
31
|
+
expect(input.selectionStart).toBe(1);
|
|
32
|
+
expect(input.selectionEnd).toBe(3);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('ignores focus outside the preserved elements', () => {
|
|
36
|
+
const child = document.createElement('div');
|
|
37
|
+
const outside = document.createElement('input');
|
|
38
|
+
container.append(child, outside);
|
|
39
|
+
outside.focus();
|
|
40
|
+
|
|
41
|
+
const state = PreservationHelper.preserve([child]);
|
|
42
|
+
|
|
43
|
+
expect(state.focus).toBe(null);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not restore focus on an element that is no longer in the document', () => {
|
|
47
|
+
const child = document.createElement('div');
|
|
48
|
+
child.innerHTML = '<input>';
|
|
49
|
+
container.appendChild(child);
|
|
50
|
+
child.querySelector('input').focus();
|
|
51
|
+
|
|
52
|
+
const state = PreservationHelper.preserve([child]);
|
|
53
|
+
child.remove();
|
|
54
|
+
PreservationHelper.restore(state);
|
|
55
|
+
|
|
56
|
+
expect(document.activeElement).not.toBe(child.querySelector('input'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('does not park anything when Element.moveBefore is unavailable', () => {
|
|
60
|
+
const child = document.createElement('div');
|
|
61
|
+
container.appendChild(child);
|
|
62
|
+
|
|
63
|
+
const state = PreservationHelper.preserve([child]);
|
|
64
|
+
|
|
65
|
+
expect(state.parking).toBe(null);
|
|
66
|
+
expect(child.parentElement).toBe(container);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('parks connected elements outside their parent with moveBefore and removes the parking spot on restore', () => {
|
|
70
|
+
Element.prototype.moveBefore = function (node, child) {
|
|
71
|
+
this.insertBefore(node, child);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const child = document.createElement('div');
|
|
76
|
+
container.appendChild(child);
|
|
77
|
+
|
|
78
|
+
const state = PreservationHelper.preserve([child]);
|
|
79
|
+
|
|
80
|
+
expect(state.parking.contains(child)).toBe(true);
|
|
81
|
+
expect(container.contains(child)).toBe(false);
|
|
82
|
+
expect(document.body.contains(state.parking)).toBe(true);
|
|
83
|
+
|
|
84
|
+
container.appendChild(child);
|
|
85
|
+
PreservationHelper.restore(state);
|
|
86
|
+
|
|
87
|
+
expect(document.body.contains(state.parking)).toBe(false);
|
|
88
|
+
} finally {
|
|
89
|
+
delete Element.prototype.moveBefore;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('restores the scroll position of a detached element and its descendants', () => {
|
|
94
|
+
const child = document.createElement('div');
|
|
95
|
+
child.innerHTML = '<div class="list"></div>';
|
|
96
|
+
container.appendChild(child);
|
|
97
|
+
const list = child.querySelector('.list');
|
|
98
|
+
// jsdom doesn't lay out, so scroll offsets are made writable by hand.
|
|
99
|
+
[child, list].forEach(element => {
|
|
100
|
+
Object.defineProperty(element, 'scrollTop', { value: 0, writable: true });
|
|
101
|
+
Object.defineProperty(element, 'scrollLeft', { value: 0, writable: true });
|
|
102
|
+
});
|
|
103
|
+
child.scrollLeft = 5;
|
|
104
|
+
list.scrollTop = 40;
|
|
105
|
+
|
|
106
|
+
const state = PreservationHelper.preserve([child]);
|
|
107
|
+
child.remove();
|
|
108
|
+
child.scrollLeft = 0;
|
|
109
|
+
list.scrollTop = 0;
|
|
110
|
+
container.appendChild(child);
|
|
111
|
+
PreservationHelper.restore(state);
|
|
112
|
+
|
|
113
|
+
expect(state.scroll.length).toBe(2);
|
|
114
|
+
expect(child.scrollLeft).toBe(5);
|
|
115
|
+
expect(child.scrollTop).toBe(0);
|
|
116
|
+
expect(list.scrollTop).toBe(40);
|
|
117
|
+
expect(list.scrollLeft).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('does not restore the scroll position of an element that is no longer in the document', () => {
|
|
121
|
+
const child = document.createElement('div');
|
|
122
|
+
container.appendChild(child);
|
|
123
|
+
Object.defineProperty(child, 'scrollTop', { value: 30, writable: true });
|
|
124
|
+
|
|
125
|
+
const state = PreservationHelper.preserve([child]);
|
|
126
|
+
child.remove();
|
|
127
|
+
child.scrollTop = 0;
|
|
128
|
+
PreservationHelper.restore(state);
|
|
129
|
+
|
|
130
|
+
expect(child.scrollTop).toBe(0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('restores focus without a selection on a focused element that is not a text field', () => {
|
|
134
|
+
const child = document.createElement('div');
|
|
135
|
+
child.innerHTML = '<button>ok</button>';
|
|
136
|
+
container.appendChild(child);
|
|
137
|
+
const button = child.querySelector('button');
|
|
138
|
+
button.focus();
|
|
139
|
+
|
|
140
|
+
const state = PreservationHelper.preserve([child]);
|
|
141
|
+
child.remove();
|
|
142
|
+
container.appendChild(child);
|
|
143
|
+
PreservationHelper.restore(state);
|
|
144
|
+
|
|
145
|
+
expect(state.focus.selection).toBe(null);
|
|
146
|
+
expect(document.activeElement).toBe(button);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('does not refocus an element that kept its focus', () => {
|
|
150
|
+
const child = document.createElement('div');
|
|
151
|
+
child.innerHTML = '<input value="hello">';
|
|
152
|
+
container.appendChild(child);
|
|
153
|
+
const input = child.querySelector('input');
|
|
154
|
+
input.focus();
|
|
155
|
+
let focusCalls = 0;
|
|
156
|
+
input.focus = () => { focusCalls++; };
|
|
157
|
+
|
|
158
|
+
const state = PreservationHelper.preserve([child]);
|
|
159
|
+
PreservationHelper.restore(state);
|
|
160
|
+
|
|
161
|
+
expect(focusCalls).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('does not park anything when none of the elements are in the document', () => {
|
|
165
|
+
Element.prototype.moveBefore = function (node, child) {
|
|
166
|
+
this.insertBefore(node, child);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
const child = document.createElement('div');
|
|
171
|
+
|
|
172
|
+
const state = PreservationHelper.preserve([child]);
|
|
173
|
+
|
|
174
|
+
expect(state.parking).toBe(null);
|
|
175
|
+
} finally {
|
|
176
|
+
delete Element.prototype.moveBefore;
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
package/templates/base/src/core/base-classes/base-component/helpers/preservation.helper.ts.hbs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as DomHelper from './dom.helper.js';
|
|
2
|
+
|
|
3
|
+
type SelectionDirection = 'forward' | 'backward' | 'none';
|
|
4
|
+
|
|
5
|
+
interface FocusState {
|
|
6
|
+
element: HTMLElement;
|
|
7
|
+
selection: [number, number, SelectionDirection] | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ScrollState {
|
|
11
|
+
element: Element;
|
|
12
|
+
top: number;
|
|
13
|
+
left: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PreservedState {
|
|
17
|
+
focus: FocusState | null;
|
|
18
|
+
scroll: ScrollState[];
|
|
19
|
+
parking: HTMLElement | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Keeps reused children's DOM state across their parent's innerHTML reassignment.
|
|
23
|
+
// Where Element.moveBefore() exists, children are parked outside the parent so
|
|
24
|
+
// they never leave the document. Otherwise they are detached, and focus,
|
|
25
|
+
// selection and scroll are restored once they're back in place.
|
|
26
|
+
export function preserve(elements: HTMLElement[]): PreservedState {
|
|
27
|
+
// Parked elements keep their focus and scroll; only detached ones need restoring.
|
|
28
|
+
const parking = park(elements);
|
|
29
|
+
if (parking) return { focus: null, scroll: [], parking };
|
|
30
|
+
|
|
31
|
+
return { focus: captureFocus(elements), scroll: captureScroll(elements), parking: null };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function restore(state: PreservedState): void {
|
|
35
|
+
state.parking?.remove();
|
|
36
|
+
state.scroll.forEach(({ element, top, left }) => {
|
|
37
|
+
if (!element.isConnected) return;
|
|
38
|
+
if (element.scrollTop !== top) element.scrollTop = top;
|
|
39
|
+
if (element.scrollLeft !== left) element.scrollLeft = left;
|
|
40
|
+
});
|
|
41
|
+
restoreFocus(state.focus);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function captureFocus(elements: HTMLElement[]): FocusState | null {
|
|
45
|
+
const active = document.activeElement;
|
|
46
|
+
if (!(active instanceof HTMLElement)) return null;
|
|
47
|
+
if (!elements.some(element => element.contains(active))) return null;
|
|
48
|
+
|
|
49
|
+
const hasSelection = (active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement)
|
|
50
|
+
&& active.selectionStart !== null
|
|
51
|
+
&& active.selectionEnd !== null;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
element: active,
|
|
55
|
+
selection: hasSelection
|
|
56
|
+
? [active.selectionStart!, active.selectionEnd!, active.selectionDirection ?? 'none']
|
|
57
|
+
: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function captureScroll(elements: HTMLElement[]): ScrollState[] {
|
|
62
|
+
const scrolled: ScrollState[] = [];
|
|
63
|
+
elements.forEach(root => {
|
|
64
|
+
[root, ...Array.from(root.querySelectorAll('*'))].forEach(element => {
|
|
65
|
+
if (element.scrollTop || element.scrollLeft) {
|
|
66
|
+
scrolled.push({ element, top: element.scrollTop, left: element.scrollLeft });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
return scrolled;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function park(elements: HTMLElement[]): HTMLElement | null {
|
|
74
|
+
const body = document.body;
|
|
75
|
+
const connected = elements.filter(element => element.isConnected);
|
|
76
|
+
if (!body || !connected.length || !DomHelper.canMoveBefore(body, connected[0]!)) return null;
|
|
77
|
+
|
|
78
|
+
const parking = document.createElement('div');
|
|
79
|
+
body.appendChild(parking);
|
|
80
|
+
DomHelper.prepareMoveTarget(parking, connected[0]!);
|
|
81
|
+
connected.forEach(element => DomHelper.moveBefore(parking, element, null));
|
|
82
|
+
return parking;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function restoreFocus(focus: FocusState | null): void {
|
|
86
|
+
if (!focus || !focus.element.isConnected) return;
|
|
87
|
+
const { element, selection } = focus;
|
|
88
|
+
|
|
89
|
+
if (document.activeElement !== element) element.focus({ preventScroll: true });
|
|
90
|
+
if (selection && (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
91
|
+
element.setSelectionRange(...selection);
|
|
92
|
+
}
|
|
93
|
+
}
|