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