typo3-typescript-sources 13.4.20
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/.github/workflows/sync-and-publish.yml +265 -0
- package/.last-synced-tag +1 -0
- package/LICENSE.txt +339 -0
- package/README-TYPO3.md +166 -0
- package/README.md +28 -0
- package/adminpanel/admin-panel.ts +375 -0
- package/adminpanel/modules/cache.ts +27 -0
- package/adminpanel/modules/preview.ts +75 -0
- package/backend/action-button/abstract-action.ts +22 -0
- package/backend/action-button/deferred-action.ts +41 -0
- package/backend/action-button/immediate-action.ts +31 -0
- package/backend/action-dispatcher.ts +104 -0
- package/backend/ajax-data-handler/message-interface.ts +18 -0
- package/backend/ajax-data-handler/response-interface.ts +20 -0
- package/backend/ajax-data-handler.ts +214 -0
- package/backend/backend-exception.ts +22 -0
- package/backend/broadcast-message.ts +50 -0
- package/backend/broadcast-service.ts +52 -0
- package/backend/browse-database.ts +38 -0
- package/backend/clear-cache.ts +88 -0
- package/backend/clipboard-panel.ts +240 -0
- package/backend/close-current-window.ts +2 -0
- package/backend/code-editor/autocomplete/completion-result.ts +105 -0
- package/backend/code-editor/autocomplete/ts-code-completion.ts +142 -0
- package/backend/code-editor/autocomplete/ts-parser.ts +485 -0
- package/backend/code-editor/autocomplete/ts-ref.ts +164 -0
- package/backend/code-editor/element/code-mirror-element.ts +282 -0
- package/backend/code-editor/language/typoscript.ts +181 -0
- package/backend/code-editor/stream-parser/typoscript.ts +1600 -0
- package/backend/color-picker.ts +158 -0
- package/backend/color-scheme-switch.ts +179 -0
- package/backend/column-selector-button.ts +302 -0
- package/backend/context-help.ts +68 -0
- package/backend/context-menu-actions.ts +322 -0
- package/backend/context-menu.ts +751 -0
- package/backend/copy-to-clipboard.ts +79 -0
- package/backend/date-time-picker.ts +270 -0
- package/backend/document-save-actions.ts +159 -0
- package/backend/drag-tooltip.ts +285 -0
- package/backend/drag-uploader.ts +914 -0
- package/backend/element/alert-element.ts +116 -0
- package/backend/element/dispatch-modal-button.ts +50 -0
- package/backend/element/draggable-resizable-element.ts +249 -0
- package/backend/element/editable-page-title.ts +306 -0
- package/backend/element/icon-element.ts +84 -0
- package/backend/element/immediate-action-element.ts +89 -0
- package/backend/element/pagination.ts +59 -0
- package/backend/element/progress-bar-element.ts +172 -0
- package/backend/element/progress-tracker-element.ts +119 -0
- package/backend/element/pseudo-button.ts +58 -0
- package/backend/element/spinner-element.ts +51 -0
- package/backend/element/thumbnail-element.ts +80 -0
- package/backend/element-browser.ts +182 -0
- package/backend/enum/data-transfer-types.ts +21 -0
- package/backend/enum/icon-types.ts +31 -0
- package/backend/enum/key-types.ts +27 -0
- package/backend/enum/severity.ts +20 -0
- package/backend/enum/viewport/scaffold-identifier.ts +24 -0
- package/backend/event/client-request.ts +25 -0
- package/backend/event/consumable.ts +18 -0
- package/backend/event/consumer-scope.ts +54 -0
- package/backend/event/event-dispatcher.ts +26 -0
- package/backend/event/interaction-request-assignment.ts +24 -0
- package/backend/event/interaction-request-map.ts +75 -0
- package/backend/event/interaction-request.ts +48 -0
- package/backend/event/trigger-request.ts +56 -0
- package/backend/form/submit-interceptor.ts +66 -0
- package/backend/form-engine/container/files-control-container.ts +716 -0
- package/backend/form-engine/container/flex-form-container-container.ts +166 -0
- package/backend/form-engine/container/flex-form-section-container.ts +210 -0
- package/backend/form-engine/container/inline-control-container.ts +1230 -0
- package/backend/form-engine/container/site-language-container.ts +616 -0
- package/backend/form-engine/element/abstract-sortable-select-items.ts +186 -0
- package/backend/form-engine/element/category-element.ts +160 -0
- package/backend/form-engine/element/color-element.ts +81 -0
- package/backend/form-engine/element/datetime-element.ts +66 -0
- package/backend/form-engine/element/extra/char-counter.ts +112 -0
- package/backend/form-engine/element/extra/select-box-filter.ts +85 -0
- package/backend/form-engine/element/folder-element.ts +66 -0
- package/backend/form-engine/element/group-element.ts +47 -0
- package/backend/form-engine/element/json-element.ts +52 -0
- package/backend/form-engine/element/link-element.ts +121 -0
- package/backend/form-engine/element/mfa-info-element.ts +169 -0
- package/backend/form-engine/element/modifier/resizable.ts +27 -0
- package/backend/form-engine/element/modifier/tabbable.ts +29 -0
- package/backend/form-engine/element/online-media-form-element.ts +65 -0
- package/backend/form-engine/element/password-element.ts +71 -0
- package/backend/form-engine/element/select-multiple-side-by-side-element.ts +83 -0
- package/backend/form-engine/element/select-single-element.ts +82 -0
- package/backend/form-engine/element/select-tree-element.ts +148 -0
- package/backend/form-engine/element/select-tree-toolbar.ts +118 -0
- package/backend/form-engine/element/select-tree.ts +210 -0
- package/backend/form-engine/element/slug-element.ts +243 -0
- package/backend/form-engine/element/suggest/result-container.ts +118 -0
- package/backend/form-engine/element/suggest/result-item.ts +90 -0
- package/backend/form-engine/element/table-permission-element.ts +101 -0
- package/backend/form-engine/element/table-wizard-element.ts +427 -0
- package/backend/form-engine/element/text-element.ts +52 -0
- package/backend/form-engine/element/text-table-element.ts +31 -0
- package/backend/form-engine/field-control/add-record.ts +40 -0
- package/backend/form-engine/field-control/edit-popup.ts +64 -0
- package/backend/form-engine/field-control/insert-clipboard.ts +50 -0
- package/backend/form-engine/field-control/link-popup.ts +53 -0
- package/backend/form-engine/field-control/list-module.ts +40 -0
- package/backend/form-engine/field-control/password-generator.ts +102 -0
- package/backend/form-engine/field-control/reset-selection.ts +48 -0
- package/backend/form-engine/field-wizard/localization-state-selector.ts +67 -0
- package/backend/form-engine/field-wizard/value-picker.ts +123 -0
- package/backend/form-engine/field-wizard/value-slider.ts +86 -0
- package/backend/form-engine/inline-relation/ajax-dispatcher.ts +124 -0
- package/backend/form-engine/inline-relation/inline-response-interface.ts +41 -0
- package/backend/form-engine-link-browser-adapter.ts +129 -0
- package/backend/form-engine-review.ts +155 -0
- package/backend/form-engine-suggest.ts +133 -0
- package/backend/form-engine-validation.ts +686 -0
- package/backend/form-engine.ts +1391 -0
- package/backend/global-event-handler.ts +252 -0
- package/backend/grid-editor.ts +1077 -0
- package/backend/hashing/md5.ts +252 -0
- package/backend/helper.ts +24 -0
- package/backend/hotkeys/hotkey-storage.ts +63 -0
- package/backend/hotkeys.ts +216 -0
- package/backend/icons.ts +236 -0
- package/backend/image-manipulation.ts +885 -0
- package/backend/info-window.ts +46 -0
- package/backend/input/clearable.ts +128 -0
- package/backend/input/index.d.ts +8 -0
- package/backend/java-script-module-import-event-handler.ts +19 -0
- package/backend/key-bindings.ts +42 -0
- package/backend/layout-module/drag-drop.ts +359 -0
- package/backend/layout-module/paste.ts +165 -0
- package/backend/link-browser.ts +89 -0
- package/backend/live-search/element/backend-search.ts +34 -0
- package/backend/live-search/element/hint.ts +33 -0
- package/backend/live-search/element/provider/default-result-item.ts +52 -0
- package/backend/live-search/element/provider/page-provider-result-item.ts +52 -0
- package/backend/live-search/element/result/item/action/action-container.ts +135 -0
- package/backend/live-search/element/result/item/action/action.ts +56 -0
- package/backend/live-search/element/result/item/item-container.ts +224 -0
- package/backend/live-search/element/result/item/item.ts +74 -0
- package/backend/live-search/element/result/result-container.ts +110 -0
- package/backend/live-search/element/result/result-detail-container.ts +50 -0
- package/backend/live-search/element/result/result-pagination.ts +122 -0
- package/backend/live-search/element/search-option-item.ts +70 -0
- package/backend/live-search/live-search-configurator.ts +54 -0
- package/backend/live-search/live-search-shortcut.ts +29 -0
- package/backend/live-search/result-types/backend-module-result-type.ts +8 -0
- package/backend/live-search/result-types/default-result-type.ts +21 -0
- package/backend/live-search/result-types/page-result-type.ts +23 -0
- package/backend/localization.ts +390 -0
- package/backend/login-refresh.ts +525 -0
- package/backend/login.ts +179 -0
- package/backend/mail-link-handler.ts +45 -0
- package/backend/modal-interface.ts +19 -0
- package/backend/modal.ts +685 -0
- package/backend/module/iframe.ts +125 -0
- package/backend/module/router.ts +275 -0
- package/backend/module-menu.ts +550 -0
- package/backend/module.ts +133 -0
- package/backend/multi-record-selection-action.ts +44 -0
- package/backend/multi-record-selection-delete-action.ts +108 -0
- package/backend/multi-record-selection-edit-action.ts +56 -0
- package/backend/multi-record-selection.ts +468 -0
- package/backend/multi-step-wizard.ts +568 -0
- package/backend/new-content-element-wizard-button.ts +51 -0
- package/backend/new-multiple-pages.ts +108 -0
- package/backend/new-record-wizard.ts +680 -0
- package/backend/notification.ts +366 -0
- package/backend/offset.ts +40 -0
- package/backend/online-media.ts +110 -0
- package/backend/page-actions.ts +91 -0
- package/backend/page-link-handler.ts +51 -0
- package/backend/pagetsconfig/pagetsconfig-includes.ts +77 -0
- package/backend/popover.ts +165 -0
- package/backend/record-download-button.ts +104 -0
- package/backend/record-link-handler.ts +31 -0
- package/backend/record-search.ts +44 -0
- package/backend/recordlist.ts +230 -0
- package/backend/resource/resource.ts +35 -0
- package/backend/security/element/csp-reports.ts +406 -0
- package/backend/security/element/sudo-mode.ts +249 -0
- package/backend/security/sudo-mode-interceptor.ts +19 -0
- package/backend/settings/editor/editable-setting.ts +231 -0
- package/backend/settings/editor.ts +451 -0
- package/backend/settings/enum/settings-mode.enum.ts +24 -0
- package/backend/settings/type/base.ts +200 -0
- package/backend/settings/type/bool.ts +56 -0
- package/backend/settings/type/color.ts +64 -0
- package/backend/settings/type/int.ts +85 -0
- package/backend/settings/type/number.ts +61 -0
- package/backend/settings/type/page.ts +100 -0
- package/backend/settings/type/string.ts +77 -0
- package/backend/settings/type/stringlist.ts +87 -0
- package/backend/severity.ts +88 -0
- package/backend/site-inline-actions.ts +32 -0
- package/backend/sortable-table.ts +208 -0
- package/backend/storage/abstract-client-storage.ts +76 -0
- package/backend/storage/browser-session.ts +28 -0
- package/backend/storage/client.ts +28 -0
- package/backend/storage/module-state-storage.ts +118 -0
- package/backend/storage/persistent.ts +204 -0
- package/backend/switch-user.ts +82 -0
- package/backend/tabs.ts +72 -0
- package/backend/telephone-link-handler.ts +40 -0
- package/backend/tests/backend/storage/browser-storage-test.ts +87 -0
- package/backend/tests/backend/storage/client-test.ts +87 -0
- package/backend/tests/backend/utility/dom-helper-test.ts +51 -0
- package/backend/tests/backend-exception-test.ts +28 -0
- package/backend/tests/element/immediate-action-element-test.ts +81 -0
- package/backend/tests/form-engine-validation-test.ts +304 -0
- package/backend/tests/grid-editor-test.ts +29 -0
- package/backend/tests/hashing/md5Test.ts +28 -0
- package/backend/tests/icons-test.ts +23 -0
- package/backend/tests/notification-test.ts +161 -0
- package/backend/tests/popover-test.ts +83 -0
- package/backend/toolbar/clear-cache-menu.ts +88 -0
- package/backend/toolbar/live-search.ts +284 -0
- package/backend/toolbar/shortcut-menu.ts +254 -0
- package/backend/toolbar/system-information-menu.ts +161 -0
- package/backend/toolbar.ts +41 -0
- package/backend/tree/file-storage-browser.ts +194 -0
- package/backend/tree/file-storage-tree-container.ts +465 -0
- package/backend/tree/file-storage-tree.ts +42 -0
- package/backend/tree/page-browser.ts +272 -0
- package/backend/tree/page-tree-element.ts +602 -0
- package/backend/tree/page-tree.ts +152 -0
- package/backend/tree/tree-module-state.ts +159 -0
- package/backend/tree/tree-node-toggle.ts +31 -0
- package/backend/tree/tree-node.ts +97 -0
- package/backend/tree/tree-toolbar.ts +117 -0
- package/backend/tree/tree.ts +1595 -0
- package/backend/url-link-handler.ts +36 -0
- package/backend/user-pass-login.ts +255 -0
- package/backend/user-settings-manager.ts +189 -0
- package/backend/utility/collapse-state-persister.ts +205 -0
- package/backend/utility/collapse-state-search.ts +183 -0
- package/backend/utility/dom-helper.ts +91 -0
- package/backend/utility/drag-drop-utility.ts +47 -0
- package/backend/utility/message-utility.ts +45 -0
- package/backend/utility/top-level-module-import.ts +32 -0
- package/backend/utility.ts +194 -0
- package/backend/viewport/abstract-container.ts +22 -0
- package/backend/viewport/content-container.ts +98 -0
- package/backend/viewport/loader.ts +28 -0
- package/backend/viewport/navigation-container.ts +119 -0
- package/backend/viewport/resizable-navigation.ts +176 -0
- package/backend/viewport/toolbar.ts +27 -0
- package/backend/viewport/topbar.ts +39 -0
- package/backend/viewport.ts +46 -0
- package/backend/window-manager.ts +84 -0
- package/backend/wizard/move-content-element.ts +107 -0
- package/backend/wizard/move-page.ts +74 -0
- package/backend/wizard.ts +331 -0
- package/belog/backend-log.ts +88 -0
- package/beuser/backend-user-listing.ts +40 -0
- package/beuser/permissions.ts +291 -0
- package/core/ajax/ajax-request-types.ts +15 -0
- package/core/ajax/ajax-request.ts +202 -0
- package/core/ajax/ajax-response.ts +57 -0
- package/core/ajax/input-transformer.ts +92 -0
- package/core/ajax/simple-response-interface.ts +5 -0
- package/core/authentication/mfa-provider/totp.ts +54 -0
- package/core/cache/dedupe-async-task.ts +95 -0
- package/core/directive/markdown.ts +82 -0
- package/core/directive/nl2br.ts +16 -0
- package/core/document-service.ts +35 -0
- package/core/event/debounce-event.ts +40 -0
- package/core/event/event-interface.ts +24 -0
- package/core/event/regular-event.ts +58 -0
- package/core/event/request-animation-frame-event.ts +42 -0
- package/core/event/throttle-event.ts +54 -0
- package/core/java-script-item-handler.ts +29 -0
- package/core/java-script-item-processor.ts +171 -0
- package/core/lit-helper.ts +126 -0
- package/core/literals.ts +17 -0
- package/core/referrer-refresh.ts +2 -0
- package/core/security-utility.ts +89 -0
- package/core/tests/ajax/ajax-request-test.ts +290 -0
- package/core/tests/ajax/input-transformer-test.ts +64 -0
- package/core/tests/cache/dedupe-async-task-test.ts +152 -0
- package/core/tests/event/throttle-event-test.ts +47 -0
- package/core/tests/literals-test.ts +24 -0
- package/core/tests/security-utility-test.ts +50 -0
- package/core/utility/types.ts +9 -0
- package/dashboard/chart-initializer.ts +136 -0
- package/dashboard/dashboard.ts +1487 -0
- package/extensionmanager/distribution-image.ts +55 -0
- package/extensionmanager/main.ts +291 -0
- package/extensionmanager/repository.ts +195 -0
- package/extensionmanager/update.ts +118 -0
- package/extensionmanager/upload-form.ts +42 -0
- package/filelist/browse-files.ts +148 -0
- package/filelist/browse-folders.ts +94 -0
- package/filelist/context-menu-actions.ts +335 -0
- package/filelist/create-folder.ts +69 -0
- package/filelist/file-delete.ts +69 -0
- package/filelist/file-list-actions.ts +132 -0
- package/filelist/file-list-dragdrop.ts +139 -0
- package/filelist/file-list-rename-handler.ts +128 -0
- package/filelist/file-list-transfer-handler.ts +138 -0
- package/filelist/file-list.ts +396 -0
- package/filelist/file-replace.ts +37 -0
- package/filelist/linkbrowser-file-handler.ts +94 -0
- package/filelist/linkbrowser-folder-handler.ts +74 -0
- package/filelist/rename-file.ts +109 -0
- package/form/backend/form-editor/component/property-grid-editor.ts +340 -0
- package/form/backend/form-editor/core.ts +2104 -0
- package/form/backend/form-editor/helper.ts +226 -0
- package/form/backend/form-editor/inspector-component.ts +2323 -0
- package/form/backend/form-editor/mediator.ts +941 -0
- package/form/backend/form-editor/modals-component.ts +433 -0
- package/form/backend/form-editor/stage-component.ts +908 -0
- package/form/backend/form-editor/tree-component.ts +548 -0
- package/form/backend/form-editor/view-model.ts +1343 -0
- package/form/backend/form-editor.ts +846 -0
- package/form/backend/form-manager/main.ts +39 -0
- package/form/backend/form-manager/view-model.ts +789 -0
- package/form/backend/form-manager.ts +180 -0
- package/form/backend/helper.ts +60 -0
- package/form/frontend/date-picker.ts +38 -0
- package/frontend/default_frontend.ts +82 -0
- package/impexp/context-menu-actions.ts +50 -0
- package/impexp/import-export.ts +62 -0
- package/indexed-search/pagination.ts +26 -0
- package/indexed-search/statistics.ts +50 -0
- package/info/translation-status.ts +48 -0
- package/install/ajax/ajax-queue.ts +83 -0
- package/install/init-install.ts +2 -0
- package/install/init-installer.ts +2 -0
- package/install/install.ts +25 -0
- package/install/installer.ts +458 -0
- package/install/message-interface.ts +18 -0
- package/install/module/abstract-inline-module.ts +27 -0
- package/install/module/abstract-interactable-module.ts +107 -0
- package/install/module/environment/environment-check.ts +92 -0
- package/install/module/environment/folder-structure.ts +145 -0
- package/install/module/environment/image-processing.ts +141 -0
- package/install/module/environment/mail-test.ts +127 -0
- package/install/module/environment/php-info.ts +50 -0
- package/install/module/environment/system-information.ts +50 -0
- package/install/module/maintenance/cache.ts +59 -0
- package/install/module/maintenance/clear-tables.ts +134 -0
- package/install/module/maintenance/clear-typo3temp-files.ts +137 -0
- package/install/module/maintenance/create-admin.ts +126 -0
- package/install/module/maintenance/database-analyzer.ts +266 -0
- package/install/module/maintenance/dump-autoload.ts +58 -0
- package/install/module/maintenance/language-packs.ts +338 -0
- package/install/module/maintenance/reset-backend-user-uc.ts +58 -0
- package/install/module/password-strength.ts +56 -0
- package/install/module/settings/change-install-tool-password.ts +101 -0
- package/install/module/settings/extension-configuration.ts +224 -0
- package/install/module/settings/features.ts +102 -0
- package/install/module/settings/local-configuration.ts +316 -0
- package/install/module/settings/presets.ts +147 -0
- package/install/module/settings/system-maintainer.ts +111 -0
- package/install/module/upgrade/core-update.ts +255 -0
- package/install/module/upgrade/extension-compat-tester.ts +195 -0
- package/install/module/upgrade/extension-scanner.ts +381 -0
- package/install/module/upgrade/tca-ext-tables-check.ts +93 -0
- package/install/module/upgrade/tca-migrations-check.ts +101 -0
- package/install/module/upgrade/upgrade-docs.ts +310 -0
- package/install/module/upgrade/upgrade-wizards.ts +612 -0
- package/install/renderable/clearable.ts +113 -0
- package/install/renderable/flash-message.ts +64 -0
- package/install/renderable/info-box.ts +66 -0
- package/install/renderable/language-packs.ts +304 -0
- package/install/renderable/offset-group.ts +38 -0
- package/install/renderable/severity.ts +49 -0
- package/install/renderable/wrap-group.ts +36 -0
- package/install/router.ts +468 -0
- package/linkvalidator/linkvalidator-modal.ts +45 -0
- package/linkvalidator/linkvalidator.ts +101 -0
- package/lowlevel/query-generator.ts +132 -0
- package/lowlevel/reference-index.ts +44 -0
- package/opendocs/toolbar/opendocs-menu.ts +117 -0
- package/package.json +34 -0
- package/recycler/recycler.ts +539 -0
- package/redirects/event-handler.ts +108 -0
- package/redirects/form-engine-evaluation.ts +34 -0
- package/redirects/redirects-module.ts +58 -0
- package/rte-ckeditor/ckeditor5.ts +402 -0
- package/rte-ckeditor/css-prefixer.ts +94 -0
- package/rte-ckeditor/legacy/ckeditor5-bundle.ts +17 -0
- package/rte-ckeditor/legacy/ckeditor5-inspector.ts +2 -0
- package/rte-ckeditor/plugin/typo3-link.ts +810 -0
- package/rte-ckeditor/plugin/whitespace.ts +114 -0
- package/rte-ckeditor/rte-link-browser.ts +112 -0
- package/rte-ckeditor/tests/css-prefixer-test.ts +314 -0
- package/scheduler/scheduler-add-group.ts +119 -0
- package/scheduler/scheduler-editable-group-name.ts +307 -0
- package/scheduler/scheduler-sortable-groups.ts +59 -0
- package/scheduler/scheduler.ts +466 -0
- package/setup/setup-module.ts +122 -0
- package/styleguide/checkbox-demo.ts +5 -0
- package/styleguide/element/theme-switcher-element.ts +101 -0
- package/styleguide/filter.ts +22 -0
- package/styleguide/find-icons.ts +78 -0
- package/styleguide/processing-indicator.ts +78 -0
- package/styleguide/render-notifications.ts +64 -0
- package/sys-note/element/delete-button.ts +78 -0
- package/tstemplate/constant-editor.ts +64 -0
- package/tstemplate/information-module.ts +31 -0
- package/tstemplate/template-analyzer.ts +77 -0
- package/viewpage/main.ts +253 -0
- package/workspaces/backend.ts +979 -0
- package/workspaces/preview.ts +232 -0
- package/workspaces/renderable/comment-view.ts +79 -0
- package/workspaces/renderable/diff-view.ts +58 -0
- package/workspaces/renderable/history-view.ts +97 -0
- package/workspaces/renderable/record-information.ts +118 -0
- package/workspaces/renderable/record-table.ts +355 -0
- package/workspaces/renderable/send-to-stage-form.ts +108 -0
- package/workspaces/toolbar/workspaces-menu.ts +190 -0
- package/workspaces/utility/icon-helper.ts +57 -0
- package/workspaces/workspaces.ts +136 -0
|
@@ -0,0 +1,1595 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the TYPO3 CMS project.
|
|
3
|
+
*
|
|
4
|
+
* It is free software; you can redistribute it and/or modify it under
|
|
5
|
+
* the terms of the GNU General Public License, either version 2
|
|
6
|
+
* of the License, or any later version.
|
|
7
|
+
*
|
|
8
|
+
* For the full copyright and license information, please read the
|
|
9
|
+
* LICENSE.txt file that was distributed with this source code.
|
|
10
|
+
*
|
|
11
|
+
* The TYPO3 project - inspiring people to share!
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { html, LitElement, type TemplateResult, type PropertyValues, nothing } from 'lit';
|
|
15
|
+
import { property, state, query } from 'lit/decorators';
|
|
16
|
+
import { repeat } from 'lit/directives/repeat';
|
|
17
|
+
import { styleMap } from 'lit/directives/style-map';
|
|
18
|
+
import { ifDefined } from 'lit/directives/if-defined';
|
|
19
|
+
import { TreeNodeCommandEnum, TreeNodePositionEnum, type TreeNodeInterface, type TreeNodeStatusInformation, type TreeNodeLabel } from './tree-node';
|
|
20
|
+
import AjaxRequest from '@typo3/core/ajax/ajax-request';
|
|
21
|
+
import Notification from '../notification';
|
|
22
|
+
import { KeyTypesEnum as KeyTypes } from '../enum/key-types';
|
|
23
|
+
import '@typo3/backend/element/icon-element';
|
|
24
|
+
import ClientStorage from '@typo3/backend/storage/client';
|
|
25
|
+
import { DataTransferTypes } from '@typo3/backend/enum/data-transfer-types';
|
|
26
|
+
import Severity from '@typo3/backend/severity';
|
|
27
|
+
import type { AjaxResponse } from '@typo3/core/ajax/ajax-response';
|
|
28
|
+
import type { DragTooltipMetadata } from '@typo3/backend/drag-tooltip';
|
|
29
|
+
|
|
30
|
+
interface TreeNodeStatus {
|
|
31
|
+
expanded: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DataTransferStringItem {
|
|
35
|
+
type: DataTransferTypes,
|
|
36
|
+
data: string,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TreeSettings {
|
|
40
|
+
[keys: string]: any;
|
|
41
|
+
defaultProperties: {[keys: string]: any};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TreeWrapper extends HTMLElement {
|
|
45
|
+
tree?: Tree
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class TreeNodeMap<T extends TreeNodeInterface = TreeNodeInterface> {
|
|
49
|
+
private readonly treeNodes: T[];
|
|
50
|
+
private treeIdentifierIndex: Record<string, number>;
|
|
51
|
+
|
|
52
|
+
constructor(nodes: T[]) {
|
|
53
|
+
this.treeNodes = nodes;
|
|
54
|
+
this.updateIndexes();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public get length(): number {
|
|
58
|
+
return this.treeNodes.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public toArray(): ReadonlyArray<T> {
|
|
62
|
+
return this.treeNodes;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public splice(start: number, deleteCount?: number, ...items: T[]) {
|
|
66
|
+
this.treeNodes.splice(start, deleteCount, ...items);
|
|
67
|
+
this.updateIndexes();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public getNodeByTreeIdentifier(treeIdentifier: string): T|null {
|
|
71
|
+
const index = this.treeIdentifierIndex[treeIdentifier] ?? null;
|
|
72
|
+
return this.treeNodes[index] ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private updateIndexes(): void {
|
|
76
|
+
this.treeIdentifierIndex = Object.fromEntries(this.treeNodes.map((node, index) => ([node.__treeIdentifier, index])));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class Tree extends LitElement {
|
|
81
|
+
@property({ type: Object }) setup?: {[keys: string]: any} = null;
|
|
82
|
+
@state() settings: TreeSettings = {
|
|
83
|
+
showIcons: false,
|
|
84
|
+
width: 300,
|
|
85
|
+
dataUrl: '',
|
|
86
|
+
filterUrl: '',
|
|
87
|
+
defaultProperties: {},
|
|
88
|
+
expandUpToLevel: null as any,
|
|
89
|
+
actions: []
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
@query('.nodes-root') root: HTMLElement;
|
|
93
|
+
@state() nodeMap: TreeNodeMap = new TreeNodeMap([]);
|
|
94
|
+
@state() currentScrollPosition: number = 0;
|
|
95
|
+
@state() currentVisibleHeight: number = 0;
|
|
96
|
+
@state() searchTerm: string|null = null;
|
|
97
|
+
@state() loading: boolean = false;
|
|
98
|
+
|
|
99
|
+
@state() hoveredNode: TreeNodeInterface|null = null;
|
|
100
|
+
@state() nodeDragAllowed: boolean = false;
|
|
101
|
+
|
|
102
|
+
public isOverRoot: boolean = false;
|
|
103
|
+
|
|
104
|
+
public nodeDragPosition: TreeNodePositionEnum|null = null;
|
|
105
|
+
public nodeDragMode: TreeNodeCommandEnum|null = null;
|
|
106
|
+
public draggingNode: TreeNodeInterface|null = null;
|
|
107
|
+
|
|
108
|
+
protected nodeHeight: number = 32;
|
|
109
|
+
protected indentWidth: number = 20;
|
|
110
|
+
protected displayNodes: TreeNodeInterface[] = [];
|
|
111
|
+
protected focusedNode: TreeNodeInterface|null = null;
|
|
112
|
+
protected lastFocusedNode: TreeNodeInterface|null = null;
|
|
113
|
+
protected editingNode: TreeNodeInterface|null = null;
|
|
114
|
+
|
|
115
|
+
protected openNodeTimeout: { targetNode: TreeNodeInterface|null, timeout: number|null } = { targetNode: null, timeout: null };
|
|
116
|
+
|
|
117
|
+
protected unfilteredNodes: string = '';
|
|
118
|
+
protected muteErrorNotifications: boolean = false;
|
|
119
|
+
|
|
120
|
+
protected networkErrorTitle: string = top.TYPO3.lang.tree_networkError;
|
|
121
|
+
protected networkErrorMessage: string = top.TYPO3.lang.tree_networkErrorDescription;
|
|
122
|
+
|
|
123
|
+
protected allowNodeEdit: boolean = false;
|
|
124
|
+
protected allowNodeDrag: boolean = false;
|
|
125
|
+
protected allowNodeSorting: boolean = false;
|
|
126
|
+
|
|
127
|
+
protected currentFilterRequest: AjaxRequest|null = null;
|
|
128
|
+
|
|
129
|
+
private __loadFinished: () => void;
|
|
130
|
+
private __loadPromise: Promise<void> = new Promise(res => this.__loadFinished = res);
|
|
131
|
+
private lastRenderScrollPosition: number = null;
|
|
132
|
+
|
|
133
|
+
public get loadComplete(): Promise<void> {
|
|
134
|
+
return this.__loadPromise;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public get nodes(): ReadonlyArray<TreeNodeInterface> {
|
|
138
|
+
return this.nodeMap.toArray();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public set nodes(nodes: TreeNodeInterface[]) {
|
|
142
|
+
this.nodeMap = new TreeNodeMap(nodes);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public getNodeFromElement(element: HTMLElement): TreeNodeInterface|null
|
|
146
|
+
{
|
|
147
|
+
if (element === null || !('treeId' in element.dataset)) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return this.getNodeByTreeIdentifier(element.dataset.treeId);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public getElementFromNode(node: TreeNodeInterface): HTMLElement|null {
|
|
155
|
+
return this.querySelector('[data-tree-id="' + this.getNodeTreeIdentifier(node) + '"]');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public hideChildren(node: TreeNodeInterface): void {
|
|
159
|
+
node.__expanded = false;
|
|
160
|
+
this.saveNodeStatus(node);
|
|
161
|
+
this.dispatchEvent(new CustomEvent('typo3:tree:expand-toggle', { detail: { node: node } }));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public async showChildren(node: TreeNodeInterface): Promise<void> {
|
|
165
|
+
node.__expanded = true;
|
|
166
|
+
await this.loadChildren(node);
|
|
167
|
+
this.saveNodeStatus(node);
|
|
168
|
+
this.dispatchEvent(new CustomEvent('typo3:tree:expand-toggle', { detail: { node: node } }));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public getDataUrl(parentNode: TreeNodeInterface|null = null): string {
|
|
172
|
+
if (parentNode === null) {
|
|
173
|
+
return this.settings.dataUrl;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return this.settings.dataUrl + '&parent=' + parentNode.identifier + '&depth=' + parentNode.depth;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public getFilterUrl(): string {
|
|
180
|
+
return this.settings.filterUrl + '&q=' + this.searchTerm;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public async loadData(): Promise<void> {
|
|
184
|
+
this.loading = true;
|
|
185
|
+
this.nodeMap = new TreeNodeMap(this.prepareNodes(await this.fetchData()));
|
|
186
|
+
this.__loadFinished();
|
|
187
|
+
this.__loadPromise = new Promise(res => this.__loadFinished = res);
|
|
188
|
+
this.loading = false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public async fetchData(parentNode: TreeNodeInterface|null = null): Promise<TreeNodeInterface[]> {
|
|
192
|
+
try {
|
|
193
|
+
const response = await new AjaxRequest(this.getDataUrl(parentNode)).get({ cache: 'no-cache' });
|
|
194
|
+
let nodes: TreeNodeInterface[] = await response.resolve();
|
|
195
|
+
|
|
196
|
+
if (!Array.isArray(nodes)) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (parentNode !== null) {
|
|
201
|
+
nodes = nodes.filter((node: TreeNodeInterface) => {
|
|
202
|
+
// Filter (already processed) parentNode from server response
|
|
203
|
+
// (if given. note: filetree does not deliver this, pagetree does)
|
|
204
|
+
return node.identifier !== parentNode.identifier;
|
|
205
|
+
});
|
|
206
|
+
// parentNode is needed for enhanceNodes to operate based on the
|
|
207
|
+
// (already processed) data in parentNode
|
|
208
|
+
nodes.unshift(parentNode);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
nodes = this.enhanceNodes(nodes);
|
|
212
|
+
if (parentNode !== null) {
|
|
213
|
+
// drop parentNode from resultset (it is always the first node, as we've added it as reference for enhanceNodes above)
|
|
214
|
+
nodes.shift();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const all = await Promise.all(
|
|
218
|
+
nodes.map(async (node: TreeNodeInterface): Promise<TreeNodeInterface[]> => {
|
|
219
|
+
const parentNodeTreeIdentifier = node.__parents.join('_');
|
|
220
|
+
const parentNode = nodes.find(p => p.__treeIdentifier === parentNodeTreeIdentifier) || null;
|
|
221
|
+
const isVisible = parentNode === null || parentNode.__expanded;
|
|
222
|
+
if (!node.loaded && node.hasChildren && node.__expanded && isVisible) {
|
|
223
|
+
const children = await this.fetchData(node);
|
|
224
|
+
node.loaded = true;
|
|
225
|
+
return [ node, ...children ];
|
|
226
|
+
} else {
|
|
227
|
+
return [ node ];
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
return all.flat();
|
|
232
|
+
} catch (error: any) {
|
|
233
|
+
this.errorNotification(error);
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
public async loadChildren(parentNode: TreeNodeInterface): Promise<void> {
|
|
239
|
+
try {
|
|
240
|
+
if (parentNode.loaded) {
|
|
241
|
+
await Promise.all(
|
|
242
|
+
this.nodes
|
|
243
|
+
.filter(n => n.__parents.join('_') === parentNode.__treeIdentifier && !n.loaded && n.hasChildren && n.__expanded)
|
|
244
|
+
.map(n => this.loadChildren(n))
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
parentNode.__loading = true;
|
|
250
|
+
|
|
251
|
+
const nodes = this.prepareNodes(await this.fetchData(parentNode));
|
|
252
|
+
const positionAfterParentNode = this.nodes.indexOf(parentNode) + 1;
|
|
253
|
+
let deleteCount = 0;
|
|
254
|
+
for (let i = positionAfterParentNode; i < this.nodes.length; ++i) {
|
|
255
|
+
if (this.nodes[i].depth <= parentNode.depth) {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
deleteCount++;
|
|
259
|
+
}
|
|
260
|
+
this.nodeMap.splice(positionAfterParentNode, deleteCount, ...nodes);
|
|
261
|
+
// @todo: do we need to "prepare" all nodes again?
|
|
262
|
+
|
|
263
|
+
parentNode.__loading = false;
|
|
264
|
+
parentNode.loaded = true;
|
|
265
|
+
} catch (error: any) {
|
|
266
|
+
this.errorNotification(error);
|
|
267
|
+
parentNode.__loading = false;
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public getIdentifier(): string
|
|
273
|
+
{
|
|
274
|
+
return this.id ?? this.setup.id;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
public getLocalStorageIdentifier(): string
|
|
278
|
+
{
|
|
279
|
+
return 'tree-state-' + this.getIdentifier();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
public getNodeStatus(node: TreeNodeInterface): TreeNodeStatus {
|
|
283
|
+
const treeState = JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier())) ?? {};
|
|
284
|
+
return treeState[node.__treeIdentifier] ?? {
|
|
285
|
+
expanded: false
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public saveNodeStatus(node: TreeNodeInterface): void {
|
|
290
|
+
const treeState = JSON.parse(ClientStorage.get(this.getLocalStorageIdentifier())) ?? {};
|
|
291
|
+
treeState[node.__treeIdentifier] = {
|
|
292
|
+
expanded: node.__expanded
|
|
293
|
+
};
|
|
294
|
+
ClientStorage.set(this.getLocalStorageIdentifier(), JSON.stringify(treeState));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
public refreshOrFilterTree(): void {
|
|
298
|
+
if (this.searchTerm !== '') {
|
|
299
|
+
this.filter(this.searchTerm);
|
|
300
|
+
} else {
|
|
301
|
+
this.loadData();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
public selectFirstNode(): void {
|
|
306
|
+
const firstNode = this.getFirstNode();
|
|
307
|
+
this.selectNode(firstNode, true);
|
|
308
|
+
this.focusNode(firstNode);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Node selection logic (triggered by different events)
|
|
313
|
+
* This represents a dummy method and is usually overridden
|
|
314
|
+
* The second argument can be interpreted by the listened events to e.g. not avoid reloading the content frame and instead
|
|
315
|
+
* used for just updating the state within the tree
|
|
316
|
+
*/
|
|
317
|
+
public selectNode(node: TreeNodeInterface, propagate: boolean = true): void
|
|
318
|
+
{
|
|
319
|
+
if (!this.isNodeSelectable(node)) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.resetSelectedNodes();
|
|
324
|
+
node.checked = true;
|
|
325
|
+
this.dispatchEvent(new CustomEvent('typo3:tree:node-selected', { detail: { node: node, propagate: propagate } }));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
public async focusNode(node: TreeNodeInterface): Promise<void>
|
|
329
|
+
{
|
|
330
|
+
this.lastFocusedNode = this.focusedNode;
|
|
331
|
+
this.focusedNode = node;
|
|
332
|
+
this.requestUpdate();
|
|
333
|
+
const element = this.getElementFromNode(this.focusedNode);
|
|
334
|
+
if (element) {
|
|
335
|
+
element.focus();
|
|
336
|
+
} else {
|
|
337
|
+
this.updateComplete.then(() => {
|
|
338
|
+
this.getElementFromNode(this.focusedNode)?.focus();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
public async editNode(node: TreeNodeInterface): Promise<void> {
|
|
344
|
+
if (this.isNodeEditable(node)) {
|
|
345
|
+
this.editingNode = node;
|
|
346
|
+
this.requestUpdate();
|
|
347
|
+
this.updateComplete.then(() => {
|
|
348
|
+
const inputField = this.getElementFromNode(this.editingNode)?.querySelector('.node-edit') as HTMLInputElement;
|
|
349
|
+
if (inputField) {
|
|
350
|
+
inputField.focus();
|
|
351
|
+
inputField.select();
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
public async deleteNode(node: TreeNodeInterface): Promise<void> {
|
|
358
|
+
if (!node.deletable) {
|
|
359
|
+
console.error('The Node cannot be deleted.');
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
this.handleNodeDelete(node);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
public async moveNode(node: TreeNodeInterface, target: TreeNodeInterface, position: TreeNodePositionEnum) {
|
|
367
|
+
this.handleNodeMove(node, target, position);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public async addNode(node: TreeNodeInterface, target: TreeNodeInterface, position: TreeNodePositionEnum) {
|
|
371
|
+
let index = this.nodes.indexOf(target);
|
|
372
|
+
const parentNode = position === TreeNodePositionEnum.INSIDE ? target : this.getParentNode(target);
|
|
373
|
+
const newNode = this.enhanceNodes([parentNode, {
|
|
374
|
+
...node,
|
|
375
|
+
depth: parentNode ? parentNode.depth + 1 : 0,
|
|
376
|
+
}]).pop();
|
|
377
|
+
|
|
378
|
+
if (parentNode) {
|
|
379
|
+
if (parentNode.hasChildren && !parentNode.__expanded) {
|
|
380
|
+
await this.showChildren(parentNode);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!parentNode.hasChildren) {
|
|
384
|
+
parentNode.hasChildren = true;
|
|
385
|
+
parentNode.__expanded = true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (position === TreeNodePositionEnum.INSIDE || position === TreeNodePositionEnum.AFTER) {
|
|
390
|
+
index++;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
this.nodeMap.splice(index, 0, newNode);
|
|
394
|
+
this.handleNodeAdd(newNode, target, position);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
public async removeNode(node: TreeNodeInterface) {
|
|
398
|
+
const index = this.nodes.indexOf(node);
|
|
399
|
+
const parentNode = this.getParentNode(node);
|
|
400
|
+
if (index > -1) {
|
|
401
|
+
this.nodeMap.splice(index, 1);
|
|
402
|
+
}
|
|
403
|
+
this.requestUpdate();
|
|
404
|
+
this.updateComplete.then(() => {
|
|
405
|
+
if (parentNode.__expanded && parentNode.hasChildren && this.getNodeChildren(parentNode).length === 0) {
|
|
406
|
+
parentNode.hasChildren = false;
|
|
407
|
+
parentNode.__expanded = false;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public filter(searchTerm?: string|null): void {
|
|
413
|
+
if (typeof searchTerm === 'string') {
|
|
414
|
+
this.searchTerm = searchTerm;
|
|
415
|
+
}
|
|
416
|
+
if (this.searchTerm && this.settings.filterUrl) {
|
|
417
|
+
this.loading = true;
|
|
418
|
+
this.currentFilterRequest?.abort();
|
|
419
|
+
this.currentFilterRequest = new AjaxRequest(this.getFilterUrl());
|
|
420
|
+
this.currentFilterRequest
|
|
421
|
+
.get({ cache: 'no-cache' })
|
|
422
|
+
.then((response: AjaxResponse) => response.resolve())
|
|
423
|
+
.then((json) => {
|
|
424
|
+
const nodes = Array.isArray(json) ? json : [];
|
|
425
|
+
if (nodes.length > 0) {
|
|
426
|
+
if (this.unfilteredNodes === '') {
|
|
427
|
+
this.unfilteredNodes = JSON.stringify(this.nodes);
|
|
428
|
+
}
|
|
429
|
+
this.nodeMap = new TreeNodeMap(this.enhanceNodes(nodes));
|
|
430
|
+
}
|
|
431
|
+
})
|
|
432
|
+
.catch((error: any) => {
|
|
433
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
434
|
+
// Request has been aborted, do not flood the error console
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
this.errorNotification(error);
|
|
439
|
+
throw error;
|
|
440
|
+
}).then(() => {
|
|
441
|
+
this.loading = false;
|
|
442
|
+
this.currentFilterRequest = null;
|
|
443
|
+
});
|
|
444
|
+
} else {
|
|
445
|
+
// restore original state without filters
|
|
446
|
+
this.resetFilter();
|
|
447
|
+
this.loading = false;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
public resetFilter(): void
|
|
452
|
+
{
|
|
453
|
+
this.searchTerm = '';
|
|
454
|
+
if (this.unfilteredNodes.length > 0) {
|
|
455
|
+
const currentlySelected = this.getSelectedNodes()[0];
|
|
456
|
+
if (typeof currentlySelected === 'undefined') {
|
|
457
|
+
this.loadData();
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
this.nodeMap = new TreeNodeMap(this.enhanceNodes(JSON.parse(this.unfilteredNodes)));
|
|
461
|
+
this.unfilteredNodes = '';
|
|
462
|
+
// re-select the node from the identifier because the nodes have been updated
|
|
463
|
+
const currentlySelectedNode = this.getNodeByTreeIdentifier(currentlySelected.__treeIdentifier);
|
|
464
|
+
if (currentlySelectedNode) {
|
|
465
|
+
this.selectNode(currentlySelectedNode, false);
|
|
466
|
+
} else {
|
|
467
|
+
this.loadData();
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
this.loadData();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Displays a notification message and refresh nodes
|
|
476
|
+
*/
|
|
477
|
+
public errorNotification(error: any = null): void {
|
|
478
|
+
if (this.muteErrorNotifications) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (Array.isArray(error)) {
|
|
482
|
+
error.forEach((message: any) => { Notification.error(
|
|
483
|
+
message.title,
|
|
484
|
+
message.message
|
|
485
|
+
);});
|
|
486
|
+
} else {
|
|
487
|
+
let title = this.networkErrorTitle;
|
|
488
|
+
if (error && error.target && (error.target.status || error.target.statusText)) {
|
|
489
|
+
title += ' - ' + (error.target.status || '') + ' ' + (error.target.statusText || '');
|
|
490
|
+
}
|
|
491
|
+
Notification.error(title, this.networkErrorMessage);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
public getSelectedNodes(): TreeNodeInterface[] {
|
|
496
|
+
return this.nodes.filter((node: TreeNodeInterface) => node.checked);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
public getNodeByTreeIdentifier(treeIdentifier: string): TreeNodeInterface|null {
|
|
500
|
+
return this.nodeMap.getNodeByTreeIdentifier(treeIdentifier);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
public getNodeDragStatusIcon(): string
|
|
504
|
+
{
|
|
505
|
+
if (this.nodeDragMode === TreeNodeCommandEnum.DELETE) {
|
|
506
|
+
return 'actions-delete';
|
|
507
|
+
}
|
|
508
|
+
if (this.nodeDragMode === TreeNodeCommandEnum.NEW) {
|
|
509
|
+
return 'actions-add';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (this.nodeDragPosition === TreeNodePositionEnum.BEFORE) {
|
|
513
|
+
return 'apps-pagetree-drag-move-above';
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (this.nodeDragPosition === TreeNodePositionEnum.INSIDE) {
|
|
517
|
+
return 'apps-pagetree-drag-move-into';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (this.nodeDragPosition === TreeNodePositionEnum.AFTER) {
|
|
521
|
+
return 'apps-pagetree-drag-move-below';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return 'actions-ban';
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
public async expandParents(parents: string[]) {
|
|
528
|
+
for (const id of parents) {
|
|
529
|
+
const node = this.nodes.find((node) => node.identifier === id.toString());
|
|
530
|
+
if (!node) {
|
|
531
|
+
// :\ user has no access
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (!node.__expanded) {
|
|
535
|
+
await this.showChildren(node);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
public async expandNodeParents(node: TreeNodeInterface) {
|
|
541
|
+
await this.expandParents(node.__parents);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
protected prepareNodes(nodes: TreeNodeInterface[]): TreeNodeInterface[] {
|
|
545
|
+
const evt = new CustomEvent('typo3:tree:nodes-prepared', { detail: { nodes }, bubbles: false });
|
|
546
|
+
this.dispatchEvent(evt);
|
|
547
|
+
return evt.detail.nodes;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Set parameters like node parents, checked.
|
|
552
|
+
* Usually called when data is loaded initially or replaced completely.
|
|
553
|
+
*/
|
|
554
|
+
protected enhanceNodes(nodes: TreeNodeInterface[]): TreeNodeInterface[] {
|
|
555
|
+
const enhancedNodes = nodes.reduce((nodes: TreeNodeInterface[], node: TreeNodeInterface) => {
|
|
556
|
+
if (node.__processed === true) {
|
|
557
|
+
return [...nodes, node];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
node = Object.assign({}, this.settings.defaultProperties, node);
|
|
561
|
+
node.__parents = [];
|
|
562
|
+
const parentNode = node.depth > 0 ? nodes.findLast(p => p.depth < node.depth) : null;
|
|
563
|
+
if (parentNode) {
|
|
564
|
+
node.__parents = [ ...parentNode.__parents, parentNode.identifier ];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Internal Identifier
|
|
568
|
+
node.__treeIdentifier = node.identifier;
|
|
569
|
+
node.__loading = false;
|
|
570
|
+
node.__treeParents = [];
|
|
571
|
+
if (parentNode) {
|
|
572
|
+
node.__treeIdentifier = parentNode.__treeIdentifier + '_' + node.__treeIdentifier;
|
|
573
|
+
node.__treeParents = [ ...parentNode.__treeParents, parentNode.__treeIdentifier ];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// State
|
|
577
|
+
if (this.searchTerm) {
|
|
578
|
+
node.__expanded = node.loaded && node.hasChildren;
|
|
579
|
+
} else if (node.hasChildren) {
|
|
580
|
+
node.__expanded = (this.settings.expandUpToLevel !== null)
|
|
581
|
+
? node.depth < this.settings.expandUpToLevel
|
|
582
|
+
: Boolean(this.getNodeStatus(node).expanded);
|
|
583
|
+
} else {
|
|
584
|
+
node.__expanded = false;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
node.__processed = true;
|
|
588
|
+
|
|
589
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
590
|
+
const that = this;
|
|
591
|
+
return [
|
|
592
|
+
...nodes,
|
|
593
|
+
new Proxy(node, {
|
|
594
|
+
set<K extends keyof TreeNodeInterface>(target: TreeNodeInterface, key: K, value: TreeNodeInterface[K]) {
|
|
595
|
+
if(target[key] !== value) {
|
|
596
|
+
target[key] = value as never;
|
|
597
|
+
that.requestUpdate();
|
|
598
|
+
}
|
|
599
|
+
return true;
|
|
600
|
+
},
|
|
601
|
+
})
|
|
602
|
+
];
|
|
603
|
+
}, []);
|
|
604
|
+
|
|
605
|
+
// get nodes with depth 0, if there is only 1 then open it and disable toggle
|
|
606
|
+
const nodesOnRootLevel = enhancedNodes.filter((node) => node.depth === 0);
|
|
607
|
+
if (nodesOnRootLevel.length === 1) {
|
|
608
|
+
enhancedNodes[0].__expanded = true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return enhancedNodes;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
protected override createRenderRoot(): HTMLElement | ShadowRoot {
|
|
615
|
+
return this;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
protected override shouldUpdate(changedProperties: PropertyValues<this>): boolean {
|
|
619
|
+
if (changedProperties.size === 1 &&
|
|
620
|
+
changedProperties.has('currentScrollPosition') &&
|
|
621
|
+
this.lastRenderScrollPosition !== null &&
|
|
622
|
+
Math.abs(this.currentScrollPosition - this.lastRenderScrollPosition) / this.nodeHeight < 20
|
|
623
|
+
) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
protected override render(): TemplateResult {
|
|
630
|
+
const loader = this.loading
|
|
631
|
+
? html`
|
|
632
|
+
<div class="nodes-loader">
|
|
633
|
+
<div class="nodes-loader-inner">
|
|
634
|
+
<typo3-backend-icon identifier="spinner-circle" size="medium"></typo3-backend-icon>
|
|
635
|
+
</div>
|
|
636
|
+
</div>`
|
|
637
|
+
: nothing;
|
|
638
|
+
|
|
639
|
+
return html`
|
|
640
|
+
<div class="nodes-container">
|
|
641
|
+
${loader}
|
|
642
|
+
<div
|
|
643
|
+
class="nodes-root"
|
|
644
|
+
@scroll="${(event: Event) => { this.currentScrollPosition = (event.currentTarget as HTMLElement).scrollTop; }}"
|
|
645
|
+
@mouseover=${() => this.isOverRoot = true}
|
|
646
|
+
@mouseout=${() => this.isOverRoot = false}
|
|
647
|
+
@keydown=${(event: KeyboardEvent) => this.handleKeyboardInteraction(event)}
|
|
648
|
+
>
|
|
649
|
+
${this.renderVisibleNodes()}
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Renders the subset of the tree nodes fitting the
|
|
657
|
+
* viewport (adding, modifying and removing nodes)
|
|
658
|
+
*/
|
|
659
|
+
protected renderVisibleNodes(): TemplateResult {
|
|
660
|
+
this.displayNodes = this.nodes.filter(node => (
|
|
661
|
+
node.__hidden !== true &&
|
|
662
|
+
!node.__treeParents.some(parentTreeIdentifier => this.getNodeByTreeIdentifier(parentTreeIdentifier).__expanded === false)
|
|
663
|
+
));
|
|
664
|
+
this.displayNodes.forEach((node: TreeNodeInterface, i: number) => {
|
|
665
|
+
node.__x = node.depth * this.indentWidth;
|
|
666
|
+
node.__y = i * this.nodeHeight;
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
this.lastRenderScrollPosition = this.currentScrollPosition;
|
|
670
|
+
const visibleRows = Math.ceil(this.currentVisibleHeight / this.nodeHeight);
|
|
671
|
+
const position = Math.floor(this.currentScrollPosition / this.nodeHeight);
|
|
672
|
+
const visibleNodes = this.displayNodes.filter((node: TreeNodeInterface, index: number) => {
|
|
673
|
+
// first node is fallback target for tabindex, needs to be available every time
|
|
674
|
+
if (this.getFirstNode() === node) {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
// focused node always needs to be available
|
|
678
|
+
if (this.focusedNode === node) {
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
// last focused node needs to be available for re-focus after scrolling (can have tabindex="0")
|
|
682
|
+
if (this.lastFocusedNode === node) {
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
return index + 40 >= position && index - 40 < position + visibleRows;
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
return html`
|
|
689
|
+
<div class="nodes-list" role="tree" style="${styleMap({ 'height': (this.displayNodes.length * this.nodeHeight) + 'px' })}">
|
|
690
|
+
${repeat(visibleNodes, (node: TreeNodeInterface) => this.getNodeTreeIdentifier(node), (node: TreeNodeInterface) => html`
|
|
691
|
+
<div
|
|
692
|
+
class="${this.getNodeClasses(node).join(' ')}"
|
|
693
|
+
role="treeitem"
|
|
694
|
+
draggable="true"
|
|
695
|
+
title="${this.getNodeTitle(node)}"
|
|
696
|
+
aria-owns="${ifDefined(node.hasChildren ? 'group-identifier-' + this.getNodeIdentifier(node) : null)}"
|
|
697
|
+
aria-expanded="${ifDefined(node.hasChildren ? (node.__expanded ? 'true' : 'false') : null)}"
|
|
698
|
+
aria-level="${(this.getNodeDepth(node) + 1)}"
|
|
699
|
+
aria-setsize="${this.getNodeSetsize(node)}"
|
|
700
|
+
aria-posinset="${this.getNodePositionInSet(node)}"
|
|
701
|
+
data-id="${this.getNodeIdentifier(node)}"
|
|
702
|
+
data-tree-id="${this.getNodeTreeIdentifier(node)}"
|
|
703
|
+
style="top: ${node.__y + 'px'}; height: ${this.nodeHeight + 'px'};"
|
|
704
|
+
tabindex="${this.getNodeTabindex(node)}"
|
|
705
|
+
|
|
706
|
+
@dragover="${(event: DragEvent) => { this.handleNodeDragOver(event); }}"
|
|
707
|
+
@dragstart="${(event: DragEvent) => { this.handleNodeDragStart(event, node); }}"
|
|
708
|
+
@dragleave="${(event: DragEvent) => { this.handleNodeDragLeave(event); }}"
|
|
709
|
+
@dragend="${(event: DragEvent) => { this.handleNodeDragEnd(event); }}"
|
|
710
|
+
@drop="${(event: DragEvent) => { this.handleNodeDrop(event); }}"
|
|
711
|
+
|
|
712
|
+
@click="${(event: PointerEvent) => { this.handleNodeClick(event, node); }}"
|
|
713
|
+
@dblclick="${(event: PointerEvent) => { this.handleNodeDoubleClick(event, node); }}"
|
|
714
|
+
@focusin="${() => { this.focusedNode = node; }}"
|
|
715
|
+
@focusout="${() => { if (this.focusedNode === node) { this.lastFocusedNode = node; this.focusedNode = null; } }}"
|
|
716
|
+
@contextmenu="${(event: MouseEvent) => { event.preventDefault(); event.stopPropagation(); this.dispatchEvent(new CustomEvent('typo3:tree:node-context', { detail: { node, originalEvent: event } })); }}"
|
|
717
|
+
>
|
|
718
|
+
${this.createNodeLabel(node)}
|
|
719
|
+
${this.createNodeGuides(node)}
|
|
720
|
+
${this.createNodeLoader(node) || this.createNodeToggle(node) || nothing}
|
|
721
|
+
${this.createNodeContent(node)}
|
|
722
|
+
${this.createNodeStatusInformation(node)}
|
|
723
|
+
${this.createNodeDeleteDropZone(node)}
|
|
724
|
+
</div>
|
|
725
|
+
`)}
|
|
726
|
+
</div>
|
|
727
|
+
`;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
protected override async firstUpdated(): Promise<void> {
|
|
731
|
+
const resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
|
732
|
+
for (const entry of entries) {
|
|
733
|
+
if (entry.target === this.root) {
|
|
734
|
+
this.currentVisibleHeight = entry.target.getBoundingClientRect().height;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
resizeObserver.observe(this.root);
|
|
739
|
+
|
|
740
|
+
Object.assign(this.settings, this.setup || {});
|
|
741
|
+
this.registerUnloadHandler();
|
|
742
|
+
await this.loadData();
|
|
743
|
+
this.dispatchEvent(new Event('tree:initialized'));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
protected resetSelectedNodes(): void {
|
|
747
|
+
this.getSelectedNodes().forEach((node: TreeNodeInterface) => {
|
|
748
|
+
if (node.checked === true) {
|
|
749
|
+
node.checked = false;
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
755
|
+
protected isNodeSelectable(node: TreeNodeInterface): boolean {
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
protected isNodeEditable(node: TreeNodeInterface): boolean {
|
|
760
|
+
return node.editable && this.allowNodeEdit;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
protected handleNodeClick(event: PointerEvent, node: TreeNodeInterface): void {
|
|
764
|
+
// only select node on single click
|
|
765
|
+
if (event.detail === 1) {
|
|
766
|
+
event.preventDefault();
|
|
767
|
+
event.stopPropagation();
|
|
768
|
+
if (this.editingNode !== node) {
|
|
769
|
+
this.selectNode(node, true);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
protected handleNodeDoubleClick(event: PointerEvent, node: TreeNodeInterface): void {
|
|
775
|
+
event.preventDefault();
|
|
776
|
+
event.stopPropagation();
|
|
777
|
+
if (this.editingNode !== node) {
|
|
778
|
+
this.editNode(node);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
//
|
|
783
|
+
// Dragging
|
|
784
|
+
//
|
|
785
|
+
protected cleanDrag(): void {
|
|
786
|
+
const allElements = this.querySelectorAll('.node');
|
|
787
|
+
allElements.forEach(function(element) {
|
|
788
|
+
element.classList.remove('node-dragging-before');
|
|
789
|
+
element.classList.remove('node-dragging-after');
|
|
790
|
+
element.classList.remove('node-hover');
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
protected getNodeFromDragEvent(event: DragEvent): TreeNodeInterface|null {
|
|
795
|
+
const target = event.target as HTMLElement;
|
|
796
|
+
return this.getNodeFromElement(target.closest('[data-tree-id]'));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
protected getTooltipDescription(node: TreeNodeInterface): string {
|
|
800
|
+
return 'ID: ' + node.identifier;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
protected handleNodeDragStart(event: DragEvent, node: TreeNodeInterface): void {
|
|
804
|
+
if (this.allowNodeDrag === false || node.depth === 0) {
|
|
805
|
+
event.preventDefault();
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
//document.querySelectorAll('iframe').forEach(frame => frame.style.pointerEvents = 'none');
|
|
810
|
+
this.draggingNode = node;
|
|
811
|
+
this.requestUpdate();
|
|
812
|
+
|
|
813
|
+
event.dataTransfer.clearData();
|
|
814
|
+
|
|
815
|
+
const metadata: DragTooltipMetadata = {
|
|
816
|
+
statusIconIdentifier: this.getNodeDragStatusIcon(),
|
|
817
|
+
tooltipIconIdentifier: node.icon,
|
|
818
|
+
tooltipLabel: node.name,
|
|
819
|
+
tooltipDescription: this.getTooltipDescription(node),
|
|
820
|
+
};
|
|
821
|
+
event.dataTransfer.setData(DataTransferTypes.dragTooltip, JSON.stringify(metadata));
|
|
822
|
+
|
|
823
|
+
this.createDataTransferItemsFromNode(node).forEach(
|
|
824
|
+
({ data, type }) => event.dataTransfer.items.add(data, type)
|
|
825
|
+
);
|
|
826
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Returns true when it was responsible for the drag
|
|
831
|
+
*/
|
|
832
|
+
protected handleNodeDragOver(event: DragEvent): boolean {
|
|
833
|
+
if (
|
|
834
|
+
!event.dataTransfer.types.includes(DataTransferTypes.treenode) &&
|
|
835
|
+
!event.dataTransfer.types.includes(DataTransferTypes.newTreenode)) {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
// Find the current hovered node
|
|
839
|
+
// Exit when no node was hovered
|
|
840
|
+
const target = event.target as HTMLElement;
|
|
841
|
+
const targetNode = this.getNodeFromDragEvent(event);
|
|
842
|
+
if (targetNode === null) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (this.draggingNode === null) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
this.cleanDrag();
|
|
850
|
+
this.refreshDragToolTip();
|
|
851
|
+
|
|
852
|
+
this.nodeDragMode = null;
|
|
853
|
+
this.nodeDragPosition = null;
|
|
854
|
+
|
|
855
|
+
// Add hover styling to the current hovered node
|
|
856
|
+
// element, during the drag the default mouse over
|
|
857
|
+
// is disabled by the browser
|
|
858
|
+
const hoverElement = this.getElementFromNode(targetNode);
|
|
859
|
+
hoverElement.classList.add('node-hover');
|
|
860
|
+
|
|
861
|
+
// Open node with children while holding the
|
|
862
|
+
// node/element over this node for 1 second
|
|
863
|
+
if (targetNode.hasChildren && !targetNode.__expanded) {
|
|
864
|
+
if (this.openNodeTimeout.targetNode != targetNode) {
|
|
865
|
+
this.openNodeTimeout.targetNode = targetNode;
|
|
866
|
+
clearTimeout(this.openNodeTimeout.timeout);
|
|
867
|
+
this.openNodeTimeout.timeout = setTimeout(() => {
|
|
868
|
+
this.showChildren(this.openNodeTimeout.targetNode);
|
|
869
|
+
this.openNodeTimeout.targetNode = null;
|
|
870
|
+
this.openNodeTimeout.timeout = null;
|
|
871
|
+
}, 1000);
|
|
872
|
+
}
|
|
873
|
+
} else {
|
|
874
|
+
clearTimeout(this.openNodeTimeout.timeout);
|
|
875
|
+
this.openNodeTimeout.targetNode = null;
|
|
876
|
+
this.openNodeTimeout.timeout = null;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Check if the target is the current node.
|
|
880
|
+
// If the current node is the target we only allow
|
|
881
|
+
// drop when the user is over the delete drop zone.
|
|
882
|
+
if (this.draggingNode == targetNode) {
|
|
883
|
+
const targetDelete = target.dataset.treeDropzone === 'delete';
|
|
884
|
+
if (targetDelete) {
|
|
885
|
+
this.nodeDragMode = TreeNodeCommandEnum.DELETE;
|
|
886
|
+
event.preventDefault();
|
|
887
|
+
this.refreshDragToolTip();
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
this.refreshDragToolTip();
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Check if the current node is a parent of the target node.
|
|
895
|
+
// A parent cannot be moved into a child node.
|
|
896
|
+
if (targetNode.__parents.includes(this.draggingNode.identifier)) {
|
|
897
|
+
this.refreshDragToolTip();
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// The default behaviour is mode, we currently
|
|
902
|
+
// do not handle copy events seperatly.
|
|
903
|
+
// Set the dragmode to new when a new node
|
|
904
|
+
// exists in the datatransfer.
|
|
905
|
+
this.nodeDragMode = TreeNodeCommandEnum.MOVE;
|
|
906
|
+
if (event.dataTransfer.types.includes(DataTransferTypes.newTreenode)) {
|
|
907
|
+
this.nodeDragMode = TreeNodeCommandEnum.NEW;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// The default drag position is inside,
|
|
911
|
+
// this is allowed by all node types
|
|
912
|
+
this.nodeDragPosition = TreeNodePositionEnum.INSIDE;
|
|
913
|
+
|
|
914
|
+
// Elements on the root, cannot be sorted.
|
|
915
|
+
// If sorting is not allowed, only dragging the node
|
|
916
|
+
// inside the target node is allowed.
|
|
917
|
+
if (targetNode.depth === 0 || this.allowNodeSorting === false) {
|
|
918
|
+
this.refreshDragToolTip();
|
|
919
|
+
event.preventDefault();
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// If sorting is allowed we need to check the position
|
|
924
|
+
// of the of the cursor to define if the element needs
|
|
925
|
+
// to be added before or after the current node.
|
|
926
|
+
//
|
|
927
|
+
// On nodes with childs we only allow positioning after
|
|
928
|
+
// the targetnode when it has no children and the node
|
|
929
|
+
// is not expanded.
|
|
930
|
+
const targetElement = this.getElementFromNode(targetNode);
|
|
931
|
+
const targetBounding = targetElement.getBoundingClientRect();
|
|
932
|
+
const targetHoverOffset = event.clientY - targetBounding.y;
|
|
933
|
+
if (targetHoverOffset < 6) {
|
|
934
|
+
this.nodeDragPosition = TreeNodePositionEnum.BEFORE;
|
|
935
|
+
hoverElement.classList.add('node-dragging-before');
|
|
936
|
+
} else if ((this.nodeHeight - targetHoverOffset) < 6 && targetNode.hasChildren === false && targetNode.__expanded === false) {
|
|
937
|
+
this.nodeDragPosition = TreeNodePositionEnum.AFTER;
|
|
938
|
+
hoverElement.classList.add('node-dragging-after');
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
this.refreshDragToolTip();
|
|
942
|
+
event.preventDefault();
|
|
943
|
+
return true;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
947
|
+
protected handleNodeDragLeave(event: DragEvent): void {
|
|
948
|
+
if (this.draggingNode !== null) {
|
|
949
|
+
this.cleanDrag();
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
954
|
+
protected handleNodeDragEnd(event: DragEvent): void {
|
|
955
|
+
this.cleanDrag();
|
|
956
|
+
this.draggingNode = null;
|
|
957
|
+
this.requestUpdate();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
protected handleNodeDrop(event: DragEvent): boolean {
|
|
961
|
+
this.cleanDrag();
|
|
962
|
+
|
|
963
|
+
if (event.dataTransfer.types.includes(DataTransferTypes.treenode)) {
|
|
964
|
+
event.preventDefault();
|
|
965
|
+
const identifier = event.dataTransfer.getData(DataTransferTypes.treenode);
|
|
966
|
+
const node = this.getNodeByTreeIdentifier(identifier);
|
|
967
|
+
// delete
|
|
968
|
+
if (this.nodeDragMode === TreeNodeCommandEnum.DELETE) {
|
|
969
|
+
this.deleteNode(node);
|
|
970
|
+
}
|
|
971
|
+
const targetNode = this.getNodeFromDragEvent(event);
|
|
972
|
+
if (targetNode === null) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// move
|
|
977
|
+
if (this.nodeDragMode === TreeNodeCommandEnum.MOVE) {
|
|
978
|
+
this.moveNode(node, targetNode, this.nodeDragPosition);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
this.nodeDragMode = null;
|
|
982
|
+
this.nodeDragPosition = null;
|
|
983
|
+
return true;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (event.dataTransfer.types.includes(DataTransferTypes.newTreenode)) {
|
|
987
|
+
event.preventDefault();
|
|
988
|
+
const targetNode = this.getNodeFromDragEvent(event);
|
|
989
|
+
if (targetNode === null) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
const newNodeData = event.dataTransfer.getData(DataTransferTypes.newTreenode);
|
|
993
|
+
//if (this.nodeDragMode === TreeNodeCommandEnum.NEW) {
|
|
994
|
+
this.addNode(JSON.parse(newNodeData), targetNode, this.nodeDragPosition);
|
|
995
|
+
|
|
996
|
+
this.nodeDragMode = null;
|
|
997
|
+
this.nodeDragPosition = null;
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
protected refreshDragToolTip() {
|
|
1004
|
+
top.document.dispatchEvent(
|
|
1005
|
+
new CustomEvent<DragTooltipMetadata>('typo3:drag-tooltip:metadata-update', {
|
|
1006
|
+
detail: {
|
|
1007
|
+
statusIconIdentifier: this.getNodeDragStatusIcon(),
|
|
1008
|
+
}
|
|
1009
|
+
})
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
//
|
|
1014
|
+
// Node Rendering
|
|
1015
|
+
//
|
|
1016
|
+
protected createNodeLabel(node: TreeNodeInterface): TemplateResult
|
|
1017
|
+
{
|
|
1018
|
+
const labels = this.getNodeLabels(node);
|
|
1019
|
+
if (labels.length === 0) {
|
|
1020
|
+
return html`${nothing}`;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const label = labels[0];
|
|
1024
|
+
const styles = { backgroundColor: label.color };
|
|
1025
|
+
|
|
1026
|
+
return html`
|
|
1027
|
+
<span class="node-label" style=${styleMap(styles)}></span>
|
|
1028
|
+
`;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
protected createNodeGuides(node: TreeNodeInterface): TemplateResult
|
|
1032
|
+
{
|
|
1033
|
+
const guides = node.__treeParents.map((treeIdentifier) => {
|
|
1034
|
+
const parentNode = this.getNodeByTreeIdentifier(treeIdentifier);
|
|
1035
|
+
let className: string = 'none';
|
|
1036
|
+
if (this.getNodeSetsize(parentNode) !== this.getNodePositionInSet(parentNode)) {
|
|
1037
|
+
className = 'line';
|
|
1038
|
+
}
|
|
1039
|
+
return html`
|
|
1040
|
+
<div
|
|
1041
|
+
class="node-treeline node-treeline--${className}"
|
|
1042
|
+
data-origin="${this.getNodeTreeIdentifier(parentNode)}"
|
|
1043
|
+
data-nodesize="${this.getNodeSetsize(parentNode)}"
|
|
1044
|
+
data-position="${this.getNodePositionInSet(parentNode)}"
|
|
1045
|
+
>
|
|
1046
|
+
</div>
|
|
1047
|
+
`;
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
if (this.getNodeSetsize(node) === this.getNodePositionInSet(node)) {
|
|
1051
|
+
guides.push(html`<div class="node-treeline node-treeline--last" data-origin="${this.getNodeTreeIdentifier(node)}"></div>`);
|
|
1052
|
+
} else {
|
|
1053
|
+
guides.push(html`<div class="node-treeline node-treeline--connect" data-origin="${this.getNodeTreeIdentifier(node)}"></div>`);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return html`<div class="node-treelines">${guides}</div>`;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
protected createNodeLoader(node: TreeNodeInterface): TemplateResult|null
|
|
1060
|
+
{
|
|
1061
|
+
return node.__loading === true
|
|
1062
|
+
? html `
|
|
1063
|
+
<span class="node-loading">
|
|
1064
|
+
<typo3-backend-icon
|
|
1065
|
+
identifier="spinner-circle"
|
|
1066
|
+
size="small"
|
|
1067
|
+
></typo3-backend-icon>
|
|
1068
|
+
</span>
|
|
1069
|
+
`
|
|
1070
|
+
: null
|
|
1071
|
+
;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
protected createNodeToggle(node: TreeNodeInterface): TemplateResult|null
|
|
1075
|
+
{
|
|
1076
|
+
return node.hasChildren === true
|
|
1077
|
+
? html `
|
|
1078
|
+
<span class="node-toggle" @click="${(event: PointerEvent) => { event.preventDefault(); event.stopImmediatePropagation(); this.handleNodeToggle(node); }}">
|
|
1079
|
+
<typo3-backend-icon
|
|
1080
|
+
identifier="${(node.__expanded ? 'actions-chevron-down' : 'actions-chevron-end')}"
|
|
1081
|
+
size="small"
|
|
1082
|
+
></typo3-backend-icon>
|
|
1083
|
+
</span>
|
|
1084
|
+
`
|
|
1085
|
+
: null
|
|
1086
|
+
;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
protected createNodeContent(node: TreeNodeInterface): TemplateResult
|
|
1090
|
+
{
|
|
1091
|
+
return html`
|
|
1092
|
+
<div class="node-content">
|
|
1093
|
+
${this.createNodeContentIcon(node)}
|
|
1094
|
+
${this.editingNode === node ? this.createNodeForm(node) : this.createNodeContentLabel(node)}
|
|
1095
|
+
${this.createNodeContentAction(node)}
|
|
1096
|
+
</div>
|
|
1097
|
+
`;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
protected createNodeContentIcon(node: TreeNodeInterface): TemplateResult
|
|
1101
|
+
{
|
|
1102
|
+
return this.settings.showIcons
|
|
1103
|
+
? html`
|
|
1104
|
+
<span class="node-icon"
|
|
1105
|
+
@click="${(event: PointerEvent) => { event.preventDefault(); event.stopImmediatePropagation(); this.dispatchEvent(new CustomEvent('typo3:tree:node-context', { detail: { node: node, originalEvent: event } })); }}"
|
|
1106
|
+
@dblclick="${(event: PointerEvent) => { event.preventDefault(); event.stopImmediatePropagation(); }}"
|
|
1107
|
+
>
|
|
1108
|
+
<typo3-backend-icon
|
|
1109
|
+
identifier="${node.icon}"
|
|
1110
|
+
overlay="${node.overlayIcon}"
|
|
1111
|
+
size="small"
|
|
1112
|
+
></typo3-backend-icon>
|
|
1113
|
+
</span>
|
|
1114
|
+
`
|
|
1115
|
+
: html`${nothing}`;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
protected createNodeContentLabel(node: TreeNodeInterface): TemplateResult
|
|
1119
|
+
{
|
|
1120
|
+
let label = (node.prefix || '') + node.name + (node.suffix || '');
|
|
1121
|
+
// make a text node out of it, and strip out any HTML (this is because the return value uses html()
|
|
1122
|
+
// instead of text() which is needed to avoid XSS in a page title
|
|
1123
|
+
const labelNode = document.createElement('div');
|
|
1124
|
+
labelNode.textContent = label;
|
|
1125
|
+
label = labelNode.innerHTML;
|
|
1126
|
+
if (this.searchTerm) {
|
|
1127
|
+
// Escape all meta characters of regular expressions: ( ) [ ] $ * + ? . { } / | ^ -
|
|
1128
|
+
const regexp = new RegExp(this.searchTerm.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'), 'gi');
|
|
1129
|
+
label = label.replace(regexp, '<span class="node-highlight-text">$&</span>');
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return html`
|
|
1133
|
+
<div class="node-contentlabel">
|
|
1134
|
+
<div class="node-name" .innerHTML="${label}"></div>
|
|
1135
|
+
${node.note ? html`<div class="node-note">${node.note}</div>` : nothing }
|
|
1136
|
+
</div>`;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
protected createNodeStatusInformation(node: TreeNodeInterface): TemplateResult
|
|
1140
|
+
{
|
|
1141
|
+
const statusInformation = this.getNodeStatusInformation(node);
|
|
1142
|
+
if (statusInformation.length === 0) {
|
|
1143
|
+
return html`${nothing}`;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const firstInformation = statusInformation[0];
|
|
1147
|
+
const severityClass = Severity.getCssClass(firstInformation.severity);
|
|
1148
|
+
const iconIdentifier = firstInformation.icon !== '' ? firstInformation.icon : 'actions-dot';
|
|
1149
|
+
const overlayIconIdentifier = firstInformation.overlayIcon !== '' ? firstInformation.overlayIcon : undefined;
|
|
1150
|
+
|
|
1151
|
+
return html`
|
|
1152
|
+
<span class="node-information">
|
|
1153
|
+
<typo3-backend-icon
|
|
1154
|
+
class="text-${severityClass}"
|
|
1155
|
+
identifier=${iconIdentifier}
|
|
1156
|
+
overlay=${ifDefined(overlayIconIdentifier)}
|
|
1157
|
+
size="small"
|
|
1158
|
+
></typo3-backend-icon>
|
|
1159
|
+
</span>
|
|
1160
|
+
`;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
protected createNodeDeleteDropZone(node: TreeNodeInterface): TemplateResult
|
|
1164
|
+
{
|
|
1165
|
+
return this.draggingNode === node && node.deletable
|
|
1166
|
+
? html`
|
|
1167
|
+
<div class="node-dropzone-delete" data-tree-dropzone="delete">
|
|
1168
|
+
<typo3-backend-icon identifier="actions-delete" size="small"></typo3-backend-icon>
|
|
1169
|
+
${TYPO3.lang.deleteItem}
|
|
1170
|
+
</div>
|
|
1171
|
+
`
|
|
1172
|
+
: html`${nothing}`;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
protected createNodeForm(node: TreeNodeInterface): TemplateResult
|
|
1176
|
+
{
|
|
1177
|
+
const command = node.identifier.startsWith('NEW') ? TreeNodeCommandEnum.NEW : TreeNodeCommandEnum.EDIT;
|
|
1178
|
+
|
|
1179
|
+
const keydownFunction = (event: KeyboardEvent) => {
|
|
1180
|
+
const code = event.key;
|
|
1181
|
+
if (([KeyTypes.ENTER, KeyTypes.TAB] as string[]).includes(code)) {
|
|
1182
|
+
const target = event.target as HTMLInputElement;
|
|
1183
|
+
const newName = target.value.trim();
|
|
1184
|
+
this.editingNode = null;
|
|
1185
|
+
this.requestUpdate();
|
|
1186
|
+
if (newName !== node.name && newName !== '') {
|
|
1187
|
+
this.handleNodeEdit(node, newName);
|
|
1188
|
+
this.focusNode(node);
|
|
1189
|
+
} else if (command === TreeNodeCommandEnum.NEW && newName === '') {
|
|
1190
|
+
this.removeNode(node);
|
|
1191
|
+
} else {
|
|
1192
|
+
this.focusNode(node);
|
|
1193
|
+
}
|
|
1194
|
+
} else if (([KeyTypes.ESCAPE] as string[]).includes(code)) {
|
|
1195
|
+
this.editingNode = null;
|
|
1196
|
+
this.requestUpdate();
|
|
1197
|
+
if (command === TreeNodeCommandEnum.NEW) {
|
|
1198
|
+
this.removeNode(node);
|
|
1199
|
+
} else {
|
|
1200
|
+
this.focusNode(node);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const blurFunction = (event: FocusEvent) => {
|
|
1206
|
+
if (this.editingNode !== null) {
|
|
1207
|
+
this.editingNode = null;
|
|
1208
|
+
const target = event.target as HTMLInputElement;
|
|
1209
|
+
const newName = target.value.trim();
|
|
1210
|
+
if (newName !== node.name && newName !== '') {
|
|
1211
|
+
this.handleNodeEdit(node, newName);
|
|
1212
|
+
} else if (command === TreeNodeCommandEnum.NEW) {
|
|
1213
|
+
this.removeNode(node);
|
|
1214
|
+
}
|
|
1215
|
+
this.requestUpdate();
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
return html`
|
|
1220
|
+
<input
|
|
1221
|
+
class="node-edit"
|
|
1222
|
+
@click="${(event: PointerEvent) => { event.stopImmediatePropagation(); }}"
|
|
1223
|
+
@blur="${blurFunction}"
|
|
1224
|
+
@keydown="${keydownFunction}"
|
|
1225
|
+
value="${node.name}"
|
|
1226
|
+
/>
|
|
1227
|
+
`;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
//
|
|
1231
|
+
// Handling
|
|
1232
|
+
//
|
|
1233
|
+
|
|
1234
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1235
|
+
protected async handleNodeEdit(node: TreeNodeInterface, newName: string): Promise<void> {
|
|
1236
|
+
console.error('The function Tree->handleNodeEdit is not implemented.');
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1240
|
+
protected handleNodeDelete(node: TreeNodeInterface) {
|
|
1241
|
+
console.error('The function Tree->handleNodeDelete is not implemented.');
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1245
|
+
protected handleNodeMove(node: TreeNodeInterface, target: TreeNodeInterface, position: TreeNodePositionEnum) {
|
|
1246
|
+
console.error('The function Tree->handleNodeMove is not implemented.');
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1250
|
+
protected async handleNodeAdd(node: TreeNodeInterface, target: TreeNodeInterface, position: TreeNodePositionEnum): Promise<void> {
|
|
1251
|
+
console.error('The function Tree->handleNodeAdd is not implemented.');
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1255
|
+
protected createNodeContentAction(node: TreeNodeInterface): TemplateResult
|
|
1256
|
+
{
|
|
1257
|
+
return html`${nothing}`;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1261
|
+
protected createDataTransferItemsFromNode(node: TreeNodeInterface): DataTransferStringItem[] {
|
|
1262
|
+
throw new Error('The function Tree->createDataTransferItemFromNode is not implemented.');
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
//
|
|
1266
|
+
// Node
|
|
1267
|
+
//
|
|
1268
|
+
protected getNodeIdentifier(node: TreeNodeInterface): string {
|
|
1269
|
+
return node.identifier;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
protected getNodeTreeIdentifier(node: TreeNodeInterface): string {
|
|
1273
|
+
return node.__treeIdentifier;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
protected getNodeParentTreeIdentifier(node: TreeNodeInterface): string {
|
|
1277
|
+
return node.__parents.join('_');
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
protected getNodeClasses(node: TreeNodeInterface): string[] {
|
|
1281
|
+
const classList: Array<string> = ['node'];
|
|
1282
|
+
|
|
1283
|
+
if (node.checked) {
|
|
1284
|
+
classList.push('node-selected');
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
if (this.draggingNode === node) {
|
|
1288
|
+
classList.push('node-dragging');
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
return classList;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
protected getNodeLabels(node: TreeNodeInterface): TreeNodeLabel[] {
|
|
1295
|
+
let labels = node.labels;
|
|
1296
|
+
if (labels.length > 0) {
|
|
1297
|
+
labels = labels.sort((a, b) => {
|
|
1298
|
+
return b.priority - a.priority;
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
return labels;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
const parentNode = this.getParentNode(node);
|
|
1305
|
+
if (parentNode === null) {
|
|
1306
|
+
return [];
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return this.getNodeLabels(parentNode);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
protected getNodeStatusInformation(node: TreeNodeInterface): TreeNodeStatusInformation[] {
|
|
1313
|
+
if (node.statusInformation.length === 0) {
|
|
1314
|
+
return [];
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const statusInformation = node.statusInformation.sort((a, b) => {
|
|
1318
|
+
if (a.severity !== b.severity) {
|
|
1319
|
+
return b.severity - a.severity;
|
|
1320
|
+
}
|
|
1321
|
+
return b.priority - a.priority;
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
return statusInformation;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
protected getNodeDepth(node: TreeNodeInterface): number {
|
|
1328
|
+
return node.depth;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
protected getNodeTabindex(node: TreeNodeInterface): number {
|
|
1332
|
+
if (this.focusedNode) {
|
|
1333
|
+
return this.focusedNode === node ? 0 : -1;
|
|
1334
|
+
}
|
|
1335
|
+
if (this.lastFocusedNode) {
|
|
1336
|
+
return this.lastFocusedNode === node ? 0 : -1;
|
|
1337
|
+
}
|
|
1338
|
+
return this.getFirstNode() === node ? 0 : -1;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
protected getNodeChildren(node: TreeNodeInterface): TreeNodeInterface[] {
|
|
1342
|
+
if (!node.hasChildren) {
|
|
1343
|
+
return [];
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
return this.displayNodes.filter((filterNode) => {
|
|
1347
|
+
return node === this.getParentNode(filterNode);
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
protected getNodeSetsize(node: TreeNodeInterface): number {
|
|
1352
|
+
if (node.depth === 0) {
|
|
1353
|
+
return this.displayNodes.filter((node) => node.depth === 0).length;
|
|
1354
|
+
}
|
|
1355
|
+
const parentNode = this.getParentNode(node);
|
|
1356
|
+
const childNodes = this.getNodeChildren(parentNode);
|
|
1357
|
+
|
|
1358
|
+
return childNodes.length;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
protected getNodePositionInSet(node: TreeNodeInterface): number {
|
|
1362
|
+
const parentNode = this.getParentNode(node);
|
|
1363
|
+
let nodeSet: TreeNodeInterface[] = [];
|
|
1364
|
+
if (node.depth === 0) {
|
|
1365
|
+
nodeSet = this.displayNodes.filter((node) => node.depth === 0);
|
|
1366
|
+
} else if (parentNode !== null) {
|
|
1367
|
+
nodeSet = this.getNodeChildren(parentNode);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
return nodeSet.indexOf(node) + 1;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
protected getFirstNode(): TreeNodeInterface|null {
|
|
1374
|
+
if (this.displayNodes.length) {
|
|
1375
|
+
return this.displayNodes[0];
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
protected getPreviousNode(node: TreeNodeInterface): TreeNodeInterface|null {
|
|
1382
|
+
const position = this.displayNodes.indexOf(node);
|
|
1383
|
+
const previousPosition = position - 1;
|
|
1384
|
+
|
|
1385
|
+
if (this.displayNodes[previousPosition]) {
|
|
1386
|
+
return this.displayNodes[previousPosition];
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
protected getNextNode(node: TreeNodeInterface): TreeNodeInterface|null {
|
|
1393
|
+
const position = this.displayNodes.indexOf(node);
|
|
1394
|
+
const nextPosition = position + 1;
|
|
1395
|
+
|
|
1396
|
+
if (this.displayNodes[nextPosition]) {
|
|
1397
|
+
return this.displayNodes[nextPosition];
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
return null;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
protected getLastNode(): TreeNodeInterface|null {
|
|
1404
|
+
if (this.displayNodes.length) {
|
|
1405
|
+
return this.displayNodes[this.displayNodes.length - 1];
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return null;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
protected getParentNode(node: TreeNodeInterface): TreeNodeInterface|null {
|
|
1412
|
+
if (node.__parents.length) {
|
|
1413
|
+
return this.getNodeByTreeIdentifier(this.getNodeParentTreeIdentifier(node));
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
protected getNodeTitle(node: TreeNodeInterface): string {
|
|
1420
|
+
let baseNodeTitle = node.tooltip ? node.tooltip : 'uid=' + node.identifier + ' ' + node.name;
|
|
1421
|
+
|
|
1422
|
+
const labels = this.getNodeLabels(node);
|
|
1423
|
+
if (labels.length) {
|
|
1424
|
+
baseNodeTitle += '; ' + labels.map(label => label.label).join('; ');
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const statusInformation = this.getNodeStatusInformation(node);
|
|
1428
|
+
if (statusInformation.length) {
|
|
1429
|
+
baseNodeTitle += '; ' + statusInformation.map(information => information.label).join('; ');
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
return baseNodeTitle;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Event handler for collapsing or expanding nodes
|
|
1437
|
+
*/
|
|
1438
|
+
protected handleNodeToggle(node: TreeNodeInterface): void {
|
|
1439
|
+
if (node.__expanded) {
|
|
1440
|
+
this.hideChildren(node);
|
|
1441
|
+
} else {
|
|
1442
|
+
this.showChildren(node);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Add keydown handling to allow keyboard navigation inside the tree
|
|
1448
|
+
*/
|
|
1449
|
+
private handleKeyboardInteraction(event: KeyboardEvent) {
|
|
1450
|
+
// Do not handle keyboard interaction if tree is
|
|
1451
|
+
// currently editing a node.
|
|
1452
|
+
if (this.editingNode !== null) {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// Only handle specific keyboard interactions
|
|
1457
|
+
const charCodes: string[] = [
|
|
1458
|
+
KeyTypes.ENTER,
|
|
1459
|
+
KeyTypes.SPACE,
|
|
1460
|
+
KeyTypes.END,
|
|
1461
|
+
KeyTypes.HOME,
|
|
1462
|
+
KeyTypes.LEFT,
|
|
1463
|
+
KeyTypes.UP,
|
|
1464
|
+
KeyTypes.RIGHT,
|
|
1465
|
+
KeyTypes.DOWN
|
|
1466
|
+
];
|
|
1467
|
+
if (charCodes.includes(event.key) === false) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const target = event.target as HTMLElement;
|
|
1472
|
+
const currentNode = this.getNodeFromElement(target);
|
|
1473
|
+
if (currentNode === null) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const parentNode = this.getParentNode(currentNode);
|
|
1478
|
+
const firstNode = this.getFirstNode();
|
|
1479
|
+
const previousNode = this.getPreviousNode(currentNode);
|
|
1480
|
+
const nextNode = this.getNextNode(currentNode);
|
|
1481
|
+
const lastNode = this.getLastNode();
|
|
1482
|
+
|
|
1483
|
+
event.preventDefault();
|
|
1484
|
+
switch (event.key) {
|
|
1485
|
+
case KeyTypes.HOME:
|
|
1486
|
+
// scroll to top, select first node
|
|
1487
|
+
if (firstNode !== null) {
|
|
1488
|
+
this.scrollNodeIntoVisibleArea(firstNode);
|
|
1489
|
+
this.focusNode(firstNode);
|
|
1490
|
+
}
|
|
1491
|
+
break;
|
|
1492
|
+
case KeyTypes.END:
|
|
1493
|
+
if (lastNode !== null) {
|
|
1494
|
+
this.scrollNodeIntoVisibleArea(lastNode);
|
|
1495
|
+
this.focusNode(lastNode);
|
|
1496
|
+
}
|
|
1497
|
+
break;
|
|
1498
|
+
case KeyTypes.UP:
|
|
1499
|
+
// select previous visible node on any level
|
|
1500
|
+
if (previousNode !== null) {
|
|
1501
|
+
this.scrollNodeIntoVisibleArea(previousNode);
|
|
1502
|
+
this.focusNode(previousNode);
|
|
1503
|
+
}
|
|
1504
|
+
break;
|
|
1505
|
+
case KeyTypes.DOWN:
|
|
1506
|
+
// select next visible node on any level
|
|
1507
|
+
if (nextNode !== null) {
|
|
1508
|
+
this.scrollNodeIntoVisibleArea(nextNode);
|
|
1509
|
+
this.focusNode(nextNode);
|
|
1510
|
+
}
|
|
1511
|
+
break;
|
|
1512
|
+
case KeyTypes.LEFT:
|
|
1513
|
+
if (currentNode.__expanded) {
|
|
1514
|
+
// collapse node if collapsible
|
|
1515
|
+
if (currentNode.hasChildren) {
|
|
1516
|
+
this.hideChildren(currentNode);
|
|
1517
|
+
}
|
|
1518
|
+
} else if (parentNode) {
|
|
1519
|
+
// go to parent node
|
|
1520
|
+
this.scrollNodeIntoVisibleArea(parentNode);
|
|
1521
|
+
this.focusNode(parentNode);
|
|
1522
|
+
}
|
|
1523
|
+
break;
|
|
1524
|
+
case KeyTypes.RIGHT:
|
|
1525
|
+
if (currentNode.__expanded && nextNode) {
|
|
1526
|
+
// the current node is expanded,
|
|
1527
|
+
// goto first child (next element on the list)
|
|
1528
|
+
this.scrollNodeIntoVisibleArea(nextNode);
|
|
1529
|
+
this.focusNode(nextNode);
|
|
1530
|
+
} else {
|
|
1531
|
+
if (currentNode.hasChildren) {
|
|
1532
|
+
// expand currentNode
|
|
1533
|
+
this.showChildren(currentNode);
|
|
1534
|
+
}
|
|
1535
|
+
//do nothing if node has no children
|
|
1536
|
+
}
|
|
1537
|
+
break;
|
|
1538
|
+
case KeyTypes.ENTER:
|
|
1539
|
+
case KeyTypes.SPACE:
|
|
1540
|
+
this.selectNode(currentNode);
|
|
1541
|
+
break;
|
|
1542
|
+
default:
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/**
|
|
1547
|
+
* Check is the node is visible in the viewport,
|
|
1548
|
+
* if not the element is scrolled into the view
|
|
1549
|
+
*/
|
|
1550
|
+
private scrollNodeIntoVisibleArea(node: TreeNodeInterface): void {
|
|
1551
|
+
const nodeAnchorTop = node.__y;
|
|
1552
|
+
const nodeAnchorBottom = node.__y + this.nodeHeight;
|
|
1553
|
+
const nodeFitsTop = nodeAnchorTop >= this.currentScrollPosition;
|
|
1554
|
+
const nodeFitsBottom = nodeAnchorBottom <= this.currentScrollPosition + this.currentVisibleHeight;
|
|
1555
|
+
const nodeFits = nodeFitsTop && nodeFitsBottom;
|
|
1556
|
+
|
|
1557
|
+
if (!nodeFits) {
|
|
1558
|
+
let scrollTop = this.currentScrollPosition;
|
|
1559
|
+
if (!nodeFitsTop && !nodeFitsBottom) {
|
|
1560
|
+
scrollTop = nodeAnchorBottom - this.currentVisibleHeight;
|
|
1561
|
+
} else if (!nodeFitsTop) {
|
|
1562
|
+
scrollTop = nodeAnchorTop;
|
|
1563
|
+
} else if (!nodeFitsBottom) {
|
|
1564
|
+
scrollTop = nodeAnchorBottom - this.currentVisibleHeight;
|
|
1565
|
+
}
|
|
1566
|
+
if (scrollTop < 0) {
|
|
1567
|
+
scrollTop = 0;
|
|
1568
|
+
}
|
|
1569
|
+
this.root.scrollTo({ 'top': scrollTop });
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
/**
|
|
1574
|
+
* If the tree component is embedded in an iframe, and if the iframe src get changed by navigating to another url,
|
|
1575
|
+
* pending AjaxRequest will get cancelled by the browser, without letting us the opportunity to properly handle
|
|
1576
|
+
* the thrown error. As a workaround, we register a pagehide event on the iframe's window,
|
|
1577
|
+
* and turn on the muteErrorNotifications flag.
|
|
1578
|
+
*/
|
|
1579
|
+
private registerUnloadHandler(): void {
|
|
1580
|
+
try {
|
|
1581
|
+
// Do not proceed if we are not embedded in an iframe (of if CSP prevent for accessing frameElement),
|
|
1582
|
+
if (!window.frameElement) {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
window.addEventListener(
|
|
1586
|
+
'pagehide',
|
|
1587
|
+
() => this.muteErrorNotifications = true,
|
|
1588
|
+
{ once: true }
|
|
1589
|
+
);
|
|
1590
|
+
} catch {
|
|
1591
|
+
console.error('Failed to check the existence of window.frameElement – using a foreign origin?');
|
|
1592
|
+
// Do nothing if an error occured during the event registration
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|