typo3-typescript-sources 14.1.0 → 14.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.last-synced-tag +1 -1
- package/README.md +1 -1
- package/backend/Contrib/bootstrap.ts +39 -0
- package/backend/action-button/deferred-action.ts +0 -2
- package/backend/action-button/immediate-action.ts +0 -2
- package/backend/action-dispatcher.ts +0 -2
- package/backend/bookmark/bookmark-manager.ts +1234 -0
- package/backend/bookmark/bookmark-store.ts +701 -0
- package/backend/bookmark/element/bookmark-button-element.ts +144 -0
- package/backend/bookmark/toolbar/bookmark-menu-element.ts +139 -0
- package/backend/clear-cache.ts +12 -4
- package/backend/clipboard-panel.ts +4 -4
- package/backend/code-editor/element/code-mirror-element.ts +6 -1
- package/backend/color-picker.ts +7 -5
- package/backend/color-scheme-switch.ts +1 -1
- package/backend/column-selector-button.ts +4 -4
- package/backend/context-menu-actions.ts +63 -9
- package/backend/context-menu.ts +3 -3
- package/backend/contextual-record-edit.ts +212 -0
- package/backend/copy-to-clipboard.ts +32 -13
- package/backend/date-time-picker.ts +7 -5
- package/backend/drag-tooltip.ts +1 -1
- package/backend/drag-uploader.ts +58 -43
- package/backend/dropdown.ts +317 -0
- package/backend/element/alert-element.ts +5 -5
- package/backend/element/breadcrumb.ts +162 -38
- package/backend/element/combobox-element.ts +3 -2
- package/backend/element/contextual-record-edit-trigger.ts +117 -0
- package/backend/element/datetime-element.ts +282 -0
- package/backend/element/dispatch-modal-button.ts +1 -1
- package/backend/element/draggable-resizable-element.ts +1 -1
- package/backend/element/editable-page-title.ts +8 -6
- package/backend/element/icon-element.ts +2 -2
- package/backend/element/link-browser-download-element.ts +86 -0
- package/backend/element/pagination.ts +4 -4
- package/backend/element/progress-bar-element.ts +6 -2
- package/backend/element/progress-tracker-element.ts +4 -4
- package/backend/element/pseudo-button.ts +1 -1
- package/backend/element/qrcode-element.ts +54 -7
- package/{redirects → backend}/element/qrcode-modal-button.ts +20 -14
- package/backend/element/sidebar-toggle-element.ts +1 -1
- package/backend/element/spinner-element.ts +1 -1
- package/backend/element-browser.ts +62 -6
- package/backend/event/form-engine-link-browser-set-link-event.ts +34 -0
- package/backend/form/submit-interceptor.ts +7 -5
- package/backend/form-engine/container/flex-form-container-container.ts +42 -32
- package/backend/form-engine/container/flex-form-section-container.ts +8 -28
- package/backend/form-engine/container/inline-control-container.ts +304 -293
- package/backend/form-engine/element/extra/char-counter.ts +3 -3
- package/backend/form-engine/element/online-media-form-element.ts +2 -2
- package/backend/form-engine/element/select-tree-toolbar.ts +6 -6
- package/backend/form-engine/element/select-tree.ts +1 -1
- package/backend/form-engine/element/suggest/result-container.ts +3 -3
- package/backend/form-engine/element/suggest/result-item.ts +1 -1
- package/backend/form-engine/element/table-wizard-element.ts +23 -22
- package/backend/form-engine/field-control/link-popup.ts +22 -3
- package/backend/form-engine/field-control/password-generator.ts +9 -3
- package/backend/form-engine-link-browser-adapter.ts +15 -86
- package/backend/form-engine-review.ts +9 -4
- package/backend/form-engine-validation.ts +13 -6
- package/backend/form-engine.ts +170 -113
- package/backend/grid-editor.ts +32 -29
- package/backend/hotkeys/events.ts +47 -0
- package/backend/hotkeys/hotkey-storage.ts +3 -18
- package/backend/hotkeys/hotkey-struct.ts +58 -0
- package/backend/hotkeys/negotiator.ts +53 -0
- package/backend/hotkeys.ts +27 -46
- package/backend/image-manipulation.ts +82 -2
- package/backend/input/clearable.ts +4 -3
- package/backend/layout-module/paste.ts +7 -6
- package/backend/link-browser.ts +1 -0
- package/backend/live-search/element/backend-search.ts +1 -1
- package/backend/live-search/element/hint.ts +1 -1
- package/backend/live-search/element/provider/default-result-item.ts +3 -3
- package/backend/live-search/element/provider/page-provider-result-item.ts +3 -3
- package/backend/live-search/element/result/item/action/action-container.ts +1 -1
- package/backend/live-search/element/result/item/action/action.ts +2 -2
- package/backend/live-search/element/result/item/item-container.ts +3 -3
- package/backend/live-search/element/result/item/item.ts +2 -1
- package/backend/live-search/element/result/result-container.ts +5 -5
- package/backend/live-search/element/result/result-detail-container.ts +1 -1
- package/backend/live-search/element/result/result-pagination.ts +2 -2
- package/backend/live-search/element/search-option-item.ts +2 -2
- package/backend/live-search/live-search-shortcut.ts +1 -14
- package/backend/{toolbar → live-search}/live-search.ts +10 -22
- package/backend/live-search/result-types/default-result-type.ts +6 -1
- package/backend/live-search/result-types/page-result-type.ts +2 -2
- package/backend/live-search/toolbar/live-search-toolbar-item.ts +24 -0
- package/backend/localization/finisher/localization-submission-service.ts +33 -0
- package/backend/localization/label-provider.ts +187 -0
- package/backend/localization/localization-button.ts +3 -3
- package/backend/localization/localization-wizard.ts +70 -351
- package/backend/localization/steps/content-record-selection-step.ts +35 -42
- package/backend/localization/steps/handler-selection-step.ts +27 -31
- package/backend/localization/steps/mode-step.ts +26 -30
- package/backend/localization/steps/source-language-step.ts +28 -30
- package/backend/localization/steps/target-language-step.ts +34 -35
- package/backend/login-refresh.ts +15 -14
- package/backend/modal.ts +81 -18
- package/backend/module/iframe.ts +5 -5
- package/backend/module/router.ts +1 -1
- package/backend/multi-record-selection-delete-action.ts +4 -2
- package/backend/multi-step-wizard.ts +7 -6
- package/backend/new-content-element-wizard-button.ts +1 -1
- package/backend/new-record-wizard.ts +14 -17
- package/backend/notification.ts +5 -5
- package/backend/online-media.ts +2 -1
- package/backend/page-wizard/finisher/page-wizard-submission-service.ts +42 -0
- package/backend/page-wizard/helper/wizard-helper.ts +34 -0
- package/backend/page-wizard/new-page-wizard-button.ts +32 -0
- package/backend/page-wizard/page-wizard-configuration.ts +20 -0
- package/backend/page-wizard/page-wizard.ts +114 -0
- package/backend/page-wizard/steps/doktype-step.ts +239 -0
- package/backend/page-wizard/steps/form-engine-step.ts +165 -0
- package/backend/page-wizard/steps/position-step.ts +158 -0
- package/backend/pagetsconfig/pagetsconfig-includes.ts +1 -1
- package/backend/record-download-button.ts +4 -4
- package/backend/security/element/csp-reports.ts +30 -30
- package/backend/security/element/sudo-mode.ts +1 -1
- package/backend/settings/editor/editable-setting.ts +7 -6
- package/backend/settings/editor.ts +9 -8
- package/backend/settings/type/base.ts +1 -1
- package/backend/settings/type/bool.ts +1 -1
- package/backend/settings/type/color.ts +1 -1
- package/backend/settings/type/int.ts +2 -2
- package/backend/settings/type/number.ts +2 -2
- package/backend/settings/type/page.ts +9 -5
- package/backend/settings/type/string.ts +2 -2
- package/backend/settings/type/stringlist.ts +1 -1
- package/backend/settings/type/url.ts +1 -1
- package/backend/sortable-table.ts +3 -2
- package/backend/switch-user.ts +1 -1
- package/backend/tab.ts +424 -0
- package/backend/tabs.ts +2 -59
- package/backend/tests/localization/label-provider-test.ts +72 -0
- package/backend/toolbar/clear-cache-menu.ts +12 -9
- package/backend/toolbar/system-information-menu.ts +6 -16
- package/backend/tree/file-storage-browser.ts +1 -1
- package/backend/tree/file-storage-tree-container.ts +14 -11
- package/backend/tree/page-browser.ts +13 -12
- package/backend/tree/page-position-select.ts +293 -0
- package/backend/tree/page-tree-element.ts +233 -83
- package/backend/tree/page-tree.ts +20 -0
- package/backend/tree/tree-module-state.ts +1 -1
- package/backend/tree/tree-node-toggle.ts +1 -1
- package/backend/tree/tree-toolbar.ts +43 -38
- package/backend/tree/tree.ts +69 -23
- package/backend/type/date-configuration.ts +31 -0
- package/backend/user-settings-manager.ts +34 -18
- package/backend/utility/collapse-state-search.ts +2 -1
- package/backend/viewport/content-container.ts +1 -1
- package/backend/viewport/content-navigation-toggle.ts +14 -4
- package/backend/viewport/content-navigation.ts +6 -6
- package/backend/viewport/loader.ts +6 -4
- package/backend/wizard/events/auto-advance-event.ts +21 -0
- package/backend/wizard/events/before-next-step-event.ts +24 -0
- package/backend/wizard/events/step-summary-event.ts +26 -0
- package/backend/wizard/finisher/finisher-config.ts +19 -0
- package/backend/{localization/localization-finisher.ts → wizard/finisher/finisher-interface.ts} +2 -11
- package/backend/wizard/finisher/finisher-result.ts +26 -0
- package/backend/{localization → wizard}/finisher/noop-finisher.ts +3 -2
- package/backend/{localization → wizard}/finisher/redirect-finisher.ts +3 -2
- package/backend/{localization → wizard}/finisher/reload-finisher.ts +4 -3
- package/backend/wizard/finisher/submission-service-interface.ts +17 -0
- package/backend/wizard/helper/dynamic-steps-loader.ts +60 -0
- package/backend/wizard/move-content-element.ts +14 -8
- package/backend/wizard/move-page.ts +7 -5
- package/backend/wizard/steps/confirm-step.ts +63 -0
- package/backend/{localization → wizard}/steps/finisher-step.ts +37 -61
- package/backend/wizard/steps/summary-item-interface.ts +19 -0
- package/backend/wizard/steps/wizard-step-after-render-interface.ts +16 -0
- package/backend/wizard/steps/wizard-step-interface.ts +26 -0
- package/backend/wizard/steps/wizard-step-summary-interface.ts +18 -0
- package/backend/wizard/steps/wizard-step-value-interface.ts +20 -0
- package/backend/wizard/wizard.ts +399 -0
- package/belog/backend-log.ts +8 -2
- package/core/authentication/mfa-provider/totp.ts +1 -1
- package/core/directive/markdown.ts +2 -2
- package/core/lit-helper.ts +2 -2
- package/core/utility/debug-exception-handler-service.ts +188 -0
- package/dashboard/dashboard.ts +51 -49
- package/dashboard/widget/bookmarks-widget-element.ts +140 -0
- package/extensionmanager/dependency-check.ts +179 -0
- package/extensionmanager/distribution-image.ts +1 -1
- package/extensionmanager/extension-toggle-button.ts +201 -0
- package/extensionmanager/main.ts +55 -69
- package/extensionmanager/repository.ts +22 -16
- package/extensionmanager/update.ts +4 -3
- package/filelist/context-menu-actions.ts +14 -13
- package/filelist/file-delete.ts +3 -2
- package/filelist/file-list-rename-handler.ts +6 -5
- package/filelist/file-list-replace-handler.ts +17 -14
- package/filelist/file-list-transfer-handler.ts +8 -11
- package/filelist/file-list.ts +38 -12
- package/filelist/rename-file.ts +9 -6
- package/form/backend/form-editor/component/date-editor.ts +265 -0
- package/form/backend/form-editor/component/form-element-selector.ts +74 -0
- package/form/backend/form-editor/component/form-element-stage-item-toolbar.ts +112 -0
- package/form/backend/form-editor/component/form-element-stage-item.ts +188 -0
- package/form/backend/form-editor/component/page-stage-item.ts +53 -0
- package/form/backend/form-editor/component/property-grid-editor.ts +54 -23
- package/form/backend/form-editor/core.ts +119 -105
- package/form/backend/form-editor/helper.ts +17 -10
- package/form/backend/form-editor/inspector-component.ts +689 -703
- package/form/backend/form-editor/mediator.ts +54 -43
- package/form/backend/form-editor/modals-component.ts +59 -61
- package/form/backend/form-editor/stage-component.ts +635 -328
- package/form/backend/form-editor/tree-component-adapter.ts +651 -0
- package/form/backend/form-editor/utility/string-utility.ts +33 -0
- package/form/backend/form-editor/view-model.ts +240 -229
- package/form/backend/form-editor-tree-container.ts +105 -0
- package/form/backend/form-editor-tree-events.ts +138 -0
- package/form/backend/form-editor-tree.ts +784 -0
- package/form/backend/form-editor.ts +26 -42
- package/form/backend/form-manager/view-model.ts +155 -674
- package/form/backend/form-manager.ts +26 -9
- package/form/backend/form-wizard/finisher/create-form-submission-service.ts +59 -0
- package/form/backend/form-wizard/finisher/duplicate-form-submission-service.ts +58 -0
- package/form/backend/form-wizard/form-wizard.ts +135 -0
- package/form/backend/form-wizard/steps/mode-step.ts +140 -0
- package/form/backend/form-wizard/steps/settings-step.ts +248 -0
- package/form/backend/form-wizard/steps/storage-step.ts +128 -0
- package/form/frontend/date-picker.ts +2 -0
- package/install/module/settings/extension-configuration.ts +0 -8
- package/install/module/settings/local-configuration.ts +0 -8
- package/install/renderable/flash-message.ts +1 -1
- package/install/renderable/info-box.ts +1 -1
- package/install/renderable/language-packs.ts +3 -3
- package/install/renderable/offset-group.ts +1 -1
- package/install/renderable/wrap-group.ts +1 -1
- package/install/router.ts +1 -1
- package/lowlevel/query-generator.ts +1 -1
- package/opendocs/open-document-store.ts +195 -0
- package/opendocs/toolbar/open-document-menu-element.ts +127 -0
- package/opendocs/widget/recent-documents-widget-element.ts +130 -0
- package/package.json +2 -2
- package/recycler/recycler.ts +55 -56
- package/redirects/event-handler.ts +15 -7
- package/redirects/short-url-generator.ts +145 -0
- package/rte-ckeditor/ckeditor5.ts +3 -3
- package/rte-ckeditor/plugin/typo3-link.ts +24 -4
- package/scheduler/new-scheduler-task-wizard-button.ts +1 -1
- package/scheduler/scheduler-add-group.ts +6 -6
- package/scheduler/setup-check-button.ts +1 -1
- package/styleguide/element/theme-switcher-element.ts +10 -9
- package/sys-note/element/delete-button.ts +1 -1
- package/tstemplate/template-analyzer.ts +1 -1
- package/workspaces/backend.ts +27 -25
- package/workspaces/element/workspace-selector-element.ts +186 -0
- package/workspaces/element/workspace-top-indicator-element.ts +159 -0
- package/workspaces/preview.ts +5 -4
- package/workspaces/renderable/comment-view.ts +3 -3
- package/workspaces/renderable/diff-view.ts +3 -3
- package/workspaces/renderable/history-view.ts +3 -3
- package/workspaces/renderable/record-information.ts +8 -12
- package/workspaces/renderable/record-table.ts +56 -32
- package/workspaces/renderable/send-to-stage-form.ts +14 -8
- package/workspaces/workspace-state.ts +251 -0
- package/workspaces/workspaces.ts +4 -5
- package/backend/form-engine/container/files-control-container.ts +0 -716
- package/backend/form-engine/container/site-language-container.ts +0 -616
- package/backend/key-bindings.ts +0 -42
- package/backend/localization/steps/confirm-step.ts +0 -61
- package/backend/toolbar/shortcut-menu.ts +0 -254
- package/form/backend/form-editor/tree-component.ts +0 -550
- package/opendocs/toolbar/opendocs-menu.ts +0 -117
- package/workspaces/toolbar/workspaces-menu.ts +0 -192
|
@@ -0,0 +1,1234 @@
|
|
|
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, nothing } from 'lit';
|
|
15
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
16
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
17
|
+
import { repeat } from 'lit/directives/repeat.js';
|
|
18
|
+
import BookmarkStore, { BookmarkStoreChangedEvent, BookmarkGroupType } from './bookmark-store';
|
|
19
|
+
import type { Bookmark, BookmarkGroup, BookmarkGroupId } from './bookmark-store';
|
|
20
|
+
import Modal from '../modal';
|
|
21
|
+
import Notification from '../notification';
|
|
22
|
+
import { SeverityEnum } from '../enum/severity';
|
|
23
|
+
import { PseudoButtonLitElement } from '@typo3/backend/element/pseudo-button';
|
|
24
|
+
import '@typo3/backend/element/spinner-element';
|
|
25
|
+
import '@typo3/backend/element/icon-element';
|
|
26
|
+
import labels from '~labels/core.bookmarks';
|
|
27
|
+
|
|
28
|
+
interface GroupSection {
|
|
29
|
+
label: string;
|
|
30
|
+
groups: BookmarkGroup[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface DragPayload {
|
|
34
|
+
type: 'bookmark' | 'group';
|
|
35
|
+
id: number | string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ViewStateInterface {
|
|
39
|
+
readonly view: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class BookmarkListViewState implements ViewStateInterface {
|
|
43
|
+
readonly view = 'list' as const;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class BookmarkEditViewState implements ViewStateInterface {
|
|
47
|
+
readonly view = 'editBookmark' as const;
|
|
48
|
+
constructor(public readonly bookmark: Bookmark) {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
class GroupCreateViewState implements ViewStateInterface {
|
|
52
|
+
readonly view = 'createGroup' as const;
|
|
53
|
+
readonly draft = { label: '' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class GroupEditViewState implements ViewStateInterface {
|
|
57
|
+
readonly view = 'editGroup' as const;
|
|
58
|
+
constructor(public readonly group: BookmarkGroup) {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class GroupListViewState implements ViewStateInterface {
|
|
62
|
+
readonly view = 'manageGroups' as const;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
type ViewContent = {
|
|
67
|
+
toolbar: TemplateResult;
|
|
68
|
+
content: TemplateResult;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Bookmark management UI rendered inside a modal. Access via openBookmarkManager().
|
|
73
|
+
*/
|
|
74
|
+
@customElement('typo3-backend-bookmark-manager-content')
|
|
75
|
+
export class BookmarkManagerContentElement extends LitElement {
|
|
76
|
+
@property({ type: Number }) editId: number | null = null;
|
|
77
|
+
|
|
78
|
+
@state() private bookmarks: Bookmark[] = [];
|
|
79
|
+
@state() private groups: BookmarkGroup[] = [];
|
|
80
|
+
@state() private selectedIds: Set<number> = new Set();
|
|
81
|
+
@state() private draggedItem: Bookmark | BookmarkGroup | null = null;
|
|
82
|
+
@state() private dropTarget: { item: Bookmark | BookmarkGroup; position: 'before' | 'after' } | null = null;
|
|
83
|
+
@state() private groupedBookmarks: Map<BookmarkGroupId, Bookmark[]> = new Map();
|
|
84
|
+
@state() private viewState: ViewStateInterface = new BookmarkListViewState();
|
|
85
|
+
|
|
86
|
+
public override connectedCallback(): void {
|
|
87
|
+
super.connectedCallback();
|
|
88
|
+
document.addEventListener(BookmarkStoreChangedEvent, this.handleStoreUpdate);
|
|
89
|
+
this.initFromStore();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public override disconnectedCallback(): void {
|
|
93
|
+
super.disconnectedCallback();
|
|
94
|
+
document.removeEventListener(BookmarkStoreChangedEvent, this.handleStoreUpdate);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected override createRenderRoot(): HTMLElement | ShadowRoot {
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected override render(): TemplateResult {
|
|
102
|
+
let viewContent: ViewContent;
|
|
103
|
+
if (this.viewState instanceof BookmarkEditViewState) {
|
|
104
|
+
viewContent = this.renderBookmarkEditView(this.viewState);
|
|
105
|
+
} else if (this.viewState instanceof GroupListViewState) {
|
|
106
|
+
viewContent = this.renderGroupListView();
|
|
107
|
+
} else if (this.viewState instanceof GroupCreateViewState) {
|
|
108
|
+
viewContent = this.renderGroupCreateView(this.viewState);
|
|
109
|
+
} else if (this.viewState instanceof GroupEditViewState) {
|
|
110
|
+
viewContent = this.renderGroupEditView(this.viewState);
|
|
111
|
+
} else {
|
|
112
|
+
viewContent = this.renderBookmarkListView();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return html`
|
|
116
|
+
<div class="bookmark-manager">
|
|
117
|
+
<div class="bookmark-manager-toolbar">${viewContent.toolbar}</div>
|
|
118
|
+
<div class="bookmark-manager-content">${viewContent.content}</div>
|
|
119
|
+
</div>
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private async initFromStore(): Promise<void> {
|
|
124
|
+
await this.syncFromStore();
|
|
125
|
+
|
|
126
|
+
if (this.editId !== null) {
|
|
127
|
+
const bookmark = this.bookmarks.find(bookmark => bookmark.id === this.editId);
|
|
128
|
+
if (bookmark) {
|
|
129
|
+
this.navigateToBookmarkEditView(bookmark);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Navigation
|
|
135
|
+
private navigateToBookmarkListView(): void {
|
|
136
|
+
this.viewState = new BookmarkListViewState();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private navigateToBookmarkEditView(bookmark: Bookmark): void {
|
|
140
|
+
this.viewState = new BookmarkEditViewState(bookmark);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private navigateToGroupListView(): void {
|
|
144
|
+
this.viewState = new GroupListViewState();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private navigateToGroupCreateView(): void {
|
|
148
|
+
this.viewState = new GroupCreateViewState();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private navigateToGroupEditView(group: BookmarkGroup): void {
|
|
152
|
+
this.viewState = new GroupEditViewState(group);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// View Rendering
|
|
156
|
+
private renderBookmarkListView(): ViewContent {
|
|
157
|
+
const groupedBookmarks = this.groupedBookmarks;
|
|
158
|
+
const hasSelection = this.selectedIds.size > 0;
|
|
159
|
+
const hasBookmarks = this.bookmarks.length > 0;
|
|
160
|
+
const allSelected = this.selectedIds.size === this.bookmarks.length && this.bookmarks.length > 0;
|
|
161
|
+
const groupSections = this.helperGetGroupSections(this.getSelectableGroups());
|
|
162
|
+
|
|
163
|
+
const toolbar = html`
|
|
164
|
+
<div class="bookmark-manager-toolbar-start">
|
|
165
|
+
<div class="form-slim">
|
|
166
|
+
<div class="form-group">
|
|
167
|
+
<div class="form-check form-check-type-toggle mb-0">
|
|
168
|
+
<input
|
|
169
|
+
type="checkbox"
|
|
170
|
+
class="form-check-input"
|
|
171
|
+
id="selectAll"
|
|
172
|
+
.checked=${allSelected}
|
|
173
|
+
@change=${(e: Event) => { this.selectedIds = (e.target as HTMLInputElement).checked ? new Set(this.bookmarks.map(bookmark => bookmark.id)) : new Set(); }}
|
|
174
|
+
/>
|
|
175
|
+
<label class="form-check-label" for="selectAll">
|
|
176
|
+
${allSelected ? labels.get('manager.deselectAll') : labels.get('manager.selectAll')}
|
|
177
|
+
</label>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
${hasSelection ? html`
|
|
181
|
+
<div class="form-group dropdown">
|
|
182
|
+
<button
|
|
183
|
+
class="btn btn-sm btn-default dropdown-toggle"
|
|
184
|
+
type="button"
|
|
185
|
+
data-bs-toggle="dropdown"
|
|
186
|
+
aria-expanded="false"
|
|
187
|
+
>
|
|
188
|
+
<typo3-backend-icon identifier="actions-bookmarks" size="small"></typo3-backend-icon>
|
|
189
|
+
${labels.get('manager.moveToGroup')}
|
|
190
|
+
</button>
|
|
191
|
+
<ul class="dropdown-menu">
|
|
192
|
+
${groupSections.map((section, index) => html`
|
|
193
|
+
${index > 0 ? html`<li><hr class="dropdown-divider"></li>` : nothing}
|
|
194
|
+
<li><div class="dropdown-header">${section.label}</div></li>
|
|
195
|
+
${section.groups.map(group => html`
|
|
196
|
+
<li>
|
|
197
|
+
<button class="dropdown-item" type="button" @click=${() => this.handleBookmarkBulkMove(group.id)}>
|
|
198
|
+
${group.label}
|
|
199
|
+
</button>
|
|
200
|
+
</li>
|
|
201
|
+
`)}
|
|
202
|
+
`)}
|
|
203
|
+
</ul>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="form-group">
|
|
206
|
+
<button
|
|
207
|
+
class="btn btn-sm btn-default"
|
|
208
|
+
type="button"
|
|
209
|
+
@click=${this.handleBookmarkBulkDelete}
|
|
210
|
+
>
|
|
211
|
+
<typo3-backend-icon identifier="actions-delete" size="small"></typo3-backend-icon>
|
|
212
|
+
${labels.get('manager.deleteSelected')}
|
|
213
|
+
</button>
|
|
214
|
+
</div>
|
|
215
|
+
` : nothing}
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="bookmark-manager-toolbar-end">
|
|
219
|
+
<button
|
|
220
|
+
class="btn btn-sm btn-default"
|
|
221
|
+
type="button"
|
|
222
|
+
@click=${this.navigateToGroupListView}
|
|
223
|
+
>
|
|
224
|
+
<typo3-backend-icon identifier="actions-cog" size="small"></typo3-backend-icon>
|
|
225
|
+
${labels.get('manager.manageGroups')}
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
/* eslint-disable @stylistic/indent */
|
|
231
|
+
const content = html`
|
|
232
|
+
${!hasBookmarks ? html`
|
|
233
|
+
<div class="alert alert-info">
|
|
234
|
+
<typo3-backend-icon identifier="actions-info-circle" size="small"></typo3-backend-icon>
|
|
235
|
+
<span class="ms-2">${labels.get('empty')}</span>
|
|
236
|
+
</div>
|
|
237
|
+
` : nothing}
|
|
238
|
+
${repeat(Array.from(groupedBookmarks.entries()), ([groupId]) => groupId, ([groupId, bookmarks]) => {
|
|
239
|
+
const group = this.groups.find(group => group.id === groupId);
|
|
240
|
+
const groupLabel = group?.label || labels.get('notGrouped');
|
|
241
|
+
const collapseId = `bookmark-group-${typeof groupId === 'number' ? groupId : groupId.replace(/-/g, '')}`;
|
|
242
|
+
return html`
|
|
243
|
+
<div
|
|
244
|
+
class="panel panel-default"
|
|
245
|
+
data-group-id=${groupId}
|
|
246
|
+
@dragover=${this.handleDragOver}
|
|
247
|
+
@drop=${(e: DragEvent) => this.handleDrop(e, groupId)}
|
|
248
|
+
>
|
|
249
|
+
<div class="panel-heading" role="tab">
|
|
250
|
+
<div class="panel-heading-row">
|
|
251
|
+
<button
|
|
252
|
+
class="panel-button"
|
|
253
|
+
type="button"
|
|
254
|
+
data-bs-toggle="collapse"
|
|
255
|
+
data-bs-target="#${collapseId}"
|
|
256
|
+
aria-expanded="true"
|
|
257
|
+
aria-controls=${collapseId}
|
|
258
|
+
>
|
|
259
|
+
<div class="panel-title">${groupLabel}</div>
|
|
260
|
+
<span class="caret"></span>
|
|
261
|
+
</button>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
<div class="panel-collapse collapse show" id=${collapseId} role="tabpanel">
|
|
265
|
+
<div class="table-fit">
|
|
266
|
+
<table class="table table-striped table-hover mb-0">
|
|
267
|
+
<tbody>
|
|
268
|
+
${repeat(bookmarks, (bookmark) => bookmark.id, (bookmark) => this.renderBookmarkRow(bookmark))}
|
|
269
|
+
</tbody>
|
|
270
|
+
</table>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
`;
|
|
275
|
+
})}
|
|
276
|
+
`;
|
|
277
|
+
return { toolbar, content };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private renderBookmarkRow(bookmark: Bookmark): TemplateResult {
|
|
281
|
+
const isSelected = this.selectedIds.has(bookmark.id);
|
|
282
|
+
const isDragged = this.draggedItem?.id === bookmark.id;
|
|
283
|
+
const isDropTarget = this.dropTarget?.item.id === bookmark.id;
|
|
284
|
+
const dropPosition = this.dropTarget?.position;
|
|
285
|
+
const rowClasses = classMap({
|
|
286
|
+
'table-active': isSelected,
|
|
287
|
+
'opacity-50': isDragged,
|
|
288
|
+
'row-drop-before': isDropTarget && dropPosition === 'before',
|
|
289
|
+
'row-drop-after': isDropTarget && dropPosition === 'after',
|
|
290
|
+
});
|
|
291
|
+
const toggleSelection = () => {
|
|
292
|
+
const set = new Set(this.selectedIds);
|
|
293
|
+
if (set.has(bookmark.id)) {
|
|
294
|
+
set.delete(bookmark.id);
|
|
295
|
+
} else {
|
|
296
|
+
set.add(bookmark.id);
|
|
297
|
+
}
|
|
298
|
+
this.selectedIds = set;
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const groupBookmarks = this.groupedBookmarks.get(bookmark.groupId) ?? [];
|
|
302
|
+
const bookmarkIndex = groupBookmarks.findIndex(item => item.id === bookmark.id);
|
|
303
|
+
const isFirst = bookmarkIndex === 0;
|
|
304
|
+
const isLast = bookmarkIndex === groupBookmarks.length - 1;
|
|
305
|
+
|
|
306
|
+
return html`
|
|
307
|
+
<tr
|
|
308
|
+
class=${rowClasses}
|
|
309
|
+
data-bookmark-id=${bookmark.id}
|
|
310
|
+
draggable=${bookmark.editable ? 'true' : 'false'}
|
|
311
|
+
@dragstart=${(e: DragEvent) => this.handleDragStart(e, 'bookmark', bookmark)}
|
|
312
|
+
@dragover=${(e: DragEvent) => this.handleItemDragOver(e, bookmark)}
|
|
313
|
+
@dragleave=${this.handleItemDragLeave}
|
|
314
|
+
@drop=${(e: DragEvent) => this.handleDrop(e, bookmark)}
|
|
315
|
+
@dragend=${this.handleDragEnd}
|
|
316
|
+
>
|
|
317
|
+
<td class="col-checkbox">
|
|
318
|
+
${bookmark.editable ? html`
|
|
319
|
+
<span class="form-check form-check-type-toggle">
|
|
320
|
+
<input
|
|
321
|
+
type="checkbox"
|
|
322
|
+
class="form-check-input"
|
|
323
|
+
.checked=${isSelected}
|
|
324
|
+
@change=${toggleSelection}
|
|
325
|
+
@click=${(e: Event) => e.stopPropagation()}
|
|
326
|
+
/>
|
|
327
|
+
</span>
|
|
328
|
+
` : html`
|
|
329
|
+
<typo3-backend-icon identifier="actions-lock" size="small" title=${labels.get('manager.locked')}></typo3-backend-icon>
|
|
330
|
+
`}
|
|
331
|
+
</td>
|
|
332
|
+
<td class="col-icon">
|
|
333
|
+
<typo3-backend-icon identifier=${bookmark.iconIdentifier} overlay=${bookmark.iconOverlayIdentifier} size="small"></typo3-backend-icon>
|
|
334
|
+
</td>
|
|
335
|
+
<td class="col-title">
|
|
336
|
+
${bookmark.accessible ? html`
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
class="btn btn-link"
|
|
340
|
+
title=${bookmark.title}
|
|
341
|
+
@click=${() => this.handleBookmarkNavigate(bookmark)}
|
|
342
|
+
>
|
|
343
|
+
${bookmark.title}
|
|
344
|
+
</button>
|
|
345
|
+
` : html`
|
|
346
|
+
<span class="text-muted" title=${bookmark.title}>${bookmark.title}</span>
|
|
347
|
+
<typo3-backend-icon class="text-warning" identifier="actions-exclamation-triangle" size="small" title=${labels.get('manager.notAccessible')}></typo3-backend-icon>
|
|
348
|
+
`}
|
|
349
|
+
</td>
|
|
350
|
+
<td class="col-control nowrap">
|
|
351
|
+
${bookmark.editable ? html`
|
|
352
|
+
<div class="btn-group">
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
class="btn btn-default btn-sm"
|
|
356
|
+
title=${labels.get('manager.editBookmark')}
|
|
357
|
+
@click=${() => this.navigateToBookmarkEditView(bookmark)}
|
|
358
|
+
>
|
|
359
|
+
<typo3-backend-icon identifier="actions-cog" size="small"></typo3-backend-icon>
|
|
360
|
+
<span class="visually-hidden">${labels.get('manager.editBookmark')}</span>
|
|
361
|
+
</button>
|
|
362
|
+
${isFirst ? html`
|
|
363
|
+
<span class="btn btn-default btn-sm disabled">
|
|
364
|
+
<typo3-backend-icon identifier="empty-empty" size="small"></typo3-backend-icon>
|
|
365
|
+
</span>
|
|
366
|
+
` : html`
|
|
367
|
+
<button
|
|
368
|
+
type="button"
|
|
369
|
+
class="btn btn-default btn-sm"
|
|
370
|
+
data-action="move-up"
|
|
371
|
+
title=${labels.get('manager.moveUp')}
|
|
372
|
+
@click=${() => this.handleBookmarkMoveUp(bookmark)}
|
|
373
|
+
>
|
|
374
|
+
<typo3-backend-icon identifier="actions-chevron-up" size="small"></typo3-backend-icon>
|
|
375
|
+
<span class="visually-hidden">${labels.get('manager.moveUp')}</span>
|
|
376
|
+
</button>
|
|
377
|
+
`}
|
|
378
|
+
${isLast ? html`
|
|
379
|
+
<span class="btn btn-default btn-sm disabled">
|
|
380
|
+
<typo3-backend-icon identifier="empty-empty" size="small"></typo3-backend-icon>
|
|
381
|
+
</span>
|
|
382
|
+
` : html`
|
|
383
|
+
<button
|
|
384
|
+
type="button"
|
|
385
|
+
class="btn btn-default btn-sm"
|
|
386
|
+
data-action="move-down"
|
|
387
|
+
title=${labels.get('manager.moveDown')}
|
|
388
|
+
@click=${() => this.handleBookmarkMoveDown(bookmark)}
|
|
389
|
+
>
|
|
390
|
+
<typo3-backend-icon identifier="actions-chevron-down" size="small"></typo3-backend-icon>
|
|
391
|
+
<span class="visually-hidden">${labels.get('manager.moveDown')}</span>
|
|
392
|
+
</button>
|
|
393
|
+
`}
|
|
394
|
+
<span
|
|
395
|
+
class="btn btn-default btn-sm"
|
|
396
|
+
style="cursor: grab;"
|
|
397
|
+
title=${labels.get('manager.dragToReorder')}
|
|
398
|
+
>
|
|
399
|
+
<typo3-backend-icon identifier="actions-drag" size="small"></typo3-backend-icon>
|
|
400
|
+
<span class="visually-hidden">${labels.get('manager.dragToReorder')}</span>
|
|
401
|
+
</span>
|
|
402
|
+
</div>
|
|
403
|
+
` : nothing}
|
|
404
|
+
</td>
|
|
405
|
+
</tr>
|
|
406
|
+
`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private renderBookmarkEditView(state: BookmarkEditViewState): ViewContent {
|
|
410
|
+
const { bookmark } = state;
|
|
411
|
+
const toolbar = html`
|
|
412
|
+
<div class="bookmark-manager-toolbar-start">
|
|
413
|
+
<button
|
|
414
|
+
type="button"
|
|
415
|
+
class="btn btn-sm btn-default"
|
|
416
|
+
@click=${this.navigateToBookmarkListView}
|
|
417
|
+
>
|
|
418
|
+
<typo3-backend-icon identifier="actions-arrow-left" size="small"></typo3-backend-icon>
|
|
419
|
+
${labels.get('manager.back')}
|
|
420
|
+
</button>
|
|
421
|
+
</div>
|
|
422
|
+
<div class="bookmark-manager-toolbar-end">
|
|
423
|
+
<button
|
|
424
|
+
type="button"
|
|
425
|
+
class="btn btn-sm btn-danger"
|
|
426
|
+
@click=${() => this.handleBookmarkDelete(bookmark.id)}
|
|
427
|
+
>
|
|
428
|
+
<typo3-backend-icon identifier="actions-delete" size="small"></typo3-backend-icon>
|
|
429
|
+
${labels.get('action.delete')}
|
|
430
|
+
</button>
|
|
431
|
+
<button
|
|
432
|
+
type="submit"
|
|
433
|
+
form="bookmark-edit-form"
|
|
434
|
+
class="btn btn-sm btn-primary"
|
|
435
|
+
>
|
|
436
|
+
<typo3-backend-icon identifier="actions-check" size="small"></typo3-backend-icon>
|
|
437
|
+
${labels.get('action.save')}
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
`;
|
|
441
|
+
const parametersRow = this.helperParseParameters(bookmark.arguments);
|
|
442
|
+
const groupSections = this.helperGetGroupSections(this.getSelectableGroups());
|
|
443
|
+
const content = html`
|
|
444
|
+
<div class="bookmark-edit-view">
|
|
445
|
+
<form id="bookmark-edit-form" @submit=${(e: Event) => this.handleBookmarkUpdate(e, bookmark)}>
|
|
446
|
+
<div class="form-group">
|
|
447
|
+
<label class="form-label" for="bookmark-title">${labels.get('fieldTitle')}</label>
|
|
448
|
+
<input
|
|
449
|
+
type="text"
|
|
450
|
+
id="bookmark-title"
|
|
451
|
+
name="title"
|
|
452
|
+
class="form-control"
|
|
453
|
+
.value=${bookmark.title}
|
|
454
|
+
@input=${(e: Event) => { bookmark.title = (e.target as HTMLInputElement).value; this.requestUpdate(); }}
|
|
455
|
+
required
|
|
456
|
+
/>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<div class="form-group">
|
|
460
|
+
<label class="form-label" for="bookmark-group">${labels.get('fieldGroup')}</label>
|
|
461
|
+
<select
|
|
462
|
+
id="bookmark-group"
|
|
463
|
+
name="group"
|
|
464
|
+
class="form-select"
|
|
465
|
+
.value=${String(bookmark.groupId)}
|
|
466
|
+
@change=${(e: Event) => { const value = (e.target as HTMLSelectElement).value; const group = this.groups.find(group => String(group.id) === value); if (group) { bookmark.groupId = group.id; this.requestUpdate(); } }}
|
|
467
|
+
>
|
|
468
|
+
${groupSections.map(section => html`
|
|
469
|
+
<optgroup label=${section.label}>
|
|
470
|
+
${section.groups.map(group => html`
|
|
471
|
+
<option value=${group.id} ?selected=${group.id === bookmark.groupId}>
|
|
472
|
+
${group.label}
|
|
473
|
+
</option>
|
|
474
|
+
`)}
|
|
475
|
+
</optgroup>
|
|
476
|
+
`)}
|
|
477
|
+
</select>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<div class="mb-3">
|
|
481
|
+
<label class="form-label">${labels.get('details')}</label>
|
|
482
|
+
<div class="table-fit">
|
|
483
|
+
<table class="table table-striped table-sm mb-0">
|
|
484
|
+
<tbody>
|
|
485
|
+
${bookmark.href ? html`
|
|
486
|
+
<tr>
|
|
487
|
+
<th class="nowrap">${labels.get('details.url')}</th>
|
|
488
|
+
<td class="text-break">${this.helperStripToken(bookmark.href)}</td>
|
|
489
|
+
</tr>
|
|
490
|
+
` : nothing}
|
|
491
|
+
<tr>
|
|
492
|
+
<th class="nowrap">${labels.get('details.route')}</th>
|
|
493
|
+
<td>${bookmark.route}</td>
|
|
494
|
+
</tr>
|
|
495
|
+
${parametersRow !== null ? html`
|
|
496
|
+
<tr>
|
|
497
|
+
<th class="nowrap align-top">${labels.get('details.parameters')}</th>
|
|
498
|
+
<td>${parametersRow}</td>
|
|
499
|
+
</tr>
|
|
500
|
+
` : nothing}
|
|
501
|
+
</tbody>
|
|
502
|
+
</table>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</form>
|
|
506
|
+
</div>
|
|
507
|
+
`;
|
|
508
|
+
return { toolbar, content };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private renderGroupListView(): ViewContent {
|
|
512
|
+
const editableGroups = this.groups.filter(group => group.editable);
|
|
513
|
+
|
|
514
|
+
const toolbar = html`
|
|
515
|
+
<div class="bookmark-manager-toolbar-start">
|
|
516
|
+
<button
|
|
517
|
+
type="button"
|
|
518
|
+
class="btn btn-sm btn-default"
|
|
519
|
+
@click=${this.navigateToBookmarkListView}
|
|
520
|
+
>
|
|
521
|
+
<typo3-backend-icon identifier="actions-arrow-left" size="small"></typo3-backend-icon>
|
|
522
|
+
${labels.get('manager.back')}
|
|
523
|
+
</button>
|
|
524
|
+
</div>
|
|
525
|
+
<div class="bookmark-manager-toolbar-end">
|
|
526
|
+
<button
|
|
527
|
+
class="btn btn-sm btn-primary"
|
|
528
|
+
type="button"
|
|
529
|
+
@click=${this.navigateToGroupCreateView}
|
|
530
|
+
>
|
|
531
|
+
<typo3-backend-icon identifier="actions-plus" size="small"></typo3-backend-icon>
|
|
532
|
+
${labels.get('manager.newGroup')}
|
|
533
|
+
</button>
|
|
534
|
+
</div>
|
|
535
|
+
`;
|
|
536
|
+
const content = html`
|
|
537
|
+
<div class="bookmark-manage-groups-view">
|
|
538
|
+
${editableGroups.length > 0 ? html`
|
|
539
|
+
<div class="table-fit">
|
|
540
|
+
<table class="table table-striped table-hover mb-0">
|
|
541
|
+
<tbody>
|
|
542
|
+
${editableGroups.map(group => this.renderGroupRow(group))}
|
|
543
|
+
</tbody>
|
|
544
|
+
</table>
|
|
545
|
+
</div>
|
|
546
|
+
` : html`
|
|
547
|
+
<div class="alert alert-info">
|
|
548
|
+
<typo3-backend-icon identifier="actions-info-circle" size="small"></typo3-backend-icon>
|
|
549
|
+
<span class="ms-2">${labels.get('manager.noGroups')}</span>
|
|
550
|
+
</div>
|
|
551
|
+
`}
|
|
552
|
+
</div>
|
|
553
|
+
`;
|
|
554
|
+
return { toolbar, content };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private renderGroupRow(group: BookmarkGroup): TemplateResult {
|
|
558
|
+
const bookmarkCount = this.bookmarks.filter(bookmark => bookmark.groupId === group.id).length;
|
|
559
|
+
const isDragged = this.draggedItem?.id === group.id;
|
|
560
|
+
const isDropTarget = this.dropTarget?.item.id === group.id;
|
|
561
|
+
const dropPosition = this.dropTarget?.position;
|
|
562
|
+
const rowClasses = classMap({
|
|
563
|
+
'opacity-50': isDragged,
|
|
564
|
+
'row-drop-before': isDropTarget && dropPosition === 'before',
|
|
565
|
+
'row-drop-after': isDropTarget && dropPosition === 'after',
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
const editableGroups = this.groups.filter(item => item.editable);
|
|
569
|
+
const groupIndex = editableGroups.findIndex(item => item.id === group.id);
|
|
570
|
+
const isFirst = groupIndex === 0;
|
|
571
|
+
const isLast = groupIndex === editableGroups.length - 1;
|
|
572
|
+
|
|
573
|
+
return html`
|
|
574
|
+
<tr
|
|
575
|
+
class=${rowClasses}
|
|
576
|
+
data-group-id=${group.id}
|
|
577
|
+
draggable="true"
|
|
578
|
+
@dragstart=${(e: DragEvent) => this.handleDragStart(e, 'group', group)}
|
|
579
|
+
@dragover=${(e: DragEvent) => this.handleItemDragOver(e, group)}
|
|
580
|
+
@dragleave=${this.handleItemDragLeave}
|
|
581
|
+
@drop=${(e: DragEvent) => this.handleDrop(e, group)}
|
|
582
|
+
@dragend=${this.handleDragEnd}
|
|
583
|
+
>
|
|
584
|
+
<td class="col-title">
|
|
585
|
+
<button
|
|
586
|
+
type="button"
|
|
587
|
+
class="btn btn-link"
|
|
588
|
+
@click=${() => this.navigateToGroupEditView(group)}
|
|
589
|
+
>
|
|
590
|
+
<typo3-backend-icon identifier="apps-pagetree-folder-default" size="small"></typo3-backend-icon>
|
|
591
|
+
${group.label}
|
|
592
|
+
${bookmarkCount > 0 ? html`
|
|
593
|
+
<span class="badge badge-default">${bookmarkCount}</span>
|
|
594
|
+
` : nothing}
|
|
595
|
+
</button>
|
|
596
|
+
</td>
|
|
597
|
+
<td class="col-control nowrap">
|
|
598
|
+
<div class="btn-group">
|
|
599
|
+
<button
|
|
600
|
+
type="button"
|
|
601
|
+
class="btn btn-default btn-sm"
|
|
602
|
+
title=${labels.get('manager.editGroup')}
|
|
603
|
+
@click=${() => this.navigateToGroupEditView(group)}
|
|
604
|
+
>
|
|
605
|
+
<typo3-backend-icon identifier="actions-cog" size="small"></typo3-backend-icon>
|
|
606
|
+
<span class="visually-hidden">${labels.get('manager.editGroup')}</span>
|
|
607
|
+
</button>
|
|
608
|
+
${isFirst ? html`
|
|
609
|
+
<span class="btn btn-default btn-sm disabled">
|
|
610
|
+
<typo3-backend-icon identifier="empty-empty" size="small"></typo3-backend-icon>
|
|
611
|
+
</span>
|
|
612
|
+
` : html`
|
|
613
|
+
<button
|
|
614
|
+
type="button"
|
|
615
|
+
class="btn btn-default btn-sm"
|
|
616
|
+
data-action="move-up"
|
|
617
|
+
title=${labels.get('manager.moveUp')}
|
|
618
|
+
@click=${() => this.handleGroupMoveUp(group)}
|
|
619
|
+
>
|
|
620
|
+
<typo3-backend-icon identifier="actions-chevron-up" size="small"></typo3-backend-icon>
|
|
621
|
+
<span class="visually-hidden">${labels.get('manager.moveUp')}</span>
|
|
622
|
+
</button>
|
|
623
|
+
`}
|
|
624
|
+
${isLast ? html`
|
|
625
|
+
<span class="btn btn-default btn-sm disabled">
|
|
626
|
+
<typo3-backend-icon identifier="empty-empty" size="small"></typo3-backend-icon>
|
|
627
|
+
</span>
|
|
628
|
+
` : html`
|
|
629
|
+
<button
|
|
630
|
+
type="button"
|
|
631
|
+
class="btn btn-default btn-sm"
|
|
632
|
+
data-action="move-down"
|
|
633
|
+
title=${labels.get('manager.moveDown')}
|
|
634
|
+
@click=${() => this.handleGroupMoveDown(group)}
|
|
635
|
+
>
|
|
636
|
+
<typo3-backend-icon identifier="actions-chevron-down" size="small"></typo3-backend-icon>
|
|
637
|
+
<span class="visually-hidden">${labels.get('manager.moveDown')}</span>
|
|
638
|
+
</button>
|
|
639
|
+
`}
|
|
640
|
+
<span
|
|
641
|
+
class="btn btn-default btn-sm"
|
|
642
|
+
style="cursor: grab;"
|
|
643
|
+
title=${labels.get('manager.dragToReorder')}
|
|
644
|
+
>
|
|
645
|
+
<typo3-backend-icon identifier="actions-drag" size="small"></typo3-backend-icon>
|
|
646
|
+
<span class="visually-hidden">${labels.get('manager.dragToReorder')}</span>
|
|
647
|
+
</span>
|
|
648
|
+
</div>
|
|
649
|
+
</td>
|
|
650
|
+
</tr>
|
|
651
|
+
`;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
private renderGroupCreateView(state: GroupCreateViewState): ViewContent {
|
|
655
|
+
const { draft } = state;
|
|
656
|
+
const toolbar = html`
|
|
657
|
+
<div class="bookmark-manager-toolbar-start">
|
|
658
|
+
<button
|
|
659
|
+
type="button"
|
|
660
|
+
class="btn btn-sm btn-default"
|
|
661
|
+
@click=${this.navigateToGroupListView}
|
|
662
|
+
>
|
|
663
|
+
<typo3-backend-icon identifier="actions-arrow-left" size="small"></typo3-backend-icon>
|
|
664
|
+
${labels.get('manager.back')}
|
|
665
|
+
</button>
|
|
666
|
+
</div>
|
|
667
|
+
<div class="bookmark-manager-toolbar-end">
|
|
668
|
+
<button
|
|
669
|
+
type="submit"
|
|
670
|
+
form="bookmark-group-create-form"
|
|
671
|
+
class="btn btn-sm btn-primary"
|
|
672
|
+
>
|
|
673
|
+
<typo3-backend-icon identifier="actions-check" size="small"></typo3-backend-icon>
|
|
674
|
+
${labels.get('manager.createGroup')}
|
|
675
|
+
</button>
|
|
676
|
+
</div>
|
|
677
|
+
`;
|
|
678
|
+
const content = html`
|
|
679
|
+
<div class="bookmark-group-create-view">
|
|
680
|
+
<form id="bookmark-group-create-form" @submit=${(e: Event) => this.handleGroupCreate(e, draft)}>
|
|
681
|
+
<div class="mb-3">
|
|
682
|
+
<label class="form-label" for="group-label">${labels.get('manager.label')}</label>
|
|
683
|
+
<input
|
|
684
|
+
type="text"
|
|
685
|
+
id="group-label"
|
|
686
|
+
name="label"
|
|
687
|
+
class="form-control"
|
|
688
|
+
.value=${draft.label}
|
|
689
|
+
@input=${(e: Event) => { draft.label = (e.target as HTMLInputElement).value; this.requestUpdate(); }}
|
|
690
|
+
required
|
|
691
|
+
autofocus
|
|
692
|
+
/>
|
|
693
|
+
</div>
|
|
694
|
+
</form>
|
|
695
|
+
</div>
|
|
696
|
+
`;
|
|
697
|
+
return { toolbar, content };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
private renderGroupEditView(state: GroupEditViewState): ViewContent {
|
|
701
|
+
const { group } = state;
|
|
702
|
+
const toolbar = html`
|
|
703
|
+
<div class="bookmark-manager-toolbar-start">
|
|
704
|
+
<button
|
|
705
|
+
type="button"
|
|
706
|
+
class="btn btn-sm btn-default"
|
|
707
|
+
@click=${this.navigateToGroupListView}
|
|
708
|
+
>
|
|
709
|
+
<typo3-backend-icon identifier="actions-arrow-left" size="small"></typo3-backend-icon>
|
|
710
|
+
${labels.get('manager.back')}
|
|
711
|
+
</button>
|
|
712
|
+
</div>
|
|
713
|
+
<div class="bookmark-manager-toolbar-end">
|
|
714
|
+
<button
|
|
715
|
+
type="button"
|
|
716
|
+
class="btn btn-sm btn-danger"
|
|
717
|
+
@click=${() => this.handleGroupDelete(group)}
|
|
718
|
+
>
|
|
719
|
+
<typo3-backend-icon identifier="actions-delete" size="small"></typo3-backend-icon>
|
|
720
|
+
${labels.get('action.delete')}
|
|
721
|
+
</button>
|
|
722
|
+
<button
|
|
723
|
+
type="submit"
|
|
724
|
+
form="bookmark-group-edit-form"
|
|
725
|
+
class="btn btn-sm btn-primary"
|
|
726
|
+
>
|
|
727
|
+
<typo3-backend-icon identifier="actions-check" size="small"></typo3-backend-icon>
|
|
728
|
+
${labels.get('action.save')}
|
|
729
|
+
</button>
|
|
730
|
+
</div>
|
|
731
|
+
`;
|
|
732
|
+
const content = html`
|
|
733
|
+
<div class="bookmark-group-edit-view">
|
|
734
|
+
<form id="bookmark-group-edit-form" @submit=${(e: Event) => this.handleGroupUpdate(e, group)}>
|
|
735
|
+
<div class="mb-3">
|
|
736
|
+
<label class="form-label" for="group-label">${labels.get('manager.label')}</label>
|
|
737
|
+
<input
|
|
738
|
+
type="text"
|
|
739
|
+
id="group-label"
|
|
740
|
+
name="label"
|
|
741
|
+
class="form-control"
|
|
742
|
+
.value=${group.label}
|
|
743
|
+
@input=${(e: Event) => { group.label = (e.target as HTMLInputElement).value; this.requestUpdate(); }}
|
|
744
|
+
required
|
|
745
|
+
/>
|
|
746
|
+
</div>
|
|
747
|
+
</form>
|
|
748
|
+
</div>
|
|
749
|
+
`;
|
|
750
|
+
return { toolbar, content };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Data Management
|
|
754
|
+
private async syncFromStore(): Promise<void> {
|
|
755
|
+
this.bookmarks = await BookmarkStore.getBookmarks();
|
|
756
|
+
this.groups = await BookmarkStore.getGroups();
|
|
757
|
+
this.groupedBookmarks = await BookmarkStore.getGroupedBookmarks();
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private getSelectableGroups(): BookmarkGroup[] {
|
|
761
|
+
return this.groups.filter(group => group.selectable);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Handlers
|
|
765
|
+
private readonly handleStoreUpdate = (): void => {
|
|
766
|
+
this.syncFromStore();
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
private handleBookmarkNavigate(bookmark: Bookmark): void {
|
|
770
|
+
this.closest('typo3-backend-modal')?.hideModal();
|
|
771
|
+
BookmarkStore.navigate(bookmark);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
private async handleBookmarkUpdate(e: Event, bookmark: Bookmark): Promise<void> {
|
|
775
|
+
e.preventDefault();
|
|
776
|
+
const result = await BookmarkStore.update(bookmark.id, bookmark.title, bookmark.groupId);
|
|
777
|
+
if (result.success) {
|
|
778
|
+
Notification.success(
|
|
779
|
+
labels.get('success.updated.title'),
|
|
780
|
+
labels.get('success.updated.message')
|
|
781
|
+
);
|
|
782
|
+
this.navigateToBookmarkListView();
|
|
783
|
+
} else {
|
|
784
|
+
Notification.error(
|
|
785
|
+
labels.get('error.updateFailed.title'),
|
|
786
|
+
result.error || labels.get('error.unknown.message')
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
private handleBookmarkDelete(id: number): void {
|
|
792
|
+
const confirmModal = Modal.confirm(
|
|
793
|
+
labels.get('confirmDelete.title'),
|
|
794
|
+
labels.get('confirmDelete.message'),
|
|
795
|
+
SeverityEnum.notice,
|
|
796
|
+
[
|
|
797
|
+
{
|
|
798
|
+
text: labels.get('action.cancel'),
|
|
799
|
+
btnClass: 'btn-default',
|
|
800
|
+
name: 'cancel',
|
|
801
|
+
trigger: () => confirmModal.hideModal(),
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
text: labels.get('action.delete'),
|
|
805
|
+
btnClass: 'btn-primary',
|
|
806
|
+
name: 'delete',
|
|
807
|
+
trigger: async () => {
|
|
808
|
+
const success = await BookmarkStore.deleteMultiple([id]);
|
|
809
|
+
if (success) {
|
|
810
|
+
Notification.success(
|
|
811
|
+
labels.get('success.deleted.title'),
|
|
812
|
+
labels.get('success.deleted.message')
|
|
813
|
+
);
|
|
814
|
+
this.navigateToBookmarkListView();
|
|
815
|
+
} else {
|
|
816
|
+
Notification.error(
|
|
817
|
+
labels.get('error.deleteFailed.title'),
|
|
818
|
+
labels.get('error.deleteFailed.message')
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
confirmModal.hideModal();
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
]
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
private readonly handleBookmarkBulkDelete = (): void => {
|
|
829
|
+
const ids = Array.from(this.selectedIds);
|
|
830
|
+
const confirmModal = Modal.confirm(
|
|
831
|
+
labels.get('confirmDeleteMultiple.title'),
|
|
832
|
+
labels.get('confirmDeleteMultiple.message', [ids.length]),
|
|
833
|
+
SeverityEnum.notice,
|
|
834
|
+
[
|
|
835
|
+
{
|
|
836
|
+
text: labels.get('action.cancel'),
|
|
837
|
+
btnClass: 'btn-default',
|
|
838
|
+
name: 'cancel',
|
|
839
|
+
trigger: () => confirmModal.hideModal(),
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
text: labels.get('action.delete'),
|
|
843
|
+
btnClass: 'btn-primary',
|
|
844
|
+
name: 'delete',
|
|
845
|
+
trigger: async () => {
|
|
846
|
+
const success = await BookmarkStore.deleteMultiple(ids);
|
|
847
|
+
if (success) {
|
|
848
|
+
Notification.success(
|
|
849
|
+
labels.get('success.deletedMultiple.title'),
|
|
850
|
+
labels.get('success.deletedMultiple.message', [ids.length])
|
|
851
|
+
);
|
|
852
|
+
this.selectedIds = new Set();
|
|
853
|
+
} else {
|
|
854
|
+
Notification.error(
|
|
855
|
+
labels.get('error.deleteFailed.title'),
|
|
856
|
+
labels.get('error.deleteFailed.message')
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
confirmModal.hideModal();
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
]
|
|
863
|
+
);
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
private async handleBookmarkBulkMove(groupId: BookmarkGroupId): Promise<void> {
|
|
867
|
+
const ids = Array.from(this.selectedIds);
|
|
868
|
+
const success = await BookmarkStore.move(ids, groupId);
|
|
869
|
+
if (success) {
|
|
870
|
+
Notification.success(
|
|
871
|
+
labels.get('success.moved.title'),
|
|
872
|
+
labels.get('success.moved.message', [ids.length])
|
|
873
|
+
);
|
|
874
|
+
this.selectedIds = new Set();
|
|
875
|
+
} else {
|
|
876
|
+
Notification.error(
|
|
877
|
+
labels.get('error.moveFailed.title'),
|
|
878
|
+
labels.get('error.moveFailed.message')
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
private async handleBookmarkMoveUp(bookmark: Bookmark): Promise<void> {
|
|
884
|
+
await this.reorderBookmarkRelative(bookmark, -1);
|
|
885
|
+
await this.restoreFocus('bookmark', bookmark.id, 'up');
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
private async handleBookmarkMoveDown(bookmark: Bookmark): Promise<void> {
|
|
889
|
+
await this.reorderBookmarkRelative(bookmark, 1);
|
|
890
|
+
await this.restoreFocus('bookmark', bookmark.id, 'down');
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
private async handleGroupMoveUp(group: BookmarkGroup): Promise<void> {
|
|
894
|
+
await this.reorderGroupRelative(group, -1);
|
|
895
|
+
await this.restoreFocus('group', group.id, 'up');
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private async handleGroupMoveDown(group: BookmarkGroup): Promise<void> {
|
|
899
|
+
await this.reorderGroupRelative(group, 1);
|
|
900
|
+
await this.restoreFocus('group', group.id, 'down');
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private async handleGroupCreate(e: Event, draft: { label: string }): Promise<void> {
|
|
904
|
+
e.preventDefault();
|
|
905
|
+
if (!draft.label.trim()) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const result = await BookmarkStore.createGroup(draft.label.trim());
|
|
910
|
+
if (result.success) {
|
|
911
|
+
Notification.success(
|
|
912
|
+
labels.get('success.groupCreated.title'),
|
|
913
|
+
labels.get('success.groupCreated.message')
|
|
914
|
+
);
|
|
915
|
+
this.navigateToGroupListView();
|
|
916
|
+
} else {
|
|
917
|
+
Notification.error(
|
|
918
|
+
labels.get('error.groupCreateFailed.title'),
|
|
919
|
+
result.error || labels.get('error.groupCreateFailed.message')
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private async handleGroupUpdate(e: Event, group: BookmarkGroup): Promise<void> {
|
|
925
|
+
e.preventDefault();
|
|
926
|
+
if (!group.label.trim() || !group.editable) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const result = await BookmarkStore.updateGroup(
|
|
931
|
+
group.id as string,
|
|
932
|
+
group.label.trim()
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
if (result.success) {
|
|
936
|
+
Notification.success(
|
|
937
|
+
labels.get('success.groupUpdated.title'),
|
|
938
|
+
labels.get('success.groupUpdated.message')
|
|
939
|
+
);
|
|
940
|
+
this.navigateToGroupListView();
|
|
941
|
+
} else {
|
|
942
|
+
Notification.error(
|
|
943
|
+
labels.get('error.groupUpdateFailed.title'),
|
|
944
|
+
result.error || labels.get('error.groupUpdateFailed.message')
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private handleGroupDelete(group: BookmarkGroup): void {
|
|
950
|
+
if (!group.editable) {
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const confirmModal = Modal.confirm(
|
|
955
|
+
labels.get('confirmDeleteGroup.title'),
|
|
956
|
+
labels.get('confirmDeleteGroup.message', [group.label]),
|
|
957
|
+
SeverityEnum.notice,
|
|
958
|
+
[
|
|
959
|
+
{
|
|
960
|
+
text: labels.get('action.cancel'),
|
|
961
|
+
btnClass: 'btn-default',
|
|
962
|
+
name: 'cancel',
|
|
963
|
+
trigger: () => confirmModal.hideModal(),
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
text: labels.get('action.delete'),
|
|
967
|
+
btnClass: 'btn-primary',
|
|
968
|
+
name: 'delete',
|
|
969
|
+
trigger: async () => {
|
|
970
|
+
const result = await BookmarkStore.deleteGroup(group.id as string);
|
|
971
|
+
if (result.success) {
|
|
972
|
+
Notification.success(
|
|
973
|
+
labels.get('success.groupDeleted.title'),
|
|
974
|
+
labels.get('success.groupDeleted.message')
|
|
975
|
+
);
|
|
976
|
+
this.navigateToGroupListView();
|
|
977
|
+
} else {
|
|
978
|
+
Notification.error(
|
|
979
|
+
labels.get('error.groupDeleteFailed.title'),
|
|
980
|
+
result.error || labels.get('error.groupDeleteFailed.message')
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
confirmModal.hideModal();
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
]
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Reordering
|
|
991
|
+
private async restoreFocus(type: 'bookmark' | 'group', id: number | string, direction: 'up' | 'down'): Promise<void> {
|
|
992
|
+
await this.updateComplete;
|
|
993
|
+
const rowSelector = type === 'bookmark' ? `tr[data-bookmark-id="${id}"]` : `tr[data-group-id="${id}"]`;
|
|
994
|
+
const row = this.querySelector(rowSelector);
|
|
995
|
+
if (!row) {
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const preferredAction = direction === 'up' ? 'move-up' : 'move-down';
|
|
999
|
+
const fallbackAction = direction === 'up' ? 'move-down' : 'move-up';
|
|
1000
|
+
const moveButton = row.querySelector<HTMLButtonElement>(`button[data-action="${preferredAction}"]`)
|
|
1001
|
+
?? row.querySelector<HTMLButtonElement>(`button[data-action="${fallbackAction}"]`);
|
|
1002
|
+
moveButton?.focus();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
private async reorderBookmarkRelative(bookmark: Bookmark, offset: number): Promise<void> {
|
|
1006
|
+
const groupBookmarks = [...(this.groupedBookmarks.get(bookmark.groupId) ?? [])];
|
|
1007
|
+
const currentIndex = groupBookmarks.findIndex(item => item.id === bookmark.id);
|
|
1008
|
+
const targetIndex = currentIndex + offset;
|
|
1009
|
+
if (currentIndex < 0 || targetIndex < 0 || targetIndex >= groupBookmarks.length) {
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
[groupBookmarks[currentIndex], groupBookmarks[targetIndex]] =
|
|
1013
|
+
[groupBookmarks[targetIndex], groupBookmarks[currentIndex]];
|
|
1014
|
+
await BookmarkStore.reorder(groupBookmarks.map(item => item.id));
|
|
1015
|
+
await this.syncFromStore();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
private async reorderBookmarkToPosition(bookmark: Bookmark, targetGroupId: BookmarkGroupId, targetIndex: number): Promise<void> {
|
|
1019
|
+
if (bookmark.groupId !== targetGroupId) {
|
|
1020
|
+
await BookmarkStore.update(bookmark.id, bookmark.title, targetGroupId);
|
|
1021
|
+
}
|
|
1022
|
+
const groupBookmarks = [...(this.groupedBookmarks.get(targetGroupId) ?? [])].filter(item => item.id !== bookmark.id);
|
|
1023
|
+
groupBookmarks.splice(targetIndex, 0, bookmark);
|
|
1024
|
+
await BookmarkStore.reorder(groupBookmarks.map(item => item.id));
|
|
1025
|
+
await this.syncFromStore();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private async reorderGroupRelative(group: BookmarkGroup, offset: number): Promise<void> {
|
|
1029
|
+
const editableGroups = this.groups.filter(item => item.editable);
|
|
1030
|
+
const currentIndex = editableGroups.findIndex(item => item.id === group.id);
|
|
1031
|
+
const targetIndex = currentIndex + offset;
|
|
1032
|
+
if (currentIndex < 0 || targetIndex < 0 || targetIndex >= editableGroups.length) {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
[editableGroups[currentIndex], editableGroups[targetIndex]] =
|
|
1036
|
+
[editableGroups[targetIndex], editableGroups[currentIndex]];
|
|
1037
|
+
await BookmarkStore.reorderGroups(editableGroups.map(item => item.id as string));
|
|
1038
|
+
await this.syncFromStore();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private async reorderGroupToPosition(group: BookmarkGroup, targetIndex: number): Promise<void> {
|
|
1042
|
+
const editableGroups = this.groups.filter(item => item.editable && item.id !== group.id);
|
|
1043
|
+
editableGroups.splice(targetIndex, 0, group);
|
|
1044
|
+
await BookmarkStore.reorderGroups(editableGroups.map(item => item.id as string));
|
|
1045
|
+
await this.syncFromStore();
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Drag and Drop
|
|
1049
|
+
private handleDragStart(e: DragEvent, type: 'bookmark' | 'group', item: Bookmark | BookmarkGroup): void {
|
|
1050
|
+
this.draggedItem = item;
|
|
1051
|
+
if (e.dataTransfer) {
|
|
1052
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
1053
|
+
const payload: DragPayload = { type, id: item.id };
|
|
1054
|
+
e.dataTransfer.setData('application/json', JSON.stringify(payload));
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
private readonly handleDragOver = (e: DragEvent): void => {
|
|
1059
|
+
e.preventDefault();
|
|
1060
|
+
if (e.dataTransfer) {
|
|
1061
|
+
e.dataTransfer.dropEffect = 'move';
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
|
|
1065
|
+
private handleItemDragOver(e: DragEvent, target: Bookmark | BookmarkGroup): void {
|
|
1066
|
+
e.preventDefault();
|
|
1067
|
+
e.stopPropagation();
|
|
1068
|
+
|
|
1069
|
+
if (this.draggedItem === null || this.draggedItem.id === target.id) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (e.dataTransfer) {
|
|
1074
|
+
e.dataTransfer.dropEffect = 'move';
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
1078
|
+
const midpoint = rect.top + rect.height / 2;
|
|
1079
|
+
const position: 'before' | 'after' = e.clientY < midpoint ? 'before' : 'after';
|
|
1080
|
+
|
|
1081
|
+
this.dropTarget = { item: target, position };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
private readonly handleItemDragLeave = (e: DragEvent): void => {
|
|
1085
|
+
const relatedTarget = e.relatedTarget as Node | null;
|
|
1086
|
+
if (relatedTarget && (e.currentTarget as HTMLElement).contains(relatedTarget)) {
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
this.dropTarget = null;
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
private readonly handleDragEnd = (): void => {
|
|
1093
|
+
this.draggedItem = null;
|
|
1094
|
+
this.dropTarget = null;
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
private async handleDrop(e: DragEvent, target: Bookmark | BookmarkGroup | BookmarkGroupId): Promise<void> {
|
|
1098
|
+
e.preventDefault();
|
|
1099
|
+
e.stopPropagation();
|
|
1100
|
+
|
|
1101
|
+
const dragged = this.draggedItem;
|
|
1102
|
+
const dropPosition = this.dropTarget?.position;
|
|
1103
|
+
this.dropTarget = null;
|
|
1104
|
+
|
|
1105
|
+
if (dragged === null) {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const payload: DragPayload = JSON.parse(e.dataTransfer?.getData('application/json') ?? '{}');
|
|
1110
|
+
|
|
1111
|
+
if (payload.type === 'bookmark') {
|
|
1112
|
+
await this.handleDropBookmark(dragged as Bookmark, target, dropPosition);
|
|
1113
|
+
} else if (payload.type === 'group') {
|
|
1114
|
+
await this.handleDropGroup(dragged as BookmarkGroup, target as BookmarkGroup, dropPosition);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
this.draggedItem = null;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
private async handleDropBookmark(draggedBookmark: Bookmark, target: Bookmark | BookmarkGroup | BookmarkGroupId, dropPosition?: 'before' | 'after'): Promise<void> {
|
|
1121
|
+
if (typeof target === 'object' && 'route' in target) {
|
|
1122
|
+
const targetBookmark = target as Bookmark;
|
|
1123
|
+
if (draggedBookmark.id === targetBookmark.id) {
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const groupBookmarks = [...(this.groupedBookmarks.get(targetBookmark.groupId) ?? [])].filter(item => item.id !== draggedBookmark.id);
|
|
1127
|
+
const targetIndex = groupBookmarks.findIndex(item => item.id === targetBookmark.id);
|
|
1128
|
+
const insertIndex = dropPosition === 'before' ? targetIndex : targetIndex + 1;
|
|
1129
|
+
await this.reorderBookmarkToPosition(draggedBookmark, targetBookmark.groupId, insertIndex);
|
|
1130
|
+
} else {
|
|
1131
|
+
const targetGroupId = target as BookmarkGroupId;
|
|
1132
|
+
if (draggedBookmark.groupId !== targetGroupId) {
|
|
1133
|
+
const groupBookmarks = [...(this.groupedBookmarks.get(targetGroupId) ?? [])];
|
|
1134
|
+
await this.reorderBookmarkToPosition(draggedBookmark, targetGroupId, groupBookmarks.length);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
private async handleDropGroup(draggedGroup: BookmarkGroup, targetGroup: BookmarkGroup, dropPosition?: 'before' | 'after'): Promise<void> {
|
|
1140
|
+
if (draggedGroup.id === targetGroup.id) {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
const editableGroups = this.groups.filter(item => item.editable && item.id !== draggedGroup.id);
|
|
1144
|
+
const targetIndex = editableGroups.findIndex(item => item.id === targetGroup.id);
|
|
1145
|
+
const insertIndex = dropPosition === 'before' ? targetIndex : targetIndex + 1;
|
|
1146
|
+
await this.reorderGroupToPosition(draggedGroup, insertIndex);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Helpers
|
|
1150
|
+
private helperGetGroupSections(groups: BookmarkGroup[]): GroupSection[] {
|
|
1151
|
+
const groupsByType = Map.groupBy(groups, (group) => group.type);
|
|
1152
|
+
const groupTypeLabels: Record<BookmarkGroupType, string> = {
|
|
1153
|
+
[BookmarkGroupType.USER]: labels.get('groupType.user'),
|
|
1154
|
+
[BookmarkGroupType.SYSTEM]: labels.get('groupType.system'),
|
|
1155
|
+
[BookmarkGroupType.GLOBAL]: labels.get('groupType.global'),
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
return Array.from(groupsByType.entries()).map(([type, typeGroups]) => ({
|
|
1159
|
+
label: groupTypeLabels[type] ?? type,
|
|
1160
|
+
groups: typeGroups,
|
|
1161
|
+
}));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
private helperParseParameters(argumentsJson: string): TemplateResult | null {
|
|
1165
|
+
if (!argumentsJson) {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
try {
|
|
1169
|
+
const entries = Object.entries(JSON.parse(argumentsJson));
|
|
1170
|
+
if (entries.length === 0) {
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
const format = (value: unknown): string => {
|
|
1174
|
+
const isObject = typeof value === 'object' && value !== null;
|
|
1175
|
+
return isObject ? JSON.stringify(value) : String(value);
|
|
1176
|
+
};
|
|
1177
|
+
return html`
|
|
1178
|
+
<ul class="list-unstyled mb-0">
|
|
1179
|
+
${entries.map(([key, value]) => html`<li><strong>${key}:</strong> ${format(value)}</li>`)}
|
|
1180
|
+
</ul>
|
|
1181
|
+
`;
|
|
1182
|
+
} catch {
|
|
1183
|
+
return html`${argumentsJson}`;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private helperStripToken(url: string): string {
|
|
1188
|
+
try {
|
|
1189
|
+
const urlObj = new URL(url, window.location.origin);
|
|
1190
|
+
urlObj.searchParams.delete('token');
|
|
1191
|
+
return decodeURIComponent(urlObj.pathname + urlObj.search);
|
|
1192
|
+
} catch {
|
|
1193
|
+
return url;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Button that opens the bookmark manager modal.
|
|
1200
|
+
*/
|
|
1201
|
+
@customElement('typo3-backend-bookmark-manager-button')
|
|
1202
|
+
export class BookmarkManagerButtonElement extends PseudoButtonLitElement {
|
|
1203
|
+
@property({ type: Number, attribute: 'edit-id' }) editId?: number;
|
|
1204
|
+
|
|
1205
|
+
protected override createRenderRoot(): HTMLElement | ShadowRoot {
|
|
1206
|
+
return this;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
protected override buttonActivated(): void {
|
|
1210
|
+
// Create element in top frame to avoid adoptedStyleSheets cross-document issues
|
|
1211
|
+
const targetDocument = top?.document ?? document;
|
|
1212
|
+
const managerElement = targetDocument.createElement('typo3-backend-bookmark-manager-content') as BookmarkManagerContentElement;
|
|
1213
|
+
if (this.editId !== undefined) {
|
|
1214
|
+
managerElement.editId = this.editId;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
Modal.advanced({
|
|
1218
|
+
type: Modal.types.default,
|
|
1219
|
+
title: labels.get('manage'),
|
|
1220
|
+
size: Modal.sizes.medium,
|
|
1221
|
+
severity: SeverityEnum.notice,
|
|
1222
|
+
content: managerElement,
|
|
1223
|
+
buttons: [],
|
|
1224
|
+
staticBackdrop: true,
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
declare global {
|
|
1230
|
+
interface HTMLElementTagNameMap {
|
|
1231
|
+
'typo3-backend-bookmark-manager-content': BookmarkManagerContentElement;
|
|
1232
|
+
'typo3-backend-bookmark-manager-button': BookmarkManagerButtonElement;
|
|
1233
|
+
}
|
|
1234
|
+
}
|