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,1487 @@
|
|
|
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, nothing, type TemplateResult } from 'lit';
|
|
15
|
+
import { customElement, property, state, query } from 'lit/decorators';
|
|
16
|
+
import { repeat } from 'lit/directives/repeat';
|
|
17
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html';
|
|
18
|
+
import { styleMap } from 'lit/directives/style-map';
|
|
19
|
+
import { Task } from '@lit/task';
|
|
20
|
+
import { animate, fadeIn, fadeOut } from '@lit-labs/motion';
|
|
21
|
+
import '@typo3/backend/element/icon-element';
|
|
22
|
+
import AjaxRequest from '@typo3/core/ajax/ajax-request';
|
|
23
|
+
import ClientStorage from '@typo3/backend/storage/client';
|
|
24
|
+
import { lll, delay } from '@typo3/core/lit-helper';
|
|
25
|
+
import Modal, { type ModalElement } from '@typo3/backend/modal';
|
|
26
|
+
import { SeverityEnum } from '@typo3/backend/enum/severity';
|
|
27
|
+
import { AjaxResponse } from '@typo3/core/ajax/ajax-response';
|
|
28
|
+
import { Categories, type DataCategoriesInterface, type NewRecordWizardItemSelectedEventInterface } from '@typo3/backend/new-record-wizard';
|
|
29
|
+
import { topLevelModuleImport } from '@typo3/backend/utility/top-level-module-import';
|
|
30
|
+
import { selector } from '@typo3/core/literals';
|
|
31
|
+
import DomHelper from '@typo3/backend/utility/dom-helper';
|
|
32
|
+
import Notification from '@typo3/backend/notification';
|
|
33
|
+
|
|
34
|
+
enum DashboardWidgetMoveIntend {
|
|
35
|
+
start = 'start',
|
|
36
|
+
end = 'end',
|
|
37
|
+
left = 'left',
|
|
38
|
+
right = 'right',
|
|
39
|
+
up = 'up',
|
|
40
|
+
down = 'down',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface DashboardInterface {
|
|
44
|
+
identifier: string,
|
|
45
|
+
title: string,
|
|
46
|
+
widgets: DashboardWidgetConfigurationInterface[]
|
|
47
|
+
widgetPositions: Record<number, DashboardWidgetPosition[]>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface DashboardPresetInterface {
|
|
51
|
+
identifier: string;
|
|
52
|
+
title: string;
|
|
53
|
+
description: string;
|
|
54
|
+
icon: string;
|
|
55
|
+
widgets: Array<string>;
|
|
56
|
+
showInWizard: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface DashboardWidgetPosition {
|
|
60
|
+
identifier: string,
|
|
61
|
+
height: number,
|
|
62
|
+
width: number,
|
|
63
|
+
y: number,
|
|
64
|
+
x: number,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface DashboardDragInformation {
|
|
68
|
+
identifier: string;
|
|
69
|
+
itemElement: HTMLElement;
|
|
70
|
+
widgetElement: DashboardWidget;
|
|
71
|
+
height: number,
|
|
72
|
+
width: number,
|
|
73
|
+
offsetY: number;
|
|
74
|
+
offsetX: number;
|
|
75
|
+
currentY: number;
|
|
76
|
+
currentX: number;
|
|
77
|
+
initialPositions: DashboardWidgetPosition[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface DashboardWidgetConfigurationInterface {
|
|
81
|
+
identifier: string,
|
|
82
|
+
type: string,
|
|
83
|
+
height: string,
|
|
84
|
+
width: string,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface DashboardWidgetInterface extends DashboardWidgetConfigurationInterface {
|
|
88
|
+
label: string,
|
|
89
|
+
content: string,
|
|
90
|
+
options: Record<string, unknown>,
|
|
91
|
+
eventdata: Record<string, unknown>,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const newRecordWizardEventName = 'typo3:dashboard:widget:add';
|
|
95
|
+
|
|
96
|
+
export class DashboardWidgetContentRenderedEvent extends Event {
|
|
97
|
+
static readonly eventName = 'typo3:dashboard:widget:content:rendered';
|
|
98
|
+
|
|
99
|
+
constructor(
|
|
100
|
+
public readonly widget: DashboardWidgetInterface,
|
|
101
|
+
) {
|
|
102
|
+
super(DashboardWidgetContentRenderedEvent.eventName, {
|
|
103
|
+
bubbles: true,
|
|
104
|
+
composed: true,
|
|
105
|
+
cancelable: false,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class DashboardWidgetMoveIntendEvent extends Event {
|
|
111
|
+
static readonly eventName = 'typo3:dashboard:widget:moveIntend';
|
|
112
|
+
|
|
113
|
+
constructor(
|
|
114
|
+
public readonly identifier: string,
|
|
115
|
+
public readonly intend: DashboardWidgetMoveIntend,
|
|
116
|
+
) {
|
|
117
|
+
super(DashboardWidgetMoveIntendEvent.eventName, {
|
|
118
|
+
bubbles: true,
|
|
119
|
+
composed: true,
|
|
120
|
+
cancelable: false,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class DashboardWidgetRemoveEvent extends Event {
|
|
126
|
+
static readonly eventName = 'typo3:dashboard:widget:remove';
|
|
127
|
+
|
|
128
|
+
constructor(
|
|
129
|
+
public readonly identifier: string,
|
|
130
|
+
) {
|
|
131
|
+
super(DashboardWidgetRemoveEvent.eventName, {
|
|
132
|
+
bubbles: true,
|
|
133
|
+
composed: true,
|
|
134
|
+
cancelable: false,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class DashboardWidgetRefreshEvent extends Event {
|
|
140
|
+
static readonly eventName = 'typo3:dashboard:widget:refresh';
|
|
141
|
+
|
|
142
|
+
constructor(
|
|
143
|
+
public readonly identifier: string
|
|
144
|
+
) {
|
|
145
|
+
super(DashboardWidgetRefreshEvent.eventName, {
|
|
146
|
+
bubbles: true,
|
|
147
|
+
composed: true,
|
|
148
|
+
cancelable: false,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
class DashboardAddEvent extends Event {
|
|
154
|
+
static readonly eventName = 'typo3:dashboard:dashboard:add';
|
|
155
|
+
|
|
156
|
+
constructor(
|
|
157
|
+
public readonly preset: string,
|
|
158
|
+
public readonly title: string,
|
|
159
|
+
) {
|
|
160
|
+
super(DashboardAddEvent.eventName);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class DashboardEditEvent extends Event {
|
|
165
|
+
static readonly eventName = 'typo3:dashboard:dashboard:edit';
|
|
166
|
+
|
|
167
|
+
constructor(
|
|
168
|
+
public readonly identifier: string,
|
|
169
|
+
public readonly title: string,
|
|
170
|
+
) {
|
|
171
|
+
super(DashboardEditEvent.eventName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
class DashboardUpdateEvent extends Event {
|
|
176
|
+
static readonly eventName = 'typo3:dashboard:dashboard:update';
|
|
177
|
+
|
|
178
|
+
constructor(
|
|
179
|
+
public readonly identifier: string,
|
|
180
|
+
public readonly widgets: DashboardWidgetConfigurationInterface[],
|
|
181
|
+
public readonly widgetPositions: Record<number, DashboardWidgetPosition[]>,
|
|
182
|
+
) {
|
|
183
|
+
super(DashboardUpdateEvent.eventName);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
class DashboardDeleteEvent extends Event {
|
|
188
|
+
static readonly eventName = 'typo3:dashboard:dashboard:delete';
|
|
189
|
+
|
|
190
|
+
constructor(
|
|
191
|
+
public readonly identifier: string,
|
|
192
|
+
) {
|
|
193
|
+
super(DashboardDeleteEvent.eventName);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function createSet(item: DashboardWidgetPosition): Set<string> {
|
|
198
|
+
const set = new Set<string>();
|
|
199
|
+
for (let y = 0; y < item.height; y++) {
|
|
200
|
+
for (let x = 0; x < item.width; x++) {
|
|
201
|
+
const cellKey = `${item.y + y}-${item.x + x}`;
|
|
202
|
+
set.add(cellKey);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return set;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@customElement('typo3-dashboard')
|
|
209
|
+
export class Dashboard extends LitElement {
|
|
210
|
+
@state() loading: boolean = false;
|
|
211
|
+
@state() dashboards: DashboardInterface[] = [];
|
|
212
|
+
@state() currentDashboard: DashboardInterface | null = null;
|
|
213
|
+
@state() columns: number = 4;
|
|
214
|
+
@state() dragInformation: DashboardDragInformation | null = null;
|
|
215
|
+
@query('.dashboard-dragging-container') draggingContainer: HTMLElement;
|
|
216
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
217
|
+
private readonly clientStorageIdentifier: string = 'dashboard/current_dashboard';
|
|
218
|
+
|
|
219
|
+
private prefersReducedMotion: boolean = false;
|
|
220
|
+
private mql: MediaQueryList | null = null;
|
|
221
|
+
|
|
222
|
+
private dragOverTimeout: number | null = null;
|
|
223
|
+
|
|
224
|
+
private activeElementRef: HTMLElement | null = null;
|
|
225
|
+
|
|
226
|
+
constructor() {
|
|
227
|
+
super();
|
|
228
|
+
|
|
229
|
+
// Refresh Widget
|
|
230
|
+
this.addEventListener(DashboardWidgetRefreshEvent.eventName, (event): void => {
|
|
231
|
+
event.preventDefault();
|
|
232
|
+
const element = this.getGridItemByIdentifier(event.identifier);
|
|
233
|
+
const widgetElement = element.querySelector('typo3-dashboard-widget');
|
|
234
|
+
widgetElement.refresh();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Remove Widget
|
|
238
|
+
this.addEventListener(DashboardWidgetRemoveEvent.eventName, (event): void => {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
const { identifier } = event;
|
|
241
|
+
(new AjaxRequest(TYPO3.settings.ajaxUrls.dashboard_widget_remove))
|
|
242
|
+
.post({
|
|
243
|
+
dashboard: this.currentDashboard.identifier,
|
|
244
|
+
identifier,
|
|
245
|
+
})
|
|
246
|
+
.then(async (response: AjaxResponse): Promise<void> => {
|
|
247
|
+
const data = await response.resolve();
|
|
248
|
+
if (data.status === 'ok') {
|
|
249
|
+
// drop widget
|
|
250
|
+
this.currentDashboard.widgets = this.currentDashboard.widgets.filter((widget) => {
|
|
251
|
+
return widget.identifier !== identifier;
|
|
252
|
+
});
|
|
253
|
+
// drop widget position
|
|
254
|
+
for (const [dashboardSize, dashboardSizeSet] of Object.entries(this.currentDashboard.widgetPositions)) {
|
|
255
|
+
const dashboardSizeNumber = Number(dashboardSize);
|
|
256
|
+
this.currentDashboard.widgetPositions[dashboardSizeNumber] = dashboardSizeSet.filter((widgetPosition) => widgetPosition.identifier !== identifier);
|
|
257
|
+
}
|
|
258
|
+
this.requestUpdate();
|
|
259
|
+
} else {
|
|
260
|
+
Notification.error('', data.message);
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Try to move widget
|
|
266
|
+
this.addEventListener(DashboardWidgetMoveIntendEvent.eventName, (event): void => {
|
|
267
|
+
event.preventDefault();
|
|
268
|
+
const { intend, identifier } = event;
|
|
269
|
+
const widgetPosition = this.widgetPositionByIdentifier(identifier);
|
|
270
|
+
|
|
271
|
+
switch (intend) {
|
|
272
|
+
case DashboardWidgetMoveIntend.up:
|
|
273
|
+
widgetPosition.y = Math.max(0, widgetPosition.y - 1);
|
|
274
|
+
break;
|
|
275
|
+
case DashboardWidgetMoveIntend.down:
|
|
276
|
+
widgetPosition.y++;
|
|
277
|
+
break;
|
|
278
|
+
case DashboardWidgetMoveIntend.left:
|
|
279
|
+
widgetPosition.x = Math.max(0, widgetPosition.x - 1);
|
|
280
|
+
break;
|
|
281
|
+
case DashboardWidgetMoveIntend.right:
|
|
282
|
+
widgetPosition.x = Math.min(this.columns - widgetPosition.width, widgetPosition.x + 1);
|
|
283
|
+
break;
|
|
284
|
+
case DashboardWidgetMoveIntend.end:
|
|
285
|
+
// Rearranging DOM elements in `widgetPositionsSort` will cause a focus-state loss,
|
|
286
|
+
// caused by `insertBefore` limitation, see
|
|
287
|
+
// https://developer.chrome.com/blog/movebefore-api#losing_state_during_dom_mutations
|
|
288
|
+
if (document.activeElement instanceof HTMLElement && document.activeElement.closest('typo3-dashboard') === this) {
|
|
289
|
+
// Keep a reference to the focussed element and re-set in `this.updated()`
|
|
290
|
+
this.activeElementRef = document.activeElement;
|
|
291
|
+
}
|
|
292
|
+
this.widgetPositionsSort(this.currentDashboard.widgetPositions[this.columns]);
|
|
293
|
+
|
|
294
|
+
this.dispatchEvent(new DashboardUpdateEvent(
|
|
295
|
+
this.currentDashboard.identifier,
|
|
296
|
+
this.currentDashboard.widgets,
|
|
297
|
+
this.currentDashboard.widgetPositions
|
|
298
|
+
));
|
|
299
|
+
return;
|
|
300
|
+
default:
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.widgetPositionChange(this.currentDashboard.widgetPositions[this.columns], widgetPosition);
|
|
305
|
+
|
|
306
|
+
this.updateComplete.then(() => {
|
|
307
|
+
const item = this.getGridItemByIdentifier(identifier);
|
|
308
|
+
if (item) {
|
|
309
|
+
const smooth = intend !== DashboardWidgetMoveIntend.up;
|
|
310
|
+
DomHelper.scrollIntoViewIfNeeded(item, smooth);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Add dashboard
|
|
316
|
+
this.addEventListener(DashboardAddEvent.eventName, (event): void => {
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
const { preset, title } = event;
|
|
319
|
+
(new AjaxRequest(TYPO3.settings.ajaxUrls.dashboard_dashboard_add))
|
|
320
|
+
.post({
|
|
321
|
+
preset,
|
|
322
|
+
title
|
|
323
|
+
})
|
|
324
|
+
.then(async (response: AjaxResponse): Promise<void> => {
|
|
325
|
+
const data = await response.resolve();
|
|
326
|
+
if (data.status === 'ok') {
|
|
327
|
+
const currentDashboard = data.dashboard;
|
|
328
|
+
this.dashboards.push(currentDashboard);
|
|
329
|
+
const selectedDashboard = this.getDashboardByIdentifier(currentDashboard.identifier) || this.getDashboardFirst();
|
|
330
|
+
this.selectDashboard(selectedDashboard);
|
|
331
|
+
this.requestUpdate();
|
|
332
|
+
} else {
|
|
333
|
+
Notification.error('', data.message);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Edit dashboard
|
|
339
|
+
this.addEventListener(DashboardEditEvent.eventName, (event): void => {
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
const { identifier, title } = event;
|
|
342
|
+
(new AjaxRequest(TYPO3.settings.ajaxUrls.dashboard_dashboard_edit))
|
|
343
|
+
.post({
|
|
344
|
+
identifier,
|
|
345
|
+
title,
|
|
346
|
+
})
|
|
347
|
+
.then(async (response: AjaxResponse): Promise<void> => {
|
|
348
|
+
const data = await response.resolve();
|
|
349
|
+
if (data.status === 'ok') {
|
|
350
|
+
const oldDashboard: DashboardInterface = this.dashboards.filter((dashboard: DashboardInterface): boolean => {
|
|
351
|
+
return dashboard.identifier === identifier;
|
|
352
|
+
})[0];
|
|
353
|
+
const index = this.dashboards.indexOf(oldDashboard);
|
|
354
|
+
const updatedDashboard = data.dashboard;
|
|
355
|
+
this.dashboards[index] = updatedDashboard;
|
|
356
|
+
if (oldDashboard.identifier === updatedDashboard.identifier) {
|
|
357
|
+
this.selectDashboard(updatedDashboard);
|
|
358
|
+
}
|
|
359
|
+
this.requestUpdate();
|
|
360
|
+
} else {
|
|
361
|
+
Notification.error('', data.message);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Update dashboard
|
|
367
|
+
this.addEventListener(DashboardUpdateEvent.eventName, (event): void => {
|
|
368
|
+
event.preventDefault();
|
|
369
|
+
|
|
370
|
+
const {
|
|
371
|
+
identifier,
|
|
372
|
+
widgets,
|
|
373
|
+
widgetPositions,
|
|
374
|
+
} = event;
|
|
375
|
+
(new AjaxRequest(TYPO3.settings.ajaxUrls.dashboard_dashboard_update))
|
|
376
|
+
.post({
|
|
377
|
+
identifier,
|
|
378
|
+
widgets,
|
|
379
|
+
widgetPositions,
|
|
380
|
+
})
|
|
381
|
+
.then(async (response: AjaxResponse): Promise<void> => {
|
|
382
|
+
const data = await response.resolve();
|
|
383
|
+
if (data.status === 'ok') {
|
|
384
|
+
const oldDashboard: DashboardInterface = this.dashboards.filter((dashboard: DashboardInterface): boolean => {
|
|
385
|
+
return dashboard.identifier === identifier;
|
|
386
|
+
})[0];
|
|
387
|
+
const index = this.dashboards.indexOf(oldDashboard);
|
|
388
|
+
const updatedDashboard = data.dashboard;
|
|
389
|
+
this.dashboards[index] = updatedDashboard;
|
|
390
|
+
if (oldDashboard.identifier === updatedDashboard.identifier) {
|
|
391
|
+
this.selectDashboard(updatedDashboard);
|
|
392
|
+
}
|
|
393
|
+
this.requestUpdate();
|
|
394
|
+
} else {
|
|
395
|
+
Notification.error('', data.message);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// Delete dashboard
|
|
401
|
+
this.addEventListener(DashboardDeleteEvent.eventName, (event): void => {
|
|
402
|
+
event.preventDefault();
|
|
403
|
+
const { identifier } = event;
|
|
404
|
+
(new AjaxRequest(TYPO3.settings.ajaxUrls.dashboard_dashboard_delete))
|
|
405
|
+
.post({
|
|
406
|
+
identifier
|
|
407
|
+
})
|
|
408
|
+
.then(async (response: AjaxResponse): Promise<void> => {
|
|
409
|
+
const data = await response.resolve();
|
|
410
|
+
if (data.status === 'ok') {
|
|
411
|
+
this.dashboards = this.dashboards.filter((dashboard: DashboardInterface): boolean => {
|
|
412
|
+
return dashboard.identifier !== identifier;
|
|
413
|
+
});
|
|
414
|
+
const selectedDashboard = this.getDashboardFirst();
|
|
415
|
+
this.selectDashboard(selectedDashboard);
|
|
416
|
+
this.requestUpdate();
|
|
417
|
+
} else {
|
|
418
|
+
Notification.error('', data.message);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
public override connectedCallback() {
|
|
425
|
+
super.connectedCallback();
|
|
426
|
+
|
|
427
|
+
// Attach resize observer to set available columns
|
|
428
|
+
this.resizeObserver = new ResizeObserver((entries) => {
|
|
429
|
+
for (const entry of entries) {
|
|
430
|
+
const { width } = entry.contentRect;
|
|
431
|
+
if (width > 950) {
|
|
432
|
+
this.columns = 4;
|
|
433
|
+
} else if (width > 750) {
|
|
434
|
+
this.columns = 2;
|
|
435
|
+
} else {
|
|
436
|
+
this.columns = 1;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
this.resizeObserver.observe(this);
|
|
441
|
+
|
|
442
|
+
this.mql = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
443
|
+
this.mqListener(this.mql);
|
|
444
|
+
this.mql.addEventListener('change', this.mqListener);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
public override disconnectedCallback() {
|
|
448
|
+
super.disconnectedCallback();
|
|
449
|
+
|
|
450
|
+
this.resizeObserver?.disconnect();
|
|
451
|
+
this.resizeObserver = null;
|
|
452
|
+
|
|
453
|
+
this.mql?.removeEventListener('change', this.mqListener);
|
|
454
|
+
this.mql = null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
protected readonly mqListener = (mql: MediaQueryList|MediaQueryListEvent): void => {
|
|
458
|
+
this.prefersReducedMotion = mql.matches;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
protected override firstUpdated(): void {
|
|
462
|
+
this.load();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
protected override updated(): void {
|
|
466
|
+
if (this.activeElementRef) {
|
|
467
|
+
this.activeElementRef.focus();
|
|
468
|
+
this.activeElementRef = null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
protected override createRenderRoot(): HTMLElement | ShadowRoot {
|
|
473
|
+
return this;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
protected override render(): TemplateResult {
|
|
477
|
+
if (this.loading) {
|
|
478
|
+
return this.renderLoader();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return html`
|
|
482
|
+
${this.renderHeader()}
|
|
483
|
+
<div class="dashboard-container"
|
|
484
|
+
@dragend=${this.handleDragEnd}
|
|
485
|
+
@dragover=${this.handleDragOver}
|
|
486
|
+
@dragstart=${this.handleDragStart}
|
|
487
|
+
>
|
|
488
|
+
${this.renderContent()}
|
|
489
|
+
<div class="dashboard-dragging-container"></div>
|
|
490
|
+
</div>
|
|
491
|
+
${this.renderFooter()}
|
|
492
|
+
`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private async load(): Promise<void> {
|
|
496
|
+
this.loading = true;
|
|
497
|
+
this.dashboards = await this.fetchDashboards();
|
|
498
|
+
const currentDashboardIdentifier = ClientStorage.get(this.clientStorageIdentifier);
|
|
499
|
+
const selectedDashboard = this.getDashboardByIdentifier(currentDashboardIdentifier) || this.getDashboardFirst();
|
|
500
|
+
this.selectDashboard(selectedDashboard);
|
|
501
|
+
this.loading = false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private async fetchData(url: string): Promise<unknown> {
|
|
505
|
+
try {
|
|
506
|
+
return (await new AjaxRequest(url).get({ cache: 'no-cache' })).resolve();
|
|
507
|
+
} catch (error: unknown) {
|
|
508
|
+
console.error(error);
|
|
509
|
+
return [];
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private async fetchPresets(): Promise<DashboardPresetInterface[]> {
|
|
514
|
+
const data = await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_presets_get);
|
|
515
|
+
return Object.values(data);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private async fetchCategories(): Promise<Categories> {
|
|
519
|
+
const data = await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_categories_get);
|
|
520
|
+
return Categories.fromData(data as DataCategoriesInterface);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private async fetchDashboards(): Promise<DashboardInterface[]> {
|
|
524
|
+
const data = await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_dashboards_get);
|
|
525
|
+
return data as DashboardInterface[];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private getDashboardByIdentifier(identifier: string): DashboardInterface | null {
|
|
529
|
+
return this.dashboards.find(dashboard => dashboard.identifier === identifier) || null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private getDashboardFirst(): DashboardInterface | null {
|
|
533
|
+
return this.dashboards.length > 0 ? this.dashboards[0] : null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private async createDashboard(): Promise<void> {
|
|
537
|
+
const presets = await this.fetchPresets();
|
|
538
|
+
const filteredPresets = presets.filter(preset => preset.showInWizard);
|
|
539
|
+
|
|
540
|
+
const content = html`
|
|
541
|
+
<form>
|
|
542
|
+
<div class="form-group">
|
|
543
|
+
<label class="form-label" for="dashboard-form-add-title">${lll('dashboard.title')}</label>
|
|
544
|
+
<input class="form-control" id="dashboard-form-add-title" type="text" name="title" required="required">
|
|
545
|
+
</div>
|
|
546
|
+
<div class="dashboard-modal-items">
|
|
547
|
+
${repeat(filteredPresets, (preset: DashboardPresetInterface) => preset.identifier, (preset: DashboardPresetInterface, index: number) => html`
|
|
548
|
+
<div class="dashboard-modal-item">
|
|
549
|
+
<input
|
|
550
|
+
type="radio"
|
|
551
|
+
name="preset"
|
|
552
|
+
value=${preset.identifier}
|
|
553
|
+
class="dashboard-modal-item-checkbox"
|
|
554
|
+
id="dashboard-form-add-preset-${preset.identifier}"
|
|
555
|
+
?checked=${index === 0}
|
|
556
|
+
>
|
|
557
|
+
<label for="dashboard-form-add-preset-${preset.identifier}" class="dashboard-modal-item-block">
|
|
558
|
+
<span class="dashboard-modal-item-icon">
|
|
559
|
+
<typo3-backend-icon identifier=${preset.icon} size="medium"></typo3-backend-icon>
|
|
560
|
+
</span>
|
|
561
|
+
<span class="dashboard-modal-item-details">
|
|
562
|
+
<span class="dashboard-modal-item-title">${preset.title}</span>
|
|
563
|
+
<span class="dashboard-modal-item-description">${preset.description}</span>
|
|
564
|
+
</span>
|
|
565
|
+
</label>
|
|
566
|
+
</div>
|
|
567
|
+
`)}
|
|
568
|
+
</div>
|
|
569
|
+
</form>
|
|
570
|
+
`;
|
|
571
|
+
|
|
572
|
+
Modal.advanced({
|
|
573
|
+
type: Modal.types.default,
|
|
574
|
+
title: lll('dashboard.add'),
|
|
575
|
+
size: Modal.sizes.medium,
|
|
576
|
+
severity: SeverityEnum.notice,
|
|
577
|
+
content,
|
|
578
|
+
callback: (currentModal: ModalElement): void => {
|
|
579
|
+
|
|
580
|
+
currentModal.addEventListener('typo3-modal-shown', (): void => {
|
|
581
|
+
(currentModal.querySelector('#dashboard-form-add-title') as HTMLInputElement)?.focus();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
currentModal.querySelector('form').addEventListener('submit', (e: Event): void => {
|
|
585
|
+
e.preventDefault();
|
|
586
|
+
const form = e.target as HTMLFormElement;
|
|
587
|
+
const formData = new FormData(form);
|
|
588
|
+
this.dispatchEvent(new DashboardAddEvent(
|
|
589
|
+
formData.get('preset') as string,
|
|
590
|
+
formData.get('title') as string,
|
|
591
|
+
));
|
|
592
|
+
currentModal.hideModal();
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
},
|
|
596
|
+
buttons: [
|
|
597
|
+
{
|
|
598
|
+
text: lll('dashboard.add.button.close'),
|
|
599
|
+
btnClass: 'btn-default',
|
|
600
|
+
name: 'cancel',
|
|
601
|
+
trigger: (e, modal) => modal.hideModal(),
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
text: lll('dashboard.add.button.ok'),
|
|
605
|
+
btnClass: 'btn-primary',
|
|
606
|
+
name: 'save',
|
|
607
|
+
trigger: (e, modal) => modal.querySelector('form').requestSubmit(),
|
|
608
|
+
},
|
|
609
|
+
]
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private editDashboard(dashboard: DashboardInterface): void {
|
|
614
|
+
const content = html`
|
|
615
|
+
<form>
|
|
616
|
+
<div class="form-group">
|
|
617
|
+
<label class="form-label" for="dashboard-form-edit-title">${lll('dashboard.title')}</label>
|
|
618
|
+
<input class="form-control" id="dashboard-form-edit-title" type="text" name="title" value=${dashboard.title || ''} required="required">
|
|
619
|
+
</div>
|
|
620
|
+
</form>
|
|
621
|
+
`;
|
|
622
|
+
|
|
623
|
+
Modal.advanced({
|
|
624
|
+
type: Modal.types.default,
|
|
625
|
+
title: lll('dashboard.configure'),
|
|
626
|
+
size: Modal.sizes.small,
|
|
627
|
+
severity: SeverityEnum.notice,
|
|
628
|
+
content,
|
|
629
|
+
callback: (currentModal: ModalElement): void => {
|
|
630
|
+
|
|
631
|
+
currentModal.addEventListener('typo3-modal-shown', (): void => {
|
|
632
|
+
(currentModal.querySelector('#dashboard-form-edit-title') as HTMLInputElement)?.focus();
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
currentModal.querySelector('form').addEventListener('submit', (e: Event): void => {
|
|
636
|
+
e.preventDefault();
|
|
637
|
+
const form = e.target as HTMLFormElement;
|
|
638
|
+
const formData = new FormData(form);
|
|
639
|
+
this.dispatchEvent(new DashboardEditEvent(
|
|
640
|
+
dashboard.identifier,
|
|
641
|
+
formData.get('title') as string,
|
|
642
|
+
));
|
|
643
|
+
currentModal.hideModal();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
},
|
|
647
|
+
buttons: [
|
|
648
|
+
{
|
|
649
|
+
text: lll('dashboard.configure.button.close'),
|
|
650
|
+
btnClass: 'btn-default',
|
|
651
|
+
name: 'cancel',
|
|
652
|
+
trigger: (e, modal) => modal.hideModal(),
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
text: lll('dashboard.configure.button.ok'),
|
|
656
|
+
btnClass: 'btn-primary',
|
|
657
|
+
name: 'save',
|
|
658
|
+
trigger: (e, modal) => modal.querySelector('form').requestSubmit(),
|
|
659
|
+
},
|
|
660
|
+
]
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private deleteDashboard(dashboard: DashboardInterface): void {
|
|
665
|
+
const modal = Modal.confirm(
|
|
666
|
+
lll('dashboard.delete'),
|
|
667
|
+
lll('dashboard.delete.sure'),
|
|
668
|
+
SeverityEnum.warning, [
|
|
669
|
+
{
|
|
670
|
+
text: lll('dashboard.delete.cancel'),
|
|
671
|
+
active: true,
|
|
672
|
+
btnClass: 'btn-default',
|
|
673
|
+
name: 'cancel',
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
text: lll('dashboard.delete.ok'),
|
|
677
|
+
btnClass: 'btn-warning',
|
|
678
|
+
name: 'delete',
|
|
679
|
+
},
|
|
680
|
+
]
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
modal.addEventListener('button.clicked', (e: Event): void => {
|
|
684
|
+
const target = e.target as HTMLButtonElement;
|
|
685
|
+
if (target.getAttribute('name') === 'delete') {
|
|
686
|
+
this.dispatchEvent(new DashboardDeleteEvent(dashboard.identifier));
|
|
687
|
+
}
|
|
688
|
+
modal.hideModal();
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
private selectDashboard(dashboard: DashboardInterface | null): void {
|
|
693
|
+
if (dashboard !== null) {
|
|
694
|
+
ClientStorage.set(this.clientStorageIdentifier, dashboard.identifier);
|
|
695
|
+
}
|
|
696
|
+
this.currentDashboard = dashboard;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private async addWidget(): Promise<void> {
|
|
700
|
+
topLevelModuleImport('@typo3/backend/new-record-wizard.js');
|
|
701
|
+
|
|
702
|
+
const wizard = top.document.createElement('typo3-backend-new-record-wizard');
|
|
703
|
+
wizard.searchPlaceholder = lll('widget.addToDashboard.searchLabel');
|
|
704
|
+
wizard.searchNothingFoundLabel = lll('widget.addToDashboard.searchNotFound');
|
|
705
|
+
wizard.categories = await this.fetchCategories();
|
|
706
|
+
wizard.addEventListener(newRecordWizardEventName, async (event): Promise<void> => {
|
|
707
|
+
const { identifier } = event.detail.item;
|
|
708
|
+
const response = await new AjaxRequest(TYPO3.settings.ajaxUrls.dashboard_widget_add)
|
|
709
|
+
.post({
|
|
710
|
+
dashboard: this.currentDashboard.identifier,
|
|
711
|
+
type: identifier,
|
|
712
|
+
});
|
|
713
|
+
const data = await response.resolve();
|
|
714
|
+
if (data.status === 'ok') {
|
|
715
|
+
this.currentDashboard.widgets.push(data.widget);
|
|
716
|
+
this.requestUpdate();
|
|
717
|
+
await this.updateComplete;
|
|
718
|
+
const item = this.getGridItemByIdentifier(data.widget.identifier);
|
|
719
|
+
if (item) {
|
|
720
|
+
DomHelper.scrollIntoViewIfNeeded(item, true);
|
|
721
|
+
window.setTimeout(() => item.querySelector<HTMLButtonElement>('.widget-actions > button:first-child')?.focus({ preventScroll: true, focusVisible: false }), 50);
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
Notification.error('', data.message);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
Modal.advanced({
|
|
729
|
+
type: Modal.types.default,
|
|
730
|
+
title: lll('widget.addToDashboard', this.currentDashboard.title),
|
|
731
|
+
size: Modal.sizes.medium,
|
|
732
|
+
severity: SeverityEnum.notice,
|
|
733
|
+
content: wizard,
|
|
734
|
+
callback: (currentModal: ModalElement): void => {
|
|
735
|
+
currentModal.addEventListener('button.clicked', (): void => {
|
|
736
|
+
currentModal.hideModal();
|
|
737
|
+
});
|
|
738
|
+
},
|
|
739
|
+
buttons: [
|
|
740
|
+
{
|
|
741
|
+
text: lll('widget.add.button.close'),
|
|
742
|
+
btnClass: 'btn-default',
|
|
743
|
+
name: 'cancel',
|
|
744
|
+
}
|
|
745
|
+
]
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private renderLoader(): TemplateResult {
|
|
750
|
+
return html`
|
|
751
|
+
<div class="dashboard-loader">
|
|
752
|
+
<typo3-backend-spinner size="medium"></typo3-backend-spinner>
|
|
753
|
+
</div>
|
|
754
|
+
`;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
private renderHeader(): TemplateResult {
|
|
758
|
+
const createButton: TemplateResult = html`
|
|
759
|
+
<button
|
|
760
|
+
class="btn btn-primary btn-sm btn-dashboard-add-tab"
|
|
761
|
+
title=${lll('dashboard.add')}
|
|
762
|
+
@click=${() => { this.createDashboard(); }}
|
|
763
|
+
>
|
|
764
|
+
<typo3-backend-icon identifier="actions-plus" size="small"></typo3-backend-icon>
|
|
765
|
+
<span class="visually-hidden">${lll('dashboard.add')}</span>
|
|
766
|
+
</button>
|
|
767
|
+
`;
|
|
768
|
+
|
|
769
|
+
const editButton = this.currentDashboard !== null
|
|
770
|
+
? html`
|
|
771
|
+
<button
|
|
772
|
+
class="btn btn-default btn-sm"
|
|
773
|
+
title=${lll('dashboard.configure')}
|
|
774
|
+
@click=${() => { this.editDashboard(this.currentDashboard); }}
|
|
775
|
+
>
|
|
776
|
+
<typo3-backend-icon identifier="actions-cog" size="small"></typo3-backend-icon>
|
|
777
|
+
<span class="visually-hidden">${lll('dashboard.configure')}</span>
|
|
778
|
+
</button>
|
|
779
|
+
`
|
|
780
|
+
: nothing;
|
|
781
|
+
|
|
782
|
+
const deleteButton = this.currentDashboard !== null
|
|
783
|
+
? html`
|
|
784
|
+
<button
|
|
785
|
+
class="btn btn-default btn-sm"
|
|
786
|
+
title=${lll('dashboard.delete')}
|
|
787
|
+
@click=${() => { this.deleteDashboard(this.currentDashboard); }}
|
|
788
|
+
>
|
|
789
|
+
<typo3-backend-icon identifier="actions-delete" size="small"></typo3-backend-icon>
|
|
790
|
+
<span class="visually-hidden">${lll('dashboard.delete')}</span>
|
|
791
|
+
</button>
|
|
792
|
+
`
|
|
793
|
+
: nothing;
|
|
794
|
+
|
|
795
|
+
return html`
|
|
796
|
+
<div class="dashboard-header">
|
|
797
|
+
<h1 class="visually-hidden">${this.currentDashboard?.title}</h1>
|
|
798
|
+
<div class="dashboard-header-container">
|
|
799
|
+
<div class="dashboard-tabs">
|
|
800
|
+
${repeat(this.dashboards, (dashboard: DashboardInterface) => dashboard.identifier, (dashboard: DashboardInterface) => html`
|
|
801
|
+
<button
|
|
802
|
+
@click=${() => { this.selectDashboard(dashboard); }}
|
|
803
|
+
class="dashboard-tab${dashboard === this.currentDashboard ? ' dashboard-tab--active' : ''}"
|
|
804
|
+
>
|
|
805
|
+
${dashboard.title}
|
|
806
|
+
</button>
|
|
807
|
+
`)}
|
|
808
|
+
${createButton}
|
|
809
|
+
</div>
|
|
810
|
+
${editButton || deleteButton ? html`<div class="dashboard-configuration btn-toolbar" role="toolbar">${editButton}${deleteButton}</div>` : nothing}
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
private renderContent(): TemplateResult | typeof nothing {
|
|
817
|
+
if (this.currentDashboard) {
|
|
818
|
+
if (this.currentDashboard.widgets.length > 0) {
|
|
819
|
+
this.initializeCurrentDashboard();
|
|
820
|
+
|
|
821
|
+
const animation = {
|
|
822
|
+
keyframeOptions: {
|
|
823
|
+
duration: 250,
|
|
824
|
+
fill: 'both' as FillMode,
|
|
825
|
+
},
|
|
826
|
+
in: fadeIn,
|
|
827
|
+
out: fadeOut,
|
|
828
|
+
skipInitial: true,
|
|
829
|
+
disabled: this.prefersReducedMotion,
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
return html`
|
|
833
|
+
<div
|
|
834
|
+
class="dashboard-grid"
|
|
835
|
+
style=${styleMap({ '--columns': this.columns })}
|
|
836
|
+
>
|
|
837
|
+
${repeat(this.currentDashboard.widgetPositions[this.columns], (widget: DashboardWidgetPosition) => widget.identifier, (widget: DashboardWidgetPosition) => html`
|
|
838
|
+
<div
|
|
839
|
+
class="dashboard-item"
|
|
840
|
+
style=${styleMap({ '--col-start': widget.x + 1, '--col-span': widget.width, '--row-start': widget.y + 1, '--row-span': widget.height })}
|
|
841
|
+
data-widget-hash=${widget.identifier}
|
|
842
|
+
data-widget-key=${this.widgetByIdentifier(widget.identifier)?.type}
|
|
843
|
+
data-widget-identifier=${widget.identifier}
|
|
844
|
+
draggable="true"
|
|
845
|
+
@pointerenter=${(e: PointerEvent) => (e.target as HTMLElement).setAttribute('draggable', 'true')}
|
|
846
|
+
@widgetRefresh="${() => this.handleLegacyWidgetRefreshEvent(widget)}"
|
|
847
|
+
${animate(animation)}
|
|
848
|
+
>
|
|
849
|
+
<typo3-dashboard-widget .identifier=${widget.identifier}></typo3-dashboard-widget>
|
|
850
|
+
</div>
|
|
851
|
+
`)}
|
|
852
|
+
</div>
|
|
853
|
+
`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
return html`
|
|
857
|
+
<div class="dashboard-empty">
|
|
858
|
+
<div class="dashboard-empty-content">
|
|
859
|
+
<h3>${lll('dashboard.empty.content.title')}</h3>
|
|
860
|
+
<p>${lll('dashboard.empty.content.description')}</p>
|
|
861
|
+
<button
|
|
862
|
+
title=${lll('widget.add')}
|
|
863
|
+
class="btn btn-primary"
|
|
864
|
+
@click=${() => { this.addWidget(); }}
|
|
865
|
+
>
|
|
866
|
+
<typo3-backend-icon identifier="actions-plus" size="small"></typo3-backend-icon>
|
|
867
|
+
${lll('dashboard.empty.content.button')}
|
|
868
|
+
</button>
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
`;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return nothing;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private renderFooter(): TemplateResult | typeof nothing{
|
|
878
|
+
return this.currentDashboard === null ? nothing : html`
|
|
879
|
+
<div class="dashboard-add-item">
|
|
880
|
+
<button
|
|
881
|
+
class="btn btn-primary btn-dashboard-add-widget"
|
|
882
|
+
title=${lll('widget.addToDashboard', this.currentDashboard.title)}
|
|
883
|
+
@click=${() => { this.addWidget(); }}
|
|
884
|
+
>
|
|
885
|
+
<typo3-backend-icon identifier="actions-plus" size="small"></typo3-backend-icon>
|
|
886
|
+
<span class="visually-hidden">${lll('widget.addToDashboard', this.currentDashboard.title)}</span>
|
|
887
|
+
</button>
|
|
888
|
+
</div>
|
|
889
|
+
`;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
private getGridItemByIdentifier(identifier: string): HTMLElement | null {
|
|
893
|
+
return this.querySelector(selector`.dashboard-item[data-widget-identifier="${identifier}"]`);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private handleDragStart(event: DragEvent): void {
|
|
897
|
+
const target = event.target as HTMLElement;
|
|
898
|
+
const itemElement = target.closest<HTMLElement>('.dashboard-item');
|
|
899
|
+
if (itemElement === null) {
|
|
900
|
+
event.preventDefault();
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const widgetHeaderElement = document.elementFromPoint(event.clientX, event.clientY).closest('.widget-header');
|
|
905
|
+
if (widgetHeaderElement === null) {
|
|
906
|
+
event.preventDefault();
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const identifier = itemElement.dataset.widgetIdentifier;
|
|
911
|
+
const widgetPosition = this.widgetPositionByIdentifier(identifier);
|
|
912
|
+
const rect = itemElement.getBoundingClientRect();
|
|
913
|
+
const widgetElement = itemElement.querySelector('typo3-dashboard-widget');
|
|
914
|
+
widgetElement.style.pointerEvents = 'none';
|
|
915
|
+
|
|
916
|
+
this.dragInformation = {
|
|
917
|
+
identifier,
|
|
918
|
+
itemElement,
|
|
919
|
+
widgetElement,
|
|
920
|
+
height: widgetPosition.height,
|
|
921
|
+
width: widgetPosition.width,
|
|
922
|
+
offsetY: event.clientY - rect.top,
|
|
923
|
+
offsetX: event.clientX - rect.left,
|
|
924
|
+
currentY: widgetPosition.y,
|
|
925
|
+
currentX: widgetPosition.x,
|
|
926
|
+
initialPositions: this.currentDashboard.widgetPositions[this.columns].map(item => ({ ...item })),
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
const ghostImage = new Image();
|
|
930
|
+
ghostImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
|
931
|
+
event.dataTransfer.setDragImage(ghostImage, 0, 0);
|
|
932
|
+
event.dataTransfer.setData('text/plain', '');
|
|
933
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
934
|
+
|
|
935
|
+
itemElement.classList.add('dashboard-item-dragging');
|
|
936
|
+
this.positionDraggingElement(event);
|
|
937
|
+
this.draggingContainer.appendChild(widgetElement);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
private positionDraggingElement(event: DragEvent) {
|
|
941
|
+
const clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));
|
|
942
|
+
const { itemElement, widgetElement } = this.dragInformation;
|
|
943
|
+
const itemRect = itemElement.getBoundingClientRect();
|
|
944
|
+
const containerRect = this.querySelector('.dashboard-container').getBoundingClientRect();
|
|
945
|
+
const gap = 20;
|
|
946
|
+
const x = clamp(
|
|
947
|
+
event.clientX - this.dragInformation.offsetX,
|
|
948
|
+
containerRect.left - gap,
|
|
949
|
+
containerRect.left + containerRect.width - itemRect.width + gap
|
|
950
|
+
);
|
|
951
|
+
const y = Math.max(
|
|
952
|
+
containerRect.top - gap,
|
|
953
|
+
event.clientY - this.dragInformation.offsetY
|
|
954
|
+
);
|
|
955
|
+
widgetElement.style.left = `${x}px`;
|
|
956
|
+
widgetElement.style.top = `${y}px`;
|
|
957
|
+
widgetElement.style.width = `${itemRect.width}px`;
|
|
958
|
+
widgetElement.style.height = `${itemRect.height}px`;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
private handleDragEnd(): void {
|
|
962
|
+
if (this.dragInformation) {
|
|
963
|
+
const { itemElement, widgetElement } = this.dragInformation;
|
|
964
|
+
itemElement.classList.remove('dashboard-item-dragging');
|
|
965
|
+
itemElement.appendChild(widgetElement);
|
|
966
|
+
widgetElement.removeAttribute('style');
|
|
967
|
+
this.dragInformation = null;
|
|
968
|
+
this.widgetPositionsSort(this.currentDashboard.widgetPositions[this.columns]);
|
|
969
|
+
this.dispatchEvent(new DashboardUpdateEvent(
|
|
970
|
+
this.currentDashboard.identifier,
|
|
971
|
+
this.currentDashboard.widgets,
|
|
972
|
+
this.currentDashboard.widgetPositions
|
|
973
|
+
));
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
private handleDragOver(event: DragEvent): void {
|
|
978
|
+
if (this.dragInformation) {
|
|
979
|
+
event.preventDefault();
|
|
980
|
+
event.dataTransfer.dropEffect = 'move';
|
|
981
|
+
|
|
982
|
+
this.positionDraggingElement(event);
|
|
983
|
+
|
|
984
|
+
const container = this.querySelector('.dashboard-grid');
|
|
985
|
+
const rect = container.getBoundingClientRect();
|
|
986
|
+
const gap = parseInt(getComputedStyle(container).gap, 10);
|
|
987
|
+
const rowHeight = parseInt(getComputedStyle(container).gridAutoRows, 10) + gap;
|
|
988
|
+
const colWidth = (rect.width + gap) / this.columns;
|
|
989
|
+
const currentY = Math.max(0, event.clientY - rect.top - this.dragInformation.offsetY);
|
|
990
|
+
const currentX = Math.max(0, event.clientX - rect.left - this.dragInformation.offsetX);
|
|
991
|
+
const row = Math.max(0, Math.round(currentY / rowHeight));
|
|
992
|
+
const col = Math.max(0, Math.min(Math.round(currentX / colWidth), this.columns - this.dragInformation.width));
|
|
993
|
+
|
|
994
|
+
// Reduce dragover recalculations when nothing changed
|
|
995
|
+
if (this.dragInformation.currentY !== row || this.dragInformation.currentX !== col) {
|
|
996
|
+
this.dragInformation.currentY = row;
|
|
997
|
+
this.dragInformation.currentX = col;
|
|
998
|
+
if (this.dragOverTimeout) {
|
|
999
|
+
clearTimeout(this.dragOverTimeout);
|
|
1000
|
+
}
|
|
1001
|
+
this.dragOverTimeout = window.setTimeout(() => {
|
|
1002
|
+
if (this.dragInformation) {
|
|
1003
|
+
const draggedWidgetPosition = this.widgetPositionByIdentifier(this.dragInformation.identifier);
|
|
1004
|
+
draggedWidgetPosition.y = this.dragInformation.currentY;
|
|
1005
|
+
draggedWidgetPosition.x = this.dragInformation.currentX;
|
|
1006
|
+
this.widgetPositionChange(this.currentDashboard.widgetPositions[this.columns], draggedWidgetPosition);
|
|
1007
|
+
}
|
|
1008
|
+
}, 100);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
private handleLegacyWidgetRefreshEvent(widget: DashboardWidgetPosition): void {
|
|
1014
|
+
this.dispatchEvent(new DashboardWidgetRefreshEvent(widget.identifier));
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
private initializeCurrentDashboard(): void {
|
|
1018
|
+
this.currentDashboard.widgetPositions = this.currentDashboard.widgetPositions ?? {};
|
|
1019
|
+
let items = this.currentDashboard.widgetPositions?.[this.columns] ?? [];
|
|
1020
|
+
const widgetSizeWidth: Record<string, number> = { small: 1, medium: 2, large: 4 };
|
|
1021
|
+
const widgetSizeHeight: Record<string, number> = { small: 1, medium: 2, large: 3 };
|
|
1022
|
+
|
|
1023
|
+
this.currentDashboard.widgets.forEach(widget => {
|
|
1024
|
+
if (items.find(widgetPosition => widgetPosition.identifier === widget.identifier) === undefined) {
|
|
1025
|
+
const height = widgetSizeHeight[widget.height] ?? 1;
|
|
1026
|
+
const width = widgetSizeWidth[widget.width] ?? 1;
|
|
1027
|
+
const widgetPosition: DashboardWidgetPosition = {
|
|
1028
|
+
identifier: widget.identifier,
|
|
1029
|
+
height: height,
|
|
1030
|
+
width: width < this.columns ? width : this.columns,
|
|
1031
|
+
y: 0,
|
|
1032
|
+
x: 0,
|
|
1033
|
+
};
|
|
1034
|
+
items.push(widgetPosition);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
items = this.widgetPositionsArrange(items);
|
|
1039
|
+
this.widgetPositionsCollapseRows(items);
|
|
1040
|
+
this.currentDashboard.widgetPositions[this.columns] = items;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private widgetByIdentifier(identifier: string): DashboardWidgetConfigurationInterface | null {
|
|
1044
|
+
return this.currentDashboard.widgets.find(item => item.identifier === identifier) ?? null;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
private widgetPositionByIdentifier(identifier: string): DashboardWidgetPosition | null {
|
|
1048
|
+
return this.currentDashboard.widgetPositions[this.columns].find(item => item.identifier === identifier) ?? null;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private widgetPositionCanPlace(widget: DashboardWidgetPosition, col: number, row: number, occupiedCells: Set<string>): boolean {
|
|
1052
|
+
if (col < 0 || col > this.columns - widget.width || row < 0) {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
return occupiedCells.isDisjointFrom(createSet({ ...widget, x: col, y: row }));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
private widgetPositionChange(items: DashboardWidgetPosition[], changedItem: DashboardWidgetPosition): void {
|
|
1060
|
+
// For the drag, we need to access the initial position of the widgets
|
|
1061
|
+
// and reevaluate them every time. If dragInformation is not set we
|
|
1062
|
+
// directly work on the dataset that is also used for later rendering.
|
|
1063
|
+
let initialPositions = structuredClone(this.dragInformation?.initialPositions ?? items);
|
|
1064
|
+
const index = initialPositions.findIndex((widget) => widget.identifier === changedItem.identifier);
|
|
1065
|
+
|
|
1066
|
+
let origItem: DashboardWidgetPosition;
|
|
1067
|
+
if (index > -1) {
|
|
1068
|
+
const [item] = initialPositions.splice(index, 1);
|
|
1069
|
+
origItem = { ...item };
|
|
1070
|
+
item.y = changedItem.y;
|
|
1071
|
+
item.x = changedItem.x;
|
|
1072
|
+
initialPositions.unshift(item);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
initialPositions = this.widgetPositionsArrange(initialPositions, this.dragInformation?.initialPositions ?? items, origItem);
|
|
1076
|
+
items.forEach(originalItem => {
|
|
1077
|
+
const updatedItem = initialPositions.find(copyItem => copyItem.identifier === originalItem.identifier);
|
|
1078
|
+
originalItem.y = updatedItem.y;
|
|
1079
|
+
originalItem.x = updatedItem.x;
|
|
1080
|
+
});
|
|
1081
|
+
this.widgetPositionsCollapseRows(items);
|
|
1082
|
+
this.requestUpdate();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private widgetTryPlacementInNeighbourCells(
|
|
1086
|
+
item: DashboardWidgetPosition,
|
|
1087
|
+
occupiedCells: Set<string>,
|
|
1088
|
+
allowedDistance?: { height: number, width: number }
|
|
1089
|
+
): DashboardWidgetPosition | null {
|
|
1090
|
+
const maxCol = this.columns;
|
|
1091
|
+
// Try place left on the same row, moving max 1 position
|
|
1092
|
+
for (let newCol = item.x; newCol >= Math.max(0, item.x - item.width); newCol--) {
|
|
1093
|
+
if (this.widgetPositionCanPlace(item, newCol, item.y, occupiedCells)) {
|
|
1094
|
+
return {
|
|
1095
|
+
...item,
|
|
1096
|
+
x: newCol
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Try place above, moving max 1 position
|
|
1102
|
+
for (let newRow = item.y; newRow >= 0; newRow--) {
|
|
1103
|
+
if (this.widgetPositionCanPlace(item, item.x, newRow, occupiedCells)) {
|
|
1104
|
+
return {
|
|
1105
|
+
...item,
|
|
1106
|
+
y: newRow
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// Try place right on the same row, moving max 1 position
|
|
1112
|
+
for (let newCol = item.x; newCol <= Math.min(maxCol, item.x + item.width); newCol++) {
|
|
1113
|
+
if (this.widgetPositionCanPlace(item, newCol, item.y, occupiedCells)) {
|
|
1114
|
+
return {
|
|
1115
|
+
...item,
|
|
1116
|
+
x: newCol
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Try place below, moving max 1 position
|
|
1122
|
+
for (let newRow = item.y; newRow <= item.y + (allowedDistance?.height ?? 3); newRow++) {
|
|
1123
|
+
if (this.widgetPositionCanPlace(item, item.x, newRow, occupiedCells)) {
|
|
1124
|
+
return {
|
|
1125
|
+
...item,
|
|
1126
|
+
y: newRow
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private widgetPositionsArrange(
|
|
1135
|
+
items: DashboardWidgetPosition[],
|
|
1136
|
+
previousArrangement?: DashboardWidgetPosition[],
|
|
1137
|
+
movedItem?: DashboardWidgetPosition
|
|
1138
|
+
): DashboardWidgetPosition[] {
|
|
1139
|
+
|
|
1140
|
+
let occupiedCells = new Set<string>();
|
|
1141
|
+
|
|
1142
|
+
const placeInCurrentPosition = (item: DashboardWidgetPosition) =>
|
|
1143
|
+
this.widgetPositionCanPlace(item, item.x, item.y, occupiedCells) ? { ...item } : null;
|
|
1144
|
+
|
|
1145
|
+
const placeInNeighboursWithoutShiftingPreviousArrangements = (item: DashboardWidgetPosition) => previousArrangement === undefined ? null :
|
|
1146
|
+
this.widgetTryPlacementInNeighbourCells(
|
|
1147
|
+
item,
|
|
1148
|
+
// Find a "free" slot (without having to move existing widget) for this widget
|
|
1149
|
+
previousArrangement
|
|
1150
|
+
// Create an occupy map that contains all cells from the previous arrangement
|
|
1151
|
+
.reduce((set, item) => set.union(createSet(item)), new Set<string>())
|
|
1152
|
+
// …without the moved item (that place is considered free)
|
|
1153
|
+
.difference(createSet(movedItem))
|
|
1154
|
+
// …and the cells that have already been occupied in this run.
|
|
1155
|
+
.union(occupiedCells),
|
|
1156
|
+
// allow items to be moved to "free" places by the dimension of the moved item (allowing items to swap)
|
|
1157
|
+
movedItem
|
|
1158
|
+
);
|
|
1159
|
+
|
|
1160
|
+
const placeInNeighbours = (item: DashboardWidgetPosition) =>
|
|
1161
|
+
this.widgetTryPlacementInNeighbourCells(item, occupiedCells);
|
|
1162
|
+
|
|
1163
|
+
const placeSomewhere = (item: DashboardWidgetPosition) => {
|
|
1164
|
+
const row = Math.max(0, item.y);
|
|
1165
|
+
const col = Math.max(0, Math.min(this.columns - item.width, item.x));
|
|
1166
|
+
const minCol = Math.max(0, col);
|
|
1167
|
+
const maxCol = this.columns;
|
|
1168
|
+
for (let newRow = item.y; newRow < (row + 100); newRow++) {
|
|
1169
|
+
for (let newCol = minCol; newCol < maxCol; newCol++) {
|
|
1170
|
+
if (this.widgetPositionCanPlace(item, newCol, newRow, occupiedCells)) {
|
|
1171
|
+
return { ...item, x: newCol, y: newRow };
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
throw new Error('Logic error: could not occupy cells');
|
|
1176
|
+
};
|
|
1177
|
+
|
|
1178
|
+
const occupy = (widgetPosition: DashboardWidgetPosition): DashboardWidgetPosition => {
|
|
1179
|
+
occupiedCells = occupiedCells.union(createSet(widgetPosition));
|
|
1180
|
+
return widgetPosition;
|
|
1181
|
+
};
|
|
1182
|
+
return items.map(item => occupy(
|
|
1183
|
+
placeInCurrentPosition(item) ??
|
|
1184
|
+
placeInNeighboursWithoutShiftingPreviousArrangements(item) ??
|
|
1185
|
+
placeInNeighbours(item) ??
|
|
1186
|
+
placeSomewhere(item)
|
|
1187
|
+
));
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
private widgetPositionsCollapseRows(items: DashboardWidgetPosition[]): void {
|
|
1191
|
+
const rowsWithItems = new Set<number>();
|
|
1192
|
+
items.forEach(item => {
|
|
1193
|
+
for (let r = 0; r < item.height; r++) {
|
|
1194
|
+
rowsWithItems.add(item.y + r);
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
const rowMapping: Record<number, number> = {};
|
|
1198
|
+
let newRowIndex = 0;
|
|
1199
|
+
for (let i = 0; i <= Math.max(...rowsWithItems); i++) {
|
|
1200
|
+
if (rowsWithItems.has(i)) {
|
|
1201
|
+
rowMapping[i] = newRowIndex++;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
items.forEach(item => {
|
|
1206
|
+
item.y = rowMapping[item.y];
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
private widgetPositionsSort(items: DashboardWidgetPosition[]): void {
|
|
1211
|
+
items.sort((a, b) => {
|
|
1212
|
+
if (a.y !== b.y) {
|
|
1213
|
+
return a.y - b.y;
|
|
1214
|
+
}
|
|
1215
|
+
return a.x - b.x;
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
@customElement('typo3-dashboard-widget')
|
|
1221
|
+
export class DashboardWidget extends LitElement {
|
|
1222
|
+
@property({ type: String, reflect: true }) public identifier: string;
|
|
1223
|
+
@state() moving: boolean = false;
|
|
1224
|
+
|
|
1225
|
+
private triggerContentRenderedEvent: boolean = false;
|
|
1226
|
+
|
|
1227
|
+
private readonly fetchTask = new Task(this, {
|
|
1228
|
+
args: () => [this.identifier] as const,
|
|
1229
|
+
task: async ([identifier], { signal }): Promise<DashboardWidgetInterface> => {
|
|
1230
|
+
const url = TYPO3.settings.ajaxUrls.dashboard_widget_get;
|
|
1231
|
+
const response = await new AjaxRequest(url)
|
|
1232
|
+
.withQueryArguments({ widget: identifier })
|
|
1233
|
+
.get({ signal });
|
|
1234
|
+
const data = await response.resolve();
|
|
1235
|
+
if (data.status !== 'ok') {
|
|
1236
|
+
throw new Error(data.message);
|
|
1237
|
+
}
|
|
1238
|
+
return data.widget;
|
|
1239
|
+
},
|
|
1240
|
+
onComplete: async () => {
|
|
1241
|
+
this.triggerContentRenderedEvent = true;
|
|
1242
|
+
},
|
|
1243
|
+
onError: (error: Error|AjaxResponse) => {
|
|
1244
|
+
console.error(`Error while retrieving widget [${this.identifier}]: ${
|
|
1245
|
+
error instanceof AjaxResponse ? `${error.response.status} ${error.response.statusText}` : error.message
|
|
1246
|
+
}`);
|
|
1247
|
+
},
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
private get widget(): DashboardWidgetInterface | null {
|
|
1251
|
+
return this.fetchTask.value ?? null;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
public refresh(): void {
|
|
1255
|
+
this.handleRefresh();
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
protected override createRenderRoot(): HTMLElement | ShadowRoot {
|
|
1259
|
+
return this;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
protected override updated(): void {
|
|
1263
|
+
if (this.triggerContentRenderedEvent) {
|
|
1264
|
+
this.triggerContentRenderedEvent = false;
|
|
1265
|
+
const { widget } = this;
|
|
1266
|
+
this.dispatchEvent(new DashboardWidgetContentRenderedEvent(widget));
|
|
1267
|
+
// Legacy event, kept for compatibility reasons
|
|
1268
|
+
this.dispatchEvent(new CustomEvent('widgetContentRendered', { bubbles: true, detail: this.widget.eventdata }));
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
protected override render(): TemplateResult | symbol {
|
|
1273
|
+
const loader = html`
|
|
1274
|
+
<div class="widget-loader">
|
|
1275
|
+
<typo3-backend-spinner size="medium"></typo3-backend-spinner>
|
|
1276
|
+
</div>
|
|
1277
|
+
`;
|
|
1278
|
+
|
|
1279
|
+
const widgetLabel = (widget: DashboardWidgetInterface | null) => widget?.label || 'ERROR';
|
|
1280
|
+
|
|
1281
|
+
const widgetContent = (widget: DashboardWidgetInterface | null) => widget
|
|
1282
|
+
? unsafeHTML(widget.content)
|
|
1283
|
+
: html`<div class="widget-content-main">${lll('widget.error')}</div>`;
|
|
1284
|
+
|
|
1285
|
+
const refreshButton = (loading: boolean = false) => html`
|
|
1286
|
+
<button
|
|
1287
|
+
type="button"
|
|
1288
|
+
title=${lll('widget.refresh')}
|
|
1289
|
+
class="widget-action widget-action-refresh"
|
|
1290
|
+
@click=${this.handleRefresh}
|
|
1291
|
+
>
|
|
1292
|
+
${loading ? html`<typo3-backend-spinner size="small"></typo3-backend-spinner>` : html`<typo3-backend-icon identifier="actions-refresh" size="small"></typo3-backend-icon>`}
|
|
1293
|
+
<span class="visually-hidden">${lll('widget.refresh')}</span>
|
|
1294
|
+
</button>
|
|
1295
|
+
`;
|
|
1296
|
+
|
|
1297
|
+
const widgetRenderer = (widget: DashboardWidgetInterface | null, loading: boolean = false) => html`
|
|
1298
|
+
<div class="widget-header">
|
|
1299
|
+
<div class="widget-title">${widgetLabel(widget)}</div>
|
|
1300
|
+
<div class="widget-actions">
|
|
1301
|
+
${widget?.options?.refreshAvailable ? refreshButton(loading) : nothing}
|
|
1302
|
+
<button
|
|
1303
|
+
type="button"
|
|
1304
|
+
title=${lll('widget.move')}
|
|
1305
|
+
class="widget-action widget-action-move"
|
|
1306
|
+
@click=${this.handleMoveClick}
|
|
1307
|
+
@focusout=${this.handleMoveFocusOut}
|
|
1308
|
+
@keydown=${this.handleMoveKeyDown}
|
|
1309
|
+
>
|
|
1310
|
+
<typo3-backend-icon identifier=${this.moving ? 'actions-thumbtack' : 'actions-move'} size="small"></typo3-backend-icon>
|
|
1311
|
+
<span class="visually-hidden">${lll('widget.move')}</span>
|
|
1312
|
+
</button>
|
|
1313
|
+
<button
|
|
1314
|
+
type="button"
|
|
1315
|
+
title=${lll('widget.remove')}
|
|
1316
|
+
class="widget-action widget-action-remove"
|
|
1317
|
+
@click=${this.handleRemove}
|
|
1318
|
+
>
|
|
1319
|
+
<typo3-backend-icon identifier="actions-delete" size="small"></typo3-backend-icon>
|
|
1320
|
+
<span class="visually-hidden">${lll('widget.remove')}</span>
|
|
1321
|
+
</button>
|
|
1322
|
+
</div>
|
|
1323
|
+
</div>
|
|
1324
|
+
<div class="widget-content"
|
|
1325
|
+
@pointerenter=${(e: PointerEvent) => (e.target as HTMLElement).closest('.dashboard-item').removeAttribute('draggable')}
|
|
1326
|
+
@pointerleave=${(e: PointerEvent) => (e.target as HTMLElement).closest('.dashboard-item').setAttribute('draggable', 'true')}
|
|
1327
|
+
>${widgetContent(widget)}</div>
|
|
1328
|
+
`;
|
|
1329
|
+
|
|
1330
|
+
const content = this.fetchTask.render({
|
|
1331
|
+
initial: () => nothing,
|
|
1332
|
+
error: () => widgetRenderer(null),
|
|
1333
|
+
pending: () => this.fetchTask.value ?
|
|
1334
|
+
// Preserve old content if refreshing, but show a spinning icon
|
|
1335
|
+
widgetRenderer(this.fetchTask.value, true) :
|
|
1336
|
+
// Delay the (initial) spinner by 80 ms to prevent flickering for fast connections.
|
|
1337
|
+
delay(80, () => loader),
|
|
1338
|
+
complete: (widget) => widgetRenderer(widget),
|
|
1339
|
+
});
|
|
1340
|
+
return html`<div class="widget ${this.moving ? ' widget-selected' : ''}">${content}</div>`;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
private moveStart(): void {
|
|
1344
|
+
if (this.moving === false) {
|
|
1345
|
+
this.moving = true;
|
|
1346
|
+
this.dispatchEvent(new DashboardWidgetMoveIntendEvent(
|
|
1347
|
+
this.widget.identifier,
|
|
1348
|
+
DashboardWidgetMoveIntend.start,
|
|
1349
|
+
));
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
private moveEnd(): void {
|
|
1354
|
+
if (this.moving === true) {
|
|
1355
|
+
this.moving = false;
|
|
1356
|
+
this.dispatchEvent(new DashboardWidgetMoveIntendEvent(
|
|
1357
|
+
this.widget.identifier,
|
|
1358
|
+
DashboardWidgetMoveIntend.end,
|
|
1359
|
+
));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
private handleMoveClick(): void
|
|
1364
|
+
{
|
|
1365
|
+
if (!this.moving) {
|
|
1366
|
+
this.moveStart();
|
|
1367
|
+
} else {
|
|
1368
|
+
this.moveEnd();
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
private handleMoveFocusOut(): void {
|
|
1373
|
+
this.moveEnd();
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
private handleMoveKeyDown(event: KeyboardEvent): void {
|
|
1377
|
+
if (!this.moving) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const handledKeys = [
|
|
1382
|
+
'ArrowDown',
|
|
1383
|
+
'ArrowUp',
|
|
1384
|
+
'ArrowLeft',
|
|
1385
|
+
'ArrowRight',
|
|
1386
|
+
'Home',
|
|
1387
|
+
'End',
|
|
1388
|
+
'Enter',
|
|
1389
|
+
'Space',
|
|
1390
|
+
'Escape',
|
|
1391
|
+
'Tab',
|
|
1392
|
+
];
|
|
1393
|
+
if (!handledKeys.includes(event.code) || event.altKey || event.ctrlKey) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
event.preventDefault();
|
|
1398
|
+
event.stopPropagation();
|
|
1399
|
+
|
|
1400
|
+
let intend: DashboardWidgetMoveIntend = DashboardWidgetMoveIntend.end;
|
|
1401
|
+
switch (event.code) {
|
|
1402
|
+
case 'Escape':
|
|
1403
|
+
case 'Enter':
|
|
1404
|
+
case 'Space':
|
|
1405
|
+
this.moveEnd();
|
|
1406
|
+
return;
|
|
1407
|
+
case 'ArrowUp':
|
|
1408
|
+
intend = DashboardWidgetMoveIntend.up;
|
|
1409
|
+
break;
|
|
1410
|
+
case 'ArrowDown':
|
|
1411
|
+
intend = DashboardWidgetMoveIntend.down;
|
|
1412
|
+
break;
|
|
1413
|
+
case 'ArrowLeft':
|
|
1414
|
+
intend = DashboardWidgetMoveIntend.left;
|
|
1415
|
+
break;
|
|
1416
|
+
case 'ArrowRight':
|
|
1417
|
+
intend = DashboardWidgetMoveIntend.right;
|
|
1418
|
+
break;
|
|
1419
|
+
default:
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
this.dispatchEvent(new DashboardWidgetMoveIntendEvent(
|
|
1424
|
+
this.widget.identifier,
|
|
1425
|
+
intend
|
|
1426
|
+
));
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
private handleRefresh(): void {
|
|
1430
|
+
this.fetchTask.run();
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
private handleRemove(event: Event): void {
|
|
1434
|
+
const modal = Modal.confirm(
|
|
1435
|
+
lll('widget.remove.confirm.title'),
|
|
1436
|
+
lll('widget.remove.confirm.message'),
|
|
1437
|
+
SeverityEnum.warning, [
|
|
1438
|
+
{
|
|
1439
|
+
text: lll('widget.remove.button.close'),
|
|
1440
|
+
active: true,
|
|
1441
|
+
btnClass: 'btn-default',
|
|
1442
|
+
name: 'cancel',
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
text: lll('widget.remove.button.ok'),
|
|
1446
|
+
btnClass: 'btn-warning',
|
|
1447
|
+
name: 'delete',
|
|
1448
|
+
},
|
|
1449
|
+
]
|
|
1450
|
+
);
|
|
1451
|
+
|
|
1452
|
+
modal.addEventListener('button.clicked', (e: Event): void => {
|
|
1453
|
+
const target = e.target as HTMLButtonElement;
|
|
1454
|
+
if (target.getAttribute('name') === 'delete') {
|
|
1455
|
+
this.dispatchEvent(new DashboardWidgetRemoveEvent(this.identifier));
|
|
1456
|
+
}
|
|
1457
|
+
modal.hideModal();
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
const trigger = event.currentTarget as HTMLButtonElement;
|
|
1461
|
+
modal.addEventListener('typo3-modal-hide', (): void => {
|
|
1462
|
+
trigger?.focus();
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
declare global {
|
|
1468
|
+
interface HTMLElementTagNameMap {
|
|
1469
|
+
'typo3-dashboard': Dashboard;
|
|
1470
|
+
'typo3-dashboard-widget': DashboardWidget;
|
|
1471
|
+
}
|
|
1472
|
+
interface HTMLElementEventMap {
|
|
1473
|
+
[DashboardWidgetContentRenderedEvent.eventName]: DashboardWidgetContentRenderedEvent;
|
|
1474
|
+
[DashboardWidgetMoveIntendEvent.eventName]: DashboardWidgetMoveIntendEvent;
|
|
1475
|
+
[DashboardWidgetRemoveEvent.eventName]: DashboardWidgetRemoveEvent;
|
|
1476
|
+
[DashboardWidgetRefreshEvent.eventName]: DashboardWidgetRefreshEvent;
|
|
1477
|
+
[DashboardAddEvent.eventName]: DashboardAddEvent;
|
|
1478
|
+
[DashboardEditEvent.eventName]: DashboardEditEvent;
|
|
1479
|
+
[DashboardUpdateEvent.eventName]: DashboardUpdateEvent;
|
|
1480
|
+
[DashboardDeleteEvent.eventName]: DashboardDeleteEvent;
|
|
1481
|
+
|
|
1482
|
+
[newRecordWizardEventName]: CustomEvent<NewRecordWizardItemSelectedEventInterface>;
|
|
1483
|
+
}
|
|
1484
|
+
interface FocusOptions {
|
|
1485
|
+
focusVisible: boolean;
|
|
1486
|
+
}
|
|
1487
|
+
}
|