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,885 @@
|
|
|
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, type TemplateResult } from 'lit';
|
|
15
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html';
|
|
16
|
+
import { styleMap } from 'lit/directives/style-map';
|
|
17
|
+
import type { AjaxResponse } from '@typo3/core/ajax/ajax-response';
|
|
18
|
+
import AjaxRequest from '@typo3/core/ajax/ajax-request';
|
|
19
|
+
import RegularEvent from '@typo3/core/event/regular-event';
|
|
20
|
+
import FormEngineValidation from '@typo3/backend/form-engine-validation';
|
|
21
|
+
import Cropper from 'cropperjs';
|
|
22
|
+
import { default as Modal, type ModalElement } from './modal';
|
|
23
|
+
import '@typo3/backend/element/spinner-element';
|
|
24
|
+
import { renderNodes } from '@typo3/core/lit-helper';
|
|
25
|
+
import { topLevelModuleImport } from '@typo3/backend/utility/top-level-module-import';
|
|
26
|
+
import { Offset } from '@typo3/backend/offset';
|
|
27
|
+
import type { DraggableResizableEvent, PointerEventNames, DraggableResizableElement } from '@typo3/backend/element/draggable-resizable-element';
|
|
28
|
+
import type { EventInterface } from '@typo3/core/event/event-interface';
|
|
29
|
+
|
|
30
|
+
export interface Area {
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
height: number;
|
|
34
|
+
width: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Ratio {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
value: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CropVariant {
|
|
44
|
+
title: string;
|
|
45
|
+
id: string;
|
|
46
|
+
selectedRatio: string;
|
|
47
|
+
cropArea?: Area;
|
|
48
|
+
focusArea?: Area;
|
|
49
|
+
coverAreas?: Area[];
|
|
50
|
+
allowedAspectRatios: {[key: string]: Ratio};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface CropperEvent extends CustomEvent {
|
|
54
|
+
detail: Cropper.Data;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Module: @typo3/backend/image-manipulation
|
|
59
|
+
* Contains all logic for the image crop GUI including setting focusAreas
|
|
60
|
+
* @exports @typo3/backend/image-manipulation
|
|
61
|
+
*/
|
|
62
|
+
class ImageManipulation {
|
|
63
|
+
private initialized: boolean = false;
|
|
64
|
+
private triggerListener: EventInterface = null;
|
|
65
|
+
private trigger: HTMLElement;
|
|
66
|
+
private currentModal: ModalElement;
|
|
67
|
+
private cropVariantTriggers: NodeListOf<HTMLElement>;
|
|
68
|
+
private activeCropVariantTrigger: HTMLElement;
|
|
69
|
+
private cropInfo: HTMLElement;
|
|
70
|
+
private imageOriginalSizeFactor: number;
|
|
71
|
+
private readonly cropImageSelector: string = '#t3js-crop-image';
|
|
72
|
+
private readonly coverAreaSelector: string = '.t3js-cropper-cover-area';
|
|
73
|
+
private readonly cropInfoSelector: string = '.t3js-cropper-info-crop';
|
|
74
|
+
private readonly focusAreaSelector: string = 'typo3-backend-draggable-resizable';
|
|
75
|
+
private readonly focusAreaVisualElementSelector: string = 'typo3-backend-draggable-resizable .cropper-focus-area';
|
|
76
|
+
private focusAreaEl: DraggableResizableElement;
|
|
77
|
+
// Initialize an empty object to prevent undefined cropBox error on modal load.
|
|
78
|
+
private cropBox: HTMLElement;
|
|
79
|
+
private cropper: Cropper;
|
|
80
|
+
private currentCropVariant: CropVariant;
|
|
81
|
+
private data: any;
|
|
82
|
+
private readonly defaultFocusArea: Area = {
|
|
83
|
+
height: 1 / 3,
|
|
84
|
+
width: 1 / 3,
|
|
85
|
+
x: 0,
|
|
86
|
+
y: 0,
|
|
87
|
+
};
|
|
88
|
+
private defaultOpts: Cropper.Options = {
|
|
89
|
+
autoCrop: true,
|
|
90
|
+
autoCropArea: 0.7,
|
|
91
|
+
dragMode: 'crop',
|
|
92
|
+
guides: true,
|
|
93
|
+
responsive: true,
|
|
94
|
+
viewMode: 1,
|
|
95
|
+
zoomable: false,
|
|
96
|
+
checkCrossOrigin: false,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @desc window.setTimeout shim
|
|
101
|
+
* @param {Function} fn - The function to execute
|
|
102
|
+
* @param {number} ms - The time in [ms] to wait until execution
|
|
103
|
+
*/
|
|
104
|
+
public static wait(fn: () => void, ms: number): void {
|
|
105
|
+
window.setTimeout(fn, ms);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @desc Takes a number, and converts it to CSS percentage length
|
|
110
|
+
* @param {number} num - The number to convert
|
|
111
|
+
*/
|
|
112
|
+
public static toCssPercent(num: number): string {
|
|
113
|
+
return `${num * 100}%`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @desc Serializes crop variants for persistence or preview
|
|
118
|
+
* @param {Object} cropVariants
|
|
119
|
+
*/
|
|
120
|
+
private static serializeCropVariants(cropVariants: object): string {
|
|
121
|
+
const omitUnused: any = (key: any, value: any) => {
|
|
122
|
+
return (
|
|
123
|
+
key === 'id'
|
|
124
|
+
|| key === 'title'
|
|
125
|
+
|| key === 'allowedAspectRatios'
|
|
126
|
+
|| key === 'coverAreas'
|
|
127
|
+
) ? undefined : value;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return JSON.stringify(cropVariants, omitUnused);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private static isEmptyObject(subject: Record<any, any>): boolean {
|
|
134
|
+
return !subject
|
|
135
|
+
|| typeof subject !== 'object'
|
|
136
|
+
|| Object.keys(subject).length === 0
|
|
137
|
+
|| JSON.stringify(subject) === '{}';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private static resolvePointerEventNames(): PointerEventNames {
|
|
141
|
+
/**
|
|
142
|
+
* To make the focusarea draggable, cropper must be disabled by register the same events as cropper does.
|
|
143
|
+
* Copied from https://github.com/fengyuanchen/cropperjs/blob/main/src/js/constants.js
|
|
144
|
+
*/
|
|
145
|
+
const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';
|
|
146
|
+
const IS_TOUCH_DEVICE = IS_BROWSER && window.document.documentElement ? 'ontouchstart' in window.document.documentElement : false;
|
|
147
|
+
const HAS_POINTER_EVENT = IS_BROWSER ? 'PointerEvent' in window : false;
|
|
148
|
+
const EVENT_TOUCH_MOVE = IS_TOUCH_DEVICE ? ['touchmove'] : ['mousemove'];
|
|
149
|
+
const EVENT_TOUCH_START = IS_TOUCH_DEVICE ? ['touchstart'] : ['mousedown'];
|
|
150
|
+
const EVENT_TOUCH_END = IS_TOUCH_DEVICE ? ['touchend', 'touchcancel'] : ['mouseup'];
|
|
151
|
+
const EVENT_POINTER_DOWN = HAS_POINTER_EVENT ? ['pointerdown'] : EVENT_TOUCH_START;
|
|
152
|
+
const EVENT_POINTER_MOVE = HAS_POINTER_EVENT ? ['pointermove'] : EVENT_TOUCH_MOVE;
|
|
153
|
+
const EVENT_POINTER_UP = HAS_POINTER_EVENT ? ['pointerup', 'pointercancel'] : EVENT_TOUCH_END;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
touchStart: EVENT_TOUCH_START,
|
|
157
|
+
touchMove: EVENT_TOUCH_MOVE,
|
|
158
|
+
touchEnd: EVENT_TOUCH_END,
|
|
159
|
+
pointerDown: EVENT_POINTER_DOWN,
|
|
160
|
+
pointerMove: EVENT_POINTER_MOVE,
|
|
161
|
+
pointerUp: EVENT_POINTER_UP,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @desc Assign a handler to .t3js-image-manipulation-trigger.
|
|
167
|
+
* Show the modal and kick-off image manipulation
|
|
168
|
+
*/
|
|
169
|
+
public initializeTrigger(): void {
|
|
170
|
+
if (this.triggerListener) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.triggerListener = new RegularEvent(
|
|
174
|
+
'click',
|
|
175
|
+
(evt: Event, target: HTMLElement): void => {
|
|
176
|
+
evt.preventDefault();
|
|
177
|
+
this.trigger = target;
|
|
178
|
+
this.show();
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
this.triggerListener.delegateTo(document, '.t3js-image-manipulation-trigger');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @desc Initialize the cropper modal and dispatch the cropper init
|
|
186
|
+
*/
|
|
187
|
+
private async initializeCropperModal(): Promise<void> {
|
|
188
|
+
const image: HTMLImageElement = this.currentModal.querySelector(this.cropImageSelector);
|
|
189
|
+
|
|
190
|
+
await new Promise<void>((resolve) => {
|
|
191
|
+
if (image.complete) {
|
|
192
|
+
resolve();
|
|
193
|
+
} else {
|
|
194
|
+
image.addEventListener('load', (): void => resolve());
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
this.init();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @desc Load the image and setup the modal UI
|
|
202
|
+
*/
|
|
203
|
+
private show(): void {
|
|
204
|
+
const triggerData = this.trigger.dataset;
|
|
205
|
+
const modalTitle: string = triggerData.modalTitle;
|
|
206
|
+
const buttonPreviewText: string = triggerData.buttonPreviewText;
|
|
207
|
+
const buttonDismissText: string = triggerData.buttonDismissText;
|
|
208
|
+
const buttonSaveText: string = triggerData.buttonSaveText;
|
|
209
|
+
const imageUri: string = triggerData.url;
|
|
210
|
+
const payload: Record<string, string> = JSON.parse(triggerData.payload);
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Open modal with image to crop
|
|
214
|
+
*/
|
|
215
|
+
this.currentModal = Modal.advanced({
|
|
216
|
+
additionalCssClasses: ['modal-image-manipulation', 'cropper'],
|
|
217
|
+
buttons: [
|
|
218
|
+
{
|
|
219
|
+
btnClass: 'btn-default float-start',
|
|
220
|
+
name: 'preview',
|
|
221
|
+
icon: 'actions-view',
|
|
222
|
+
text: buttonPreviewText,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
btnClass: 'btn-default',
|
|
226
|
+
name: 'dismiss',
|
|
227
|
+
icon: 'actions-close',
|
|
228
|
+
text: buttonDismissText,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
btnClass: 'btn-primary',
|
|
232
|
+
name: 'save',
|
|
233
|
+
icon: 'actions-document-save',
|
|
234
|
+
text: buttonSaveText,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
content: html`<div class="modal-loading"><typo3-backend-spinner size="large"></typo3-backend-spinner></div>`,
|
|
238
|
+
size: Modal.sizes.full,
|
|
239
|
+
style: Modal.styles.dark,
|
|
240
|
+
title: modalTitle,
|
|
241
|
+
staticBackdrop: true
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
this.currentModal.addEventListener('typo3-modal-shown', (): void => {
|
|
245
|
+
new AjaxRequest(imageUri).post(payload).then(async (response: AjaxResponse): Promise<void> => {
|
|
246
|
+
const htmlResponse = await response.resolve();
|
|
247
|
+
this.currentModal.templateResultContent = html`${unsafeHTML(htmlResponse)}`;
|
|
248
|
+
this.currentModal.updateComplete.then(() => this.initializeCropperModal());
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
this.currentModal.addEventListener('typo3-modal-hide', (): void => {
|
|
253
|
+
this.destroy();
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* @desc Initializes the cropper UI and sets up all the event bindings for the UI
|
|
259
|
+
*/
|
|
260
|
+
private init(): void {
|
|
261
|
+
const image: HTMLImageElement = this.currentModal.querySelector(this.cropImageSelector);
|
|
262
|
+
const data: string = this.trigger.dataset.cropVariants;
|
|
263
|
+
|
|
264
|
+
if (!data) {
|
|
265
|
+
throw new TypeError('ImageManipulation: No cropVariants data found for image');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// if we have data already set we assume an internal reinit eg. after resizing
|
|
269
|
+
this.data = ImageManipulation.isEmptyObject(this.data) ? JSON.parse(data) : this.data;
|
|
270
|
+
|
|
271
|
+
this.cropVariantTriggers = this.currentModal.querySelectorAll('.t3js-crop-variant-trigger');
|
|
272
|
+
this.activeCropVariantTrigger = this.currentModal.querySelector('.t3js-crop-variant-trigger.is-active');
|
|
273
|
+
this.cropInfo = this.currentModal.querySelector(this.cropInfoSelector);
|
|
274
|
+
this.currentCropVariant = this.data[this.activeCropVariantTrigger.dataset.cropVariantId];
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Assign EventListener to cropVariantTriggers
|
|
278
|
+
*/
|
|
279
|
+
this.cropVariantTriggers.forEach((el: HTMLElement) =>
|
|
280
|
+
el.addEventListener('click', (e: Event): void => {
|
|
281
|
+
/**
|
|
282
|
+
* Is the current cropVariantTrigger is active, bail out.
|
|
283
|
+
* Bootstrap doesn't provide this functionality when collapsing the Collapse panels
|
|
284
|
+
*/
|
|
285
|
+
if ((e.currentTarget as HTMLElement).classList.contains('is-active')) {
|
|
286
|
+
e.stopPropagation();
|
|
287
|
+
e.preventDefault();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.activeCropVariantTrigger.classList.remove('is-active');
|
|
292
|
+
(e.currentTarget as HTMLElement).classList.add('is-active');
|
|
293
|
+
this.activeCropVariantTrigger = e.currentTarget as HTMLElement;
|
|
294
|
+
const cropVariant: CropVariant = this.data[this.activeCropVariantTrigger.dataset.cropVariantId];
|
|
295
|
+
const imageData: Cropper.ImageData = this.cropper.getImageData();
|
|
296
|
+
cropVariant.cropArea = this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
|
|
297
|
+
this.currentCropVariant = Object.assign({}, cropVariant);
|
|
298
|
+
this.update(cropVariant);
|
|
299
|
+
})
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Assign EventListener to aspectRatioTrigger
|
|
304
|
+
*/
|
|
305
|
+
new RegularEvent(
|
|
306
|
+
'click',
|
|
307
|
+
(evt: Event, target: HTMLElement): void => {
|
|
308
|
+
const ratioId: string = target.dataset.bsOption;
|
|
309
|
+
this.handleAspectRatioChange(ratioId);
|
|
310
|
+
}
|
|
311
|
+
).delegateTo(this.currentModal, 'label[data-method=setAspectRatio]');
|
|
312
|
+
|
|
313
|
+
new RegularEvent('keydown', (evt: KeyboardEvent, target: HTMLElement): void => {
|
|
314
|
+
if (!['Enter', 'Space'].includes(evt.code)) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
evt.preventDefault();
|
|
318
|
+
evt.stopImmediatePropagation();
|
|
319
|
+
|
|
320
|
+
const setAspectRatioLabel = target.closest('label[data-method="setAspectRatio"]') as HTMLElement;
|
|
321
|
+
const ratioId: string = setAspectRatioLabel.dataset.bsOption;
|
|
322
|
+
|
|
323
|
+
setAspectRatioLabel.querySelector('input').checked = true;
|
|
324
|
+
this.handleAspectRatioChange(ratioId);
|
|
325
|
+
}).delegateTo(this.currentModal, 'label[data-method="setAspectRatio"] input[type="radio"]');
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Assign EventListener to saveButton
|
|
329
|
+
*/
|
|
330
|
+
new RegularEvent('click', (): void => this.save(this.data))
|
|
331
|
+
.delegateTo(this.currentModal, 'button[name=save]');
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Assign EventListener to previewButton if preview url exists
|
|
335
|
+
*/
|
|
336
|
+
if (this.trigger.dataset.previewUrl) {
|
|
337
|
+
new RegularEvent('click', (): void => this.openPreview(this.data))
|
|
338
|
+
.delegateTo(this.currentModal, 'button[name=preview]');
|
|
339
|
+
} else {
|
|
340
|
+
this.currentModal.querySelectorAll('button[name=preview]')
|
|
341
|
+
.forEach((previewButton: HTMLElement) => previewButton.style.display = 'none');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Assign EventListener to dismissButton
|
|
346
|
+
*/
|
|
347
|
+
new RegularEvent('click', (): void => this.currentModal.hideModal())
|
|
348
|
+
.delegateTo(this.currentModal, 'button[name=dismiss]');
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Assign EventListener to resetButton
|
|
352
|
+
*/
|
|
353
|
+
new RegularEvent(
|
|
354
|
+
'click',
|
|
355
|
+
(evt: Event, target: HTMLElement): void => {
|
|
356
|
+
const imageData: Cropper.ImageData = this.cropper.getImageData();
|
|
357
|
+
const resetCropVariantString: string = target.dataset.cropVariant;
|
|
358
|
+
evt.preventDefault();
|
|
359
|
+
evt.stopPropagation();
|
|
360
|
+
if (!resetCropVariantString) {
|
|
361
|
+
throw new TypeError('TYPO3 Cropper: No cropVariant data attribute found on reset element.');
|
|
362
|
+
}
|
|
363
|
+
const resetCropVariant: CropVariant = JSON.parse(resetCropVariantString);
|
|
364
|
+
const absoluteCropArea: Area = this.convertRelativeToAbsoluteCropArea(resetCropVariant.cropArea, imageData);
|
|
365
|
+
this.currentCropVariant = Object.assign({}, resetCropVariant, { cropArea: absoluteCropArea });
|
|
366
|
+
this.update(this.currentCropVariant);
|
|
367
|
+
}
|
|
368
|
+
).delegateTo(this.currentModal, 'button[name=reset]');
|
|
369
|
+
|
|
370
|
+
// if we start without an cropArea, maximize the cropper
|
|
371
|
+
if (ImageManipulation.isEmptyObject(this.currentCropVariant.cropArea)) {
|
|
372
|
+
this.defaultOpts = Object.assign({ autoCropArea: 1 }, this.defaultOpts);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Initialise the cropper
|
|
377
|
+
*/
|
|
378
|
+
this.cropper = new Cropper(image, Object.assign({}, this.defaultOpts, {
|
|
379
|
+
ready: () => {
|
|
380
|
+
this.cropBuiltHandler();
|
|
381
|
+
// Initialize current crop variant
|
|
382
|
+
this.update(this.currentCropVariant);
|
|
383
|
+
},
|
|
384
|
+
crop: this.cropMoveHandler.bind(this),
|
|
385
|
+
data: this.currentCropVariant.cropArea,
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @desc Internal cropper handler. Called when the cropper has been instantiated
|
|
391
|
+
*/
|
|
392
|
+
private readonly cropBuiltHandler = (): void => {
|
|
393
|
+
this.initialized = true;
|
|
394
|
+
|
|
395
|
+
const imageData: Cropper.ImageData = this.cropper.getImageData();
|
|
396
|
+
const image: HTMLImageElement = this.currentModal.querySelector(this.cropImageSelector);
|
|
397
|
+
|
|
398
|
+
// Make the image in the backdrop visible again.
|
|
399
|
+
// TODO: Check why this doesn't happen automatically.
|
|
400
|
+
this.currentModal.querySelector('.cropper-canvas img')?.classList.remove('cropper-hide');
|
|
401
|
+
|
|
402
|
+
this.imageOriginalSizeFactor = parseInt(image.dataset.originalWidth, 10) / imageData.naturalWidth;
|
|
403
|
+
|
|
404
|
+
// iterate over the crop variants and set up their respective preview
|
|
405
|
+
this.cropVariantTriggers.forEach((elem: HTMLElement): void => {
|
|
406
|
+
const cropVariantId: string = elem.dataset.cropVariantId;
|
|
407
|
+
const cropArea: Area = this.convertRelativeToAbsoluteCropArea(
|
|
408
|
+
this.data[cropVariantId].cropArea,
|
|
409
|
+
imageData,
|
|
410
|
+
);
|
|
411
|
+
const variant: CropVariant = Object.assign({}, this.data[cropVariantId], { cropArea });
|
|
412
|
+
this.updatePreviewThumbnail(variant, elem);
|
|
413
|
+
this.currentModal.querySelector(`[data-crop-variant-container="${variant.id}"]`)?.querySelector(`[data-bs-option="${variant.selectedRatio}"]`)?.classList.add('active');
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
this.currentCropVariant.cropArea = this.convertRelativeToAbsoluteCropArea(
|
|
417
|
+
this.currentCropVariant.cropArea,
|
|
418
|
+
imageData,
|
|
419
|
+
);
|
|
420
|
+
// can't use .t3js-* as selector because it is an extraneous selector
|
|
421
|
+
this.cropBox = this.currentModal.querySelector('.cropper-crop-box');
|
|
422
|
+
|
|
423
|
+
if (this.currentCropVariant.selectedRatio) {
|
|
424
|
+
// set data explicitly or setAspectRatio up-scales the crop
|
|
425
|
+
this.currentModal.querySelector(`[data-bs-option='${this.currentCropVariant.selectedRatio}']`)
|
|
426
|
+
?.classList.add('active');
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @desc Internal cropper handler. Called when the cropping area is moving
|
|
432
|
+
*/
|
|
433
|
+
private readonly cropMoveHandler = (e: CropperEvent): void => {
|
|
434
|
+
if (!this.initialized) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const minCroppedWidth = 15;
|
|
439
|
+
const minCroppedHeight = 15;
|
|
440
|
+
let width = Math.floor(e.detail.width);
|
|
441
|
+
let height = Math.floor(e.detail.height);
|
|
442
|
+
|
|
443
|
+
if (width < minCroppedWidth || height < minCroppedHeight) {
|
|
444
|
+
width = Math.max(minCroppedHeight, height);
|
|
445
|
+
height = Math.max(minCroppedWidth, width);
|
|
446
|
+
|
|
447
|
+
this.cropper.setData({
|
|
448
|
+
width: width,
|
|
449
|
+
height: height,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
this.currentCropVariant.cropArea = Object.assign({}, this.currentCropVariant.cropArea, {
|
|
454
|
+
width: Math.floor(width),
|
|
455
|
+
height: Math.floor(height),
|
|
456
|
+
x: Math.floor(e.detail.x),
|
|
457
|
+
y: Math.floor(e.detail.y),
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (this.focusAreaEl && this.currentCropVariant?.focusArea) {
|
|
461
|
+
this.focusAreaEl.offset = this.convertAreaToOffset(this.currentCropVariant.focusArea, this.cropBox);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
|
|
465
|
+
this.updateCropVariantData(this.currentCropVariant);
|
|
466
|
+
const naturalWidth: number = Math.round(this.currentCropVariant.cropArea.width * this.imageOriginalSizeFactor);
|
|
467
|
+
const naturalHeight: number = Math.round(this.currentCropVariant.cropArea.height * this.imageOriginalSizeFactor);
|
|
468
|
+
this.cropInfo.innerText = `${naturalWidth}×${naturalHeight} px`;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
private handleAspectRatioChange(ratioId: string): void {
|
|
472
|
+
const temp: CropVariant = Object.assign({}, this.currentCropVariant);
|
|
473
|
+
const ratio: Ratio = temp.allowedAspectRatios[ratioId];
|
|
474
|
+
this.setAspectRatio(ratio);
|
|
475
|
+
// set data explicitly or setAspectRatio upscales the crop
|
|
476
|
+
this.setCropArea(temp.cropArea);
|
|
477
|
+
this.currentCropVariant = Object.assign({}, temp, { selectedRatio: ratioId });
|
|
478
|
+
|
|
479
|
+
this.update(this.currentCropVariant);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* @desc Update current cropArea position and size when changing cropVariants
|
|
484
|
+
* @param {CropVariant} cropVariant - The new cropVariant to update the UI with
|
|
485
|
+
*/
|
|
486
|
+
private async update(cropVariant: CropVariant): Promise<void> {
|
|
487
|
+
const temp: CropVariant = Object.assign({}, cropVariant);
|
|
488
|
+
const selectedRatio: Ratio = cropVariant.allowedAspectRatios[cropVariant.selectedRatio];
|
|
489
|
+
|
|
490
|
+
// Set cropInfo to current container context
|
|
491
|
+
this.cropInfo = this.currentModal.querySelector(`[data-crop-variant-container="${cropVariant.id}"]`)?.querySelector(this.cropInfoSelector);
|
|
492
|
+
// highlight the currently selected ratio of the active cropping variant
|
|
493
|
+
this.currentModal.querySelector(`[data-crop-variant-container="${cropVariant.id}"]`)?.querySelector('[data-bs-option].active')?.classList.remove('active');
|
|
494
|
+
this.currentModal.querySelector(`[data-crop-variant-container="${cropVariant.id}"]`)?.querySelector(`[data-bs-option="${cropVariant.selectedRatio}"]`)?.classList.add('active');
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Setting the aspect ratio cause a redraw of the crop area so we need to manually reset it to last data
|
|
498
|
+
*/
|
|
499
|
+
this.setAspectRatio(selectedRatio);
|
|
500
|
+
this.setCropArea(temp.cropArea);
|
|
501
|
+
this.currentCropVariant = Object.assign({}, temp, cropVariant);
|
|
502
|
+
this.cropBox?.querySelectorAll(this.coverAreaSelector)?.forEach((el: HTMLElement) => el.remove());
|
|
503
|
+
this.cropBox?.querySelectorAll(this.focusAreaSelector)?.forEach((el: HTMLElement) => el.remove());
|
|
504
|
+
|
|
505
|
+
// check if new cropVariant has focusArea
|
|
506
|
+
if (cropVariant.focusArea) {
|
|
507
|
+
// init or reinit focusArea
|
|
508
|
+
if (ImageManipulation.isEmptyObject(cropVariant.focusArea)) {
|
|
509
|
+
this.currentCropVariant.focusArea = Object.assign({}, this.defaultFocusArea);
|
|
510
|
+
}
|
|
511
|
+
this.focusAreaEl = this.initFocusArea(this.cropBox);
|
|
512
|
+
} else {
|
|
513
|
+
this.focusAreaEl = null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// check if new cropVariant has coverAreas
|
|
517
|
+
if (cropVariant.coverAreas) {
|
|
518
|
+
// init or reinit focusArea
|
|
519
|
+
this.initCoverAreas(this.cropBox, this.currentCropVariant.coverAreas);
|
|
520
|
+
}
|
|
521
|
+
this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* @desc Initializes the focus area inside a container and registers the resizable and draggable interfaces to it
|
|
526
|
+
* @param {HTMLElement} container
|
|
527
|
+
*/
|
|
528
|
+
private initFocusArea(container: HTMLElement): DraggableResizableElement {
|
|
529
|
+
topLevelModuleImport('@typo3/backend/element/draggable-resizable-element.js');
|
|
530
|
+
|
|
531
|
+
const focusAreaEl = top.document.createElement('typo3-backend-draggable-resizable');
|
|
532
|
+
// assign area declaration (as persisted in the database)
|
|
533
|
+
focusAreaEl.setAttribute('offset', JSON.stringify(this.convertAreaToOffset(this.currentCropVariant.focusArea, container)));
|
|
534
|
+
// use the same events as cropper.js does
|
|
535
|
+
focusAreaEl.setAttribute('pointereventnames', JSON.stringify(ImageManipulation.resolvePointerEventNames()));
|
|
536
|
+
|
|
537
|
+
focusAreaEl.addEventListener('draggable-resizable-started', () => {
|
|
538
|
+
// disable outer cropper, when interacting with inner draggable-resizable-element
|
|
539
|
+
this.cropper.disable();
|
|
540
|
+
});
|
|
541
|
+
focusAreaEl.addEventListener('draggable-resizable-updated', () => {
|
|
542
|
+
const coverAreas = this.currentCropVariant.coverAreas;
|
|
543
|
+
const focusArea = this.convertOffsetToArea(focusAreaEl.offset, container);
|
|
544
|
+
// retrieve the inner visual element of the lit-element
|
|
545
|
+
const visualElement = focusAreaEl.querySelector(this.focusAreaVisualElementSelector);
|
|
546
|
+
if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
|
|
547
|
+
visualElement.classList.add('has-nodrop');
|
|
548
|
+
} else {
|
|
549
|
+
visualElement.classList.remove('has-nodrop');
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
focusAreaEl.addEventListener('draggable-resizable-finished', (evt: DraggableResizableEvent) => {
|
|
553
|
+
const coverAreas = this.currentCropVariant.coverAreas;
|
|
554
|
+
const focusArea = this.convertOffsetToArea(focusAreaEl.offset, container);
|
|
555
|
+
if (this.checkFocusAndCoverAreasCollision(focusArea, coverAreas)) {
|
|
556
|
+
focusAreaEl.revert(evt.detail.originOffset);
|
|
557
|
+
} else {
|
|
558
|
+
this.scaleAndMoveFocusArea(focusArea);
|
|
559
|
+
}
|
|
560
|
+
const visualElement = focusAreaEl.querySelector(this.focusAreaVisualElementSelector);
|
|
561
|
+
visualElement.classList.remove('has-nodrop');
|
|
562
|
+
// re-enable outer cropper again
|
|
563
|
+
this.cropper.enable();
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
container.appendChild(focusAreaEl);
|
|
567
|
+
this.scaleAndMoveFocusArea(this.currentCropVariant.focusArea);
|
|
568
|
+
|
|
569
|
+
return focusAreaEl;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* @desc Initialise cover areas inside the cropper container
|
|
574
|
+
* @param {HTMLElement} container - The container element to append the cover areas
|
|
575
|
+
* @param {Array<Area>} coverAreas - An array of areas to construct the cover area elements from
|
|
576
|
+
*/
|
|
577
|
+
private initCoverAreas(container: HTMLElement, coverAreas: Area[]): void {
|
|
578
|
+
coverAreas.forEach((coverArea: Area): void => {
|
|
579
|
+
const styles = {
|
|
580
|
+
height: ImageManipulation.toCssPercent(coverArea.height),
|
|
581
|
+
left: ImageManipulation.toCssPercent(coverArea.x),
|
|
582
|
+
top: ImageManipulation.toCssPercent(coverArea.y),
|
|
583
|
+
width: ImageManipulation.toCssPercent(coverArea.width),
|
|
584
|
+
};
|
|
585
|
+
const coverAreaCanvas = html`
|
|
586
|
+
<div class="cropper-cover-area t3js-cropper-cover-area" style=${styleMap(styles)}></div>
|
|
587
|
+
`;
|
|
588
|
+
this.renderElements(coverAreaCanvas, container);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* @desc Sync the cropping (and focus area) to the preview thumbnail
|
|
594
|
+
* @param {CropVariant} cropVariant - The crop variant to preview in the thumbnail
|
|
595
|
+
* @param {HTMLElement} cropVariantTrigger - The crop variant element containing the thumbnail
|
|
596
|
+
*/
|
|
597
|
+
private updatePreviewThumbnail(cropVariant: CropVariant, cropVariantTrigger: HTMLElement): void {
|
|
598
|
+
const cropperPreviewThumbnailCrop: HTMLElement =
|
|
599
|
+
cropVariantTrigger.querySelector('.t3js-cropper-preview-thumbnail-crop-area');
|
|
600
|
+
const cropperPreviewThumbnailImage: HTMLElement =
|
|
601
|
+
cropVariantTrigger.querySelector('.t3js-cropper-preview-thumbnail-crop-image');
|
|
602
|
+
const cropperPreviewThumbnailFocus: HTMLElement =
|
|
603
|
+
cropVariantTrigger.querySelector('.t3js-cropper-preview-thumbnail-focus-area');
|
|
604
|
+
const imageData: Cropper.ImageData = this.cropper.getImageData();
|
|
605
|
+
|
|
606
|
+
// update the position/dimension of the crop area in the preview
|
|
607
|
+
Object.assign(cropperPreviewThumbnailCrop.style, {
|
|
608
|
+
height: ImageManipulation.toCssPercent(cropVariant.cropArea.height / imageData.naturalHeight),
|
|
609
|
+
left: ImageManipulation.toCssPercent(cropVariant.cropArea.x / imageData.naturalWidth),
|
|
610
|
+
top: ImageManipulation.toCssPercent(cropVariant.cropArea.y / imageData.naturalHeight),
|
|
611
|
+
width: ImageManipulation.toCssPercent(cropVariant.cropArea.width / imageData.naturalWidth),
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// show and update focusArea in the preview only if we really have one configured
|
|
615
|
+
if (cropVariant.focusArea) {
|
|
616
|
+
Object.assign(cropperPreviewThumbnailFocus.style, {
|
|
617
|
+
height: ImageManipulation.toCssPercent(cropVariant.focusArea.height),
|
|
618
|
+
left: ImageManipulation.toCssPercent(cropVariant.focusArea.x),
|
|
619
|
+
top: ImageManipulation.toCssPercent(cropVariant.focusArea.y),
|
|
620
|
+
width: ImageManipulation.toCssPercent(cropVariant.focusArea.width),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// destruct the preview container's CSS properties
|
|
625
|
+
const cropperPreviewThumbnailCropStyles = getComputedStyle(cropperPreviewThumbnailCrop);
|
|
626
|
+
const styles = {
|
|
627
|
+
width: cropperPreviewThumbnailCropStyles.getPropertyValue('width'),
|
|
628
|
+
height: cropperPreviewThumbnailCropStyles.getPropertyValue('height'),
|
|
629
|
+
left: cropperPreviewThumbnailCropStyles.getPropertyValue('left'),
|
|
630
|
+
top: cropperPreviewThumbnailCropStyles.getPropertyValue('top'),
|
|
631
|
+
};
|
|
632
|
+
/**
|
|
633
|
+
* Apply negative margins on the previewThumbnailImage to make the illusion of an offset
|
|
634
|
+
*/
|
|
635
|
+
Object.assign(cropperPreviewThumbnailImage.style, {
|
|
636
|
+
height: `${parseFloat(styles.height) * (1 / (cropVariant.cropArea.height / imageData.naturalHeight))}px`,
|
|
637
|
+
margin: `${-1 * parseFloat(styles.left)}px`,
|
|
638
|
+
marginTop: `${-1 * parseFloat(styles.top)}px`,
|
|
639
|
+
width: `${parseFloat(styles.width) * (1 / (cropVariant.cropArea.width / imageData.naturalWidth))}px`,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* @desc Calculation logic for moving the focus area given the
|
|
645
|
+
* specified constrains of a crop and an optional cover area
|
|
646
|
+
* @param {Area} focusArea - The translation data
|
|
647
|
+
*/
|
|
648
|
+
private scaleAndMoveFocusArea(focusArea: Area): void {
|
|
649
|
+
this.currentCropVariant.focusArea = focusArea;
|
|
650
|
+
this.updatePreviewThumbnail(this.currentCropVariant, this.activeCropVariantTrigger);
|
|
651
|
+
this.updateCropVariantData(this.currentCropVariant);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* @desc Immutably updates the currently selected cropVariant data
|
|
656
|
+
* @param {CropVariant} currentCropVariant - The cropVariant to immutably save
|
|
657
|
+
*/
|
|
658
|
+
private updateCropVariantData(currentCropVariant: CropVariant): void {
|
|
659
|
+
const imageData: Cropper.ImageData = this.cropper.getImageData();
|
|
660
|
+
const absoluteCropArea: Area = this.convertAbsoluteToRelativeCropArea(currentCropVariant.cropArea, imageData);
|
|
661
|
+
this.data[currentCropVariant.id] = Object.assign({}, currentCropVariant, { cropArea: absoluteCropArea });
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* @desc Sets the cropper to a specific ratio
|
|
666
|
+
* @param {ratio} ratio - The ratio value to apply
|
|
667
|
+
*/
|
|
668
|
+
private setAspectRatio(ratio: Ratio): void {
|
|
669
|
+
this.cropper.setAspectRatio(ratio.value);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* @desc Sets the cropper to a specific crop area
|
|
674
|
+
* @param {cropArea} cropArea - The crop area to apply
|
|
675
|
+
*/
|
|
676
|
+
private setCropArea(cropArea: Area): void {
|
|
677
|
+
const currentRatio: Ratio = this.currentCropVariant.allowedAspectRatios[this.currentCropVariant.selectedRatio];
|
|
678
|
+
if (currentRatio.value === 0) {
|
|
679
|
+
this.cropper.setData({
|
|
680
|
+
height: cropArea.height,
|
|
681
|
+
width: cropArea.width,
|
|
682
|
+
x: cropArea.x,
|
|
683
|
+
y: cropArea.y,
|
|
684
|
+
});
|
|
685
|
+
} else {
|
|
686
|
+
this.cropper.setData({
|
|
687
|
+
height: cropArea.height,
|
|
688
|
+
width: cropArea.height * currentRatio.value,
|
|
689
|
+
x: cropArea.x,
|
|
690
|
+
y: cropArea.y,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* @desc Checks is one focus area and one or more cover areas overlap
|
|
697
|
+
* @param focusArea
|
|
698
|
+
* @param coverAreas
|
|
699
|
+
*/
|
|
700
|
+
private checkFocusAndCoverAreasCollision(focusArea: Area, coverAreas: Area[]): boolean {
|
|
701
|
+
if (!coverAreas) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
return coverAreas
|
|
705
|
+
.some((coverArea: Area): boolean => {
|
|
706
|
+
return focusArea.x < coverArea.x + coverArea.width
|
|
707
|
+
&& coverArea.x < focusArea.x + focusArea.width
|
|
708
|
+
&& focusArea.y < coverArea.y + coverArea.height
|
|
709
|
+
&& coverArea.y < focusArea.height + focusArea.y;
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* @desc Converts a crop area from absolute pixel-based into relative length values
|
|
715
|
+
* @param {Area} cropArea - The crop area to convert from
|
|
716
|
+
* @param {Cropper.ImageData} imageData - The image data
|
|
717
|
+
*/
|
|
718
|
+
private convertAbsoluteToRelativeCropArea(cropArea: Area, imageData: Cropper.ImageData): Area {
|
|
719
|
+
const { height, width, x, y }: Area = cropArea;
|
|
720
|
+
return {
|
|
721
|
+
height: height / imageData.naturalHeight,
|
|
722
|
+
width: width / imageData.naturalWidth,
|
|
723
|
+
x: x / imageData.naturalWidth,
|
|
724
|
+
y: y / imageData.naturalHeight,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* @desc Converts a crop area from relative into absolute pixel-based length values
|
|
730
|
+
* @param {Area} cropArea - The crop area to convert from
|
|
731
|
+
* @param {Cropper.ImageData} imageData - The image data
|
|
732
|
+
*/
|
|
733
|
+
private convertRelativeToAbsoluteCropArea(cropArea: Area, imageData: Cropper.ImageData): Area {
|
|
734
|
+
const { height, width, x, y }: Area = cropArea;
|
|
735
|
+
return {
|
|
736
|
+
height: height * imageData.naturalHeight,
|
|
737
|
+
width: width * imageData.naturalWidth,
|
|
738
|
+
x: x * imageData.naturalWidth,
|
|
739
|
+
y: y * imageData.naturalHeight,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* @desc Updates the preview images in the editing section with the respective crop variants
|
|
745
|
+
* @param {Object} data - The internal crop variants state
|
|
746
|
+
*/
|
|
747
|
+
private setPreviewImages(data: {[key: string]: CropVariant}): void {
|
|
748
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
749
|
+
// @ts-ignore
|
|
750
|
+
const image: HTMLImageElement = this.cropper.image;
|
|
751
|
+
const imageData: Cropper.ImageData = this.cropper.getImageData();
|
|
752
|
+
|
|
753
|
+
// iterate over the crop variants and set up their respective preview
|
|
754
|
+
Object.keys(data).forEach((cropVariantId: string) => {
|
|
755
|
+
const cropVariant: CropVariant = data[cropVariantId];
|
|
756
|
+
const cropData: Area = this.convertRelativeToAbsoluteCropArea(cropVariant.cropArea, imageData);
|
|
757
|
+
|
|
758
|
+
const preview = this.trigger
|
|
759
|
+
.closest('.form-group')
|
|
760
|
+
.querySelector(`.t3js-image-manipulation-preview[data-crop-variant-id="${cropVariantId}"]`);
|
|
761
|
+
const previewSelectedRatio = this.trigger
|
|
762
|
+
.closest('.form-group')
|
|
763
|
+
.querySelector(`.t3js-image-manipulation-selected-ratio[data-crop-variant-id="${cropVariantId}"]`); // tslint:disable-line:max-line-length
|
|
764
|
+
|
|
765
|
+
if (!(preview instanceof HTMLElement)) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
let previewWidth: number = preview.getBoundingClientRect().width;
|
|
770
|
+
let previewHeight: number = parseInt(preview.dataset.previewHeight, 10);
|
|
771
|
+
|
|
772
|
+
// adjust aspect ratio of preview width/height
|
|
773
|
+
const aspectRatio: number = cropData.width / cropData.height;
|
|
774
|
+
const tmpHeight: number = previewWidth / aspectRatio;
|
|
775
|
+
if (tmpHeight > previewHeight) {
|
|
776
|
+
previewWidth = previewHeight * aspectRatio;
|
|
777
|
+
} else {
|
|
778
|
+
previewHeight = tmpHeight;
|
|
779
|
+
}
|
|
780
|
+
// preview should never be up-scaled
|
|
781
|
+
if (previewWidth > cropData.width) {
|
|
782
|
+
previewWidth = cropData.width;
|
|
783
|
+
previewHeight = cropData.height;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const ratio: number = previewWidth / cropData.width;
|
|
787
|
+
const imageStyles = {
|
|
788
|
+
height: `${imageData.naturalHeight * ratio}px`,
|
|
789
|
+
left: `${-cropData.x * ratio}px`,
|
|
790
|
+
top: `${-cropData.y * ratio}px`,
|
|
791
|
+
width: `${imageData.naturalWidth * ratio}px`,
|
|
792
|
+
};
|
|
793
|
+
const containerStyles = {
|
|
794
|
+
width: `${previewWidth}px`,
|
|
795
|
+
height: `${previewHeight}px`,
|
|
796
|
+
};
|
|
797
|
+
const viewBox = html`
|
|
798
|
+
<span class="thumbnail thumbnail-status">
|
|
799
|
+
<div class="cropper-preview-container" style="${styleMap(containerStyles)}">
|
|
800
|
+
<img src="${image.src}" style="${styleMap(imageStyles)}">
|
|
801
|
+
</div>
|
|
802
|
+
</span>
|
|
803
|
+
`;
|
|
804
|
+
while (preview.firstChild) {
|
|
805
|
+
preview.removeChild(preview.firstChild);
|
|
806
|
+
}
|
|
807
|
+
this.renderElements(viewBox, preview);
|
|
808
|
+
|
|
809
|
+
const ratioTitleWindow = this.currentModal.ownerDocument.defaultView;
|
|
810
|
+
const ratioTitleText = this.currentModal.querySelector(`.t3-js-ratio-title[data-ratio-id="${cropVariant.id}${cropVariant.selectedRatio}"]`); // tslint:disable-line:max-line-length
|
|
811
|
+
if (previewSelectedRatio instanceof HTMLElement && ratioTitleText instanceof ratioTitleWindow.HTMLElement) {
|
|
812
|
+
previewSelectedRatio.innerText = ratioTitleText.innerText;
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* @desc Opens a preview view with the crop variants
|
|
819
|
+
* @param {object} data - The whole data object containing all the cropVariants
|
|
820
|
+
*/
|
|
821
|
+
private openPreview(data: object): void {
|
|
822
|
+
const cropVariants: string = ImageManipulation.serializeCropVariants(data);
|
|
823
|
+
let previewUrl: string = this.trigger.dataset.previewUrl;
|
|
824
|
+
previewUrl = previewUrl + (previewUrl.includes('?') ? '&' : '?') + 'cropVariants=' + encodeURIComponent(cropVariants);
|
|
825
|
+
window.open(previewUrl, 'TYPO3ImageManipulationPreview');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* @desc Saves the edited cropVariants to a hidden field
|
|
830
|
+
* @param {Record<string, CropVariant>} data - The whole data object containing all the cropVariants
|
|
831
|
+
*/
|
|
832
|
+
private save(data: Record<string, CropVariant>): void {
|
|
833
|
+
const cropVariants: string = ImageManipulation.serializeCropVariants(data);
|
|
834
|
+
const hiddenField: HTMLInputElement = document.querySelector(`#${this.trigger.dataset.field}`);
|
|
835
|
+
this.trigger.dataset.cropVariants = JSON.stringify(data);
|
|
836
|
+
this.setPreviewImages(data);
|
|
837
|
+
hiddenField.value = cropVariants;
|
|
838
|
+
FormEngineValidation.markFieldAsChanged(hiddenField);
|
|
839
|
+
this.currentModal.hideModal();
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* @desc Destroy the ImageManipulation including cropper and alike
|
|
844
|
+
*/
|
|
845
|
+
private destroy(): void {
|
|
846
|
+
if (this.currentModal) {
|
|
847
|
+
if (this.cropper instanceof Cropper) {
|
|
848
|
+
this.cropper.destroy();
|
|
849
|
+
}
|
|
850
|
+
this.initialized = false;
|
|
851
|
+
this.cropper = null;
|
|
852
|
+
this.currentModal = null;
|
|
853
|
+
this.data = null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private convertAreaToOffset(area: Area, container: HTMLElement): Offset {
|
|
858
|
+
const containerBounds = container.getBoundingClientRect();
|
|
859
|
+
return new Offset(
|
|
860
|
+
area.x * containerBounds.width,
|
|
861
|
+
area.y * containerBounds.height,
|
|
862
|
+
area.width * containerBounds.width,
|
|
863
|
+
area.height * containerBounds.height,
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private convertOffsetToArea(offset: Offset, container: HTMLElement): Area {
|
|
868
|
+
const containerBounds = container.getBoundingClientRect();
|
|
869
|
+
return {
|
|
870
|
+
x: offset.left / containerBounds.width,
|
|
871
|
+
y: offset.top / containerBounds.height,
|
|
872
|
+
width: offset.width / containerBounds.width,
|
|
873
|
+
height: offset.height / containerBounds.height,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private renderElements(template: TemplateResult, target: HTMLElement, selector?: string): HTMLElement|null {
|
|
878
|
+
const nodes = renderNodes(template);
|
|
879
|
+
const elements = Array.from(nodes).filter((node: Node): boolean => node instanceof HTMLElement);
|
|
880
|
+
elements.forEach((element: HTMLElement) => target.appendChild(element));
|
|
881
|
+
return selector ? target.querySelector(selector) : null;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export default new ImageManipulation();
|