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.
Files changed (415) hide show
  1. package/.github/workflows/sync-and-publish.yml +265 -0
  2. package/.last-synced-tag +1 -0
  3. package/LICENSE.txt +339 -0
  4. package/README-TYPO3.md +166 -0
  5. package/README.md +28 -0
  6. package/adminpanel/admin-panel.ts +375 -0
  7. package/adminpanel/modules/cache.ts +27 -0
  8. package/adminpanel/modules/preview.ts +75 -0
  9. package/backend/action-button/abstract-action.ts +22 -0
  10. package/backend/action-button/deferred-action.ts +41 -0
  11. package/backend/action-button/immediate-action.ts +31 -0
  12. package/backend/action-dispatcher.ts +104 -0
  13. package/backend/ajax-data-handler/message-interface.ts +18 -0
  14. package/backend/ajax-data-handler/response-interface.ts +20 -0
  15. package/backend/ajax-data-handler.ts +214 -0
  16. package/backend/backend-exception.ts +22 -0
  17. package/backend/broadcast-message.ts +50 -0
  18. package/backend/broadcast-service.ts +52 -0
  19. package/backend/browse-database.ts +38 -0
  20. package/backend/clear-cache.ts +88 -0
  21. package/backend/clipboard-panel.ts +240 -0
  22. package/backend/close-current-window.ts +2 -0
  23. package/backend/code-editor/autocomplete/completion-result.ts +105 -0
  24. package/backend/code-editor/autocomplete/ts-code-completion.ts +142 -0
  25. package/backend/code-editor/autocomplete/ts-parser.ts +485 -0
  26. package/backend/code-editor/autocomplete/ts-ref.ts +164 -0
  27. package/backend/code-editor/element/code-mirror-element.ts +282 -0
  28. package/backend/code-editor/language/typoscript.ts +181 -0
  29. package/backend/code-editor/stream-parser/typoscript.ts +1600 -0
  30. package/backend/color-picker.ts +158 -0
  31. package/backend/color-scheme-switch.ts +179 -0
  32. package/backend/column-selector-button.ts +302 -0
  33. package/backend/context-help.ts +68 -0
  34. package/backend/context-menu-actions.ts +322 -0
  35. package/backend/context-menu.ts +751 -0
  36. package/backend/copy-to-clipboard.ts +79 -0
  37. package/backend/date-time-picker.ts +270 -0
  38. package/backend/document-save-actions.ts +159 -0
  39. package/backend/drag-tooltip.ts +285 -0
  40. package/backend/drag-uploader.ts +914 -0
  41. package/backend/element/alert-element.ts +116 -0
  42. package/backend/element/dispatch-modal-button.ts +50 -0
  43. package/backend/element/draggable-resizable-element.ts +249 -0
  44. package/backend/element/editable-page-title.ts +306 -0
  45. package/backend/element/icon-element.ts +84 -0
  46. package/backend/element/immediate-action-element.ts +89 -0
  47. package/backend/element/pagination.ts +59 -0
  48. package/backend/element/progress-bar-element.ts +172 -0
  49. package/backend/element/progress-tracker-element.ts +119 -0
  50. package/backend/element/pseudo-button.ts +58 -0
  51. package/backend/element/spinner-element.ts +51 -0
  52. package/backend/element/thumbnail-element.ts +80 -0
  53. package/backend/element-browser.ts +182 -0
  54. package/backend/enum/data-transfer-types.ts +21 -0
  55. package/backend/enum/icon-types.ts +31 -0
  56. package/backend/enum/key-types.ts +27 -0
  57. package/backend/enum/severity.ts +20 -0
  58. package/backend/enum/viewport/scaffold-identifier.ts +24 -0
  59. package/backend/event/client-request.ts +25 -0
  60. package/backend/event/consumable.ts +18 -0
  61. package/backend/event/consumer-scope.ts +54 -0
  62. package/backend/event/event-dispatcher.ts +26 -0
  63. package/backend/event/interaction-request-assignment.ts +24 -0
  64. package/backend/event/interaction-request-map.ts +75 -0
  65. package/backend/event/interaction-request.ts +48 -0
  66. package/backend/event/trigger-request.ts +56 -0
  67. package/backend/form/submit-interceptor.ts +66 -0
  68. package/backend/form-engine/container/files-control-container.ts +716 -0
  69. package/backend/form-engine/container/flex-form-container-container.ts +166 -0
  70. package/backend/form-engine/container/flex-form-section-container.ts +210 -0
  71. package/backend/form-engine/container/inline-control-container.ts +1230 -0
  72. package/backend/form-engine/container/site-language-container.ts +616 -0
  73. package/backend/form-engine/element/abstract-sortable-select-items.ts +186 -0
  74. package/backend/form-engine/element/category-element.ts +160 -0
  75. package/backend/form-engine/element/color-element.ts +81 -0
  76. package/backend/form-engine/element/datetime-element.ts +66 -0
  77. package/backend/form-engine/element/extra/char-counter.ts +112 -0
  78. package/backend/form-engine/element/extra/select-box-filter.ts +85 -0
  79. package/backend/form-engine/element/folder-element.ts +66 -0
  80. package/backend/form-engine/element/group-element.ts +47 -0
  81. package/backend/form-engine/element/json-element.ts +52 -0
  82. package/backend/form-engine/element/link-element.ts +121 -0
  83. package/backend/form-engine/element/mfa-info-element.ts +169 -0
  84. package/backend/form-engine/element/modifier/resizable.ts +27 -0
  85. package/backend/form-engine/element/modifier/tabbable.ts +29 -0
  86. package/backend/form-engine/element/online-media-form-element.ts +65 -0
  87. package/backend/form-engine/element/password-element.ts +71 -0
  88. package/backend/form-engine/element/select-multiple-side-by-side-element.ts +83 -0
  89. package/backend/form-engine/element/select-single-element.ts +82 -0
  90. package/backend/form-engine/element/select-tree-element.ts +148 -0
  91. package/backend/form-engine/element/select-tree-toolbar.ts +118 -0
  92. package/backend/form-engine/element/select-tree.ts +210 -0
  93. package/backend/form-engine/element/slug-element.ts +243 -0
  94. package/backend/form-engine/element/suggest/result-container.ts +118 -0
  95. package/backend/form-engine/element/suggest/result-item.ts +90 -0
  96. package/backend/form-engine/element/table-permission-element.ts +101 -0
  97. package/backend/form-engine/element/table-wizard-element.ts +427 -0
  98. package/backend/form-engine/element/text-element.ts +52 -0
  99. package/backend/form-engine/element/text-table-element.ts +31 -0
  100. package/backend/form-engine/field-control/add-record.ts +40 -0
  101. package/backend/form-engine/field-control/edit-popup.ts +64 -0
  102. package/backend/form-engine/field-control/insert-clipboard.ts +50 -0
  103. package/backend/form-engine/field-control/link-popup.ts +53 -0
  104. package/backend/form-engine/field-control/list-module.ts +40 -0
  105. package/backend/form-engine/field-control/password-generator.ts +102 -0
  106. package/backend/form-engine/field-control/reset-selection.ts +48 -0
  107. package/backend/form-engine/field-wizard/localization-state-selector.ts +67 -0
  108. package/backend/form-engine/field-wizard/value-picker.ts +123 -0
  109. package/backend/form-engine/field-wizard/value-slider.ts +86 -0
  110. package/backend/form-engine/inline-relation/ajax-dispatcher.ts +124 -0
  111. package/backend/form-engine/inline-relation/inline-response-interface.ts +41 -0
  112. package/backend/form-engine-link-browser-adapter.ts +129 -0
  113. package/backend/form-engine-review.ts +155 -0
  114. package/backend/form-engine-suggest.ts +133 -0
  115. package/backend/form-engine-validation.ts +686 -0
  116. package/backend/form-engine.ts +1391 -0
  117. package/backend/global-event-handler.ts +252 -0
  118. package/backend/grid-editor.ts +1077 -0
  119. package/backend/hashing/md5.ts +252 -0
  120. package/backend/helper.ts +24 -0
  121. package/backend/hotkeys/hotkey-storage.ts +63 -0
  122. package/backend/hotkeys.ts +216 -0
  123. package/backend/icons.ts +236 -0
  124. package/backend/image-manipulation.ts +885 -0
  125. package/backend/info-window.ts +46 -0
  126. package/backend/input/clearable.ts +128 -0
  127. package/backend/input/index.d.ts +8 -0
  128. package/backend/java-script-module-import-event-handler.ts +19 -0
  129. package/backend/key-bindings.ts +42 -0
  130. package/backend/layout-module/drag-drop.ts +359 -0
  131. package/backend/layout-module/paste.ts +165 -0
  132. package/backend/link-browser.ts +89 -0
  133. package/backend/live-search/element/backend-search.ts +34 -0
  134. package/backend/live-search/element/hint.ts +33 -0
  135. package/backend/live-search/element/provider/default-result-item.ts +52 -0
  136. package/backend/live-search/element/provider/page-provider-result-item.ts +52 -0
  137. package/backend/live-search/element/result/item/action/action-container.ts +135 -0
  138. package/backend/live-search/element/result/item/action/action.ts +56 -0
  139. package/backend/live-search/element/result/item/item-container.ts +224 -0
  140. package/backend/live-search/element/result/item/item.ts +74 -0
  141. package/backend/live-search/element/result/result-container.ts +110 -0
  142. package/backend/live-search/element/result/result-detail-container.ts +50 -0
  143. package/backend/live-search/element/result/result-pagination.ts +122 -0
  144. package/backend/live-search/element/search-option-item.ts +70 -0
  145. package/backend/live-search/live-search-configurator.ts +54 -0
  146. package/backend/live-search/live-search-shortcut.ts +29 -0
  147. package/backend/live-search/result-types/backend-module-result-type.ts +8 -0
  148. package/backend/live-search/result-types/default-result-type.ts +21 -0
  149. package/backend/live-search/result-types/page-result-type.ts +23 -0
  150. package/backend/localization.ts +390 -0
  151. package/backend/login-refresh.ts +525 -0
  152. package/backend/login.ts +179 -0
  153. package/backend/mail-link-handler.ts +45 -0
  154. package/backend/modal-interface.ts +19 -0
  155. package/backend/modal.ts +685 -0
  156. package/backend/module/iframe.ts +125 -0
  157. package/backend/module/router.ts +275 -0
  158. package/backend/module-menu.ts +550 -0
  159. package/backend/module.ts +133 -0
  160. package/backend/multi-record-selection-action.ts +44 -0
  161. package/backend/multi-record-selection-delete-action.ts +108 -0
  162. package/backend/multi-record-selection-edit-action.ts +56 -0
  163. package/backend/multi-record-selection.ts +468 -0
  164. package/backend/multi-step-wizard.ts +568 -0
  165. package/backend/new-content-element-wizard-button.ts +51 -0
  166. package/backend/new-multiple-pages.ts +108 -0
  167. package/backend/new-record-wizard.ts +680 -0
  168. package/backend/notification.ts +366 -0
  169. package/backend/offset.ts +40 -0
  170. package/backend/online-media.ts +110 -0
  171. package/backend/page-actions.ts +91 -0
  172. package/backend/page-link-handler.ts +51 -0
  173. package/backend/pagetsconfig/pagetsconfig-includes.ts +77 -0
  174. package/backend/popover.ts +165 -0
  175. package/backend/record-download-button.ts +104 -0
  176. package/backend/record-link-handler.ts +31 -0
  177. package/backend/record-search.ts +44 -0
  178. package/backend/recordlist.ts +230 -0
  179. package/backend/resource/resource.ts +35 -0
  180. package/backend/security/element/csp-reports.ts +406 -0
  181. package/backend/security/element/sudo-mode.ts +249 -0
  182. package/backend/security/sudo-mode-interceptor.ts +19 -0
  183. package/backend/settings/editor/editable-setting.ts +231 -0
  184. package/backend/settings/editor.ts +451 -0
  185. package/backend/settings/enum/settings-mode.enum.ts +24 -0
  186. package/backend/settings/type/base.ts +200 -0
  187. package/backend/settings/type/bool.ts +56 -0
  188. package/backend/settings/type/color.ts +64 -0
  189. package/backend/settings/type/int.ts +85 -0
  190. package/backend/settings/type/number.ts +61 -0
  191. package/backend/settings/type/page.ts +100 -0
  192. package/backend/settings/type/string.ts +77 -0
  193. package/backend/settings/type/stringlist.ts +87 -0
  194. package/backend/severity.ts +88 -0
  195. package/backend/site-inline-actions.ts +32 -0
  196. package/backend/sortable-table.ts +208 -0
  197. package/backend/storage/abstract-client-storage.ts +76 -0
  198. package/backend/storage/browser-session.ts +28 -0
  199. package/backend/storage/client.ts +28 -0
  200. package/backend/storage/module-state-storage.ts +118 -0
  201. package/backend/storage/persistent.ts +204 -0
  202. package/backend/switch-user.ts +82 -0
  203. package/backend/tabs.ts +72 -0
  204. package/backend/telephone-link-handler.ts +40 -0
  205. package/backend/tests/backend/storage/browser-storage-test.ts +87 -0
  206. package/backend/tests/backend/storage/client-test.ts +87 -0
  207. package/backend/tests/backend/utility/dom-helper-test.ts +51 -0
  208. package/backend/tests/backend-exception-test.ts +28 -0
  209. package/backend/tests/element/immediate-action-element-test.ts +81 -0
  210. package/backend/tests/form-engine-validation-test.ts +304 -0
  211. package/backend/tests/grid-editor-test.ts +29 -0
  212. package/backend/tests/hashing/md5Test.ts +28 -0
  213. package/backend/tests/icons-test.ts +23 -0
  214. package/backend/tests/notification-test.ts +161 -0
  215. package/backend/tests/popover-test.ts +83 -0
  216. package/backend/toolbar/clear-cache-menu.ts +88 -0
  217. package/backend/toolbar/live-search.ts +284 -0
  218. package/backend/toolbar/shortcut-menu.ts +254 -0
  219. package/backend/toolbar/system-information-menu.ts +161 -0
  220. package/backend/toolbar.ts +41 -0
  221. package/backend/tree/file-storage-browser.ts +194 -0
  222. package/backend/tree/file-storage-tree-container.ts +465 -0
  223. package/backend/tree/file-storage-tree.ts +42 -0
  224. package/backend/tree/page-browser.ts +272 -0
  225. package/backend/tree/page-tree-element.ts +602 -0
  226. package/backend/tree/page-tree.ts +152 -0
  227. package/backend/tree/tree-module-state.ts +159 -0
  228. package/backend/tree/tree-node-toggle.ts +31 -0
  229. package/backend/tree/tree-node.ts +97 -0
  230. package/backend/tree/tree-toolbar.ts +117 -0
  231. package/backend/tree/tree.ts +1595 -0
  232. package/backend/url-link-handler.ts +36 -0
  233. package/backend/user-pass-login.ts +255 -0
  234. package/backend/user-settings-manager.ts +189 -0
  235. package/backend/utility/collapse-state-persister.ts +205 -0
  236. package/backend/utility/collapse-state-search.ts +183 -0
  237. package/backend/utility/dom-helper.ts +91 -0
  238. package/backend/utility/drag-drop-utility.ts +47 -0
  239. package/backend/utility/message-utility.ts +45 -0
  240. package/backend/utility/top-level-module-import.ts +32 -0
  241. package/backend/utility.ts +194 -0
  242. package/backend/viewport/abstract-container.ts +22 -0
  243. package/backend/viewport/content-container.ts +98 -0
  244. package/backend/viewport/loader.ts +28 -0
  245. package/backend/viewport/navigation-container.ts +119 -0
  246. package/backend/viewport/resizable-navigation.ts +176 -0
  247. package/backend/viewport/toolbar.ts +27 -0
  248. package/backend/viewport/topbar.ts +39 -0
  249. package/backend/viewport.ts +46 -0
  250. package/backend/window-manager.ts +84 -0
  251. package/backend/wizard/move-content-element.ts +107 -0
  252. package/backend/wizard/move-page.ts +74 -0
  253. package/backend/wizard.ts +331 -0
  254. package/belog/backend-log.ts +88 -0
  255. package/beuser/backend-user-listing.ts +40 -0
  256. package/beuser/permissions.ts +291 -0
  257. package/core/ajax/ajax-request-types.ts +15 -0
  258. package/core/ajax/ajax-request.ts +202 -0
  259. package/core/ajax/ajax-response.ts +57 -0
  260. package/core/ajax/input-transformer.ts +92 -0
  261. package/core/ajax/simple-response-interface.ts +5 -0
  262. package/core/authentication/mfa-provider/totp.ts +54 -0
  263. package/core/cache/dedupe-async-task.ts +95 -0
  264. package/core/directive/markdown.ts +82 -0
  265. package/core/directive/nl2br.ts +16 -0
  266. package/core/document-service.ts +35 -0
  267. package/core/event/debounce-event.ts +40 -0
  268. package/core/event/event-interface.ts +24 -0
  269. package/core/event/regular-event.ts +58 -0
  270. package/core/event/request-animation-frame-event.ts +42 -0
  271. package/core/event/throttle-event.ts +54 -0
  272. package/core/java-script-item-handler.ts +29 -0
  273. package/core/java-script-item-processor.ts +171 -0
  274. package/core/lit-helper.ts +126 -0
  275. package/core/literals.ts +17 -0
  276. package/core/referrer-refresh.ts +2 -0
  277. package/core/security-utility.ts +89 -0
  278. package/core/tests/ajax/ajax-request-test.ts +290 -0
  279. package/core/tests/ajax/input-transformer-test.ts +64 -0
  280. package/core/tests/cache/dedupe-async-task-test.ts +152 -0
  281. package/core/tests/event/throttle-event-test.ts +47 -0
  282. package/core/tests/literals-test.ts +24 -0
  283. package/core/tests/security-utility-test.ts +50 -0
  284. package/core/utility/types.ts +9 -0
  285. package/dashboard/chart-initializer.ts +136 -0
  286. package/dashboard/dashboard.ts +1487 -0
  287. package/extensionmanager/distribution-image.ts +55 -0
  288. package/extensionmanager/main.ts +291 -0
  289. package/extensionmanager/repository.ts +195 -0
  290. package/extensionmanager/update.ts +118 -0
  291. package/extensionmanager/upload-form.ts +42 -0
  292. package/filelist/browse-files.ts +148 -0
  293. package/filelist/browse-folders.ts +94 -0
  294. package/filelist/context-menu-actions.ts +335 -0
  295. package/filelist/create-folder.ts +69 -0
  296. package/filelist/file-delete.ts +69 -0
  297. package/filelist/file-list-actions.ts +132 -0
  298. package/filelist/file-list-dragdrop.ts +139 -0
  299. package/filelist/file-list-rename-handler.ts +128 -0
  300. package/filelist/file-list-transfer-handler.ts +138 -0
  301. package/filelist/file-list.ts +396 -0
  302. package/filelist/file-replace.ts +37 -0
  303. package/filelist/linkbrowser-file-handler.ts +94 -0
  304. package/filelist/linkbrowser-folder-handler.ts +74 -0
  305. package/filelist/rename-file.ts +109 -0
  306. package/form/backend/form-editor/component/property-grid-editor.ts +340 -0
  307. package/form/backend/form-editor/core.ts +2104 -0
  308. package/form/backend/form-editor/helper.ts +226 -0
  309. package/form/backend/form-editor/inspector-component.ts +2323 -0
  310. package/form/backend/form-editor/mediator.ts +941 -0
  311. package/form/backend/form-editor/modals-component.ts +433 -0
  312. package/form/backend/form-editor/stage-component.ts +908 -0
  313. package/form/backend/form-editor/tree-component.ts +548 -0
  314. package/form/backend/form-editor/view-model.ts +1343 -0
  315. package/form/backend/form-editor.ts +846 -0
  316. package/form/backend/form-manager/main.ts +39 -0
  317. package/form/backend/form-manager/view-model.ts +789 -0
  318. package/form/backend/form-manager.ts +180 -0
  319. package/form/backend/helper.ts +60 -0
  320. package/form/frontend/date-picker.ts +38 -0
  321. package/frontend/default_frontend.ts +82 -0
  322. package/impexp/context-menu-actions.ts +50 -0
  323. package/impexp/import-export.ts +62 -0
  324. package/indexed-search/pagination.ts +26 -0
  325. package/indexed-search/statistics.ts +50 -0
  326. package/info/translation-status.ts +48 -0
  327. package/install/ajax/ajax-queue.ts +83 -0
  328. package/install/init-install.ts +2 -0
  329. package/install/init-installer.ts +2 -0
  330. package/install/install.ts +25 -0
  331. package/install/installer.ts +458 -0
  332. package/install/message-interface.ts +18 -0
  333. package/install/module/abstract-inline-module.ts +27 -0
  334. package/install/module/abstract-interactable-module.ts +107 -0
  335. package/install/module/environment/environment-check.ts +92 -0
  336. package/install/module/environment/folder-structure.ts +145 -0
  337. package/install/module/environment/image-processing.ts +141 -0
  338. package/install/module/environment/mail-test.ts +127 -0
  339. package/install/module/environment/php-info.ts +50 -0
  340. package/install/module/environment/system-information.ts +50 -0
  341. package/install/module/maintenance/cache.ts +59 -0
  342. package/install/module/maintenance/clear-tables.ts +134 -0
  343. package/install/module/maintenance/clear-typo3temp-files.ts +137 -0
  344. package/install/module/maintenance/create-admin.ts +126 -0
  345. package/install/module/maintenance/database-analyzer.ts +266 -0
  346. package/install/module/maintenance/dump-autoload.ts +58 -0
  347. package/install/module/maintenance/language-packs.ts +338 -0
  348. package/install/module/maintenance/reset-backend-user-uc.ts +58 -0
  349. package/install/module/password-strength.ts +56 -0
  350. package/install/module/settings/change-install-tool-password.ts +101 -0
  351. package/install/module/settings/extension-configuration.ts +224 -0
  352. package/install/module/settings/features.ts +102 -0
  353. package/install/module/settings/local-configuration.ts +316 -0
  354. package/install/module/settings/presets.ts +147 -0
  355. package/install/module/settings/system-maintainer.ts +111 -0
  356. package/install/module/upgrade/core-update.ts +255 -0
  357. package/install/module/upgrade/extension-compat-tester.ts +195 -0
  358. package/install/module/upgrade/extension-scanner.ts +381 -0
  359. package/install/module/upgrade/tca-ext-tables-check.ts +93 -0
  360. package/install/module/upgrade/tca-migrations-check.ts +101 -0
  361. package/install/module/upgrade/upgrade-docs.ts +310 -0
  362. package/install/module/upgrade/upgrade-wizards.ts +612 -0
  363. package/install/renderable/clearable.ts +113 -0
  364. package/install/renderable/flash-message.ts +64 -0
  365. package/install/renderable/info-box.ts +66 -0
  366. package/install/renderable/language-packs.ts +304 -0
  367. package/install/renderable/offset-group.ts +38 -0
  368. package/install/renderable/severity.ts +49 -0
  369. package/install/renderable/wrap-group.ts +36 -0
  370. package/install/router.ts +468 -0
  371. package/linkvalidator/linkvalidator-modal.ts +45 -0
  372. package/linkvalidator/linkvalidator.ts +101 -0
  373. package/lowlevel/query-generator.ts +132 -0
  374. package/lowlevel/reference-index.ts +44 -0
  375. package/opendocs/toolbar/opendocs-menu.ts +117 -0
  376. package/package.json +34 -0
  377. package/recycler/recycler.ts +539 -0
  378. package/redirects/event-handler.ts +108 -0
  379. package/redirects/form-engine-evaluation.ts +34 -0
  380. package/redirects/redirects-module.ts +58 -0
  381. package/rte-ckeditor/ckeditor5.ts +402 -0
  382. package/rte-ckeditor/css-prefixer.ts +94 -0
  383. package/rte-ckeditor/legacy/ckeditor5-bundle.ts +17 -0
  384. package/rte-ckeditor/legacy/ckeditor5-inspector.ts +2 -0
  385. package/rte-ckeditor/plugin/typo3-link.ts +810 -0
  386. package/rte-ckeditor/plugin/whitespace.ts +114 -0
  387. package/rte-ckeditor/rte-link-browser.ts +112 -0
  388. package/rte-ckeditor/tests/css-prefixer-test.ts +314 -0
  389. package/scheduler/scheduler-add-group.ts +119 -0
  390. package/scheduler/scheduler-editable-group-name.ts +307 -0
  391. package/scheduler/scheduler-sortable-groups.ts +59 -0
  392. package/scheduler/scheduler.ts +466 -0
  393. package/setup/setup-module.ts +122 -0
  394. package/styleguide/checkbox-demo.ts +5 -0
  395. package/styleguide/element/theme-switcher-element.ts +101 -0
  396. package/styleguide/filter.ts +22 -0
  397. package/styleguide/find-icons.ts +78 -0
  398. package/styleguide/processing-indicator.ts +78 -0
  399. package/styleguide/render-notifications.ts +64 -0
  400. package/sys-note/element/delete-button.ts +78 -0
  401. package/tstemplate/constant-editor.ts +64 -0
  402. package/tstemplate/information-module.ts +31 -0
  403. package/tstemplate/template-analyzer.ts +77 -0
  404. package/viewpage/main.ts +253 -0
  405. package/workspaces/backend.ts +979 -0
  406. package/workspaces/preview.ts +232 -0
  407. package/workspaces/renderable/comment-view.ts +79 -0
  408. package/workspaces/renderable/diff-view.ts +58 -0
  409. package/workspaces/renderable/history-view.ts +97 -0
  410. package/workspaces/renderable/record-information.ts +118 -0
  411. package/workspaces/renderable/record-table.ts +355 -0
  412. package/workspaces/renderable/send-to-stage-form.ts +108 -0
  413. package/workspaces/toolbar/workspaces-menu.ts +190 -0
  414. package/workspaces/utility/icon-helper.ts +57 -0
  415. 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
+ }