diff --git a/modules/app/src/main/resources/assets/page-editor/styles/partials/item-view.less b/modules/app/src/main/resources/assets/page-editor/styles/partials/item-view.less index 761decf74c..378a6293a0 100644 --- a/modules/app/src/main/resources/assets/page-editor/styles/partials/item-view.less +++ b/modules/app/src/main/resources/assets/page-editor/styles/partials/item-view.less @@ -17,10 +17,21 @@ display: flex; justify-content: center; align-items: center; + height: @placeholder-height; .empty-descriptor-block { position: relative; // works as setting the z-index to 1 } + + .selectable-listbox-wrapper { + text-align: left; + + .selectable-listbox { + margin: 0; + padding: 0; + z-index: @z-index-empty-component-options; + } + } } } @@ -104,11 +115,17 @@ text-align: center; box-sizing: border-box; + .selectable-listbox { + margin: 0; + padding: 0; + z-index: @z-index-empty-component-options; + } + .display-name { display: none; color: @page-editor-message-blue; margin: 0; - line-height: 37px; + line-height: 36px; position: relative; // set the position to appear on top of the absolutely positioned :before .ellipsis(); @@ -159,7 +176,7 @@ input[type="text"] { font-family: @admin-font-family; font-size: 15px; - padding: 4px 37px 4px 10px; + padding: 4px 36px 4px 10px; .placeholder(1em, italic); .input-glow(); // need to use glow explicitly because it is used out of the form diff --git a/modules/app/src/main/resources/assets/page-editor/styles/partials/layout-view.less b/modules/app/src/main/resources/assets/page-editor/styles/partials/layout-view.less index 41f9a20dab..703457cbe9 100644 --- a/modules/app/src/main/resources/assets/page-editor/styles/partials/layout-view.less +++ b/modules/app/src/main/resources/assets/page-editor/styles/partials/layout-view.less @@ -1,3 +1,10 @@ .@{_CLS_PREFIX}layout-placeholder { // } + +.@{_CLS_PREFIX}layout-placeholder { + .common-page-dropdown { + max-width: 540px; + margin: 0 auto; + } +} diff --git a/modules/app/src/main/resources/assets/page-editor/styles/partials/part-view.less b/modules/app/src/main/resources/assets/page-editor/styles/partials/part-view.less index f739856aac..5af36f2b96 100644 --- a/modules/app/src/main/resources/assets/page-editor/styles/partials/part-view.less +++ b/modules/app/src/main/resources/assets/page-editor/styles/partials/part-view.less @@ -2,3 +2,10 @@ a[data-portal-component-type="part"] { display: block; pointer-events: auto; } + +.@{_CLS_PREFIX}part-placeholder { + .common-page-dropdown { + max-width: 540px; + margin: 0 auto; + } +} diff --git a/modules/app/src/main/resources/assets/page-editor/styles/partials/text-view.less b/modules/app/src/main/resources/assets/page-editor/styles/partials/text-view.less index b545b9779e..2f6359a0d6 100644 --- a/modules/app/src/main/resources/assets/page-editor/styles/partials/text-view.less +++ b/modules/app/src/main/resources/assets/page-editor/styles/partials/text-view.less @@ -76,10 +76,6 @@ margin-top: 0; } - > *:last-child { - margin-bottom: 0; - } - a { pointer-events: auto; cursor: auto; diff --git a/modules/app/tsconfig.json b/modules/app/tsconfig.json index a7990c6523..9b1a428ad2 100644 --- a/modules/app/tsconfig.json +++ b/modules/app/tsconfig.json @@ -6,7 +6,6 @@ "types": [ "ckeditor", "jquery", - "slickgrid", "jqueryui", "q" ], diff --git a/modules/lib/package.json b/modules/lib/package.json index 1e4831b576..7739f81e07 100644 --- a/modules/lib/package.json +++ b/modules/lib/package.json @@ -30,7 +30,8 @@ "jquery-ui": "^1.13.2", "jsondiffpatch": "^0.5.0", "lodash": "^4.17.21", - "q": "^1.5.1" + "q": "^1.5.1", + "sortablejs": "^1.15.2" }, "devDependencies": { "@enonic/eslint-config": "^2.0.0", @@ -42,6 +43,7 @@ "@types/jquery": "^3.5.25", "@types/jqueryui": "^1.12.19", "@types/q": "^1.5.7", + "@types/sortablejs": "^1.15.8", "circular-dependency-plugin": "^5.2.2", "eslint": "^9.10.0", "globals": "^15.10.0", diff --git a/modules/lib/src/main/resources/assets/js/app/ContentAppContainer.ts b/modules/lib/src/main/resources/assets/js/app/ContentAppContainer.ts index ebd3d1f361..073f03f865 100644 --- a/modules/lib/src/main/resources/assets/js/app/ContentAppContainer.ts +++ b/modules/lib/src/main/resources/assets/js/app/ContentAppContainer.ts @@ -57,16 +57,7 @@ export class ContentAppContainer private initSearchPanelListener(panel: ContentAppPanel) { ToggleSearchPanelWithDependenciesGlobalEvent.on((event) => { - if (!panel.getBrowsePanel().getTreeGrid().isEmpty()) { - new ToggleSearchPanelWithDependenciesEvent({item: event.getContent(), inbound: event.isInbound()}).fire(); - } else { - const handler = () => { - new ToggleSearchPanelWithDependenciesEvent({item: event.getContent(), inbound: event.isInbound()}).fire(); - panel.getBrowsePanel().getTreeGrid().unLoaded(handler); - }; - - panel.getBrowsePanel().getTreeGrid().onLoaded(handler); - } + new ToggleSearchPanelWithDependenciesEvent({item: event.getContent(), inbound: event.isInbound()}).fire(); }); } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentBrowsePanel.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentBrowsePanel.ts index 7ead20587d..6903ff1291 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/ContentBrowsePanel.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/ContentBrowsePanel.ts @@ -2,9 +2,7 @@ import * as Q from 'q'; import {AppHelper} from '@enonic/lib-admin-ui/util/AppHelper'; import {ResponsiveManager} from '@enonic/lib-admin-ui/ui/responsive/ResponsiveManager'; import {ResponsiveItem} from '@enonic/lib-admin-ui/ui/responsive/ResponsiveItem'; -import {ActionName, ContentTreeGridActions} from './action/ContentTreeGridActions'; import {ContentBrowseToolbar} from './ContentBrowseToolbar'; -import {ContentTreeGrid, State} from './ContentTreeGrid'; import {ContentBrowseFilterPanel} from './filter/ContentBrowseFilterPanel'; import {ContentBrowseItemPanel} from './ContentBrowseItemPanel'; import {Router} from '../Router'; @@ -26,7 +24,7 @@ import {ProjectContext} from '../project/ProjectContext'; import {ContentServerChangeItem} from '../event/ContentServerChangeItem'; import {DeletedContentItem} from './DeletedContentItem'; import {IsRenderableRequest} from '../resource/IsRenderableRequest'; -import {ContentSummary} from '../content/ContentSummary'; +import {ContentSummary, ContentSummaryBuilder} from '../content/ContentSummary'; import {ContentId} from '../content/ContentId'; import {ContentPath} from '../content/ContentPath'; import {NonMobileContextPanelToggleButton} from '../view/context/button/NonMobileContextPanelToggleButton'; @@ -37,13 +35,26 @@ import {ContentQuery} from '../content/ContentQuery'; import {StatusCode} from '@enonic/lib-admin-ui/rest/StatusCode'; import {SearchAndExpandItemEvent} from './SearchAndExpandItemEvent'; import {ContentItemPreviewPanel} from '../view/ContentItemPreviewPanel'; +import {ListBoxToolbar} from '@enonic/lib-admin-ui/ui/selector/list/ListBoxToolbar'; +import {TreeGridContextMenu} from '@enonic/lib-admin-ui/ui/treegrid/TreeGridContextMenu'; +import {SelectableListBoxPanel} from '@enonic/lib-admin-ui/ui/panel/SelectableListBoxPanel'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; +import {SelectableTreeListBoxKeyNavigator} from '@enonic/lib-admin-ui/ui/selector/list/SelectableTreeListBoxKeyNavigator'; +import {ActionName, ContentTreeActions} from './ContentTreeActions'; +import {ContentAndStatusTreeSelectorItem} from '../item/ContentAndStatusTreeSelectorItem'; +import {ContentsTreeGridList, ContentsTreeGridListElement} from './ContentsTreeGridList'; +import {ContentsTreeGridRootList} from './ContentsTreeGridRootList'; +import {State} from './State'; +import {showFeedback} from '@enonic/lib-admin-ui/notify/MessageBus'; +import {i18n} from '@enonic/lib-admin-ui/util/Messages'; +import {EditContentEvent} from '../event/EditContentEvent'; +import {GetContentSummaryByIdRequest} from '../resource/GetContentSummaryByIdRequest'; import {ContentActionMenuButton} from '../ContentActionMenuButton'; import {MenuButtonDropdownPos} from '@enonic/lib-admin-ui/ui/button/MenuButton'; export class ContentBrowsePanel extends ResponsiveBrowsePanel { - protected treeGrid: ContentTreeGrid; protected browseToolbar: ContentBrowseToolbar; protected filterPanel: ContentBrowseFilterPanel; private debouncedFilterRefresh: () => void; @@ -51,6 +62,22 @@ export class ContentBrowsePanel private browseActionsAndPreviewUpdateRequired: boolean = false; private contextPanelToggler: NonMobileContextPanelToggleButton; + private state: State; + + protected treeListBox: ContentsTreeGridRootList; + + protected treeActions: ContentTreeActions; + + protected toolbar: ListBoxToolbar; + + protected contextMenu: TreeGridContextMenu; + + protected keyNavigator: SelectableTreeListBoxKeyNavigator; + + protected selectionWrapper: SelectableListBoxWrapper; + + protected selectableListBoxPanel: SelectableListBoxPanel; + protected initElements() { super.initElements(); @@ -71,13 +98,13 @@ export class ContentBrowsePanel this.getBrowseActions().setState(State.DISABLED); this.toggleFilterPanelAction.setEnabled(false); this.contextPanelToggler.setEnabled(false); - this.treeGrid.setState(State.DISABLED); + this.setContentTreeState(State.DISABLED); const projectSetHandler = () => { this.getBrowseActions().setState(State.ENABLED); this.toggleFilterPanelAction.setEnabled(true); this.contextPanelToggler.setEnabled(true); - this.treeGrid.setState(State.ENABLED); + this.setContentTreeState(State.ENABLED); Router.get().setHash(UrlAction.BROWSE); ProjectContext.get().unProjectChanged(projectSetHandler); }; @@ -88,36 +115,63 @@ export class ContentBrowsePanel protected initListeners() { super.initListeners(); - this.onShown(() => { - this.treeGrid.resizeCanvas(); - }); - this.filterPanel.onSearchEvent((query?: ContentQuery) => { - this.treeGrid.setTargetBranch(this.filterPanel.getTargetBranch()); - this.treeGrid.setFilterQuery(query); + this.treeListBox.setTargetBranch(this.filterPanel.getTargetBranch()); + this.treeListBox.setFilterQuery(query); }); this.handleGlobalEvents(); - this.treeGrid.onSelectionOrHighlightingChanged(() => { + this.selectableListBoxPanel.onSelectionChanged(() => { const previewPanel: ContentItemPreviewPanel = this.getPreviewPanel(); - const selectedItem: ContentSummaryAndCompareStatus = this.treeGrid.getLastSelectedOrHighlightedItem(); + const selectedItem: ContentSummaryAndCompareStatus = this.selectableListBoxPanel.getLastSelectedItem(); if (!!selectedItem && previewPanel.isPreviewUpdateNeeded(selectedItem)) { previewPanel.showMask(); } - }, false); + }); - this.treeGrid.onDoubleClick(() => { - const previewPanel: ContentItemPreviewPanel = this.getPreviewPanel(); + this.treeListBox.onItemsAdded((items: ContentSummaryAndCompareStatus[], itemViews: ContentsTreeGridListElement[]) => { + items.forEach((item: ContentSummaryAndCompareStatus, index) => { + const listElement = itemViews[index]?.getDataView(); - if (previewPanel.isMaskOn()) { - previewPanel.hideMask(); // dbl click, item is not selected, no need to show a load mask - } + listElement?.onDblClicked(() => { + new EditContentEvent([item]).fire(); + }); + + listElement?.onContextMenu((event: MouseEvent) => { + event.preventDefault(); + this.contextMenu.showAt(event.clientX, event.clientY); + }); + }); + }); + } + + createListBoxPanel(): SelectableListBoxPanel { + this.treeListBox = new ContentsTreeGridRootList({scrollParent: this}); + + this.selectionWrapper = new SelectableListBoxWrapper(this.treeListBox, { + className: 'content-list-box-wrapper content-tree-grid', + maxSelected: 0, + checkboxPosition: 'left', + highlightMode: true, + }); + + this.toolbar = new ListBoxToolbar(this.selectionWrapper, { + refreshAction: () => this.treeListBox.load(), }); + + this.treeActions = new ContentTreeActions(this.selectionWrapper); + this.contextMenu = new TreeGridContextMenu(this.treeActions); + this.keyNavigator = new SelectableTreeListBoxKeyNavigator(this.selectionWrapper); + + const panel = new SelectableListBoxPanel(this.selectionWrapper, this.toolbar); + panel.addClass('content-selectable-list-box-panel'); + + return panel; } - protected getBrowseActions(): ContentTreeGridActions { - return super.getBrowseActions() as ContentTreeGridActions; + protected getBrowseActions(): ContentTreeActions { + return this.treeActions; } getNonToolbarActions(): Action[] { @@ -132,10 +186,6 @@ export class ContentBrowsePanel return new ContentBrowseToolbar(this.getBrowseActions().getPublishAction()); } - protected createTreeGrid(): ContentTreeGrid { - return new ContentTreeGrid(); - } - protected createBrowseItemPanel(): ContentBrowseItemPanel { return new ContentBrowseItemPanel(); } @@ -145,7 +195,7 @@ export class ContentBrowsePanel const showMask = () => { if (this.isVisible()) { - this.treeGrid.mask(); + // show mask on tree? } }; @@ -155,17 +205,18 @@ export class ContentBrowsePanel } protected updateFilterPanelOnSelectionChange() { - this.filterPanel.setSelectedItems(this.treeGrid.getSelectedItems()); + this.filterPanel.setSelectedItems(this.selectableListBoxPanel.getSelectedItems().map(item => item.getId())); } protected enableSelectionMode() { - this.filterPanel.setSelectedItems(this.treeGrid.getSelectedItems()); + this.filterPanel.setSelectedItems(this.selectableListBoxPanel.getSelectedItems().map(item => item.getId())); } protected disableSelectionMode() { this.filterPanel.resetConstraints(); this.hideFilterPanel(); super.disableSelectionMode(); + this.treeListBox.setFilterQuery(null); } protected createContextView(): ContextView { @@ -198,8 +249,8 @@ export class ContentBrowsePanel }); ToggleSearchPanelWithDependenciesEvent.on((event: ToggleSearchPanelWithDependenciesEvent) => { - if (this.treeGrid.getToolbar().getSelectionPanelToggler().isActive()) { - this.treeGrid.getToolbar().getSelectionPanelToggler().setActive(false); + if (this.toolbar.getSelectionPanelToggler().isActive()) { + this.toolbar.getSelectionPanelToggler().setActive(false); } this.showFilterPanel(); @@ -210,18 +261,11 @@ export class ContentBrowsePanel SearchAndExpandItemEvent.on((event: SearchAndExpandItemEvent) => { const contentId: ContentId = event.getContentId(); - if (this.treeGrid.getToolbar().getSelectionPanelToggler().isActive()) { - this.treeGrid.getToolbar().getSelectionPanelToggler().setActive(false); + if (this.toolbar.getSelectionPanelToggler().isActive()) { + this.toolbar.getSelectionPanelToggler().setActive(false); } this.showFilterPanel(); - const expandLoadedItemHandler = () => { - if (this.treeGrid.isFiltered()) { - this.treeGrid.unLoaded(expandLoadedItemHandler); - this.treeGrid.expandNodeByDataId(contentId.toString()); - } - }; - this.treeGrid.onLoaded(expandLoadedItemHandler); this.filterPanel.searchItemById(contentId); }); @@ -237,24 +281,22 @@ export class ContentBrowsePanel RepositoryEvent.on(event => { if (event.isRestored()) { - this.treeGrid.reload().then(() => { - this.updatePreviewItem(); - }); + this.treeListBox.load(); } }); ProjectContext.get().onProjectChanged(() => { - this.treeGrid.deselectAll(); + this.selectionWrapper.deselectAll(true); this.filterPanel.reset().then(() => { this.hideFilterPanel(); this.toggleFilterPanelButton.removeClass('filtered'); - this.treeGrid.reload(); }); }); ProjectContext.get().onNoProjectsAvailable(() => { this.handleProjectNotSet(); - this.treeGrid.clean(); + this.selectionWrapper.deselectAll(true); + this.treeListBox.clearItems(true); }); } @@ -262,7 +304,7 @@ export class ContentBrowsePanel const path: string = this.getPathFromInlinePath(contentInlinePath); if (path) { - this.treeGrid.selectInlinedContentInGrid(ContentPath.create().fromString(path).build()); + // possibly don't need this, but presumes expanding tree structure till the item is found } } @@ -310,17 +352,50 @@ export class ContentBrowsePanel if (data?.length > 0) { this.handleCUD(); - this.treeGrid.addContentNodes(data); + this.addNewItemsToList(data); this.refreshFilterWithDelay(); } } + private addNewItemsToList(data: ContentSummaryAndCompareStatus[]): void { + data.forEach((item: ContentSummaryAndCompareStatus) => { + this.addNewItemToList(item); + }); + } + + private addNewItemToList(item: ContentSummaryAndCompareStatus): void { + this.treeListBox.findParentLists(item).forEach(list => { + if (!this.treeListBox.isFiltered() || list !== this.treeListBox) { // if filtered, don't add to root list + list.addNewItems([item]); + this.setListItemHasChildren(list); // if item didn't have children before then need to update it without re-fetching + + if (list.getParentItem() && this.selectionWrapper.isItemSelected(list.getParentItem())) { + this.updateBrowseActions(); + } + } + }); + } + + private setListItemHasChildren(list: ContentsTreeGridList): void { + const listItem = list.getParentItem(); + + if (listItem && !listItem.hasChildren()) { + const newContSumm = new ContentSummaryBuilder(listItem.getContentSummary()).setHasChildren(true).build(); + const newContSummAndCompStatus = ContentSummaryAndCompareStatus.fromContentAndCompareAndPublishStatus(newContSumm, + listItem.getCompareStatus(), listItem.getPublishStatus()); + list.getParentListElement().replaceItems(newContSummAndCompStatus); + } + } + private handleContentRenamed(data: ContentSummaryAndCompareStatus[], oldPaths: ContentPath[]) { if (ContentBrowsePanel.debug) { console.debug('ContentBrowsePanel: renamed', data, oldPaths); } - this.treeGrid.renameContentNodes(data); + data.forEach((item: ContentSummaryAndCompareStatus) => { + this.treeListBox.findParentLists(item).forEach(list => list.replaceItems(item)); + }); + this.refreshFilterWithDelay(); } @@ -342,12 +417,19 @@ export class ContentBrowsePanel } if (!data || data.length === 0 || - !data.some((summary: ContentSummaryAndCompareStatus) => this.treeGrid.hasItemWithDataId(summary.getId()))) { + !data.some((summary: ContentSummaryAndCompareStatus) => this.treeListBox.getItem(summary.getId()))) { return; } - this.treeGrid.copyStatusFromExistingNodes(data); - this.treeGrid.updateNodes(data); + data.forEach((newItem: ContentSummaryAndCompareStatus) => { + const existingItem: ContentSummaryAndCompareStatus = this.treeListBox.getItem(newItem.getId()); + + if (existingItem) { + newItem.setCompareStatus(existingItem.getCompareStatus()); + } + }); + + this.treeListBox.replaceItems(data); } private handleContentDeleted(items: DeletedContentItem[]) { @@ -356,16 +438,62 @@ export class ContentBrowsePanel } this.handleCUD(); - this.treeGrid.deleteItems(items); + this.deleteTreeItems(items); - if (this.treeGrid.isFiltered() && this.treeGrid.isEmpty()) { - this.treeGrid.resetFilter(); + if (this.treeListBox.isFiltered() && this.treeListBox.getItems().length === 0) { + this.treeListBox.setFilterQuery(null); } this.updateContextPanelOnNodesDelete(items); this.refreshFilterWithDelay(); } + private deleteTreeItems(toDeleteItems: DeletedContentItem[]): void { + const itemsToDeselect = toDeleteItems.map(toDeleteItem => ContentSummaryAndCompareStatus.fromContentSummary( + new ContentSummaryBuilder().setId(toDeleteItem.id.toString()).build())); + this.selectionWrapper.deselect(itemsToDeselect); + + toDeleteItems.forEach((toDeleteItem) => { + this.findAndUpdateParentsLists(toDeleteItem); + }); + } + + private findAndUpdateParentsLists(toDeleteItem: DeletedContentItem): void { + this.treeListBox.findParentLists(toDeleteItem.path).forEach(parentList => { + if (parentList.wasAlreadyShownAndLoaded()) { + this.removeItemFromParentList(parentList, toDeleteItem); + } + + this.updateParentListHasChildren(parentList); + }); + } + + private removeItemFromParentList(parentList: ContentsTreeGridList, toDeleteItem: DeletedContentItem): void { + const itemInList = parentList.getItems().find(item => item.getContentId().equals(toDeleteItem.id)); + + if (itemInList) { + parentList.removeItems(itemInList); + } + } + + private updateParentListHasChildren(parentList: ContentsTreeGridList): void { + const parentItem = parentList.getParentItem(); + + if (!parentItem) { + return; + } + + new GetContentSummaryByIdRequest(parentItem.getContentId()).sendAndParse().then((updatedItem) => { + if (!updatedItem.hasChildren()) { + const newContSumm = new ContentSummaryBuilder(updatedItem).build(); + const newContSummAndCompStatus = ContentSummaryAndCompareStatus.fromContentAndCompareAndPublishStatus( + newContSumm, + parentItem.getCompareStatus(), parentItem.getPublishStatus()); + parentList.getParentList().replaceItems(newContSummAndCompStatus); + } + }).catch(DefaultErrorHandler.handle); + } + private updateContextPanelOnNodesDelete(items: DeletedContentItem[]) { const itemInDetailPanel: ContentSummaryAndCompareStatus = this.contextView.getItem(); @@ -403,8 +531,20 @@ export class ContentBrowsePanel private doHandleContentUpdate(data: ContentSummaryAndCompareStatus[]) { this.handleCUD(); this.updateContextPanel(data); - this.treeGrid.copyPermissionsFromExistingNodes(data); - this.treeGrid.updateNodes(data); + + data.forEach((newItem: ContentSummaryAndCompareStatus) => { + const existingItem: ContentSummaryAndCompareStatus = this.treeListBox.getItem(newItem.getId()); + + if (existingItem) { + newItem.setReadOnly(existingItem.isReadOnly()); + const parentLists = this.treeListBox.findParentLists(newItem); + parentLists.forEach(list => list.replaceItems(newItem)); + } else if (this.selectionWrapper.isItemSelected(newItem)) { + // Need to update selected item anyway + this.selectionWrapper.updateItemIfSelected(newItem); + } + }); + this.refreshFilterWithDelay(); } @@ -413,13 +553,25 @@ export class ContentBrowsePanel console.debug('ContentBrowsePanel: sorted', data); } - this.treeGrid.sortNodesChildren(data); + data.forEach((item: ContentSummaryAndCompareStatus) => { + this.treeListBox.findParentLists(item).forEach(list => { + list.replaceItems(item); + + const itemElement = list.getItemView(item) as ContentsTreeGridListElement; + const itemList = itemElement.getList() as ContentsTreeGridList; + + if (itemList.wasAlreadyShownAndLoaded()) { + itemList.load(); + } + }); + }); + this.updateContextPanel(data); } private handleNewMediaUpload(event: NewMediaUploadEvent) { event.getUploadItems().forEach((item: UploadItem) => { - this.treeGrid.appendUploadNode(item); + this.appendUploadNode(item); }); } @@ -456,7 +608,7 @@ export class ContentBrowsePanel } private createContentPublishMenuButton() { - const browseActions: ContentTreeGridActions = this.getBrowseActions(); + const browseActions: ContentTreeActions = this.getBrowseActions(); const contentActionMenuButton: ContentActionMenuButton = new ContentActionMenuButton({ defaultAction: browseActions.getAction(ActionName.MARK_AS_READY), menuActions: [ @@ -472,20 +624,16 @@ export class ContentBrowsePanel dropdownPosition: MenuButtonDropdownPos.RIGHT }); - this.treeGrid.onSelectionChanged(() => { - const totalSelected: number = this.treeGrid.getTotalSelected(); + this.selectionWrapper.onSelectionChanged(() => { + const totalSelected: number = this.selectionWrapper.getSelectedItems().length; if (totalSelected === 0) { contentActionMenuButton.setItem(null); } else if (totalSelected === 1) { - contentActionMenuButton.setItem(this.treeGrid.getFirstSelectedItem()); + contentActionMenuButton.setItem(this.selectionWrapper.getSelectedItems()[0]); } }); - this.treeGrid.onHighlightingChanged(() => { - contentActionMenuButton.setItem(this.treeGrid.hasHighlightedNode() ? this.treeGrid.getHighlightedItem() : null); - }); - this.browseToolbar.addContainer(contentActionMenuButton, contentActionMenuButton.getChildControls()); this.browseToolbar.addElement(this.contextPanelToggler); @@ -501,7 +649,7 @@ export class ContentBrowsePanel private handleCUD() { IsRenderableRequest.clearCache(); - if (this.treeGrid.hasSelectedOrHighlightedNode()) { + if (this.selectableListBoxPanel.getSelectedItems().length > 0) { this.browseActionsAndPreviewUpdateRequired = true; this.debouncedBrowseActionsAndPreviewRefreshOnDemand(); } @@ -510,11 +658,11 @@ export class ContentBrowsePanel protected updateActionsAndPreview(): void { this.browseActionsAndPreviewUpdateRequired = false; - const selectedItem: ContentSummaryAndCompareStatus = this.treeGrid.getLastSelectedOrHighlightedItem(); + const selectedItem: ContentSummaryAndCompareStatus = this.selectionWrapper.getSelectedItems().pop(); if (selectedItem) { new IsRenderableRequest(selectedItem.getContentSummary()).sendAndParse().then((statusCode: number) => { - this.treeGrid.updateItemIsRenderable(selectedItem.getId(), statusCode === StatusCode.OK); + this.treeListBox.getItem(selectedItem.getId())?.setRenderable(statusCode === StatusCode.OK); super.updateActionsAndPreview(); }).catch(DefaultErrorHandler.handle); } else { @@ -529,4 +677,45 @@ export class ContentBrowsePanel protected updateContextView(item: ContentSummaryAndCompareStatus): Q.Promise { return this.contextView.setItem(item); } + + setContentTreeState(state: State) { + this.state = state; + + if (this.state === State.ENABLED) { + this.toolbar.enable(); + this.keyNavigator.enableKeys(); + } else { + this.toolbar.disable(); + this.keyNavigator.disableKeys(); + } + } + + appendUploadNode(item: UploadItem) { + const data: ContentSummaryAndCompareStatus = ContentSummaryAndCompareStatus.fromUploadItem(item); + const parentLists = this.treeListBox.findParentLists(data); + const pLists = parentLists.length > 0 ? parentLists : [this.treeListBox]; + + pLists.forEach(parent => { + parent.addNewItems([data]); + this.addUploadItemListeners(data); + }); + } + + private addUploadItemListeners(data: ContentSummaryAndCompareStatus) { + const uploadItem: UploadItem = data.getUploadItem(); + const listElement = this.treeListBox.getItemView(data) as ContentsTreeGridListElement; + + uploadItem.onProgress(() => { + listElement.setItem(data); + }); + + uploadItem.onUploaded(() => { + this.treeListBox.removeItems(data); + showFeedback(i18n('notify.item.created', data.getContentSummary().getType().toString(), uploadItem.getName())); + }); + + uploadItem.onFailed(() => { + this.treeListBox.removeItems(data); + }); + } } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentGridDragHandler.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentGridDragHandler.ts index 77e6088eff..4bb998c93e 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/ContentGridDragHandler.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/ContentGridDragHandler.ts @@ -1,16 +1,19 @@ import {OrderChildMovements} from '../resource/order/OrderChildMovements'; import {OrderChildMovement} from '../resource/order/OrderChildMovement'; import {ContentSummaryAndCompareStatus} from '../content/ContentSummaryAndCompareStatus'; -import {GridDragHandler} from '@enonic/lib-admin-ui/ui/grid/GridDragHandler'; -import {TreeGrid} from '@enonic/lib-admin-ui/ui/treegrid/TreeGrid'; -import {ContentId} from '../content/ContentId'; +import {DragHandler} from './DragHandler'; +import {ListBox} from '@enonic/lib-admin-ui/ui/selector/list/ListBox'; -export class ContentGridDragHandler extends GridDragHandler { +export class ContentGridDragHandler extends DragHandler { private movements: OrderChildMovements; - constructor(treeGrid: TreeGrid) { - super(treeGrid); + private listBox: ListBox; + + constructor(listBox: ListBox) { + super(listBox); + + this.listBox = listBox; this.movements = new OrderChildMovements(); } @@ -22,11 +25,10 @@ export class ContentGridDragHandler extends GridDragHandler from ? to + 1 : to]; - getModelId(model: ContentSummaryAndCompareStatus) { - return model ? model.getContentId() : null; + this.movements.addChildMovement(new OrderChildMovement(movedItem.getContentId(), moveBeforeItem?.getContentId())); } } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentRowFormatter.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentRowFormatter.ts deleted file mode 100644 index 797fe11b6c..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/browse/ContentRowFormatter.ts +++ /dev/null @@ -1,104 +0,0 @@ -import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; -import {SpanEl} from '@enonic/lib-admin-ui/dom/SpanEl'; -import {ObjectHelper} from '@enonic/lib-admin-ui/ObjectHelper'; -import {ProgressBar} from '@enonic/lib-admin-ui/ui/ProgressBar'; -import {TreeNode} from '@enonic/lib-admin-ui/ui/treegrid/TreeNode'; -import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; -import {ContentSummaryAndCompareStatus} from '../content/ContentSummaryAndCompareStatus'; -import {ContentSummaryAndCompareStatusViewer} from '../content/ContentSummaryAndCompareStatusViewer'; -import {ContentSummaryListViewer} from '../content/ContentSummaryListViewer'; -import {MediaTreeSelectorItem} from '../inputtype/ui/selector/media/MediaTreeSelectorItem'; -import {ContentAndStatusTreeSelectorItem} from '../item/ContentAndStatusTreeSelectorItem'; -import {ContentTreeSelectorItem} from '../item/ContentTreeSelectorItem'; - -export class ContentRowFormatter { - - public static nameFormatter(_row: number, _cell: number, _value: unknown, _columnDef: unknown, - node: TreeNode): string { - const data = node.getData(); - if (data.getContentSummary() || data.getUploadItem()) { - let viewer = node.getViewer('name') as ContentSummaryAndCompareStatusViewer; - if (!viewer) { - viewer = new ContentSummaryListViewer(); - node.setViewer('name', viewer); - } - viewer.setIsRelativePath(node.calcLevel() > 1); - viewer.setObject(node.getData()); - return viewer ? viewer.toString() : ''; - } - - return ''; - } - - public static orderFormatter(_row: number, _cell: number, value: string, _columnDef: object, - node: TreeNode): string { - let wrapper = new SpanEl(); - - if (!StringHelper.isBlank(value)) { - wrapper.setTitle(value); - } - - if (node.getData().getContentSummary()) { - let childOrder = node.getData().getContentSummary().getChildOrder(); - let icon; - if (!childOrder.isDefault()) { - let iconCls = 'sort-dialog-trigger '; - if (!childOrder.isManual()) { - if (childOrder.isDesc()) { - iconCls += childOrder.isAlpha() ? 'icon-sort-alpha-desc' : 'icon-sort-num-desc'; - } else { - iconCls += childOrder.isAlpha() ? 'icon-sort-alpha-asc' : 'icon-sort-num-asc'; - } - } else { - iconCls += 'icon-menu'; - } - - icon = new DivEl(iconCls); - wrapper.appendChild(icon); - } - } - return wrapper.toString(); - } - - public static statusFormatter(_row: number, _cell: number, value: number, _columnDef: object, - dataContext: TreeNode): string { - return ContentRowFormatter.doStatusFormat(dataContext.getData()); - } - - public static statusSelectorFormatter(_row: number, _cell: number, value: ContentTreeSelectorItem): string { - - if (ObjectHelper.iFrameSafeInstanceOf(value, ContentAndStatusTreeSelectorItem) || - ObjectHelper.iFrameSafeInstanceOf(value, MediaTreeSelectorItem)) { - - const item = value as ContentAndStatusTreeSelectorItem; - if (item.isSelectable() && (item.getCompareStatus() != null || item.getPublishStatus() != null)) { - return ContentRowFormatter.doStatusFormat( - ContentSummaryAndCompareStatus.fromContentAndCompareAndPublishStatus(value.getContent(), - item.getCompareStatus(), - item.getPublishStatus())); - } - } - - return ''; - } - - private static doStatusFormat(data: ContentSummaryAndCompareStatus): string { - - if (data?.getContentSummary()) { - - let status = new SpanEl(); - - status.addClass(data.getStatusClass()); - status.setHtml(data.getStatusText()); - - return status.toString(); - } - - if (data.getUploadItem()) { // uploading node - const compareStatusText = new ProgressBar(data.getUploadItem().getProgress()); - const wrapper: SpanEl = new SpanEl(); - wrapper.getEl().setWidth('100%'); - return wrapper.appendChild(compareStatusText).toString(); - } - } -} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeActions.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeActions.ts new file mode 100644 index 0000000000..6a7d817b33 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeActions.ts @@ -0,0 +1,327 @@ +import * as Q from 'q'; +import {i18n} from '@enonic/lib-admin-ui/util/Messages'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; + + +import {Action} from '@enonic/lib-admin-ui/ui/Action'; +import {TreeGridActions} from '@enonic/lib-admin-ui/ui/treegrid/actions/TreeGridActions'; +import {ManagedActionManager} from '@enonic/lib-admin-ui/managedaction/ManagedActionManager'; +import {ManagedActionState} from '@enonic/lib-admin-ui/managedaction/ManagedActionState'; +import {ManagedActionExecutor} from '@enonic/lib-admin-ui/managedaction/ManagedActionExecutor'; +import {NotifyManager} from '@enonic/lib-admin-ui/notify/NotifyManager'; +import {ContentSummaryAndCompareStatus} from '../content/ContentSummaryAndCompareStatus'; +import {ContentTreeGridAction} from './action/ContentTreeGridAction'; +import {ShowNewContentDialogAction} from './action/ShowNewContentDialogAction'; +import {PreviewContentAction} from './action/PreviewContentAction'; +import {EditContentAction} from './action/EditContentAction'; +import {DuplicateContentAction} from './action/DuplicateContentAction'; +import {ArchiveContentAction} from './action/ArchiveContentAction'; +import {MoveContentAction} from './action/MoveContentAction'; +import {SortContentAction} from './action/SortContentAction'; +import {PublishContentAction} from './action/PublishContentAction'; +import {PublishTreeContentAction} from './action/PublishTreeContentAction'; +import {UnpublishContentAction} from './action/UnpublishContentAction'; +import {MarkAsReadyContentAction} from './action/MarkAsReadyContentAction'; +import {RequestPublishContentAction} from './action/RequestPublishContentAction'; +import {CreateIssueAction} from './action/CreateIssueAction'; +import {ToggleSearchPanelAction} from './action/ToggleSearchPanelAction'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; +import {Permission} from '../access/Permission'; +import {ContentTreeGridItemsState} from './action/ContentTreeGridItemsState'; +import {GetPermittedActionsRequest} from '../resource/GetPermittedActionsRequest'; +import {ContentId} from '../content/ContentId'; +import {HasUnpublishedChildrenRequest} from '../resource/HasUnpublishedChildrenRequest'; +import {HasUnpublishedChildren, HasUnpublishedChildrenResult} from '../resource/HasUnpublishedChildrenResult'; +import {GetContentTypeByNameRequest} from '../resource/GetContentTypeByNameRequest'; +import {ContentType} from '../inputtype/schema/ContentType'; +import {State} from './State'; + + +export enum ActionName { + SHOW_NEW_DIALOG, PREVIEW, EDIT, ARCHIVE, DUPLICATE, MOVE, SORT, PUBLISH, PUBLISH_TREE, UNPUBLISH, MARK_AS_READY, REQUEST_PUBLISH, + CREATE_ISSUE, TOGGLE_SEARCH_PANEL +} + +export class ContentTreeActions implements TreeGridActions { + + private readonly grid: SelectableListBoxWrapper; + + private actionsMap: Map = new Map(); + + private beforeActionsStashedListeners: (() => void)[] = []; + + private actionsUnStashedListeners: (() => void)[] = []; + + private state: State = State.ENABLED; + + constructor(grid: SelectableListBoxWrapper) { + this.grid = grid; + this.initActions(); + this.initListeners(); + } + + private initActions() { + this.actionsMap.set(ActionName.SHOW_NEW_DIALOG, new ShowNewContentDialogAction(this.grid)); + this.actionsMap.set(ActionName.PREVIEW, new PreviewContentAction(this.grid)); + this.actionsMap.set(ActionName.EDIT, new EditContentAction(this.grid)); + this.actionsMap.set(ActionName.ARCHIVE, new ArchiveContentAction(this.grid)); + this.actionsMap.set(ActionName.DUPLICATE, new DuplicateContentAction(this.grid)); + this.actionsMap.set(ActionName.MOVE, new MoveContentAction(this.grid)); + this.actionsMap.set(ActionName.SORT, new SortContentAction(this.grid)); + this.actionsMap.set(ActionName.PUBLISH, new PublishContentAction(this.grid)); + this.actionsMap.set(ActionName.PUBLISH_TREE, new PublishTreeContentAction(this.grid)); + this.actionsMap.set(ActionName.UNPUBLISH, new UnpublishContentAction(this.grid)); + this.actionsMap.set(ActionName.MARK_AS_READY, new MarkAsReadyContentAction(this.grid)); + this.actionsMap.set(ActionName.REQUEST_PUBLISH, new RequestPublishContentAction(this.grid)); + this.actionsMap.set(ActionName.CREATE_ISSUE, new CreateIssueAction(this.grid)); + this.actionsMap.set(ActionName.TOGGLE_SEARCH_PANEL, new ToggleSearchPanelAction(this.grid)); + } + + private initListeners() { + const previewStateChangedHandler = value => { + this.actionsMap.get(ActionName.PREVIEW).setEnabled(value); + }; + + const managedActionsHandler = (state: ManagedActionState, executor: ManagedActionExecutor) => { + if (state === ManagedActionState.PREPARING) { + this.notifyBeforeActionsStashed(); + this.actionsMap.forEach((action: ContentTreeGridAction) => action.stash()); + } else if (state === ManagedActionState.ENDED) { + this.actionsMap.forEach((action: ContentTreeGridAction) => action.unStash()); + this.notifyActionsUnStashed(); + } + }; + + ManagedActionManager.instance().onManagedActionStateChanged(managedActionsHandler); + + this.grid.onRemoved(() => { + ManagedActionManager.instance().unManagedActionStateChanged(managedActionsHandler); + }); + } + + setState(state: State) { + this.state = state; + + if (this.state === State.DISABLED) { + this.disableAllActions(); + } else { + this.updateActionsEnabledState([]); + } + } + + private disableAllActions() { + this.actionsMap.forEach((action: ContentTreeGridAction) => action.setEnabled(false)); + } + + onBeforeActionsStashed(listener: () => void) { + this.beforeActionsStashedListeners.push(listener); + } + + private notifyBeforeActionsStashed() { + this.beforeActionsStashedListeners.forEach((listener) => { + listener(); + }); + } + + onActionsUnStashed(listener: () => void) { + this.actionsUnStashedListeners.push(listener); + } + + private notifyActionsUnStashed() { + this.actionsUnStashedListeners.forEach((listener) => { + listener(); + }); + } + + getAllCommonActions(): Action[] { + return [ + this.getAction(ActionName.SHOW_NEW_DIALOG), + this.getAction(ActionName.EDIT), + this.getAction(ActionName.ARCHIVE), + this.getAction(ActionName.DUPLICATE), + this.getAction(ActionName.MOVE), + this.getAction(ActionName.SORT), + this.getAction(ActionName.PREVIEW) + ]; + } + + getPublishActions(): Action[] { + return [ + this.getAction(ActionName.PUBLISH), + this.getAction(ActionName.UNPUBLISH) + ]; + } + + getPublishAction(): Action { + return this.getAction(ActionName.PUBLISH); + } + + getToggleSearchPanelAction(): Action { + return this.getAction(ActionName.TOGGLE_SEARCH_PANEL); + } + + getAllActionsNoPublish(): Action[] { + return [ + ...this.getAllCommonActions() + ]; + } + + getAllActionsNoPendingDelete(): Action[] { + return [ + ...this.getAllCommonActions(), + this.getAction(ActionName.UNPUBLISH) + ]; + } + + getEditAction(): Action { + return this.getAction(ActionName.EDIT); + } + + getAllActions(): Action[] { + return [ + ...this.getAllActionsNoPublish(), + ...this.getPublishActions() + ]; + } + + updateActionsEnabledState(items: ContentSummaryAndCompareStatus[]): Q.Promise { + if (this.state === State.DISABLED) { + return Q(null); + } + + this.getAction(ActionName.TOGGLE_SEARCH_PANEL).setVisible(false); + + const parallelPromises: Q.Promise[] = [ + this.doUpdateActionsEnabledState(items) + ]; + + return Q.all(parallelPromises).catch(DefaultErrorHandler.handle); + } + + private showDefaultActions() { + const defaultActions = [ + this.getAction(ActionName.SHOW_NEW_DIALOG), + this.getAction(ActionName.EDIT), + this.getAction(ActionName.ARCHIVE), + this.getAction(ActionName.DUPLICATE), + this.getAction(ActionName.MOVE), + this.getAction(ActionName.SORT), + this.getAction(ActionName.PREVIEW), + this.getAction(ActionName.PUBLISH) + ]; + defaultActions.forEach(action => action.setVisible(true)); + } + + private doUpdateActionsEnabledState(items: ContentSummaryAndCompareStatus[]): Q.Promise { + return this.getAllowedPermissions(items).then((permissions: Permission[]) => { + const state: ContentTreeGridItemsState = new ContentTreeGridItemsState(items, permissions); + this.toggleActions(state); + + if (items.length === 0) { + this.toggleVisibilityNoItemsSelected(); + return Q(null); + } + + this.toggleVisibility(state); + return this.updateDefaultActionsMultipleItemsSelected(items); + }); + } + + private getAllowedPermissions(items: ContentSummaryAndCompareStatus[]): Q.Promise { + const request: GetPermittedActionsRequest = new GetPermittedActionsRequest(); + + if (items.length === 0) { + request.addPermissionsToBeChecked(Permission.CREATE); + } else { + const contentIds: ContentId[] = items.map((item: ContentSummaryAndCompareStatus) => item.getContentId()); + request.addContentIds(...contentIds); + request.addPermissionsToBeChecked(Permission.CREATE, Permission.DELETE, Permission.PUBLISH, Permission.MODIFY); + } + + return request.sendAndParse(); + } + + private toggleActions(state: ContentTreeGridItemsState) { + this.actionsMap.forEach((action: ContentTreeGridAction) => action.setEnabledByState(state)); + } + + private toggleVisibility(state: ContentTreeGridItemsState) { + if (state.hasAnyPendingDelete()) { + const invisibleActions: Action[] = state.hasAllPendingDelete() + ? this.getAllActionsNoPendingDelete() + : this.getAllActions(); + invisibleActions.forEach(action => action.setVisible(false)); + } else { + this.getAllCommonActions().forEach(action => action.setVisible(true)); + this.getAction(ActionName.UNPUBLISH).setVisible(this.getAction(ActionName.UNPUBLISH).isEnabled()); + } + + this.getAction(ActionName.PUBLISH).setVisible( + state.hasAllPendingDelete() || this.getAction(ActionName.PUBLISH).isEnabled()); + (this.getAction(ActionName.EDIT) as EditContentAction).updateLabel(state); + } + + private toggleVisibilityNoItemsSelected() { + this.getAction(ActionName.UNPUBLISH).setVisible(false); + (this.getAction(ActionName.EDIT) as EditContentAction).resetLabel(); + this.showDefaultActions(); + } + + private handleContentTypeNotFound(selectedItem: ContentSummaryAndCompareStatus) { + NotifyManager.get().showWarning( + i18n('notify.contentType.notFound', selectedItem.getContentSummary().getType().getLocalName())); + + this.disableAllActions(); + this.getAction(ActionName.CREATE_ISSUE).setEnabled(true); + + this.getAction(ActionName.UNPUBLISH).setVisible(false); + (this.getAction(ActionName.EDIT) as EditContentAction).resetLabel(); + this.showDefaultActions(); + } + + private updateDefaultActionsMultipleItemsSelected(items: ContentSummaryAndCompareStatus[]): Q.Promise { + const promises: Q.Promise[] = []; + + if (items.length === 1 && + (this.getAction(ActionName.SHOW_NEW_DIALOG).isEnabled() || this.getAction(ActionName.SORT).isEnabled())) { + promises.push(this.checkIsChildrenAllowedByContentType(items[0]).then((childrenAllowed: boolean) => { + if (!childrenAllowed) { + this.getAction(ActionName.SHOW_NEW_DIALOG).setEnabled(false); + this.getAction(ActionName.SORT).setEnabled(false); + } + + return Q(); + })); + } + + if (this.getAction(ActionName.PUBLISH_TREE).isEnabled()) { + promises.push(this.updatePublishTreeAction(items)); + } + + return Q.all(promises).thenResolve(null); + } + + private updatePublishTreeAction(items: ContentSummaryAndCompareStatus[]): Q.Promise { + return new HasUnpublishedChildrenRequest(items.map((item: ContentSummaryAndCompareStatus) => item.getContentId())) + .sendAndParse().then((hasUnpublishedChildrenResult: HasUnpublishedChildrenResult) => { + const hasUnpublishedChildren: boolean = + hasUnpublishedChildrenResult.getResult().some((item: HasUnpublishedChildren) => item.getHasChildren()); + + this.getAction(ActionName.PUBLISH_TREE).setEnabled(hasUnpublishedChildren); + }).catch(reason => DefaultErrorHandler.handle(reason)); + } + + private checkIsChildrenAllowedByContentType(selectedItem: ContentSummaryAndCompareStatus): Q.Promise { + const deferred = Q.defer(); + + new GetContentTypeByNameRequest(selectedItem.getContentSummary().getType()).sendAndParse() + .then((contentType: ContentType) => deferred.resolve(contentType && contentType.isAllowChildContent())) + .fail(() => this.handleContentTypeNotFound(selectedItem)); + + return deferred.promise; + } + + getAction(name: ActionName): Action { + return this.actionsMap.get(name); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeGrid.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeGrid.ts deleted file mode 100644 index 42c7f91863..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeGrid.ts +++ /dev/null @@ -1,842 +0,0 @@ -import * as Q from 'q'; -import {ElementHelper} from '@enonic/lib-admin-ui/dom/ElementHelper'; -import {i18n} from '@enonic/lib-admin-ui/util/Messages'; -import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; -import {SortContentEvent} from './sort/SortContentEvent'; -import {ContentTreeGridActions} from './action/ContentTreeGridActions'; -import {ContentTreeGridToolbar} from './ContentTreeGridToolbar'; -import {ContentTreeGridLoadedEvent} from './ContentTreeGridLoadedEvent'; -import {ContentQueryRequest} from '../resource/ContentQueryRequest'; -import {ContentQueryResult} from '../resource/ContentQueryResult'; -import {ContentSummaryAndCompareStatusFetcher} from '../resource/ContentSummaryAndCompareStatusFetcher'; -import {ContentResponse} from '../resource/ContentResponse'; -import {ContentRowFormatter} from './ContentRowFormatter'; -import {EditContentEvent} from '../event/EditContentEvent'; -import {ContentSummaryAndCompareStatus} from '../content/ContentSummaryAndCompareStatus'; -import {ContentQuery} from '../content/ContentQuery'; -import {ResultMetadata} from '../resource/ResultMetadata'; -import {TreeGrid} from '@enonic/lib-admin-ui/ui/treegrid/TreeGrid'; -import {TreeNode} from '@enonic/lib-admin-ui/ui/treegrid/TreeNode'; -import {TreeGridBuilder} from '@enonic/lib-admin-ui/ui/treegrid/TreeGridBuilder'; -import {DateTimeFormatter} from '@enonic/lib-admin-ui/ui/treegrid/DateTimeFormatter'; -import {TreeGridContextMenu} from '@enonic/lib-admin-ui/ui/treegrid/TreeGridContextMenu'; -import {Expand} from '@enonic/lib-admin-ui/rest/Expand'; -import {UploadItem} from '@enonic/lib-admin-ui/ui/uploader/UploadItem'; -import {GridColumnConfig} from '@enonic/lib-admin-ui/ui/grid/GridColumn'; -import {showFeedback} from '@enonic/lib-admin-ui/notify/MessageBus'; -import {DeletedContentItem} from './DeletedContentItem'; -import {ContentSummary, ContentSummaryBuilder} from '../content/ContentSummary'; -import {ChildOrder} from '../resource/order/ChildOrder'; -import {ContentId} from '../content/ContentId'; -import {ContentSummaryJson} from '../content/ContentSummaryJson'; -import {ContentPath} from '../content/ContentPath'; -import {ContentTreeGridDeselectAllEvent} from './ContentTreeGridDeselectAllEvent'; -import {Branch} from '../versioning/Branch'; - -export enum State { - ENABLED, DISABLED -} - -export class ContentTreeGrid - extends TreeGrid { - - static MAX_FETCH_SIZE: number = 10; - - private filterQuery: ContentQuery; - - private state: State; - - private contentFetcher: ContentSummaryAndCompareStatusFetcher; - - private doubleClickListeners: (() => void)[] = []; - - private branch: Branch = Branch.DRAFT; - - constructor() { - const builder: TreeGridBuilder = - new TreeGridBuilder() - .setColumnConfig(ContentTreeGrid.createColumnConfig()) - .setPartialLoadEnabled(true) - .setLoadBufferSize(20) - .prependClasses('content-tree-grid'); - - super(builder); - - this.contentFetcher = new ContentSummaryAndCompareStatusFetcher(); - this.state = State.ENABLED; - this.setContextMenu(new TreeGridContextMenu(new ContentTreeGridActions(this))); - - this.initEventHandlers(); - } - - private static createColumnConfig(): GridColumnConfig[] { - return [{ - name: 'Name', - id: 'displayName', - field: 'contentSummary.displayName', - formatter: ContentRowFormatter.nameFormatter, - style: {cssClass: 'name', minWidth: 130} - }, { - name: 'Order', - id: 'order', - field: 'contentSummary.order', - formatter: ContentRowFormatter.orderFormatter, - style: {cssClass: 'order', minWidth: 25, maxWidth: 40} - }, { - name: 'CompareStatus', - id: 'compareStatus', - field: 'compareStatus', - formatter: ContentRowFormatter.statusFormatter, - style: {cssClass: 'status', minWidth: 75, maxWidth: 75} - }, { - name: 'ModifiedTime', - id: 'modifiedTime', - field: 'contentSummary.modifiedTime', - formatter: DateTimeFormatter.format, - style: {cssClass: 'modified', minWidth: 135, maxWidth: 135} - }]; - } - - protected createToolbar() { - return new ContentTreeGridToolbar(this); - } - - protected editItem(node: TreeNode) { - if (node.getDataId()) { // default event - new EditContentEvent([node.getData()]).fire(); - } - } - - private initEventHandlers() { - this.getGrid().subscribeOnClick(this.handleGridClick.bind(this)); - this.getGrid().subscribeOnDblClick(this.handleGridDoubleClick.bind(this)); - - this.onLoaded(() => { - new ContentTreeGridLoadedEvent().fire(); - }); - - ContentTreeGridDeselectAllEvent.on(() => { - this.deselectAll(); - }); - } - - private handleGridClick(event: JQuery.EventBase) { - const elem: ElementHelper = new ElementHelper(event.target); - if (elem.hasClass('sort-dialog-trigger')) { - new SortContentEvent(this.getSelectedDataList()).fire(); - } - } - - private handleGridDoubleClick(event: JQuery.EventBase, data: Slick.OnDblClickEventArgs) { - if (this.isActive() && this.isEditAllowed(event, data)) { - const node: TreeNode = this.getGrid().getDataView().getItem(data.row); - if (!node.getData().isPendingDelete()) { - /* - * Empty node double-clicked. Additional %MAX_FETCH_SIZE% - * nodes will be loaded and displayed. If any other - * node is clicked, edit event will be triggered by default. - */ - this.editItem(node); - } - } - - this.notifyDoubleClick(); - } - - private isEditAllowed(event: JQuery.EventBase, data: Slick.OnDblClickEventArgs): boolean { - if (data?.cell === 0) { - return false; - } - - return !event?.target?.classList?.contains('toggle'); - } - - setState(state: State) { - this.state = state; - - if (this.state === State.ENABLED) { - this.getToolbar().enable(); - this.enableKeys(); - } else { - this.getToolbar().disable(); - this.disableKeys(); - } - } - - setTargetBranch(branch: Branch): void { - this.branch = branch; - } - - clean() { - this.deselectAll(); - this.getGridData().setItems([]); - } - - reload(): Q.Promise { - if (this.state === State.DISABLED) { - return Q(null); - } - - return super.reload(); - } - - isEmptyNode(node: TreeNode): boolean { - const data = node.getData(); - return !data.getContentSummary() && !data.getUploadItem(); - } - - hasChildren(data: ContentSummaryAndCompareStatus): boolean { - return data.hasChildren(); - } - - fetch(node: TreeNode, dataId?: string): Q.Promise { - return this.fetchById(node.getData().getContentId()); - } - - private fetchById(id: ContentId): Q.Promise { - return this.contentFetcher.fetch(id); - } - - fetchChildren(parentNode?: TreeNode): Q.Promise { - if (!parentNode || this.isRootNode(parentNode)) { - return this.fetchRoot(); - } else { - return this.doFetchChildren(parentNode); - } - } - - setFilterQuery(query: ContentQuery | null): void { - this.filterQuery = query ? new ContentQuery() : null; - - if (query) { - this.filterQuery - .setSize(ContentTreeGrid.MAX_FETCH_SIZE) - .setQueryFilters(query.getQueryFilters()) - .setQuery(query.getQuery()) - .setQuerySort(query.getQuerySort()) - .setContentTypeNames(query.getContentTypes()) - .setMustBeReferencedById(query.getMustBeReferencedById()); - - this.getRoot().setFiltered(true); - this.reload().catch(DefaultErrorHandler.handle); - } else { - this.resetFilter(); - } - } - - private isRootNode(node: TreeNode): boolean { - return !node.hasParent(); - } - - fetchRoot(): Q.Promise { - const root: TreeNode = this.getRoot().getCurrentRoot(); - - this.removeEmptyNode(root); - - if (this.isFiltered()) { - return this.fetchFilteredContents(root); - } - - return this.fetchRootContents(root); - } - - private removeEmptyNode(node: TreeNode) { - if (node.hasChildren() && this.isEmptyNode(node.getChildren()[node.getChildren().length - 1])) { - node.getChildren().pop(); - } - } - - private fetchRootContents(root: TreeNode): Q.Promise { - const from: number = root.getChildren().length; - - return this.contentFetcher.fetchRoot(from, ContentTreeGrid.MAX_FETCH_SIZE).then( - (data: ContentResponse) => { - return this.processContentResponse(root, data, from); - }); - } - - private processContentResponse(node: TreeNode, data: ContentResponse, - from: number): ContentSummaryAndCompareStatus[] { - const contents: ContentSummaryAndCompareStatus[] = node.getChildren().map((el) => { - return el.getData(); - }).slice(0, from).concat(data.getContents()); - - const meta: ResultMetadata = data.getMetadata(); - node.setMaxChildren(meta.getTotalHits()); - if (this.isEmptyNodeNeeded(meta, from)) { - contents.push(new ContentSummaryAndCompareStatus()); - } - - return contents; - } - - private isEmptyNodeNeeded(meta: ResultMetadata, from: number = 0): boolean { - return from + meta.getHits() < meta.getTotalHits(); - } - - private fetchFilteredContents(node: TreeNode): Q.Promise { - const from: number = node.getChildren().length; - - return this.sendContentQueryRequest(from, ContentTreeGrid.MAX_FETCH_SIZE).then( - (contentQueryResult: ContentQueryResult) => { - return this.processContentQueryResponse(node, contentQueryResult, from); - }); - } - - private sendContentQueryRequest(from: number, size: number): Q.Promise> { - return this.makeContentQueryRequest(from, size).sendAndParse(); - } - - private makeContentQueryRequest(from: number, size: number): ContentQueryRequest { - this.filterQuery.setFrom(from).setSize(size); - - return new ContentQueryRequest(this.filterQuery) - .setTargetBranch(this.branch) - .setExpand(Expand.SUMMARY); - } - - private processContentQueryResponse(node: TreeNode, - data: ContentQueryResult, - from: number): Q.Promise { - return this.contentFetcher - .updateReadonlyAndCompareStatus(data.getContents()) - .then((processedContents: ContentSummaryAndCompareStatus[]) => { - - const contents: ContentSummaryAndCompareStatus[] = - node.getChildren() - .map((el: TreeNode) => el.getData()) - .slice(0, from) - .concat(processedContents); - - const meta: ResultMetadata = data.getMetadata(); - if (this.isEmptyNodeNeeded(meta, from)) { - contents.push(new ContentSummaryAndCompareStatus()); - } - node.setMaxChildren(meta.getTotalHits()); - return contents; - }); - } - - private doFetchChildren(parentNode: TreeNode): Q.Promise { - this.removeEmptyNode(parentNode); - - return this.fetchChildrenContents(parentNode); - } - - private fetchChildrenContents(parentNode: TreeNode): Q.Promise { - const parentContentId: ContentId = parentNode.getData().getContentId(); - const from: number = parentNode.getChildren().length; - - return this.contentFetcher.fetchChildren(parentContentId, from, ContentTreeGrid.MAX_FETCH_SIZE).then( - (data: ContentResponse) => { - return this.processContentResponse(parentNode, data, from); - }); - } - - appendUploadNode(item: UploadItem) { - const data: ContentSummaryAndCompareStatus = ContentSummaryAndCompareStatus.fromUploadItem(item); - const parent: TreeNode = - this.getFirstSelectedOrHighlightedNode() || this.getRoot().getDefaultRoot(); - - if (!parent.isExpandable() || parent.hasChildren()) { - const uploadNode: TreeNode = this.dataToTreeNode(data, parent); - this.insertNodeToParentNode(uploadNode, parent, 0); - if (!parent.isExpanded()) { - this.expandNode(parent); - } - this.addUploadItemListeners(uploadNode, data); - } - } - - private addUploadItemListeners(uploadNode: TreeNode, data: ContentSummaryAndCompareStatus) { - const uploadItem: UploadItem = uploadNode.getData().getUploadItem(); - uploadItem.onProgress(() => { - this.invalidateNodes([uploadNode]); - }); - uploadItem.onUploaded(() => { - this.deleteNode(uploadNode); - showFeedback(i18n('notify.item.created', data.getContentSummary().getType().toString(), uploadItem.getName())); - }); - uploadItem.onFailed(() => { - this.deleteNode(uploadNode); - }); - } - - sortNodeChildren(node: TreeNode) { - if (this.isSortableNode(node)) { - this.doSortNodeChildren(node); - } - } - - private isSortableNode(node: TreeNode): boolean { - if (!node.hasChildren()) { - return false; - } - - if (node === this.getRoot().getCurrentRoot()) { - return false; - } - - return true; - } - - private doSortNodeChildren(node: TreeNode) { - node.setChildren([]); - node.setMaxChildren(0); - - this.fetchChildren(node).then((dataList: ContentSummaryAndCompareStatus[]) => { - const parentNode: TreeNode = this.getRoot().getCurrentRoot().findNode(node.getDataId()); - parentNode.setChildren(this.dataToTreeNodes(dataList, parentNode)); - this.reInitData(); - }).catch(DefaultErrorHandler.handle).done(); - } - - private reInitData() { - const rootList: TreeNode[] = this.getRoot().getCurrentRoot().treeToList(); - this.initData(rootList); - } - - selectAll() { - this.getGrid().mask(); - setTimeout(() => { - super.selectAll(); - this.getGrid().unmask(); - }, 5); - } - - private selectNodeByPath(targetPath: ContentPath) { - const currentSelectedNode: TreeNode = this.getFirstSelectedOrHighlightedNode(); - let nodeToSearchTargetIn: TreeNode; - - if (currentSelectedNode && targetPath.isDescendantOf(currentSelectedNode.getData().getPath())) { - nodeToSearchTargetIn = currentSelectedNode; - } else { - nodeToSearchTargetIn = this.getRoot().getCurrentRoot(); - } - - // go down and expand path's parents level by level until we reach the desired element within the list of fetched children - this.doSelectNodeByPath(nodeToSearchTargetIn, targetPath); - } - - private doSelectNodeByPath(nodeToSearchTargetIn: TreeNode, targetPath: ContentPath) { - this.expandNode(nodeToSearchTargetIn).then(() => { - // if true means one of direct children of node is searched target node - if (this.isTargetNodeLevelReached(nodeToSearchTargetIn, targetPath)) { - this.findChildNodeByPath(nodeToSearchTargetIn, targetPath).then((targetNode) => { - this.selectNode(targetNode.getDataId()); - this.scrollToRow(this.getGrid().getDataView().getRowById(targetNode.getId())); - }); - } else { - const nextLevelChildPath: ContentPath = targetPath.getPathAtLevel(!!nodeToSearchTargetIn.getData() - ? nodeToSearchTargetIn.getData().getPath().getLevel() + 1 - : 1); - this.findChildNodeByPath(nodeToSearchTargetIn, nextLevelChildPath).then((targetNode) => { - this.doSelectNodeByPath(targetNode, targetPath); - }); - } - }).catch((reason) => { - this.handleError(reason); - }).done(); - } - - private isTargetNodeLevelReached(nodeToSearchTargetIn: TreeNode, targetPath: ContentPath): boolean { - const nodeToExpandLevel: number = !!nodeToSearchTargetIn.getData() ? nodeToSearchTargetIn.getData().getPath().getLevel() : 0; - const targetNodeLevelReached: boolean = (targetPath.getLevel() - 1) === nodeToExpandLevel; - - return targetNodeLevelReached; - } - - private findChildNodeByPath(node: TreeNode, - childNodePath: ContentPath): Q.Promise> { - const childNode: TreeNode = this.doFindChildNodeByPath(node, childNodePath); - - if (childNode) { - return Q.resolve(childNode); - } - - return this.waitChildrenLoadedAndFindChildNodeByPath(node, childNodePath); - } - - private doFindChildNodeByPath(node: TreeNode, - childNodePath: ContentPath): TreeNode { - const children: TreeNode[] = node.getChildren(); - for (const child of children) { - const childPath: ContentPath = child.getData().getPath(); - - if (childPath && childPath.equals(childNodePath)) { - return child; - } - } - - // scrolling to last child of node to make node load the rest - const child: TreeNode = children[children.length - 1]; - this.scrollToRow(this.getGrid().getDataView().getRowById(child.getId())); - - return null; - } - - private waitChildrenLoadedAndFindChildNodeByPath(node: TreeNode, - childNodePath: ContentPath): Q.Promise> { - const deferred = Q.defer>(); - - const dataChangedHandler = () => { - const childNode: TreeNode = this.doFindChildNodeByPath(node, childNodePath); - if (childNode) { - this.unDataChanged(dataChangedHandler); - deferred.resolve(this.doFindChildNodeByPath(node, childNodePath)); - } - }; - - this.onDataChanged(dataChangedHandler); - - dataChangedHandler(); - - return deferred.promise; - } - - private updatePathsInChildren(parentNode: TreeNode) { - parentNode.getChildren().forEach((child: TreeNode) => { - this.updatePathInChild(parentNode, child); - }); - } - - private updatePathInChild(parentNode: TreeNode, child: TreeNode) { - const nodeSummary: ContentSummary = parentNode.getData() ? parentNode.getData().getContentSummary() : null; - const childSummary: ContentSummary = child.getData() ? child.getData().getContentSummary() : null; - - if (nodeSummary && childSummary) { - const path: ContentPath = ContentPath.create().fromParent(nodeSummary.getPath(), childSummary.getPath().getName()).build(); - const newData: ContentSummaryAndCompareStatus = child.getData(); - newData.setContentSummary(new ContentSummaryBuilder(childSummary).setPath(path).build()); - this.doUpdateNodeByData(child, newData); - this.updatePathsInChildren(child); - } - } - - updateNodes(data: ContentSummaryAndCompareStatus[]): void { - // when items sorting was changed from manual to inherited manual we have to trigger sort ourselves since no sort event coming - const isSortingChangedToManualInheritance: ContentSummaryAndCompareStatus = data.find( - (item: ContentSummaryAndCompareStatus) => this.isSortingChangedToManualInheritance(item)); - - if (isSortingChangedToManualInheritance) { - this.sortNodesChildren([isSortingChangedToManualInheritance]); - } else { - this.updateNodesByData(data); - } - } - - private isSortingChangedToManualInheritance(item: ContentSummaryAndCompareStatus): boolean { - if (!item.getContentSummary()?.getChildOrder().isManual()) { - return false; - } - - if (!item.isSortInherited()) { - return false; - } - - const node: TreeNode = this.getRoot().getNodeByDataIdFromCurrent(item.getId()); - - return node && !node.getData().isSortInherited(); - } - - sortNodesChildren(data: ContentSummaryAndCompareStatus[]) { - this.updateNodesByData(data); - - const changed: TreeNode[] = []; - data.forEach((item: ContentSummaryAndCompareStatus) => { - const node: TreeNode = this.getRoot().getNodeByDataIdFromCurrent(item.getId()); - if (node) { - changed.push(node); - } - }); - - changed.forEach((node: TreeNode) => { - this.sortNodeChildren(node); - }); - - this.invalidateNodes(changed); - } - - protected handleItemMetadata(row: number) { - const node: TreeNode = this.getItem(row); - if (this.isEmptyNode(node)) { - return {cssClasses: 'empty-node'}; - } - - let cssClasses: string = ''; - - if (node.getData().getContentSummary()?.isDataInherited()) { - cssClasses += 'data-inherited'; - } - - if (node.getData().getContentSummary()?.isSortInherited()) { - cssClasses += ' sort-inherited'; - } - - if (node.getData().isReadOnly()) { - cssClasses += ' readonly'; - } - - return {cssClasses: cssClasses.trim()}; - } - - getSelectedOrHighlightedItems(): ContentSummaryAndCompareStatus[] { - const selectedItems: ContentSummaryAndCompareStatus[] = this.getFullSelection(); - - if (selectedItems.length > 0) { - return selectedItems; - } - - if (this.hasHighlightedNode()) { - return [this.getHighlightedItem()]; - } - - return []; - } - - renameContentNodes(renamedItems: ContentSummaryAndCompareStatus[]) { - this.updateNodesByData(renamedItems); - - renamedItems.forEach((renamedItem: ContentSummaryAndCompareStatus) => { - this.getRoot().getNodesByDataId(renamedItem.getId()).forEach(this.updatePathsInChildren.bind(this)); - }); - } - - addContentNodes(itemsToAdd: ContentSummaryAndCompareStatus[]) { - if (this.isFiltered()) { - this.addContentItemsTo(itemsToAdd, true); - } - - this.addContentItemsTo(itemsToAdd, false); - } - - private addContentItemsTo(itemsToAdd: ContentSummaryAndCompareStatus[], isInFiltered: boolean): void { - const parentsOfChildrenToAdd: Map, ContentSummaryAndCompareStatus[]> = - this.getParentsOfItemsToAdd(itemsToAdd, isInFiltered); - - parentsOfChildrenToAdd.forEach((items: ContentSummaryAndCompareStatus[], parentNode: TreeNode) => { - this.addItemsToParent(items, parentNode, isInFiltered); - }); - } - - private addItemsToParent(items: ContentSummaryAndCompareStatus[], parentNode: TreeNode, - isInFiltered: boolean): void { - if (parentNode.isExpandable() && !parentNode.hasChildren()) { - return; - } - - if (!parentNode.isExpandable() && parentNode.hasData()) { - this.updateNodeHasChildren(parentNode, true); - return; - } - - const parentId: ContentId = parentNode.hasData() ? parentNode.getData().getContentId() : null; - - this.fetchChildrenIds(parentId).then((childrenIds: ContentId[]) => { - items.forEach((item: ContentSummaryAndCompareStatus) => { - const contentId: ContentId = item.getContentId(); - const insertPosition: number = childrenIds.findIndex((childId: ContentId) => contentId.equals(childId)); - - if (insertPosition > -1 && insertPosition <= parentNode.getChildren().length) { - if (isInFiltered || !this.isFiltered()) { - this.insertDataToParentNode(item, parentNode, insertPosition); - } else { - // if this grid is filtered, and we need to insert an item into the non-filtered root node, then - // "insertDataToParentNode" will add an item immediately to the current (filtered) root, thus need to: - const nodeToInsert = this.dataToTreeNode(item, parentNode); - parentNode.insertChild(nodeToInsert, insertPosition); - parentNode.setExpandable(true); - } - } - }); - }); - } - - private fetchChildrenIds(parentId: ContentId): Q.Promise { - if (this.isFiltered() && !parentId) { // need to perform query and return root children ids - return this.makeContentQueryRequest(0, -1).setExpand(Expand.NONE).sendAndParse().then( - (result: ContentQueryResult) => { - return result.getContents().map((item) => item as unknown as ContentId); - }); - } - - const order: ChildOrder = !!parentId ? null : this.contentFetcher.createRootChildOrder(); - - return this.contentFetcher.fetchChildrenIds(parentId, order); - } - - private getParentsOfItemsToAdd(itemsToAdd: ContentSummaryAndCompareStatus[], isInFiltered: boolean): - Map, ContentSummaryAndCompareStatus[]> { - const parentsOfChildrenToAdd: Map, ContentSummaryAndCompareStatus[]> = - new Map, ContentSummaryAndCompareStatus[]>(); - const allNodes: TreeNode[] = isInFiltered - ? this.getRoot().getAllFilteredRootNodes() - : this.getRoot().getAllDefaultRootNodes(); - - - itemsToAdd - .filter((item: ContentSummaryAndCompareStatus) => !this.hasItemWithDataId(item.getId())) - .forEach((itemToAdd: ContentSummaryAndCompareStatus) => { - const parentPathOfItemToAdd: ContentPath = itemToAdd.getPath().getParentPath(); - const parentNode: TreeNode = this.getParentNodeByPath(parentPathOfItemToAdd, allNodes, - isInFiltered); - - if (parentNode) { - if (parentsOfChildrenToAdd.has(parentNode)) { - parentsOfChildrenToAdd.get(parentNode).push(itemToAdd); - } else { - parentsOfChildrenToAdd.set(parentNode, [itemToAdd]); - } - } - }); - - return parentsOfChildrenToAdd; - } - - private getParentNodeByPath(parentPath: ContentPath, - nodes: TreeNode[], - isInFiltered: boolean): TreeNode { - if (parentPath.isRoot()) { - return isInFiltered ? this.getRoot().getFilteredRoot() : this.getRoot().getDefaultRoot(); - } - - return nodes.find((node: TreeNode) => { - return parentPath.equals(node.getData().getPath()); - }); - } - - private updateNodeHasChildren(node: TreeNode, hasChildren: boolean) { - node.setExpandable(hasChildren); - const oldData: ContentSummaryAndCompareStatus = node.getData(); - const newContentSummary: ContentSummary = new ContentSummaryBuilder(oldData.getContentSummary()).setHasChildren( - hasChildren).build(); - const newData: ContentSummaryAndCompareStatus = ContentSummaryAndCompareStatus.fromContentAndCompareAndPublishStatus( - newContentSummary, oldData.getCompareStatus(), oldData.getPublishStatus()); - this.doUpdateNodeByData(node, newData); - } - - selectInlinedContentInGrid(contentPath: ContentPath) { - if (this.hasSelectedOrHighlightedNode() && !this.isGivenPathSelectedInGrid(contentPath)) { - this.selectNodeByPath(contentPath); - } - } - - private isGivenPathSelectedInGrid(path: ContentPath): boolean { - const item: ContentSummaryAndCompareStatus = this.getFirstSelectedOrHighlightedItem(); - - if (item) { - return item.getPath().equals(path); - } - - return false; - } - - getRootItemsIds(): string[] { - return this.getRoot().getDefaultRoot().getChildren().map((item: TreeNode) => item.getDataId()); - } - - deleteItems(items: DeletedContentItem[]): void { - if (this.isFiltered()) { - this.deleteItemsInFilteredRoot(items); - } - - this.deleteItemsInDefaultRoot(items); - } - - private deleteItemsInDefaultRoot(items: DeletedContentItem[]): void { - this.doDeleteItems(items, this.getRoot().getAllDefaultRootNodes(), false); - } - - private deleteItemsInFilteredRoot(items: DeletedContentItem[]): void { - this.doDeleteItems(items, this.getRoot().getAllFilteredRootNodes(), true); - } - - private doDeleteItems(items: DeletedContentItem[], allNodes: TreeNode[], isInFiltered: boolean): void { - const nodesToDelete: TreeNode[] = []; - - items.forEach((item: DeletedContentItem) => { - const nodesForItem: TreeNode[] = this.findNodeByItem(item, allNodes); - - if (nodesForItem?.length > 0) { - nodesToDelete.push(...nodesForItem); - } - - this.updateParentHasChildren(item, allNodes, isInFiltered); - }); - - if (nodesToDelete.length > 0) { - this.deleteNodes(nodesToDelete); - } - } - - private updateParentHasChildren(item: DeletedContentItem, allNodes: TreeNode[], - isInFiltered: boolean): void { - const parentPath: ContentPath = item.path.getParentPath(); - - if (parentPath && !parentPath.isRoot()) { - const parentNode: TreeNode = this.getParentNodeByPath(parentPath, allNodes, isInFiltered); - - if (parentNode && !parentNode.hasChildren()) { - this.contentFetcher.fetchChildrenIds(parentNode.getData().getContentId()).then((ids: ContentId[]) => { - if (ids.length === 0) { - this.updateNodeHasChildren(parentNode, false); - } - }).catch(DefaultErrorHandler.handle).done(); - } - } - } - - private findNodeByItem(item: DeletedContentItem, - allNodes: TreeNode[]): TreeNode[] { - return allNodes.filter((node: TreeNode) => { - return node.getData()?.getPath()?.equals(item.path) || node.getData()?.getContentId()?.equals(item.id); - }); - } - - resizeCanvas(): void { - this.getGrid().resizeCanvas(); - } - - getHighlightedItem(): ContentSummaryAndCompareStatus { - // returning highlighted item from current root, super version returns from default root even when grid is filtered - return this.getHighlightedNode()?.getData() || super.getHighlightedItem(); - } - - updateItemIsRenderable(id: string, isRenderable: boolean): void { - if (this.isFiltered()) { - this.getRoot().getNodeByDataIdFromFiltered(id)?.getData().setRenderable(isRenderable); - } - - this.getRoot().getNodeByDataIdFromDefault(id)?.getData().setRenderable(isRenderable); - } - - onDoubleClick(listener: () => void) { - this.doubleClickListeners.push(listener); - } - - unDoubleClick(listener: () => void) { - this.doubleClickListeners = this.doubleClickListeners.filter((current) => (current !== listener)); - } - - private notifyDoubleClick() { - this.doubleClickListeners.forEach((listener: () => void) => listener()); - } - - copyStatusFromExistingNodes(data: ContentSummaryAndCompareStatus[]) { - data.forEach((newItem: ContentSummaryAndCompareStatus) => { - const existingItem: ContentSummaryAndCompareStatus = this.getRoot().getNodeByDataIdFromCurrent(newItem.getId())?.getData(); - if (existingItem) { - newItem.setCompareStatus(existingItem.getCompareStatus()); - } - }); - } - - copyPermissionsFromExistingNodes(data: ContentSummaryAndCompareStatus[]) { - data.forEach((newItem: ContentSummaryAndCompareStatus) => { - const existingItem: ContentSummaryAndCompareStatus = this.getRoot().getNodeByDataIdFromCurrent(newItem.getId())?.getData(); - if (existingItem) { - newItem.setReadOnly(existingItem.isReadOnly()); - } - }); - } -} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeGridListViewer.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeGridListViewer.ts new file mode 100644 index 0000000000..ecaac5c023 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeGridListViewer.ts @@ -0,0 +1,143 @@ +import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; +import {ContentSummaryAndCompareStatus} from '../content/ContentSummaryAndCompareStatus'; +import Q from 'q'; +import {ContentSummaryAndCompareStatusViewer} from '../content/ContentSummaryAndCompareStatusViewer'; +import {SpanEl} from '@enonic/lib-admin-ui/dom/SpanEl'; +import {ProgressBar} from '@enonic/lib-admin-ui/ui/ProgressBar'; +import {DateTimeFormatter} from '@enonic/lib-admin-ui/ui/treegrid/DateTimeFormatter'; +import {SortContentEvent} from './sort/SortContentEvent'; +import {ContentSummaryListViewer} from '../content/ContentSummaryListViewer'; + +export class ContentTreeGridListViewer + extends DivEl { + + private item: ContentSummaryAndCompareStatus; + + private summaryViewer: ContentSummaryAndCompareStatusViewer; + + private sortColumn: DivEl; + + private statusColumn: StatusBlock; + + private modifiedColumn: DivEl; + + constructor() { + super('content-tree-grid-list-viewer'); + + this.initElements(); + this.initListeners(); + } + + private initElements(): void { + this.summaryViewer = new ContentSummaryListViewer(); + this.sortColumn = new DivEl(); + this.statusColumn = new StatusBlock(); + this.modifiedColumn = new DivEl('content-tree-grid-modified'); + } + + private initListeners(): void { + this.sortColumn.onClicked(() => { + if (this.sortColumn.hasClass('sort-dialog-trigger')) { + new SortContentEvent([this.item]).fire(); + } + }); + } + + setItem(item: ContentSummaryAndCompareStatus) { + this.item = item; + this.summaryViewer.setObject(item); + this.sortColumn.setClass(this.calcSortIconCls()); + this.statusColumn.setItem(item); + this.modifiedColumn.setHtml(DateTimeFormatter.createHtml(item.getContentSummary().getModifiedTime())); + this.toggleClass('data-inherited', item.isDataInherited()); + this.toggleClass('sort-inherited', item.isSortInherited()); + this.toggleClass('readonly', item.isReadOnly()); + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.appendChild(this.summaryViewer); + this.appendChild(this.sortColumn); + this.appendChild(this.statusColumn); + this.appendChild(this.modifiedColumn); + + return rendered; + }); + } + + private calcSortIconCls(): string { + const childOrder = this.item.getContentSummary().getChildOrder(); + + let iconCls = 'content-tree-grid-sort '; + + if (!childOrder.isDefault()) { + iconCls += 'sort-dialog-trigger '; + + if (!childOrder.isManual()) { + if (childOrder.isDesc()) { + iconCls += childOrder.isAlpha() ? 'icon-sort-alpha-desc' : 'icon-sort-num-desc'; + } else { + iconCls += childOrder.isAlpha() ? 'icon-sort-alpha-asc' : 'icon-sort-num-asc'; + } + } else { + iconCls += 'icon-menu'; + } + } + + return iconCls; + } +} + +class StatusBlock + extends DivEl { + + private item: ContentSummaryAndCompareStatus; + + private statusEl: SpanEl; + + private progressEl?: ProgressBar; + + constructor() { + super('content-tree-grid-status'); + + this.statusEl = new SpanEl('status-text'); + } + + setItem(item: ContentSummaryAndCompareStatus): void { + this.item = item; + + if (item.getUploadItem()) { + this.updateProgressBar(); + } else { + this.updateStatus(); + } + } + + private updateStatus(): void { + this.progressEl?.remove(); + this.statusEl.setHtml(this.item.getStatusText()); + this.statusEl.setClass(this.item.getStatusClass()); + } + + private updateProgressBar(): void { + if (!this.progressEl) { + this.progressEl = new ProgressBar(this.item.getUploadItem().getProgress()); + } else { + this.progressEl.setValue(this.item.getUploadItem().getProgress()); + } + + if (!this.statusEl.hasChild(this.progressEl)) { + this.statusEl.setHtml(''); + this.statusEl.setClass(''); + this.statusEl.appendChild(this.progressEl); + } + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.appendChild(this.statusEl); + + return rendered; + }); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeGridToolbar.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeGridToolbar.ts deleted file mode 100644 index 4208b266bc..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/browse/ContentTreeGridToolbar.ts +++ /dev/null @@ -1,12 +0,0 @@ -import {ContentTreeGrid} from './ContentTreeGrid'; -import {TreeGridToolbar} from '@enonic/lib-admin-ui/ui/treegrid/TreeGridToolbar'; - -export class ContentTreeGridToolbar - extends TreeGridToolbar { - - constructor(treeGrid: ContentTreeGrid) { - super(treeGrid); - this.addClass('content-tree-grid-toolbar'); - } - -} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentsTreeGridList.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentsTreeGridList.ts new file mode 100644 index 0000000000..a30ff2dbfc --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/browse/ContentsTreeGridList.ts @@ -0,0 +1,213 @@ +import Q from 'q'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {TreeListBox, TreeListBoxParams, TreeListElement, TreeListElementParams} from '@enonic/lib-admin-ui/ui/selector/list/TreeListBox'; +import {ContentSummaryAndCompareStatus} from '../content/ContentSummaryAndCompareStatus'; +import {ContentSummaryAndCompareStatusFetcher} from '../resource/ContentSummaryAndCompareStatusFetcher'; +import {ContentResponse} from '../resource/ContentResponse'; +import {ContentPath} from '../content/ContentPath'; +import {ContentTreeGridListViewer} from './ContentTreeGridListViewer'; +import {ChildOrder} from '../resource/order/ChildOrder'; + +export class ContentsTreeGridList + extends TreeListBox { + + public static FETCH_SIZE: number = 10; + + protected readonly fetcher: ContentSummaryAndCompareStatusFetcher; + + protected newItems: Map = new Map(); + + private wasShownAndLoaded: boolean = false; + + constructor(params?: TreeListBoxParams) { + super(params); + + this.fetcher = new ContentSummaryAndCompareStatusFetcher(); + } + + protected createItemView(item: ContentSummaryAndCompareStatus, readOnly: boolean): ContentsTreeGridListElement { + return new ContentsTreeGridListElement(item, {scrollParent: this.scrollParent, level: this.level, parentList: this}); + } + + protected getItemId(item: ContentSummaryAndCompareStatus): string { + return item.getId(); + } + + protected handleLazyLoad(): void { + this.wasShownAndLoaded = true; + + this.fetch().then((items: ContentSummaryAndCompareStatus[]) => { + if (items.length > 0) { + // first remove new items that are now to be added to avoid being shown twice + items.forEach((item: ContentSummaryAndCompareStatus) => { + const itemId = this.getItemId(item); + + if (this.newItems.has(itemId)) { + this.newItems.delete(itemId); + this.removeItems([item], true); + } + }); + + this.addItems(items); + } + }).catch(DefaultErrorHandler.handle); + } + + private fetch(): Q.Promise { + return this.isRootList() ? this.fetchRootItems() : this.fetchItems(); + } + + protected isRootList(): boolean { + return !this.getParentItem(); + } + + protected fetchRootItems(): Q.Promise { + return this.fetchItems(this.fetcher.createRootChildOrder()); + } + + protected fetchItems(order?: ChildOrder): Q.Promise { + const from: number = this.getItemCount() - this.newItems.size; + const size: number = ContentsTreeGridList.FETCH_SIZE; + const parent = this.getParentItem()?.getContentId(); + + return this.fetcher.fetchChildren(parent, from, size, order).then((data: ContentResponse) => { + return data.getContents(); + }); + } + + // new items to be shown on top of the list and must be taken into account when fetching new items, or removed on refresh + addNewItems(items: ContentSummaryAndCompareStatus[]): void { + if (this.wasShownAndLoaded) { + items.forEach((item: ContentSummaryAndCompareStatus) => { + this.newItems.set(this.getItemId(item), item); + }); + + this.addItems(items); + } else { // if parent didn't have children before then update it to show expand toggle + (this.options.parentListElement as ContentsTreeGridListElement)?.setContainsChildren(true); + } + } + + protected insertItemView(itemView: ContentsTreeGridListElement): void { + const itemId = this.getItemId(itemView.getItem()); + + if (this.newItems.has(itemId)) { + this.prependChild(itemView); + } else { + super.insertItemView(itemView); + } + } + + protected addItemView(item: ContentSummaryAndCompareStatus, readOnly?: boolean, + index?: number): TreeListElement { + (this.options.parentListElement as ContentsTreeGridListElement)?.setContainsChildren(true); + return super.addItemView(item, readOnly, index); + } + + protected removeItemView(item: ContentSummaryAndCompareStatus): void { + const id: string = this.getItemId(item); + this.newItems.delete(id); + super.removeItemView(item); + + if (this.itemViews.size === 0) { + (this.options.parentListElement as ContentsTreeGridListElement)?.setContainsChildren(false); + } + } + + load(): void { + this.clearItems(true); + this.newItems = new Map(); + this.handleLazyLoad(); + } + + wasAlreadyShownAndLoaded(): boolean { + return this.wasShownAndLoaded; + } + + findParentLists(item: ContentSummaryAndCompareStatus | ContentPath): ContentsTreeGridList[] { + const parents: ContentsTreeGridList[] = []; + const itemPath = item instanceof ContentSummaryAndCompareStatus ? item.getPath() : item; + const thisPath = this.getParentItem()?.getPath() || ContentPath.getRoot(); + + if (itemPath.isDescendantOf(thisPath)) { + // if the list is filtered then root may contain the item no matter what path is + if (itemPath.isChildOf(thisPath) || this.getItems().some(i => i.getPath().equals(itemPath))) { + parents.push(this); + } + + this.getItemViews().forEach((listElement: ContentsTreeGridListElement) => { + const moreParents = listElement.findParentLists(item); + + if (moreParents.length > 0) { + parents.push(...moreParents); + } + }); + } + + return parents; + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.addClass('content-tree-grid-list'); + + return rendered; + }); + } + +} + +export class ContentsTreeGridListElement extends TreeListElement { + + protected childrenList: ContentsTreeGridList; + + private containsChildren: boolean; + + constructor(content: ContentSummaryAndCompareStatus, params: TreeListElementParams) { + super(content, params); + } + + protected initElements(): void { + this.containsChildren = this.item.hasChildren(); + super.initElements(); + } + + protected createChildrenList(params?: TreeListElementParams): ContentsTreeGridList { + return new ContentsTreeGridList(params); + } + + hasChildren(): boolean { + return this.containsChildren; + } + + setContainsChildren(value: boolean): void { + this.containsChildren = value; + this.updateExpandableState(); + } + + protected createItemViewer(item: ContentSummaryAndCompareStatus): ContentTreeGridListViewer { + const viewer = new ContentTreeGridListViewer(); + viewer.setItem(item); + return viewer; + } + + findParentLists(item: ContentSummaryAndCompareStatus | ContentPath): ContentsTreeGridList[] { + return this.childrenList.findParentLists(item); + } + + setItem(item: ContentSummaryAndCompareStatus): void { + super.setItem(item); + (this.itemViewer as ContentTreeGridListViewer).setItem(item); + this.containsChildren = this.item.hasChildren(); + this.updateExpandableState(); + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.addClass('content-tree-list-element'); + + return rendered; + }); + } + +} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentsTreeGridRootList.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentsTreeGridRootList.ts new file mode 100644 index 0000000000..dfadf469c6 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/browse/ContentsTreeGridRootList.ts @@ -0,0 +1,74 @@ +import {ContentsTreeGridList} from './ContentsTreeGridList'; +import {ContentQuery} from '../content/ContentQuery'; +import {Branch} from '../versioning/Branch'; +import Q from 'q'; +import {ContentSummaryAndCompareStatus} from '../content/ContentSummaryAndCompareStatus'; +import {ContentQueryResult} from '../resource/ContentQueryResult'; +import {ContentSummary} from '../content/ContentSummary'; +import {ContentSummaryJson} from '../content/ContentSummaryJson'; +import {ContentQueryRequest} from '../resource/ContentQueryRequest'; +import {Expand} from '@enonic/lib-admin-ui/rest/Expand'; +import {ContentTreeGridLoadedEvent} from './ContentTreeGridLoadedEvent'; + +export class ContentsTreeGridRootList extends ContentsTreeGridList { + + private filterQuery: ContentQuery; + + private branch: Branch = Branch.DRAFT; + + protected initListeners(): void { + super.initListeners(); + + const listener = () => { + new ContentTreeGridLoadedEvent().fire(); + this.unItemsAdded(listener); + }; + + this.onItemsAdded(listener); + } + + setTargetBranch(branch: Branch): void { + this.branch = branch; + } + + setFilterQuery(query: ContentQuery | null): void { + this.filterQuery = query ? new ContentQuery() : null; + + if (query) { + this.filterQuery + .setSize(ContentsTreeGridList.FETCH_SIZE) + .setQueryFilters(query.getQueryFilters()) + .setQuery(query.getQuery()) + .setQuerySort(query.getQuerySort()) + .setContentTypeNames(query.getContentTypes()) + .setMustBeReferencedById(query.getMustBeReferencedById()); + } + + this.load(); + } + + isFiltered(): boolean { + return !!this.filterQuery; + } + + protected fetchRootItems(): Q.Promise { + if (!this.filterQuery) { + return super.fetchRootItems(); + } + + const from: number = this.getItemCount() - this.newItems.size; + + return this.makeContentQueryRequest(from).sendAndParse().then( + (contentQueryResult: ContentQueryResult) => { + return this.fetcher.updateReadonlyAndCompareStatus(contentQueryResult.getContents()); + }); + } + + private makeContentQueryRequest(from: number): ContentQueryRequest { + this.filterQuery.setFrom(from).setSize(ContentsTreeGridList.FETCH_SIZE); + + return new ContentQueryRequest(this.filterQuery) + .setTargetBranch(this.branch) + .setExpand(Expand.SUMMARY); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ContentsTreeList.ts b/modules/lib/src/main/resources/assets/js/app/browse/ContentsTreeList.ts new file mode 100644 index 0000000000..d2e0ca0568 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/browse/ContentsTreeList.ts @@ -0,0 +1,122 @@ +import Q from 'q'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {TreeListBox, TreeListBoxParams, TreeListElement, TreeListElementParams} from '@enonic/lib-admin-ui/ui/selector/list/TreeListBox'; +import {ContentAndStatusSelectorViewer} from '../inputtype/selector/ContentAndStatusSelectorViewer'; +import {Element} from '@enonic/lib-admin-ui/dom/Element'; +import {ContentTreeSelectorItem} from '../item/ContentTreeSelectorItem'; +import {ContentSummaryOptionDataLoader} from '../inputtype/ui/selector/ContentSummaryOptionDataLoader'; +import {OptionDataLoaderData} from '@enonic/lib-admin-ui/ui/selector/OptionDataLoader'; +import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; + +export interface ContentsListParams extends TreeListBoxParams { + loader: ContentSummaryOptionDataLoader; +} + +export class ContentsTreeList + extends TreeListBox { + + public static FETCH_SIZE: number = 10; + + protected readonly loader: ContentSummaryOptionDataLoader; + + constructor(params?: ContentsListParams) { + super(params); + + this.loader = params.loader; + } + + protected createItemView(item: ContentTreeSelectorItem, readOnly: boolean): ContentListElement { + return new ContentListElement(item, {loader: this.loader, scrollParent: this.scrollParent, level: this.level}); + } + + protected getItemId(item: ContentTreeSelectorItem): string { + return item.getId(); + } + + protected handleLazyLoad(): void { + // if ContentTreeSelectorQueryRequest is used (not really smart mode), then all items are loaded at once + if (this.loader.isSmartTreeMode()) { + if (this.getItemCount() === 0 && !this.loader.isLoading()) { + this.load(); + } + } else { + this.load(); + } + } + + private fetch(): Q.Promise> { + const from: number = this.getItemCount(); + const size: number = ContentsTreeList.FETCH_SIZE; + + const data = this.options.parentListElement ? Option.create() + .setValue(this.getParentItem().getId()) + .setDisplayValue(this.getParentItem()) + .build() : null; + + return this.loader.fetchChildren(data, from, size); + } + + load(): void { + this.fetch().then((data: OptionDataLoaderData) => { + if (data.getHits() > 0) { + this.addItems(data.getData()); + } + }).catch(DefaultErrorHandler.handle); + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.addClass('content-tree-list'); + + return rendered; + }); + } + +} + +export interface ContentsListElementParams extends TreeListElementParams { + loader: ContentSummaryOptionDataLoader +} + +export class ContentListElement extends TreeListElement { + + protected readonly options: ContentsListElementParams; + + protected childrenList: ContentsTreeList; + + constructor(content: ContentTreeSelectorItem, params: ContentsListElementParams) { + super(content, params); + } + + protected createChildrenListParams(): ContentsListParams { + const params = super.createChildrenListParams() as ContentsListParams; + + params.loader = this.options.loader; + + return params; + } + + protected createChildrenList(params?: ContentsListParams): ContentsTreeList { + return new ContentsTreeList(params); + } + + hasChildren(): boolean { + return this.item.hasChildren(); + } + + protected createItemViewer(item: ContentTreeSelectorItem): Element { + const viewer = new ContentAndStatusSelectorViewer(); + viewer.setObject(item); + return viewer; + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.addClass('content-tree-list-element'); + this.toggleClass('non-selectable', !this.item.isSelectable()); + + return rendered; + }); + } + +} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/DragHandler.ts b/modules/lib/src/main/resources/assets/js/app/browse/DragHandler.ts new file mode 100644 index 0000000000..87f2846891 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/browse/DragHandler.ts @@ -0,0 +1,56 @@ +import {Element} from '@enonic/lib-admin-ui/dom/Element'; +import Sortable, {SortableEvent} from 'sortablejs'; + +export class DragHandler { + + protected rootElement: Element; + + protected sortable: Sortable; + + private positionChangedListeners: (() => void)[] = []; + + constructor(root: Element) { + this.rootElement = root; + + this.initSortable(); + } + + protected initSortable(): void { + this.sortable = new Sortable(this.rootElement.getHTMLElement(), { + group: this.getGroup(), + sort: true, + animation: 150, + onUpdate: (event: SortableEvent) => this.handleUpdate(event), + }); + } + + protected getGroup(): string { + return null; + } + + protected handleMovements(from: number, to: number): void { + return; + } + + protected handleUpdate(event: SortableEvent): void { + this.handleMovements(event.oldIndex, event.newIndex); + + this.notifyPositionChanged(); + } + + onPositionChanged(listener: () => void) { + this.positionChangedListeners.push(listener); + } + + unPositionChanged(listener: () => void) { + this.positionChangedListeners = this.positionChangedListeners.filter((currentListener: () => void) => { + return currentListener !== listener; + }); + } + + private notifyPositionChanged() { + this.positionChangedListeners.forEach((listener: () => void) => { + listener.call(this); + }); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/ResponsiveBrowsePanel.ts b/modules/lib/src/main/resources/assets/js/app/browse/ResponsiveBrowsePanel.ts index 7d898cd8e2..f0691f3cb8 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/ResponsiveBrowsePanel.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/ResponsiveBrowsePanel.ts @@ -9,6 +9,7 @@ import * as Q from 'q'; import {ViewItem} from '@enonic/lib-admin-ui/app/view/ViewItem'; import {SplitPanelSize} from '@enonic/lib-admin-ui/ui/panel/SplitPanelSize'; import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {SelectionMode} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export abstract class ResponsiveBrowsePanel extends BrowsePanel { @@ -33,13 +34,12 @@ export abstract class ResponsiveBrowsePanel extends BrowsePanel { Body.get().toggleClass(ResponsiveBrowsePanel.MOBILE_MODE_CLASS, isMobile); this.toggleClass(ResponsiveBrowsePanel.MOBILE_MODE_CLASS, isMobile); - this.treeGrid.toggleClass(ResponsiveBrowsePanel.MOBILE_MODE_CLASS, isMobile); + this.selectableListBoxPanel.toggleClass(ResponsiveBrowsePanel.MOBILE_MODE_CLASS, isMobile); }); this.browseToolbar.onFoldClicked(() => { this.contextSplitPanel.hideContextPanel(); this.toggleMobilePreviewMode(false); - this.treeGrid.removeHighlighting(); }); } @@ -73,10 +73,11 @@ export abstract class ResponsiveBrowsePanel extends BrowsePanel { protected updatePreviewItem(): void { super.updatePreviewItem(); - const item: ViewItem = this.treeGrid.getLastSelectedOrHighlightedItem(); + const item: ViewItem = this.selectableListBoxPanel.getLastSelectedItem(); this.updateContextView(item).catch(DefaultErrorHandler.handle); - if (this.treeGrid.hasHighlightedNode()) { + if (this.selectableListBoxPanel.getSelectedItems().length > 0 && this.selectableListBoxPanel.getSelectionMode() === + SelectionMode.HIGHLIGHT) { if (this.contextSplitPanel.isMobileMode()) { this.toggleMobilePreviewMode(true); } @@ -90,7 +91,7 @@ export abstract class ResponsiveBrowsePanel extends BrowsePanel { if (isMobile) { this.browseToolbar.enableMobileMode(); - this.browseToolbar.setFoldButtonLabel(this.treeGrid.getLastSelectedOrHighlightedItem().getDisplayName()); + this.browseToolbar.setFoldButtonLabel(this.selectableListBoxPanel.getLastSelectedItem().getDisplayName()); } else { this.browseToolbar.disableMobileMode(); this.browseToolbar.updateFoldButtonLabel(); diff --git a/modules/lib/src/main/resources/assets/js/app/browse/State.ts b/modules/lib/src/main/resources/assets/js/app/browse/State.ts new file mode 100644 index 0000000000..c01331385c --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/browse/State.ts @@ -0,0 +1,3 @@ +export enum State { + ENABLED, DISABLED +} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/TreeNodeParentOfContent.ts b/modules/lib/src/main/resources/assets/js/app/browse/TreeNodeParentOfContent.ts deleted file mode 100644 index 13017a6a07..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/browse/TreeNodeParentOfContent.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {TreeNode} from '@enonic/lib-admin-ui/ui/treegrid/TreeNode'; -import {ContentSummaryAndCompareStatus} from '../content/ContentSummaryAndCompareStatus'; - -export class TreeNodeParentOfContent { - - private children: ContentSummaryAndCompareStatus[]; - - private node: TreeNode; - - constructor(children: ContentSummaryAndCompareStatus[] = [], node: TreeNode) { - this.children = children; - this.node = node; - } - - getChildren(): ContentSummaryAndCompareStatus[] { - return this.children; - } - - getNode(): TreeNode { - return this.node; - } - - addChild(child: ContentSummaryAndCompareStatus) { - this.children.push(child); - } -} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/ArchiveContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/ArchiveContentAction.ts index 7335d13462..644d4dea0d 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/ArchiveContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/ArchiveContentAction.ts @@ -1,21 +1,20 @@ -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentDeletePromptEvent} from '../ContentDeletePromptEvent'; -import {CompareStatus} from '../../content/CompareStatus'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class ArchiveContentAction extends ContentTreeGridAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.archiveMore'), 'mod+del'); this.setEnabled(false).setClass('archive'); } protected handleExecuted() { - new ContentDeletePromptEvent(this.grid.getSelectedDataList()).fire(); + new ContentDeletePromptEvent(this.grid.getSelectedItems()).fire(); } isToBeEnabled(state: ContentTreeGridItemsState): boolean { diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/ContentTreeGridAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/ContentTreeGridAction.ts index 954a34b5a6..2e8219dfe1 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/ContentTreeGridAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/ContentTreeGridAction.ts @@ -1,16 +1,17 @@ import {Action} from '@enonic/lib-admin-ui/ui/Action'; -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; +import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; export class ContentTreeGridAction extends Action { - protected grid: ContentTreeGrid; + protected grid: SelectableListBoxWrapper; protected stashedState: boolean; protected stashed: boolean; - constructor(grid: ContentTreeGrid, label?: string, shortcut?: string, global?: boolean) { + constructor(grid: SelectableListBoxWrapper, label?: string, shortcut?: string, global?: boolean) { super(label, shortcut, global); this.grid = grid; diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/ContentTreeGridActions.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/ContentTreeGridActions.ts index 5c79a86086..e69de29bb2 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/ContentTreeGridActions.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/ContentTreeGridActions.ts @@ -1,325 +0,0 @@ -import * as Q from 'q'; -import {i18n} from '@enonic/lib-admin-ui/util/Messages'; -import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; -import {ContentTreeGrid} from '../ContentTreeGrid'; -import {ToggleSearchPanelAction} from './ToggleSearchPanelAction'; -import {ShowNewContentDialogAction} from './ShowNewContentDialogAction'; -import {PreviewContentAction} from './PreviewContentAction'; -import {EditContentAction} from './EditContentAction'; -import {ArchiveContentAction} from './ArchiveContentAction'; -import {DuplicateContentAction} from './DuplicateContentAction'; -import {MoveContentAction} from './MoveContentAction'; -import {SortContentAction} from './SortContentAction'; -import {PublishContentAction} from './PublishContentAction'; -import {PublishTreeContentAction} from './PublishTreeContentAction'; -import {UnpublishContentAction} from './UnpublishContentAction'; -import {CreateIssueAction} from './CreateIssueAction'; -import {GetPermittedActionsRequest} from '../../resource/GetPermittedActionsRequest'; -import {GetContentTypeByNameRequest} from '../../resource/GetContentTypeByNameRequest'; -import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; -import {ContentType} from '../../inputtype/schema/ContentType'; -import {Permission} from '../../access/Permission'; -import {HasUnpublishedChildrenRequest} from '../../resource/HasUnpublishedChildrenRequest'; -import {HasUnpublishedChildren, HasUnpublishedChildrenResult} from '../../resource/HasUnpublishedChildrenResult'; -import {RequestPublishContentAction} from './RequestPublishContentAction'; -import {Action} from '@enonic/lib-admin-ui/ui/Action'; -import {TreeGridActions} from '@enonic/lib-admin-ui/ui/treegrid/actions/TreeGridActions'; -import {ManagedActionManager} from '@enonic/lib-admin-ui/managedaction/ManagedActionManager'; -import {ManagedActionState} from '@enonic/lib-admin-ui/managedaction/ManagedActionState'; -import {ManagedActionExecutor} from '@enonic/lib-admin-ui/managedaction/ManagedActionExecutor'; -import {NotifyManager} from '@enonic/lib-admin-ui/notify/NotifyManager'; -import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; -import {ContentTreeGridAction} from './ContentTreeGridAction'; -import {MarkAsReadyContentAction} from './MarkAsReadyContentAction'; -import {ContentId} from '../../content/ContentId'; - -export enum ActionName { - SHOW_NEW_DIALOG, PREVIEW, EDIT, ARCHIVE, DUPLICATE, MOVE, SORT, PUBLISH, PUBLISH_TREE, UNPUBLISH, MARK_AS_READY, REQUEST_PUBLISH, - CREATE_ISSUE, TOGGLE_SEARCH_PANEL -} - -export type ActionMap = Map; - -export enum State { - ENABLED, DISABLED -} - -export class ContentTreeGridActions implements TreeGridActions { - - private readonly grid: ContentTreeGrid; - - private actionsMap: ActionMap = new Map(); - - private beforeActionsStashedListeners: (() => void)[] = []; - - private actionsUnStashedListeners: (() => void)[] = []; - - private state: State = State.ENABLED; - - constructor(grid: ContentTreeGrid) { - this.grid = grid; - this.initActions(); - this.initListeners(); - } - - private initActions() { - this.actionsMap.set(ActionName.SHOW_NEW_DIALOG, new ShowNewContentDialogAction(this.grid)); - this.actionsMap.set(ActionName.PREVIEW, new PreviewContentAction(this.grid)); - this.actionsMap.set(ActionName.EDIT, new EditContentAction(this.grid)); - this.actionsMap.set(ActionName.ARCHIVE, new ArchiveContentAction(this.grid)); - this.actionsMap.set(ActionName.DUPLICATE, new DuplicateContentAction(this.grid)); - this.actionsMap.set(ActionName.MOVE, new MoveContentAction(this.grid)); - this.actionsMap.set(ActionName.SORT, new SortContentAction(this.grid)); - this.actionsMap.set(ActionName.PUBLISH, new PublishContentAction(this.grid)); - this.actionsMap.set(ActionName.PUBLISH_TREE, new PublishTreeContentAction(this.grid)); - this.actionsMap.set(ActionName.UNPUBLISH, new UnpublishContentAction(this.grid)); - this.actionsMap.set(ActionName.MARK_AS_READY, new MarkAsReadyContentAction(this.grid)); - this.actionsMap.set(ActionName.REQUEST_PUBLISH, new RequestPublishContentAction(this.grid)); - this.actionsMap.set(ActionName.CREATE_ISSUE, new CreateIssueAction(this.grid)); - this.actionsMap.set(ActionName.TOGGLE_SEARCH_PANEL, new ToggleSearchPanelAction(this.grid)); - } - - private initListeners() { - const previewStateChangedHandler = value => { - this.actionsMap.get(ActionName.PREVIEW).setEnabled(value); - }; - - const managedActionsHandler = (state: ManagedActionState, executor: ManagedActionExecutor) => { - if (state === ManagedActionState.PREPARING) { - this.notifyBeforeActionsStashed(); - this.actionsMap.forEach((action: ContentTreeGridAction) => action.stash()); - } else if (state === ManagedActionState.ENDED) { - this.actionsMap.forEach((action: ContentTreeGridAction) => action.unStash()); - this.notifyActionsUnStashed(); - } - }; - - ManagedActionManager.instance().onManagedActionStateChanged(managedActionsHandler); - - this.grid.onRemoved(() => { - ManagedActionManager.instance().unManagedActionStateChanged(managedActionsHandler); - }); - } - - setState(state: State) { - this.state = state; - - if (this.state === State.DISABLED) { - this.disableAllActions(); - } else { - this.updateActionsEnabledState([]); - } - } - - private disableAllActions() { - this.actionsMap.forEach((action: ContentTreeGridAction) => action.setEnabled(false)); - } - - onBeforeActionsStashed(listener: () => void) { - this.beforeActionsStashedListeners.push(listener); - } - - private notifyBeforeActionsStashed() { - this.beforeActionsStashedListeners.forEach((listener) => { - listener(); - }); - } - - onActionsUnStashed(listener: () => void) { - this.actionsUnStashedListeners.push(listener); - } - - private notifyActionsUnStashed() { - this.actionsUnStashedListeners.forEach((listener) => { - listener(); - }); - } - - getAllCommonActions(): Action[] { - return [ - this.getAction(ActionName.SHOW_NEW_DIALOG), - this.getAction(ActionName.EDIT), - this.getAction(ActionName.ARCHIVE), - this.getAction(ActionName.DUPLICATE), - this.getAction(ActionName.MOVE), - this.getAction(ActionName.SORT), - this.getAction(ActionName.PREVIEW) - ]; - } - - getPublishActions(): Action[] { - return [ - this.getAction(ActionName.PUBLISH), - this.getAction(ActionName.UNPUBLISH) - ]; - } - - getPublishAction(): Action { - return this.getAction(ActionName.PUBLISH); - } - - getToggleSearchPanelAction(): Action { - return this.getAction(ActionName.TOGGLE_SEARCH_PANEL); - } - - getAllActionsNoPublish(): Action[] { - return [ - ...this.getAllCommonActions() - ]; - } - - getAllActionsNoPendingDelete(): Action[] { - return [ - ...this.getAllCommonActions(), - this.getAction(ActionName.UNPUBLISH) - ]; - } - - getAllActions(): Action[] { - return [ - ...this.getAllActionsNoPublish(), - ...this.getPublishActions() - ]; - } - - updateActionsEnabledState(items: ContentSummaryAndCompareStatus[]): Q.Promise { - if (this.state === State.DISABLED) { - return Q(null); - } - - this.getAction(ActionName.TOGGLE_SEARCH_PANEL).setVisible(false); - - const parallelPromises: Q.Promise[] = [ - this.doUpdateActionsEnabledState(items) - ]; - - return Q.all(parallelPromises).catch(DefaultErrorHandler.handle); - } - - private showDefaultActions() { - const defaultActions = [ - this.getAction(ActionName.SHOW_NEW_DIALOG), - this.getAction(ActionName.EDIT), - this.getAction(ActionName.ARCHIVE), - this.getAction(ActionName.DUPLICATE), - this.getAction(ActionName.MOVE), - this.getAction(ActionName.SORT), - this.getAction(ActionName.PREVIEW), - this.getAction(ActionName.PUBLISH) - ]; - defaultActions.forEach(action => action.setVisible(true)); - } - - private doUpdateActionsEnabledState(items: ContentSummaryAndCompareStatus[]): Q.Promise { - return this.getAllowedPermissions(items).then((permissions: Permission[]) => { - const state: ContentTreeGridItemsState = new ContentTreeGridItemsState(items, permissions); - this.toggleActions(state); - - if (items.length === 0) { - this.toggleVisibilityNoItemsSelected(); - return Q(null); - } - - this.toggleVisibility(state); - return this.updateDefaultActionsMultipleItemsSelected(items); - }); - } - - private getAllowedPermissions(items: ContentSummaryAndCompareStatus[]): Q.Promise { - const request: GetPermittedActionsRequest = new GetPermittedActionsRequest(); - - if (items.length === 0) { - request.addPermissionsToBeChecked(Permission.CREATE); - } else { - const contentIds: ContentId[] = items.map((item: ContentSummaryAndCompareStatus) => item.getContentId()); - request.addContentIds(...contentIds); - request.addPermissionsToBeChecked(Permission.CREATE, Permission.DELETE, Permission.PUBLISH, Permission.MODIFY); - } - - return request.sendAndParse(); - } - - private toggleActions(state: ContentTreeGridItemsState) { - this.actionsMap.forEach((action: ContentTreeGridAction) => action.setEnabledByState(state)); - } - - private toggleVisibility(state: ContentTreeGridItemsState) { - if (state.hasAnyPendingDelete()) { - const invisibleActions: Action[] = state.hasAllPendingDelete() - ? this.getAllActionsNoPendingDelete() - : this.getAllActions(); - invisibleActions.forEach(action => action.setVisible(false)); - } else { - this.getAllCommonActions().forEach(action => action.setVisible(true)); - this.getAction(ActionName.UNPUBLISH).setVisible(this.getAction(ActionName.UNPUBLISH).isEnabled()); - } - - this.getAction(ActionName.PUBLISH).setVisible( - state.hasAllPendingDelete() || this.getAction(ActionName.PUBLISH).isEnabled()); - (this.getAction(ActionName.EDIT) as EditContentAction).updateLabel(state); - } - - private toggleVisibilityNoItemsSelected() { - this.getAction(ActionName.UNPUBLISH).setVisible(false); - (this.getAction(ActionName.EDIT) as EditContentAction).resetLabel(); - this.showDefaultActions(); - } - - private handleContentTypeNotFound(selectedItem: ContentSummaryAndCompareStatus) { - NotifyManager.get().showWarning( - i18n('notify.contentType.notFound', selectedItem.getContentSummary().getType().getLocalName())); - - this.disableAllActions(); - this.getAction(ActionName.CREATE_ISSUE).setEnabled(true); - - this.getAction(ActionName.UNPUBLISH).setVisible(false); - (this.getAction(ActionName.EDIT) as EditContentAction).resetLabel(); - this.showDefaultActions(); - } - - private updateDefaultActionsMultipleItemsSelected(items: ContentSummaryAndCompareStatus[]): Q.Promise { - const promises: Q.Promise[] = []; - - if (items.length === 1 && - (this.getAction(ActionName.SHOW_NEW_DIALOG).isEnabled() || this.getAction(ActionName.SORT).isEnabled())) { - promises.push(this.checkIsChildrenAllowedByContentType(items[0]).then((childrenAllowed: boolean) => { - if (!childrenAllowed) { - this.getAction(ActionName.SHOW_NEW_DIALOG).setEnabled(false); - this.getAction(ActionName.SORT).setEnabled(false); - } - - return Q(); - })); - } - - if (this.getAction(ActionName.PUBLISH_TREE).isEnabled()) { - promises.push(this.updatePublishTreeAction(items)); - } - - return Q.all(promises).thenResolve(null); - } - - private updatePublishTreeAction(items: ContentSummaryAndCompareStatus[]): Q.Promise { - return new HasUnpublishedChildrenRequest(items.map((item: ContentSummaryAndCompareStatus) => item.getContentId())) - .sendAndParse().then((hasUnpublishedChildrenResult: HasUnpublishedChildrenResult) => { - const hasUnpublishedChildren: boolean = - hasUnpublishedChildrenResult.getResult().some((item: HasUnpublishedChildren) => item.getHasChildren()); - - this.getAction(ActionName.PUBLISH_TREE).setEnabled(hasUnpublishedChildren); - }).catch(reason => DefaultErrorHandler.handle(reason)); - } - - private checkIsChildrenAllowedByContentType(selectedItem: ContentSummaryAndCompareStatus): Q.Promise { - const deferred = Q.defer(); - - new GetContentTypeByNameRequest(selectedItem.getContentSummary().getType()).sendAndParse() - .then((contentType: ContentType) => deferred.resolve(contentType && contentType.isAllowChildContent())) - .fail(() => this.handleContentTypeNotFound(selectedItem)); - - return deferred.promise; - } - - getAction(name: ActionName): Action { - return this.actionsMap.get(name); - } -} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/CreateIssueAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/CreateIssueAction.ts index 783ae86589..6cd268a5af 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/CreateIssueAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/CreateIssueAction.ts @@ -1,20 +1,20 @@ -import {ContentTreeGrid} from '../ContentTreeGrid'; import {CreateIssuePromptEvent} from '../CreateIssuePromptEvent'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class CreateIssueAction extends ContentTreeGridAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.createIssueMore')); this.setEnabled(false).setClass('create-issue'); } protected handleExecuted() { - const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); + const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems(); new CreateIssuePromptEvent(contents).fire(); } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/DuplicateContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/DuplicateContentAction.ts index 36b9e1d0e2..b66dacddf2 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/DuplicateContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/DuplicateContentAction.ts @@ -1,26 +1,25 @@ -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentDuplicatePromptEvent} from '../ContentDuplicatePromptEvent'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class DuplicateContentAction extends ContentTreeGridAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.duplicateMore')); this.setEnabled(false).setClass('duplicate'); } protected handleExecuted() { - const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); + const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems(); new ContentDuplicatePromptEvent(contents) .setYesCallback(() => { - const deselected = this.grid.getSelectedDataList().map(content => content.getId()); - this.grid.deselectNodes(deselected); + this.grid.deselect(this.grid.getSelectedItems()); }).fire(); } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/EditContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/EditContentAction.ts index ee97f9c7c7..bc09648cd2 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/EditContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/EditContentAction.ts @@ -1,13 +1,12 @@ -import {ContentTreeGrid} from '../ContentTreeGrid'; import {EditContentEvent} from '../../event/EditContentEvent'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {showWarning} from '@enonic/lib-admin-ui/notify/MessageBus'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; -import {ProjectContext} from '../../project/ProjectContext'; import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; import {ContentsLocalizer} from './ContentsLocalizer'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class EditContentAction extends ContentTreeGridAction { @@ -17,14 +16,14 @@ export class EditContentAction extends ContentTreeGridAction { private contentsLocalizer?: ContentsLocalizer; - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.edit'), 'mod+e'); this.setEnabled(false).setClass('edit'); } protected handleExecuted() { - const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); + const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems(); if (contents.length > EditContentAction.MAX_ITEMS_TO_EDIT) { showWarning(i18n('notify.edit.tooMuch')); diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/MarkAsReadyContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/MarkAsReadyContentAction.ts index f9bb94593a..94ad67de9a 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/MarkAsReadyContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/MarkAsReadyContentAction.ts @@ -1,5 +1,4 @@ import {Action} from '@enonic/lib-admin-ui/ui/Action'; -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {MarkAsReadyRequest} from '../../resource/MarkAsReadyRequest'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; @@ -10,6 +9,7 @@ import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; import {ContentPublishPromptEvent} from '../ContentPublishPromptEvent'; import {ContentId} from '../../content/ContentId'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class MarkAsReadyContentAction extends ContentTreeGridAction { @@ -18,7 +18,7 @@ export class MarkAsReadyContentAction private confirmDialog: ConfirmationDialog; - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.markAsReady')); this.setEnabled(false).setClass('mark-as-ready'); @@ -29,8 +29,8 @@ export class MarkAsReadyContentAction } protected handleExecuted() { - const content: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); - const contentToMarkAsReady: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList() + const content: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems(); + const contentToMarkAsReady: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems() .filter((item: ContentSummaryAndCompareStatus) => item.canBeMarkedAsReady()); const isSingleItem: boolean = contentToMarkAsReady.length === 1; diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/MoveContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/MoveContentAction.ts index 9068502ae9..396d9b64fb 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/MoveContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/MoveContentAction.ts @@ -1,23 +1,22 @@ -import {ArrayHelper} from '@enonic/lib-admin-ui/util/ArrayHelper'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {ContentMovePromptEvent} from '../../move/ContentMovePromptEvent'; -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class MoveContentAction extends ContentTreeGridAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.moveMore'), 'alt+m'); this.setEnabled(false).setClass('move'); } protected handleExecuted() { - const contents = this.grid.getSelectedDataList().map(content => content.getContentSummary()); - new ContentMovePromptEvent(contents, this.grid).fire(); + const contents = this.grid.getSelectedItems().map(content => content.getContentSummary()); + new ContentMovePromptEvent(contents).fire(); } isToBeEnabled(state: ContentTreeGridItemsState): boolean { @@ -25,15 +24,6 @@ export class MoveContentAction } private isAnyRootItemNotSelected(): boolean { - // if there's at least one non-selected root item then there's at least one option where to move selected items - const selectedIds: string[] = this.getSelectedOrHighlightedItemsIds(); - const rootItemsIds: string[] = this.grid.getRootItemsIds(); - const diff: string[] = ArrayHelper.difference(rootItemsIds, selectedIds, (a: string, b: string) => a === b); - - return diff.length > 0; - } - - private getSelectedOrHighlightedItemsIds(): string[] { - return this.grid.getSelectedOrHighlightedItems().map((item: ContentSummaryAndCompareStatus) => item.getId()); + return this.grid.getSelectedItems().length > 0; } } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/PreviewContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/PreviewContentAction.ts index 73d0a042c5..de7a15fbb8 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/PreviewContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/PreviewContentAction.ts @@ -1,4 +1,3 @@ -import {ContentTreeGrid} from '../ContentTreeGrid'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {showWarning} from '@enonic/lib-admin-ui/notify/MessageBus'; import {ContentTreeGridAction} from './ContentTreeGridAction'; @@ -7,6 +6,7 @@ import {BrowserHelper} from '@enonic/lib-admin-ui/BrowserHelper'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {ContentSummary} from '../../content/ContentSummary'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class PreviewContentAction extends ContentTreeGridAction { @@ -17,7 +17,7 @@ export class PreviewContentAction private static BLOCK_COUNT: number = 10; - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.preview'), BrowserHelper.isOSX() ? 'alt+space' : 'mod+alt+space', true); this.setEnabled(false).setClass('preview'); @@ -27,7 +27,7 @@ export class PreviewContentAction protected handleExecuted() { if (this.totalSelected < PreviewContentAction.BLOCK_COUNT) { - const contentSummaries: ContentSummary[] = this.grid.getSelectedDataList() + const contentSummaries: ContentSummary[] = this.grid.getSelectedItems() .filter((item: ContentSummaryAndCompareStatus) => item.isRenderable()) .map((data: ContentSummaryAndCompareStatus) => data.getContentSummary()); diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/PublishContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/PublishContentAction.ts index e11de6a8bf..f1ceaf69a5 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/PublishContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/PublishContentAction.ts @@ -1,15 +1,15 @@ import {ContentPublishPromptEvent} from '../ContentPublishPromptEvent'; -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class PublishContentAction extends ContentTreeGridAction { private includeChildItems: boolean = false; - constructor(grid: ContentTreeGrid, includeChildItems: boolean = false, useShortcut: boolean = true) { + constructor(grid: SelectableListBoxWrapper, includeChildItems: boolean = false, useShortcut: boolean = true) { super(grid, i18n('action.publishMore'), useShortcut ? 'ctrl+alt+p' : null); this.setEnabled(false).setClass('publish'); @@ -18,7 +18,7 @@ export class PublishContentAction extends ContentTreeGridAction { } protected handleExecuted() { - const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); + const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems(); new ContentPublishPromptEvent({model: contents, includeChildItems: this.includeChildItems}).fire(); } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/PublishTreeContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/PublishTreeContentAction.ts index 0a853a9523..f01662d931 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/PublishTreeContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/PublishTreeContentAction.ts @@ -1,11 +1,12 @@ import {PublishContentAction} from './PublishContentAction'; -import {ContentTreeGrid} from '../ContentTreeGrid'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; +import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; export class PublishTreeContentAction extends PublishContentAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, true, false); this.setClass('publish-tree').setLabel(i18n('action.publishTreeMore')); diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/RequestPublishContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/RequestPublishContentAction.ts index 1e3f7b0682..16b00865ff 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/RequestPublishContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/RequestPublishContentAction.ts @@ -1,21 +1,21 @@ -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {RequestContentPublishPromptEvent} from '../RequestContentPublishPromptEvent'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class RequestPublishContentAction extends ContentTreeGridAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.requestPublishMore')); this.setEnabled(false).setClass('request-publish'); } protected handleExecuted() { - const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); + const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems(); new RequestContentPublishPromptEvent(contents).fire(); } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/ShowNewContentDialogAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/ShowNewContentDialogAction.ts index 95b33f4c49..474114f6c1 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/ShowNewContentDialogAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/ShowNewContentDialogAction.ts @@ -1,20 +1,20 @@ import {ShowNewContentDialogEvent} from '../ShowNewContentDialogEvent'; -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class ShowNewContentDialogAction extends ContentTreeGridAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.newMore'), 'alt+n'); this.setEnabled(true).setClass('new'); } protected handleExecuted() { - const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); + const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems(); new ShowNewContentDialogEvent(contents.length > 0 ? contents[0] : null).fire(); } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/SortContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/SortContentAction.ts index 92d750c448..725e93242c 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/SortContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/SortContentAction.ts @@ -1,20 +1,20 @@ -import {ContentTreeGrid} from '../ContentTreeGrid'; import {SortContentEvent} from '../sort/SortContentEvent'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class SortContentAction extends ContentTreeGridAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.sortMore')); this.setEnabled(false).setClass('sort'); } protected handleExecuted() { - const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); + const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems(); new SortContentEvent(contents).fire(); } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/ToggleSearchPanelAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/ToggleSearchPanelAction.ts index ea39669f75..d135f1fa84 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/ToggleSearchPanelAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/ToggleSearchPanelAction.ts @@ -1,11 +1,12 @@ import {ToggleSearchPanelEvent} from '../ToggleSearchPanelEvent'; import {ContentTreeGridAction} from './ContentTreeGridAction'; -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; +import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; export class ToggleSearchPanelAction extends ContentTreeGridAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, '', 'shift+f', true); this.setIconClass('icon-search3').setClass('search'); diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/UndoPendingDeleteContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/UndoPendingDeleteContentAction.ts deleted file mode 100644 index 0cf5fe0f96..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/UndoPendingDeleteContentAction.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {ContentTreeGrid} from '../ContentTreeGrid'; -import {UndoPendingDeleteContentRequest} from '../../resource/UndoPendingDeleteContentRequest'; -import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; -import {i18n} from '@enonic/lib-admin-ui/util/Messages'; -import {ContentTreeGridAction} from './ContentTreeGridAction'; -import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; - -export class UndoPendingDeleteContentAction extends ContentTreeGridAction { - - constructor(grid: ContentTreeGrid) { - super(grid, i18n('action.undoDelete')); - - this.setEnabled(true); - } - - protected handleExecuted() { - const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); - new UndoPendingDeleteContentRequest(contents.map((content) => content.getContentId())) - .sendAndParse().then((result: number) => UndoPendingDeleteContentRequest.showResponse(result)); - } - - isToBeEnabled(state: ContentTreeGridItemsState): boolean { - return true; - } -} diff --git a/modules/lib/src/main/resources/assets/js/app/browse/action/UnpublishContentAction.ts b/modules/lib/src/main/resources/assets/js/app/browse/action/UnpublishContentAction.ts index c316f355f9..6849b46263 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/action/UnpublishContentAction.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/action/UnpublishContentAction.ts @@ -1,20 +1,20 @@ import {ContentUnpublishPromptEvent} from '../ContentUnpublishPromptEvent'; -import {ContentTreeGrid} from '../ContentTreeGrid'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentTreeGridAction} from './ContentTreeGridAction'; import {ContentTreeGridItemsState} from './ContentTreeGridItemsState'; +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; export class UnpublishContentAction extends ContentTreeGridAction { - constructor(grid: ContentTreeGrid) { + constructor(grid: SelectableListBoxWrapper) { super(grid, i18n('action.unpublishMore')); this.setEnabled(false).setClass('unpublish'); } protected handleExecuted() { - const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedDataList(); + const contents: ContentSummaryAndCompareStatus[] = this.grid.getSelectedItems(); new ContentUnpublishPromptEvent(contents).fire(); } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/filter/FilterableBucketAggregationView.ts b/modules/lib/src/main/resources/assets/js/app/browse/filter/FilterableBucketAggregationView.ts index 42a1435293..bf7f38229c 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/filter/FilterableBucketAggregationView.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/filter/FilterableBucketAggregationView.ts @@ -3,15 +3,15 @@ import {BucketAggregation} from '@enonic/lib-admin-ui/aggregation/BucketAggregat import {BucketAggregationView} from '@enonic/lib-admin-ui/aggregation/BucketAggregationView'; import {Bucket} from '@enonic/lib-admin-ui/aggregation/Bucket'; import {BucketView} from '@enonic/lib-admin-ui/aggregation/BucketView'; -import {SelectableListBoxDropdown} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxDropdown'; import {BucketListBox} from '@enonic/lib-admin-ui/aggregation/BucketListBox'; import {BucketViewSelectionChangedEvent} from '@enonic/lib-admin-ui/aggregation/BucketViewSelectionChangedEvent'; import {Aggregation} from '@enonic/lib-admin-ui/aggregation/Aggregation'; +import {FilterableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/FilterableListBoxWrapper'; export class FilterableBucketAggregationView extends BucketAggregationView { - private listBoxDropdown: SelectableListBoxDropdown; + private listBoxDropdown: FilterableListBoxWrapper; private bucketListBox: BucketListBox = new BucketListBox(); @@ -28,9 +28,9 @@ export class FilterableBucketAggregationView super.initElements(); this.bucketListBox = new BucketListBox(); - this.listBoxDropdown = new SelectableListBoxDropdown(this.bucketListBox, { + this.listBoxDropdown = new FilterableListBoxWrapper(this.bucketListBox, { filter: this.filterBuckets, - multiple: true + maxSelected: 0 }); } @@ -71,7 +71,7 @@ export class FilterableBucketAggregationView } protected addBucket(bucket: Bucket, isSelected?: boolean) { - this.bucketListBox.addItem(bucket); + this.bucketListBox.addItems(bucket); if (isSelected || this.isBucketToBeAlwaysOnTop(bucket)) { super.addBucket(bucket, isSelected); @@ -84,6 +84,7 @@ export class FilterableBucketAggregationView removeAll(): void { super.removeAll(); + this.listBoxDropdown.deselectAll(true); this.bucketListBox.clearItems(); } diff --git a/modules/lib/src/main/resources/assets/js/app/browse/sort/SortContentTreeGrid.ts b/modules/lib/src/main/resources/assets/js/app/browse/sort/SortContentTreeGrid.ts index 9c5c4bc958..94c2a6db1d 100644 --- a/modules/lib/src/main/resources/assets/js/app/browse/sort/SortContentTreeGrid.ts +++ b/modules/lib/src/main/resources/assets/js/app/browse/sort/SortContentTreeGrid.ts @@ -1,16 +1,17 @@ -import {TreeGrid} from '@enonic/lib-admin-ui/ui/treegrid/TreeGrid'; -import {TreeNode} from '@enonic/lib-admin-ui/ui/treegrid/TreeNode'; -import {TreeGridBuilder} from '@enonic/lib-admin-ui/ui/treegrid/TreeGridBuilder'; -import {DateTimeFormatter} from '@enonic/lib-admin-ui/ui/treegrid/DateTimeFormatter'; import {ContentSummaryAndCompareStatusFetcher} from '../../resource/ContentSummaryAndCompareStatusFetcher'; import {ContentResponse} from '../../resource/ContentResponse'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {ContentSummaryViewer} from '../../content/ContentSummaryViewer'; import {ContentId} from '../../content/ContentId'; import {ChildOrder} from '../../resource/order/ChildOrder'; -import {ResultMetadata} from '../../resource/ResultMetadata'; +import {Element} from '@enonic/lib-admin-ui/dom/Element'; +import {LazyListBox} from '@enonic/lib-admin-ui/ui/selector/list/LazyListBox'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {LiEl} from '@enonic/lib-admin-ui/dom/LiEl'; -export class SortContentTreeGrid extends TreeGrid { +export class SortContentTreeGrid extends LazyListBox { + + static MAX_FETCH_SIZE: number = 30; private contentId: ContentId; @@ -18,98 +19,47 @@ export class SortContentTreeGrid extends TreeGrid() - .setColumnConfig([{ - name: 'Name', - id: 'displayName', - field: 'contentSummary.displayName', - formatter: SortContentTreeGrid.nameFormatter, - style: {minWidth: 130}, - behavior: 'selectAndMove' - }, { - name: 'ModifiedTime', - id: 'modifiedTime', - field: 'contentSummary.modifiedTime', - formatter: DateTimeFormatter.format, - style: {cssClass: 'modified', minWidth: 150, maxWidth: 170}, - behavior: 'selectAndMove' - }]) - .setPartialLoadEnabled(true) - .setLoadBufferSize(20) - .setAutoLoad(false) - .setCheckableRows(false) - .setShowToolbar(false) - .setDragAndDrop(true) - .disableMultipleSelection(true) - .prependClasses('content-tree-grid') - .setSelectedCellCssClass('selected-sort-row') - ); + constructor(scrollContainer: Element) { + super('sort-content-tree-grid'); + this.scrollContainer = scrollContainer; this.contentFetcher = new ContentSummaryAndCompareStatusFetcher(); - this.getOptions().setHeight('100%'); } - public static nameFormatter(row: number, cell: number, value: unknown, columnDef: Slick.Column, node: TreeNode) { - const data = node.getData(); - if (data.getContentSummary()) { - let viewer: ContentSummaryViewer = node.getViewer('name') as ContentSummaryViewer; - if (!viewer) { - viewer = new ContentSummaryViewer(); - viewer.setIsRelativePath(node.calcLevel() > 1); - viewer.setObject(node.getData().getContentSummary()); - node.setViewer('name', viewer); - } - return viewer.toString(); - - } - - return ''; + protected createItemView(item: ContentSummaryAndCompareStatus, readOnly: boolean): Element { + const liEl = new LiEl(''); + const viewer = new ContentSummaryViewer(); + viewer.setObject(item.getContentSummary()); + liEl.appendChild(viewer); + return liEl; } - isEmptyNode(node: TreeNode): boolean { - const data = node.getData(); - return !data.getContentSummary(); - } - - sortNodeChildren(node: TreeNode) { - this.initData(this.getRoot().getCurrentRoot().treeToList()); - } + protected handleLazyLoad(): void { + const from: number = this.getItemCount(); - fetchChildren(): Q.Promise { - let parentNode = this.getRoot().getCurrentRoot(); - if (parentNode.getData()) { - parentNode.setData(null); - } - - let from = parentNode.getChildren().length; - if (from > 0 && !parentNode.getChildren()[from - 1].getData().getContentSummary()) { - parentNode.getChildren().pop(); - from--; - } - - return this.contentFetcher.fetchChildren(this.contentId, from, SortContentTreeGrid.MAX_FETCH_SIZE, this.curChildOrder).then( + this.contentFetcher.fetchChildren(this.contentId, from, SortContentTreeGrid.MAX_FETCH_SIZE, this.curChildOrder).then( (data: ContentResponse) => { - const contents: ContentSummaryAndCompareStatus[] = parentNode.getChildren().map((el) => { - return el.getData(); - }).slice(0, from).concat(data.getContents()); - const meta: ResultMetadata = data.getMetadata(); - parentNode.setMaxChildren(meta.getTotalHits()); - if (from + meta.getHits() < meta.getTotalHits()) { - contents.push(new ContentSummaryAndCompareStatus()); + const items = data.getContents(); + + if (items.length > 0) { + this.addItems(items); } - return contents; - }); + }).catch(DefaultErrorHandler.handle); + } + + protected getScrollContainer(): Element { + return this.scrollContainer; } - hasChildren(data: ContentSummaryAndCompareStatus): boolean { - return data.hasChildren(); + load(): void { + this.clearItems(); + this.handleLazyLoad(); } - getDataId(data: ContentSummaryAndCompareStatus): string { - return data.getId(); + protected getItemId(item: ContentSummaryAndCompareStatus): string { + return item.getContentId().toString(); } setContentId(value: ContentId) { @@ -126,8 +76,7 @@ export class SortContentTreeGrid extends TreeGrid void; - private saveButton: DialogButton; private subHeader: H6El; @@ -56,7 +54,7 @@ export class SortContentDialog this.initTabMenu(); this.sortContentMenu = new SortContentTabMenu(); - this.contentGrid = new SortContentTreeGrid(); + this.contentGrid = new SortContentTreeGrid(this.getBody()); this.gridDragHandler = new ContentGridDragHandler(this.contentGrid); this.sortAction = new SaveSortedContentAction(this).setClass('save-button'); this.saveButton = this.addAction(this.sortAction); @@ -84,11 +82,6 @@ export class SortContentDialog this.handleSortApplied(); }); - this.gridLoadedHandler = () => { - this.notifyResize(); - this.contentGrid.getGrid().resizeCanvas(); - }; - OpenSortDialogEvent.on((event) => { this.handleOpenSortDialogEvent(event); }); @@ -111,15 +104,13 @@ export class SortContentDialog show() { super.show(); - this.contentGrid.onLoaded(this.gridLoadedHandler); - this.contentGrid.reload(); + this.contentGrid.load(); this.sortContentMenu.focus(); this.sortAction.setEnabled(false); } close() { this.remove(); - this.contentGrid.unLoaded(this.gridLoadedHandler); super.close(); this.contentGrid.reset(); this.gridDragHandler.clearContentMovements(); @@ -236,8 +227,8 @@ export class SortContentDialog this.contentGrid.toggleClass('inherited', this.sortContentMenu.isInheritedItemSelected()); this.updateSubHeaderText(); - if (!newOrder.isManual()) { - this.contentGrid.reload(); + if (!newOrder.isManual() && this.isOpen()) { + this.contentGrid.load(); this.gridDragHandler.clearContentMovements(); } } diff --git a/modules/lib/src/main/resources/assets/js/app/content/ContentSummaryAndCompareStatusViewer.ts b/modules/lib/src/main/resources/assets/js/app/content/ContentSummaryAndCompareStatusViewer.ts index fd96b3f7f1..8fcb14a9d2 100644 --- a/modules/lib/src/main/resources/assets/js/app/content/ContentSummaryAndCompareStatusViewer.ts +++ b/modules/lib/src/main/resources/assets/js/app/content/ContentSummaryAndCompareStatusViewer.ts @@ -75,6 +75,7 @@ export class ContentSummaryAndCompareStatusViewer this.toggleClass('invalid', invalid); this.toggleClass('has-origin-project', object.hasOriginProject()); this.toggleClass('data-inherited', object.isDataInherited()); + this.toggleClass('sort-inherited', object.isSortInherited()); this.toggleClass('icon-variant', object.isVariant()); if (object.isReadOnly()) { diff --git a/modules/lib/src/main/resources/assets/js/app/dialog/CompareContentVersionsDialog.ts b/modules/lib/src/main/resources/assets/js/app/dialog/CompareContentVersionsDialog.ts index 9dd188b259..415b01a554 100644 --- a/modules/lib/src/main/resources/assets/js/app/dialog/CompareContentVersionsDialog.ts +++ b/modules/lib/src/main/resources/assets/js/app/dialog/CompareContentVersionsDialog.ts @@ -3,12 +3,10 @@ import {GetContentVersionRequest} from '../resource/GetContentVersionRequest'; import {Delta, DiffPatcher, formatters, HtmlFormatter} from 'jsondiffpatch'; import {DefaultModalDialogHeader, ModalDialog, ModalDialogConfig, ModalDialogHeader} from '@enonic/lib-admin-ui/ui/dialog/ModalDialog'; import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; -import {OptionSelectedEvent} from '@enonic/lib-admin-ui/ui/selector/OptionSelectedEvent'; import {CheckboxBuilder} from '@enonic/lib-admin-ui/ui/Checkbox'; import {Element} from '@enonic/lib-admin-ui/dom/Element'; import {LabelEl} from '@enonic/lib-admin-ui/dom/LabelEl'; import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; -import {Dropdown} from '@enonic/lib-admin-ui/ui/selector/dropdown/Dropdown'; import {Button} from '@enonic/lib-admin-ui/ui/button/Button'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {H6El} from '@enonic/lib-admin-ui/dom/H6El'; @@ -28,6 +26,9 @@ import {VersionHistoryHelper} from '../view/context/widget/version/VersionHistor import {ContentVersion} from '../ContentVersion'; import {ObjectHelper} from '@enonic/lib-admin-ui/ObjectHelper'; import {ContentJson} from '../content/ContentJson'; +import {ListBox} from '@enonic/lib-admin-ui/ui/selector/list/ListBox'; +import {FilterableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/FilterableListBoxWrapper'; +import {SelectionChange} from '@enonic/lib-admin-ui/util/SelectionChange'; export class CompareContentVersionsDialog extends ModalDialog { @@ -44,9 +45,9 @@ export class CompareContentVersionsDialog private toolbar: DivEl; - private leftDropdown: Dropdown; + private leftDropdown: CompareDropdown; - private rightDropdown: Dropdown; + private rightDropdown: CompareDropdown; private leftLabel: LabelEl; @@ -124,21 +125,19 @@ export class CompareContentVersionsDialog }); } - private createVersionDropdown(stylePrefix: string, versionId: string): Dropdown { - const dropdown = new Dropdown(`${stylePrefix}-version`, { - optionDisplayValueViewer: new ContentVersionViewer(), - rowHeight: 50, - disableFilter: true, - dataIdProperty: 'value', - value: versionId - }); + private createVersionDropdown(stylePrefix: string): CompareDropdown { + const dropdown = new CompareDropdown(); + dropdown.addClass(`${stylePrefix}-version`); + + dropdown.onSelectionChanged((selectionChange: SelectionChange) => { + if (selectionChange.selected.length > 0) { + if (!this.isRendered()) { + return; + } - dropdown.onOptionSelected((event: OptionSelectedEvent) => { - if (!this.isRendered()) { - return; + this.handleVersionChanged(dropdown === this.leftDropdown); } - this.handleVersionChanged(dropdown === this.leftDropdown); }); this.onClosed(() => dropdown.hideDropdown()); @@ -147,7 +146,7 @@ export class CompareContentVersionsDialog } private getSelectedVersionId(isLeft: boolean): string { - return (isLeft ? this.leftDropdown : this.rightDropdown).getSelectedOption().getValue(); + return (isLeft ? this.leftDropdown : this.rightDropdown).getSelectedItems()[0]?.getSecondaryId(); } private handleVersionChanged(isLeft: boolean) { @@ -169,9 +168,9 @@ export class CompareContentVersionsDialog } } - private createVersionRevertButton(dropdown: Dropdown): Button { + private createVersionRevertButton(dropdown: CompareDropdown): Button { const revertAction: Action = new Action(i18n('field.version.revert')).onExecuted(() => { - const version: VersionHistoryItem = dropdown.getSelectedOption().getDisplayValue(); + const version: VersionHistoryItem = dropdown.getSelectedItems()[0]; this.revertVersionCallback(version.getId(), version.getContentVersion().getTimestamp()); }); revertAction.setTitle(i18n('field.version.makeCurrent')); @@ -182,7 +181,7 @@ export class CompareContentVersionsDialog }); const button = new Button(); - button.addClass('context-menu transparent icon-more_vert icon-large'); + button.addClass('context-menu transparent icon-more_vert icon-large revert-button'); button.onClicked((event: MouseEvent) => { event.stopImmediatePropagation(); event.preventDefault(); @@ -229,8 +228,8 @@ export class CompareContentVersionsDialog this.toolbar = new DivEl('toolbar-container'); this.comparisonContainer = new DivEl('jsondiffpatch-delta'); - this.leftDropdown = this.createVersionDropdown('left', this.leftVersionId); - this.leftDropdown.onExpanded(() => this.disableLeftVersions()); + this.leftDropdown = this.createVersionDropdown('left'); + this.leftDropdown.getList().onShown(() => this.disableLeftVersions()); this.revertLeftButton = this.createVersionRevertButton(this.leftDropdown); this.leftLabel = new LabelEl(i18n('dialog.compareVersions.olderVersion')); @@ -238,8 +237,8 @@ export class CompareContentVersionsDialog leftContainer.appendChildren(this.leftLabel, this.leftDropdown, this.revertLeftButton); this.rightLabel = new LabelEl(i18n('dialog.compareVersions.newerVersion')); - this.rightDropdown = this.createVersionDropdown('right', this.rightVersionId); - this.rightDropdown.onExpanded(() => this.disableRightVersions()); + this.rightDropdown = this.createVersionDropdown('right'); + this.rightDropdown.getList().onShown(() => this.disableRightVersions()); this.revertRightButton = this.createVersionRevertButton(this.rightDropdown); const rightContainer = new DivEl('container right'); @@ -258,6 +257,16 @@ export class CompareContentVersionsDialog return this.reloadVersions().then(() => { this.toolbar.appendChildren(leftContainer, rightContainer); + const itemToSelectInLeft = this.leftDropdown.getList().getItem(this.leftVersionId); + if (itemToSelectInLeft) { + this.leftDropdown.select(itemToSelectInLeft); + } + + const itemToSelectInRight = this.rightDropdown.getList().getItem(this.rightVersionId); + if (itemToSelectInRight) { + this.rightDropdown.select(itemToSelectInRight); + } + this.appendChildToHeader(this.toolbar); this.appendChildToContentPanel(this.comparisonContainer); @@ -339,14 +348,14 @@ export class CompareContentVersionsDialog } private updateLeftDropdown(items: VersionHistoryItem[]): void { - this.leftDropdown?.removeAllOptions(); + this.leftDropdown?.getList().clearItems(); const newestVersionOption: VersionHistoryItem = this.getNewestVersionOption(items); const leftAliases: VersionHistoryItem[] = this.createLeftAliases(items, newestVersionOption.getContentVersion().getTimestamp().getTime()); - const options: Option[] = - leftAliases.concat(items).sort(this.itemsSorter.bind(this)).map(this.createOption.bind(this)); + const itemsToSet: VersionHistoryItem[] = + leftAliases.concat(items).sort(this.itemsSorter.bind(this)); - this.leftDropdown.setOptions(options); + this.leftDropdown.getList().setItems(itemsToSet); if (!this.leftVersionId) { const prev: VersionHistoryItem = @@ -354,25 +363,24 @@ export class CompareContentVersionsDialog this.leftVersionId = prev.getSecondaryId() || newestVersionOption.getSecondaryId(); } - const leftOptionToSelect: Option = this.getNewOptionToSelect(this.leftDropdown, this.leftVersionId); + const leftOptionToSelect: VersionHistoryItem = this.getNewOptionToSelect(this.leftDropdown, this.leftVersionId); if (leftOptionToSelect) { - this.leftDropdown.selectOption(leftOptionToSelect, true); + this.leftDropdown.select(leftOptionToSelect, true); } } private updateRightDropdown(items: VersionHistoryItem[]): void { - this.rightDropdown?.removeAllOptions(); + this.rightDropdown?.getList().clearItems(); const rightAliases: VersionHistoryItem[] = this.createRightAliases(items); - const options: Option[] = - rightAliases.concat(items).sort(this.itemsSorter.bind(this)).map(this.createOption.bind(this)); - this.rightDropdown.setOptions(options); + const itemsToSet: VersionHistoryItem[] = + rightAliases.concat(items).sort(this.itemsSorter.bind(this)); + this.rightDropdown.getList().setItems(itemsToSet); - const rightOptionToSelect: Option = - this.getNewOptionToSelect(this.rightDropdown, this.rightVersionId); + const rightOptionToSelect: VersionHistoryItem = this.getNewOptionToSelect(this.rightDropdown, this.rightVersionId); if (rightOptionToSelect) { - this.rightDropdown.selectOption(rightOptionToSelect, true); + this.rightDropdown.select(rightOptionToSelect, true); } } @@ -384,40 +392,36 @@ export class CompareContentVersionsDialog .toVersionHistoryItems(); } - private getNewOptionToSelect(dropdown: Dropdown, versionId: string): Option { - const newOptionToSelect: Option = dropdown.getOptionByValue(versionId); - const currentSelectedValue: string = dropdown.getValue(); + private getNewOptionToSelect(dropdown: CompareDropdown, versionId: string): VersionHistoryItem { + const newOptionToSelect: VersionHistoryItem = dropdown.getList().getItem(versionId); + const currentSelectedValue: string = dropdown.getSelectedItems()[0]?.getSecondaryId(); return (!!newOptionToSelect && versionId !== currentSelectedValue) ? newOptionToSelect : null; } private updateAliases(isLeft: boolean): boolean { - const dropdown: Dropdown = isLeft ? this.leftDropdown : this.rightDropdown; + const dropdown: CompareDropdown = isLeft ? this.leftDropdown : this.rightDropdown; let selectedAliasType: AliasType; - const selectedOption: Option = dropdown.getSelectedOption(); - if (selectedOption) { - selectedAliasType = selectedOption.getDisplayValue().getAliasType(); + const selectedItem: VersionHistoryItem = dropdown.getSelectedItems()[0]; + + if (selectedItem) { + selectedAliasType = selectedItem.getAliasType(); } - let nextOption: Option = dropdown.getOptionByRow(0); - while (nextOption.getDisplayValue().isAlias()) { - dropdown.removeOption(nextOption); - nextOption = dropdown.getOptionByRow(0); + let nextOption: VersionHistoryItem = dropdown.getList().getItems()[0]; + while (nextOption.isAlias()) { + dropdown.getList().removeItems(nextOption); + nextOption = dropdown.getList().getItems()[0]; } - let aliasFound: boolean; + let aliasFound: VersionHistoryItem; - const options = dropdown.getOptions(); - const items = options.map(option => option.getDisplayValue()); + const items = dropdown.getList().getItems(); const aliases: VersionHistoryItem[] = isLeft ? this.createLeftAliases(items, - selectedOption.getDisplayValue().getContentVersion().getTimestamp().getTime()) : this.createRightAliases(items); + selectedItem?.getContentVersion().getTimestamp().getTime()) : this.createRightAliases(items); aliases.forEach((alias: VersionHistoryItem) => { - const aliasOption = this.createOption(alias); - dropdown.addOption(aliasOption); - if (alias.getAliasType() === selectedAliasType) { - aliasFound = true; - dropdown.selectOption(aliasOption, true); + aliasFound = alias; // update version with new alias id if (isLeft) { this.leftVersionId = alias.getSecondaryId(); @@ -426,8 +430,14 @@ export class CompareContentVersionsDialog } } }); + // sort first to ensure correct order - dropdown.sort(this.optionSorter.bind(this)); + const itemsToSet = items.concat(aliases).sort(this.itemsSorter.bind(this)); + dropdown.getList().setItems(itemsToSet); + + if (aliasFound) { + dropdown.select(aliasFound, true); + } if (selectedAliasType && !aliasFound) { // set same value as in the other dropdown @@ -497,7 +507,8 @@ export class CompareContentVersionsDialog if (newestPublishedVersion) { aliases.push( - this.createAliasVersionHistoryItem(newestPublishedVersion, i18n('dialog.compareVersions.publishedVersion'), AliasType.PUBLISHED) + this.createAliasVersionHistoryItem(newestPublishedVersion, i18n('dialog.compareVersions.publishedVersion'), + AliasType.PUBLISHED) ); } @@ -517,7 +528,8 @@ export class CompareContentVersionsDialog if (newestPublishedVersion) { aliases.push( - this.createAliasVersionHistoryItem(newestPublishedVersion, i18n('dialog.compareVersions.publishedVersion'), AliasType.PUBLISHED) + this.createAliasVersionHistoryItem(newestPublishedVersion, i18n('dialog.compareVersions.publishedVersion'), + AliasType.PUBLISHED) ); } @@ -579,61 +591,58 @@ export class CompareContentVersionsDialog return b.getContentVersion().getTimestamp().getTime() - a.getContentVersion().getTimestamp().getTime(); } - private optionSorter(a: Option, b: Option): number { - return this.itemsSorter(a.getDisplayValue(), b.getDisplayValue()); - } - private leftVersionRequiresForcedSelection() { - const leftTime: Date = this.leftDropdown.getSelectedOption().getDisplayValue().getContentVersion().getTimestamp(); - const rightTime: Date = this.rightDropdown.getSelectedOption().getDisplayValue().getContentVersion().getTimestamp(); + const leftTime: Date = this.leftDropdown.getSelectedItems()[0].getContentVersion().getTimestamp(); + const rightTime: Date = this.rightDropdown.getSelectedItems()[0].getContentVersion().getTimestamp(); return leftTime.getTime() > rightTime.getTime(); } - private forceSelectVersion(dropdown: Dropdown, versionId: string, silent?: boolean) { - const newOption: Option = dropdown.getOptionByValue(versionId); - const selectedValue: string = dropdown.getValue(); + private forceSelectVersion(dropdown: CompareDropdown, versionId: string, silent?: boolean) { + const newOption: VersionHistoryItem = dropdown.getList().getItem(versionId); + const selectedValue: VersionHistoryItem = dropdown.getSelectedItems()[0]; - if (!!newOption && versionId !== selectedValue) { - dropdown.selectOption(newOption, silent); + if (!!newOption && versionId !== selectedValue?.getSecondaryId()) { + dropdown.select(newOption, silent); } } - private disableLeftVersions() { + private disableLeftVersions(): void { let markReadOnly = false; - const rightOption = this.rightDropdown.getSelectedOption(); - const isNextSelectedInRightDropdown = !!rightOption ? rightOption.getDisplayValue().getAliasType() === AliasType.NEXT : null; + const rightOption = this.rightDropdown.getSelectedItems()[0]; + const isNextSelectedInRightDropdown = !!rightOption ? rightOption.getAliasType() === AliasType.NEXT : null; - this.leftDropdown.getOptions().slice().reverse().forEach((option: Option) => { + this.leftDropdown.getList().getItems().slice().reverse().forEach((option: VersionHistoryItem) => { // slice first to create new array and prevent modification of original options // doing reverse to be sure to go through regular versions before aliases // and make everything in one go - if (option.getDisplayValue().isAlias()) { - option.setReadOnly(isNextSelectedInRightDropdown && option.getDisplayValue().getAliasType() === AliasType.PREV); + const view = this.leftDropdown.getList().getItemView(option); + const isReadonly = option.isAlias() ? isNextSelectedInRightDropdown && option.getAliasType() === AliasType.PREV : markReadOnly; + + if (isReadonly) { + view.getParentElement().addClass('readonly'); } else { - option.setReadOnly(markReadOnly); + view.getParentElement().removeClass('readonly'); } - if (option.getValue() === this.rightVersionId) { + + if (option.getSecondaryId() === this.rightVersionId) { // marking readOnly all versions after rightVersion markReadOnly = true; } }); - - this.leftDropdown.refresh(); // making readonly changes in options visible } - private disableRightVersions() { - const leftOption = this.leftDropdown.getSelectedOption(); - const isPrevSelectedInLeftDropdown = !!leftOption ? leftOption.getDisplayValue().getAliasType() === AliasType.PREV : null; + private disableRightVersions(): void { + const leftOption = this.leftDropdown.getSelectedItems()[0]; + const isPrevSelectedInLeftDropdown = !!leftOption ? leftOption.getAliasType() === AliasType.PREV : null; - this.rightDropdown.getOptions().filter(option => option.getDisplayValue().isAlias()).forEach(option => { - option.setReadOnly(isPrevSelectedInLeftDropdown && option.getDisplayValue().getAliasType() === AliasType.NEXT); + this.rightDropdown.getList().getItems().filter(option => option.isAlias()).forEach(option => { + const view = this.rightDropdown.getList().getItemView(option); + view.getParentElement().toggleClass('readonly', isPrevSelectedInLeftDropdown && option.getAliasType() === AliasType.NEXT); }); - - this.rightDropdown.refresh(); // making readonly changes in options visible } private fetchVersionPromise(versionId: string): Q.Promise { @@ -685,8 +694,8 @@ export class CompareContentVersionsDialog } private updateButtonsState() { - const leftVersion: VersionHistoryItem = this.leftDropdown.getSelectedOption().getDisplayValue(); - const rightVersion: VersionHistoryItem = this.rightDropdown.getSelectedOption().getDisplayValue(); + const leftVersion: VersionHistoryItem = this.leftDropdown.getSelectedItems()[0]; + const rightVersion: VersionHistoryItem = this.rightDropdown.getSelectedItems()[0]; this.revertLeftButton.setEnabled(this.isVersionRevertable(leftVersion)); this.revertRightButton.setEnabled(this.isVersionRevertable(rightVersion)); @@ -752,3 +761,79 @@ class CompareContentVersionsDialogHeader } } + +class CompareDropdown + extends FilterableListBoxWrapper { + + private selectedItemViewer: ContentVersionViewer; + + constructor() { + super(new CompareList(), { + className: 'compare-dropdown', + maxSelected: 1, + } + ); + } + + protected initElements(): void { + super.initElements(); + + this.selectedItemViewer = new ContentVersionViewer(); + } + + createSelectedOption(item: VersionHistoryItem): Option { + return Option.create() + .setValue(item.getSecondaryId()) + .setDisplayValue(item) + .build(); + } + + protected initListeners(): void { + super.initListeners(); + + this.onSelectionChanged(() => { + this.selectedItemViewer.setObject(this.getSelectedItems()[0]); + }); + + this.selectedItemViewer.onClicked(() => { + this.handleDropdownHandleClicked(); + }); + } + + hideDropdown(): void { + super.hideDropdown(); + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.filterContainer.appendChild(this.selectedItemViewer); + + return rendered; + }); + } + + protected handleUserToggleAction(item: VersionHistoryItem): void { + if (item.getSecondaryId() === this.selectedItemViewer.getObject().getSecondaryId()) { + return; + } + + super.handleUserToggleAction(item); + } + +} + +class CompareList + extends ListBox { + + protected createItemView(item: VersionHistoryItem, readOnly: boolean): ContentVersionViewer { + const viewer = new ContentVersionViewer(); + viewer.setObject(item); + return viewer; + } + + protected getItemId(item: VersionHistoryItem): string { + return item?.getSecondaryId(); + } + + +} diff --git a/modules/lib/src/main/resources/assets/js/app/dialog/DialogMainItemsList.ts b/modules/lib/src/main/resources/assets/js/app/dialog/DialogMainItemsList.ts index be4b3ceea3..e5ae95541a 100644 --- a/modules/lib/src/main/resources/assets/js/app/dialog/DialogMainItemsList.ts +++ b/modules/lib/src/main/resources/assets/js/app/dialog/DialogMainItemsList.ts @@ -42,7 +42,7 @@ export class DialogMainItemsList statusItem.setIsRemovableFn(() => this.isItemRemovable(statusItem)); statusItem.setRemoveHandlerFn(() => { - this.removeItem(item); + this.removeItems(item); this.notifyItemRemoveClicked(item); }); diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelector.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelector.ts index 50f9ee0777..98def96a49 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelector.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelector.ts @@ -4,9 +4,7 @@ import {PropertyArray} from '@enonic/lib-admin-ui/data/PropertyArray'; import {Value} from '@enonic/lib-admin-ui/data/Value'; import {ValueType} from '@enonic/lib-admin-ui/data/ValueType'; import {ValueTypes} from '@enonic/lib-admin-ui/data/ValueTypes'; -import {SelectedOptionEvent} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionEvent'; import {UriHelper} from '@enonic/lib-admin-ui/util/UriHelper'; -import {RichComboBox} from '@enonic/lib-admin-ui/ui/selector/combobox/RichComboBox'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; import {CustomSelectorItem} from './CustomSelectorItem'; import {CustomSelectorComboBox, CustomSelectorSelectedOptionsView} from './CustomSelectorComboBox'; @@ -17,12 +15,13 @@ import {ValueTypeConverter} from '@enonic/lib-admin-ui/data/ValueTypeConverter'; import {InputTypeManager} from '@enonic/lib-admin-ui/form/inputtype/InputTypeManager'; import {Class} from '@enonic/lib-admin-ui/Class'; import {UrlAction} from '../../UrlAction'; -import {CustomSelectorLoader} from './CustomSelectorLoader'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {ContentServerEventsHandler} from '../../event/ContentServerEventsHandler'; import {ProjectContext} from '../../project/ProjectContext'; import {Branch} from '../../versioning/Branch'; import {ContentSummary} from '../../content/ContentSummary'; +import {SelectionChange} from '@enonic/lib-admin-ui/util/SelectionChange'; +import {ObjectHelper} from '@enonic/lib-admin-ui/ObjectHelper'; export class CustomSelector extends BaseInputTypeManagingAdd { @@ -35,7 +34,9 @@ export class CustomSelector private content?: ContentSummary; - private comboBox: RichComboBox; + private comboBox: CustomSelectorComboBox; + + private initiallySelectedItems: string[]; private static serviceUrlPrefix: string; @@ -102,6 +103,7 @@ export class CustomSelector } return super.layout(input, propertyArray).then(() => { + this.initiallySelectedItems = this.getSelectedItemsIds(); this.comboBox = this.createComboBox(input, propertyArray); this.appendChild(this.comboBox); @@ -113,23 +115,19 @@ export class CustomSelector } update(propertyArray: PropertyArray, unchangedOnly?: boolean): Q.Promise { - const superPromise = super.update(propertyArray, unchangedOnly); + const isDirty = this.isDirty(); - if (!unchangedOnly || !this.comboBox.isDirty()) { - return superPromise.then(() => { - this.comboBox.setValue(this.getValueFromPropertyArray(propertyArray)); - }); - } else if (this.comboBox.isDirty()) { - this.comboBox.forceChangedEvent(); - } - return superPromise; + return super.update(propertyArray, unchangedOnly).then(() => { + this.initiallySelectedItems = this.getSelectedItemsIds(); + + if (!unchangedOnly || !isDirty) { + this.comboBox.setSelectedItems(this.initiallySelectedItems); + } + }); } reset() { // value is not set yet if not rendered, resetting will overwrite original value with empty value - if (this.comboBox.isRendered()) { - this.comboBox.resetBaseValues(); - } } private getRequestPath(): string { @@ -138,59 +136,58 @@ export class CustomSelector return StringHelper.format(this.requestPath, projectId, contentId); } - private createLoader(): CustomSelectorLoader { - const loader: CustomSelectorLoader = new CustomSelectorLoader(); - loader.onLoadingData(() => { - loader.setRequestPath(this.getRequestPath()); - }); - - return loader; + private isDirty(): boolean { + return !ObjectHelper.stringArrayEquals(this.initiallySelectedItems, this.getSelectedItemsIds()); } - createComboBox(input: Input, propertyArray: PropertyArray): RichComboBox { + private createComboBox(input: Input, propertyArray: PropertyArray): CustomSelectorComboBox { + const comboBox: CustomSelectorComboBox = new CustomSelectorComboBox({ + maxSelected: input.getOccurrences().getMaximum(), + }); - const comboBox: CustomSelectorComboBox = CustomSelectorComboBox.create() - .setComboBoxName(input.getName()) - .setMaximumOccurrences(input.getOccurrences().getMaximum()) - .setValue(this.getValueFromPropertyArray(propertyArray)) - .setLoader(this.createLoader()) - .build() as CustomSelectorComboBox; + comboBox.getLoader().setRequestPath(this.getRequestPath()); + comboBox.setSelectedItems(this.initiallySelectedItems); - comboBox.onOptionSelected((event: SelectedOptionEvent) => { + comboBox.onSelectionChanged((selectionChange: SelectionChange) => { this.ignorePropertyChange(true); - const option = event.getSelectedOption(); - let value = new Value(String(option.getOption().getValue()), ValueTypes.STRING); - if (option.getIndex() >= 0) { - this.getPropertyArray().set(option.getIndex(), value); - } else { - this.getPropertyArray().add(value); - } - this.refreshSortable(); + selectionChange.selected?.forEach((item: CustomSelectorItem) => { + const value = new Value(item.getId().toString(), ValueTypes.STRING); - this.ignorePropertyChange(false); + if (this.comboBox.countSelected() === 1) { // overwrite initial value + this.getPropertyArray().set(0, value); + } else { + this.getPropertyArray().add(value); + } + }); - this.handleValueChanged(false); - this.fireFocusSwitchEvent(event); - }); + selectionChange.deselected?.forEach((item: CustomSelectorItem) => { + const property = this.getPropertyArray().getProperties().find((property) => { + const propertyValue = property.hasNonNullValue() ? property.getString() : ''; + return propertyValue === item.getId().toString(); + }); - comboBox.onOptionDeselected((event: SelectedOptionEvent) => { - this.ignorePropertyChange(true); + if (property) { + this.getPropertyArray().remove(property.getIndex()); + } - this.getPropertyArray().remove(event.getSelectedOption().getIndex()); + }); this.refreshSortable(); this.ignorePropertyChange(false); this.handleValueChanged(false); + this.validate(false); }); comboBox.onOptionMoved((moved: SelectedOption, fromIndex: number) => this.handleMove(moved, fromIndex)); - comboBox.onValueLoaded(() => this.handleValueChanged(false)); - return comboBox; } + private getSelectedItemsIds(): string[] { + return this.getValueFromPropertyArray(this.getPropertyArray()).split(';').filter((id) => !StringHelper.isBlank(id)); + } + protected getNumberOfValids(): number { return this.getPropertyArray().getSize(); } @@ -234,7 +231,7 @@ export class CustomSelector private getSelectedOptionsView(): CustomSelectorSelectedOptionsView { this.updateSelectedOptionStyle(); - return this.comboBox.getSelectedOptionView() as CustomSelectorSelectedOptionsView; + return this.comboBox.getSelectedOptionView(); } private updateSelectedOptionStyle() { diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorComboBox.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorComboBox.ts index 48c0b2c078..7e31359109 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorComboBox.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorComboBox.ts @@ -1,34 +1,112 @@ -/*global Q*/ -import {RichComboBox, RichComboBoxBuilder} from '@enonic/lib-admin-ui/ui/selector/combobox/RichComboBox'; import {BaseSelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/BaseSelectedOptionsView'; import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; import {CustomSelectorItem} from './CustomSelectorItem'; import {CustomSelectorItemViewer} from './CustomSelectorItemViewer'; import {RichSelectedOptionView, RichSelectedOptionViewBuilder} from '@enonic/lib-admin-ui/ui/selector/combobox/RichSelectedOptionView'; -import {Viewer} from '@enonic/lib-admin-ui/ui/Viewer'; -import {SelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionsView'; -import {BaseLoader} from '@enonic/lib-admin-ui/util/loader/BaseLoader'; +import { + FilterableListBoxWrapperWithSelectedView, + ListBoxInputOptions +} from '@enonic/lib-admin-ui/ui/selector/list/FilterableListBoxWrapperWithSelectedView'; +import {CustomSelectorLoader} from './CustomSelectorLoader'; +import {CustomSelectorListBox} from './CustomSelectorListBox'; +import {LoadedDataEvent} from '@enonic/lib-admin-ui/util/loader/event/LoadedDataEvent'; +import * as Q from 'q'; +import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; +import {AppHelper} from '@enonic/lib-admin-ui/util/AppHelper'; +import {ValueChangedEvent} from '@enonic/lib-admin-ui/ValueChangedEvent'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; + +interface CustomSelectorComboBoxOptions extends ListBoxInputOptions { + loader: CustomSelectorLoader; +} + +export interface CustomSelectorBuilderOptions { + maxSelected: number; +} export class CustomSelectorComboBox - extends RichComboBox { + extends FilterableListBoxWrapperWithSelectedView { - constructor(builder: CustomSelectorComboBoxBuilder) { - super(builder); + protected options: CustomSelectorComboBoxOptions; + + constructor(options: CustomSelectorBuilderOptions) { + const loader = new CustomSelectorLoader(); + + super(new CustomSelectorListBox(loader), { + selectedOptionsView: new CustomSelectorSelectedOptionsView(), + className: 'custom-selector-combobox', + loader: loader, + maxSelected: options.maxSelected, + } as CustomSelectorComboBoxOptions); } - protected reload(inputValue: string): Q.Promise { - const loader: BaseLoader = this.getLoader(); + protected initListeners(): void { + super.initListeners(); + + this.options.loader.onLoadedData((event: LoadedDataEvent) => { + const entries = event.getData(); + + if (event.isPostLoad()) { + this.listBox.addItems(entries.slice(this.listBox.getItemCount())); + } else { + this.listBox.setItems(entries); + } + return Q.resolve(null); + }); + + this.listBox.whenShown(() => { + // if not empty then search will be performed after finished typing + if (StringHelper.isBlank(this.optionFilterInput.getValue())) { + this.search(this.optionFilterInput.getValue()); + } + }); + + let searchValue = ''; + + const debouncedSearch = AppHelper.debounce(() => { + this.search(searchValue); + }, 300); + + this.optionFilterInput.onValueChanged((event: ValueChangedEvent) => { + searchValue = event.getNewValue(); + debouncedSearch(); + }); + } - if (loader.isLoaded() && loader.getSearchString() === inputValue) { - return loader.search(inputValue); - } + protected search(value?: string): void { + this.options.loader.search(value).catch(DefaultErrorHandler.handle); + } - return super.reload(inputValue, true); + createSelectedOption(item: CustomSelectorItem): Option { + return Option.create() + .setValue(item.getId()) + .setDisplayValue(item) + .build(); } - static create(): CustomSelectorComboBoxBuilder { - return new CustomSelectorComboBoxBuilder(); + onOptionMoved(handler: (selectedOption: SelectedOption, fromIndex: number) => void): void { + this.selectedOptionsView.onOptionMoved(handler); + } + + getLoader(): CustomSelectorLoader { + return this.options.loader; + } + + getSelectedOptionView(): CustomSelectorSelectedOptionsView { + return this.selectedOptionsView; + } + + setSelectedItems(selectedIds: string[]): void { + this.deselectAll(true); + + if (selectedIds?.length > 0) { + this.getLoader().sendPreLoadRequest(selectedIds).then((items: CustomSelectorItem[]) => { + items.sort((a, b) => selectedIds.indexOf(a.getId().toString()) - selectedIds.indexOf(b.getId().toString())); + const toSelect = items.filter((item) => selectedIds.indexOf(item.getId().toString()) >= 0); + this.select(toSelect, true); + }).catch(DefaultErrorHandler.handle); + } } } @@ -57,18 +135,11 @@ export class CustomSelectorSelectedOptionView return viewer; } -} - -export class CustomSelectorComboBoxBuilder - extends RichComboBoxBuilder { - - optionDisplayValueViewer: Viewer = new CustomSelectorItemViewer(); - - delayedInputValueChangedHandling: number = 300; - - selectedOptionsView: SelectedOptionsView = new CustomSelectorSelectedOptionsView(); + doRender(): Q.Promise { + return super.doRender().then((rendered) => { + this.addClass('custom-selector-selected-option-view'); - build(): CustomSelectorComboBox { - return new CustomSelectorComboBox(this); + return rendered; + }); } } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorListBox.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorListBox.ts new file mode 100644 index 0000000000..1a2d3803ad --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorListBox.ts @@ -0,0 +1,39 @@ +import {LazyListBox} from '@enonic/lib-admin-ui/ui/selector/list/LazyListBox'; +import {Element} from '@enonic/lib-admin-ui/dom/Element'; +import {CustomSelectorItem} from './CustomSelectorItem'; +import {CustomSelectorItemViewer} from './CustomSelectorItemViewer'; +import {CustomSelectorLoader} from './CustomSelectorLoader'; + +export class CustomSelectorListBox + extends LazyListBox { + + private loader: CustomSelectorLoader; + + constructor(loader: CustomSelectorLoader) { + super('custom-selector-list-box'); + + this.loader = loader; + } + + protected createItemView(item: CustomSelectorItem, readOnly: boolean): CustomSelectorItemViewer { + const viewer = new CustomSelectorItemViewer(); + viewer.setObject(item); + return viewer; + } + + protected getItemId(item: CustomSelectorItem): string { + return item.getId(); + } + + protected getScrollContainer(): Element { + return this; + } + + protected handleLazyLoad(): void { + super.handleLazyLoad(); + + if (this.loader.isPartiallyLoaded()) { + this.loader.load(true); + } + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorLoader.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorLoader.ts index 3972305581..9bde666821 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorLoader.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/customselector/CustomSelectorLoader.ts @@ -32,6 +32,11 @@ export class CustomSelectorLoader }, 200); } + search(searchString: string): Q.Promise { + this.setSearchString(searchString); + return this.load(); + } + load(postLoad: boolean = false): Q.Promise { this.getRequest().setPostLoading(postLoad); @@ -66,11 +71,14 @@ export class CustomSelectorLoader return deferred.promise; } - protected sendPreLoadRequest(ids: string): Q.Promise { + sendPreLoadRequest(ids: string | string[]): Q.Promise { if (!this.request.hasRequestPath()) { return Q.reject(i18n('field.customSelector.noService')); } - return this.getRequest().setIds(ids.split(';')).sendAndParse().then((results) => { + + const idsToLoad = Array.isArray(ids) ? ids : ids.split(';'); + + return this.getRequest().setIds(idsToLoad).sendAndParse().then((results) => { this.getRequest().setIds([]); return results; }); diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeComboBox.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeComboBox.ts index 65e3ba6f0e..78200c8df1 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeComboBox.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeComboBox.ts @@ -1,27 +1,10 @@ import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; -import {RichComboBox, RichComboBoxBuilder} from '@enonic/lib-admin-ui/ui/selector/combobox/RichComboBox'; import {BaseSelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/BaseSelectedOptionsView'; import {RichSelectedOptionView, RichSelectedOptionViewBuilder} from '@enonic/lib-admin-ui/ui/selector/combobox/RichSelectedOptionView'; import {ContentTypeSummary} from '@enonic/lib-admin-ui/schema/content/ContentTypeSummary'; import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; -import {ContentTypeSummaryViewer} from '../ui/schema/ContentTypeSummaryViewer'; -import {Viewer} from '@enonic/lib-admin-ui/ui/Viewer'; -import {SelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionsView'; import {SchemaBuilder} from '@enonic/lib-admin-ui/schema/Schema'; -export class ContentTypeComboBox - extends RichComboBox { - - constructor(builder: ContentTypeComboBoxBuilder) { - super(builder); - } - - static create(): ContentTypeComboBoxBuilder { - return new ContentTypeComboBoxBuilder(); - } - -} - export class ContentTypeSelectedOptionsView extends BaseSelectedOptionsView { constructor() { @@ -66,16 +49,3 @@ export class ContentTypeSelectedOptionView } } - -export class ContentTypeComboBoxBuilder - extends RichComboBoxBuilder { - - optionDisplayValueViewer: Viewer = new ContentTypeSummaryViewer(); - - selectedOptionsView: SelectedOptionsView = new ContentTypeSelectedOptionsView(); - - build(): ContentTypeComboBox { - return new ContentTypeComboBox(this); - } - -} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeFilter.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeFilter.ts index 7d649343f0..3b6b2c40d2 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeFilter.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeFilter.ts @@ -5,34 +5,34 @@ import {Value} from '@enonic/lib-admin-ui/data/Value'; import {ValueType} from '@enonic/lib-admin-ui/data/ValueType'; import {ValueTypes} from '@enonic/lib-admin-ui/data/ValueTypes'; import {ContentTypeSummary} from '@enonic/lib-admin-ui/schema/content/ContentTypeSummary'; -import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; -import {SelectedOptionEvent} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionEvent'; import {BaseLoader} from '@enonic/lib-admin-ui/util/loader/BaseLoader'; import {BaseInputTypeManagingAdd} from '@enonic/lib-admin-ui/form/inputtype/support/BaseInputTypeManagingAdd'; import {ContentInputTypeViewContext} from '../ContentInputTypeViewContext'; import {PageTemplateContentTypeLoader} from './PageTemplateContentTypeLoader'; -import {ContentTypeComboBox} from './ContentTypeComboBox'; import {ContentTypeSummaryLoader} from './ContentTypeSummaryLoader'; import {ContentTypeComparator} from './ContentTypeComparator'; import {ValueTypeConverter} from '@enonic/lib-admin-ui/data/ValueTypeConverter'; import {InputTypeManager} from '@enonic/lib-admin-ui/form/inputtype/InputTypeManager'; import {Class} from '@enonic/lib-admin-ui/Class'; import {ContentId} from '../../content/ContentId'; +import {ContentTypeFilterDropdown} from './ContentTypeFilterDropdown'; +import {SelectionChange} from '@enonic/lib-admin-ui/util/SelectionChange'; +import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; +import {ObjectHelper} from '@enonic/lib-admin-ui/ObjectHelper'; export class ContentTypeFilter extends BaseInputTypeManagingAdd { protected context: ContentInputTypeViewContext; - private combobox: ContentTypeComboBox; - - private readonly onContentTypesLoadedHandler: (contentTypeArray: ContentTypeSummary[]) => void; + private typesListDropdown: ContentTypeFilterDropdown; private isContextDependent: boolean; + private initiallySelectedItems: string[]; + constructor(context: ContentInputTypeViewContext) { super(context, 'content-type-filter'); - this.onContentTypesLoadedHandler = this.onContentTypesLoaded.bind(this); } protected readInputConfig(): void { @@ -43,7 +43,7 @@ export class ContentTypeFilter } getValueType(): ValueType { - return ValueTypes.STRING; + return ValueTypes.STRING; } newInitialValue(): Value { @@ -51,57 +51,23 @@ export class ContentTypeFilter } private createLoader(): BaseLoader { - let loader: BaseLoader; + return this.doCreateLoader().setComparator(new ContentTypeComparator()); + } + private doCreateLoader(): BaseLoader { if (this.context.formContext.getContentTypeName()?.isPageTemplate()) { const contentId: ContentId = this.context.site?.getContentId(); - loader = new PageTemplateContentTypeLoader(contentId, this.context.project); - } else { - const contentId: ContentId = this.isContextDependent ? this.context.content?.getContentId() : null; - loader = new ContentTypeSummaryLoader(contentId, this.context.project); + return new PageTemplateContentTypeLoader(contentId, this.context.project); } - loader.setComparator(new ContentTypeComparator()); - - return loader; - } - - private createComboBox(): ContentTypeComboBox { - const loader: PageTemplateContentTypeLoader | ContentTypeSummaryLoader = this.createLoader(); - const comboBox: ContentTypeComboBox = ContentTypeComboBox.create() - .setLoader(loader) - .setMaximumOccurrences(this.getInput().getOccurrences().getMaximum()) - .setDisplayMissingSelectedOptions(true) - .build() as ContentTypeComboBox; - - comboBox.onLoaded(this.onContentTypesLoadedHandler); - - comboBox.onOptionSelected((event: SelectedOptionEvent) => { - this.fireFocusSwitchEvent(event); - this.onContentTypeSelected(event.getSelectedOption()); - }); - - comboBox.onOptionDeselected((event: SelectedOptionEvent) => - this.onContentTypeDeselected(event.getSelectedOption())); - - return comboBox; + const contentId: ContentId = this.isContextDependent ? this.context.content?.getContentId() : null; + return new ContentTypeSummaryLoader(contentId, this.context.project); } - private onContentTypesLoaded(): void { - - this.combobox.getComboBox().setValue(this.getValueFromPropertyArray(this.getPropertyArray())); - - this.setLayoutInProgress(false); - this.combobox.unLoaded(this.onContentTypesLoadedHandler); - } - - private onContentTypeSelected(selectedOption: SelectedOption): void { - if (this.isLayoutInProgress()) { - return; - } + private onContentTypeSelected(contentType: ContentTypeSummary): void { this.ignorePropertyChange(true); - let value = new Value(selectedOption.getOption().getDisplayValue().getContentTypeName().toString(), ValueTypes.STRING); - if (this.combobox.countSelected() === 1) { // overwrite initial value + let value = new Value(contentType.getContentTypeName().toString(), ValueTypes.STRING); + if (this.typesListDropdown.countSelected() === 1) { // overwrite initial value this.getPropertyArray().set(0, value); } else { this.getPropertyArray().add(value); @@ -111,11 +77,18 @@ export class ContentTypeFilter this.ignorePropertyChange(false); } - private onContentTypeDeselected(option: SelectedOption): void { - this.ignorePropertyChange(true); - this.getPropertyArray().remove(option.getIndex()); - this.handleValueChanged(false); - this.ignorePropertyChange(false); + private onContentTypeDeselected(item: ContentTypeSummary): void { + const property = this.getPropertyArray().getProperties().find((property) => { + const propertyValue = property.hasNonNullValue() ? property.getString() : ''; + return propertyValue === item.getId(); + }); + + if (property) { + this.ignorePropertyChange(true); + this.getPropertyArray().remove(property.getIndex()); + this.handleValueChanged(false); + this.ignorePropertyChange(false); + } } layout(input: Input, propertyArray: PropertyArray): Q.Promise { @@ -124,36 +97,76 @@ export class ContentTypeFilter } return super.layout(input, propertyArray).then(() => { - this.appendChild(this.combobox = this.createComboBox()); + this.initiallySelectedItems = this.getSelectedItemsIds(); + this.typesListDropdown = this.createListDropdown(); + this.appendChild(this.typesListDropdown); + }).finally(() => { + this.setLayoutInProgress(false); + }); + } + + private createListDropdown(): ContentTypeFilterDropdown { + const typesListDropdown = new ContentTypeFilterDropdown({ + maxSelected: this.getInput().getOccurrences().getMaximum(), + loader: this.createLoader(), + getSelectedItems: () => this.getSelectedItemsIds(), + }); + + typesListDropdown.onSelectionChanged((selectionChange: SelectionChange) => { + selectionChange.selected?.forEach((item: ContentTypeSummary) => { + this.onContentTypeSelected(item); + }); - return this.combobox.getLoader().load().then(() => { - this.validate(false); - return Q(null); + selectionChange.deselected?.forEach((item: ContentTypeSummary) => { + this.onContentTypeDeselected(item); }); }); + + return typesListDropdown; + } + + private getSelectedItemsIds(): string[] { + return this.getValueFromPropertyArray(this.getPropertyArray()).split(';').filter((id) => !StringHelper.isBlank(id)); } update(propertyArray: PropertyArray, unchangedOnly: boolean): Q.Promise { - let superPromise = super.update(propertyArray, unchangedOnly); + const isDirty = this.isDirty(); - if (!unchangedOnly || !this.combobox.isDirty()) { - return superPromise.then(() => { + return super.update(propertyArray, unchangedOnly).then(() => { + this.initiallySelectedItems = this.getSelectedItemsIds(); - return this.combobox.getLoader().load().then(this.onContentTypesLoadedHandler); - }); - } else if (this.combobox.isDirty()) { - this.combobox.forceChangedEvent(); - } - return superPromise; + if (!unchangedOnly || !isDirty) { + this.typesListDropdown.updateSelectedItems(); + } else if (isDirty) { + this.updateDirty(); + } + }); + } + + private isDirty(): boolean { + return !ObjectHelper.stringArrayEquals(this.initiallySelectedItems, this.getSelectedItemsIds()); + } + + private updateDirty(): void { + this.ignorePropertyChange(true); + + this.getPropertyArray().removeAll(true); + + this.typesListDropdown.getSelectedOptions().filter((option) => { + const value = new Value(option.getOption().getDisplayValue().getContentTypeName().toString(), ValueTypes.STRING); + this.getPropertyArray().add(value); + }); + + this.ignorePropertyChange(false); } reset() { - this.combobox.resetBaseValues(); + this.typesListDropdown.updateSelectedItems(); } setEnabled(enable: boolean): void { super.setEnabled(enable); - this.combobox.setEnabled(enable); + this.typesListDropdown.setEnabled(enable); } protected getNumberOfValids(): number { @@ -161,23 +174,23 @@ export class ContentTypeFilter } giveFocus(): boolean { - return this.combobox.maximumOccurrencesReached() ? false : this.combobox.giveFocus(); + return this.typesListDropdown.maximumOccurrencesReached() ? false : this.typesListDropdown.giveFocus(); } onFocus(listener: (event: FocusEvent) => void) { - this.combobox.onFocus(listener); + this.typesListDropdown.onFocus(listener); } unFocus(listener: (event: FocusEvent) => void) { - this.combobox.unFocus(listener); + this.typesListDropdown.unFocus(listener); } onBlur(listener: (event: FocusEvent) => void) { - this.combobox.onBlur(listener); + this.typesListDropdown.onBlur(listener); } unBlur(listener: (event: FocusEvent) => void) { - this.combobox.unBlur(listener); + this.typesListDropdown.unBlur(listener); } } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeFilterDropdown.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeFilterDropdown.ts new file mode 100644 index 0000000000..9c667eb7b3 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeFilterDropdown.ts @@ -0,0 +1,117 @@ +import {ContentTypeSummary} from '@enonic/lib-admin-ui/schema/content/ContentTypeSummary'; +import {ContentTypeList} from './ContentTypeList'; +import {BaseLoader} from '@enonic/lib-admin-ui/util/loader/BaseLoader'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {ContentTypeSelectedOptionsView} from './ContentTypeComboBox'; +import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; +import {GetContentTypeByNameRequest} from '../../resource/GetContentTypeByNameRequest'; +import {ContentTypeName} from '@enonic/lib-admin-ui/schema/content/ContentTypeName'; +import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; +import {FilterableListBoxWrapperWithSelectedView} from '@enonic/lib-admin-ui/ui/selector/list/FilterableListBoxWrapperWithSelectedView'; +import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; +import * as Q from 'q'; + +export interface ContentTypeFilterDropdownOptions { + maxSelected?: number; + loader: BaseLoader; + getSelectedItems: () => string[]; +} + +export class ContentTypeFilterDropdown + extends FilterableListBoxWrapperWithSelectedView { + + private readonly loader: BaseLoader; + + private readonly getSelectedItemsHandler: () => string[]; + + constructor(options: ContentTypeFilterDropdownOptions) { + super(new ContentTypeList(), { + selectedOptionsView: new ContentTypeSelectedOptionsView(), + maxSelected: options.maxSelected, + filter: options.loader.filterFn.bind(options.loader), + }); + + this.loader = options.loader; + this.getSelectedItemsHandler = options.getSelectedItems; + } + + protected filterItem(item: ContentTypeSummary, searchString: string): void { + this.loader.setSearchString(searchString); + super.filterItem(item, searchString); + } + + protected initListeners(): void { + super.initListeners(); + + this.listBox.whenShown(() => { + this.loadMask.show(); + + this.loader.load().then((contentTypes: ContentTypeSummary[]) => { + this.listBox.setItems(contentTypes); + + this.selectLoadedListItems(contentTypes); + + if (this.listBox.isVisible() && this.optionFilterInput.getValue()) { // filtering loaded items with search string if present + this.optionFilterInput.forceChangedEvent(); + } + + }).catch(DefaultErrorHandler.handle).finally(() => this.loadMask.hide()); + }); + } + + private selectLoadedListItems(contentTypes: ContentTypeSummary[]): void { + const selectedItems: string[] = this.getSelectedItemsHandler(); + + contentTypes.forEach((contentType: ContentTypeSummary) => { + const id = contentType.getId(); + + if (selectedItems.indexOf(id) >= 0) { + this.select(contentType, true); + } + }); + } + + private preSelectItems(): void { + const ids = this.getSelectedItemsHandler().filter(id => !StringHelper.isBlank(id)); + + if (ids.length > 0) { + this.fetchItems(ids).then((contentTypes) => { + if (contentTypes.length > 0) { + const options = contentTypes.map((item) => this.createSelectedOption(item)); + this.selectedOptionsView.addOptions(options, true, -1); + this.checkSelectionLimitReached(); + } + }).catch(DefaultErrorHandler.handle); + } + } + + private fetchItems(ids: string[]): Q.Promise { + const promises = ids.map((id) => new GetContentTypeByNameRequest(new ContentTypeName(id)).sendAndParse()); + return Q.all(promises); + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.preSelectItems(); + + return rendered; + }); + } + + createSelectedOption(item: ContentTypeSummary): Option { + return Option.create() + .setValue(item.getId()) + .setDisplayValue(item) + .build(); + } + + updateSelectedItems(): void { + // unselecting all items + this.selectedOptionsView.getSelectedOptions().forEach((selectedOption: SelectedOption) => { + this.selectedOptionsView.removeOption(selectedOption.getOption(), true); + }); + + // selecting items from property array + this.preSelectItems(); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeList.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeList.ts new file mode 100644 index 0000000000..ad29128a8f --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/schema/ContentTypeList.ts @@ -0,0 +1,23 @@ +import {ListBox} from '@enonic/lib-admin-ui/ui/selector/list/ListBox'; +import {ContentTypeSummary} from '@enonic/lib-admin-ui/schema/content/ContentTypeSummary'; +import {ContentTypeSummaryViewer} from '../ui/schema/ContentTypeSummaryViewer'; + +export class ContentTypeList extends ListBox { + + constructor() { + super('content-type-list'); + } + + protected createItemView(item: ContentTypeSummary, readOnly: boolean): ContentTypeSummaryViewer { + const viewer = new ContentTypeSummaryViewer(); + + viewer.setObject(item); + + return viewer; + } + + protected getItemId(item: ContentTypeSummary): string { + return item.getId(); + } + +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentAndStatusSelectorViewer.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentAndStatusSelectorViewer.ts new file mode 100644 index 0000000000..b8dbec0f20 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentAndStatusSelectorViewer.ts @@ -0,0 +1,37 @@ +import {ContentTreeSelectorItemViewer} from '../../item/ContentTreeSelectorItemViewer'; +import {ContentAndStatusTreeSelectorItem} from '../../item/ContentAndStatusTreeSelectorItem'; +import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; +import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; +import {SpanEl} from '@enonic/lib-admin-ui/dom/SpanEl'; + +export class ContentAndStatusSelectorViewer extends ContentTreeSelectorItemViewer { + + private statusColumn: DivEl; + + doLayout(object: ContentAndStatusTreeSelectorItem) { + super.doLayout(object); + + if (!this.statusColumn) { + this.statusColumn = this.createStatusColumn(object); + this.appendChild(this.statusColumn); + } + } + + private createStatusColumn(item: ContentAndStatusTreeSelectorItem): DivEl { + const content = this.makeContentFromItem(item); + const statusElement = new DivEl('status'); + const statusTextEl = new SpanEl(); + statusTextEl.addClass(content.getStatusClass()); + statusTextEl.setHtml(content.getStatusText()); + statusElement.appendChild(statusTextEl); + + return statusElement; + } + + + private makeContentFromItem(item: ContentAndStatusTreeSelectorItem): ContentSummaryAndCompareStatus { + return ContentSummaryAndCompareStatus.fromContentAndCompareAndPublishStatus(item.getContent(), + item.getCompareStatus(), + item.getPublishStatus()); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentListBox.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentListBox.ts new file mode 100644 index 0000000000..bc7a23c841 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentListBox.ts @@ -0,0 +1,43 @@ +import {LazyListBox} from '@enonic/lib-admin-ui/ui/selector/list/LazyListBox'; +import {ContentTreeSelectorItem} from '../../item/ContentTreeSelectorItem'; +import {ContentSummaryOptionDataLoader} from '../ui/selector/ContentSummaryOptionDataLoader'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {ContentAndStatusSelectorViewer} from './ContentAndStatusSelectorViewer'; +import {Element} from '@enonic/lib-admin-ui/dom/Element'; + +export interface ContentListBoxOptions { + loader: ContentSummaryOptionDataLoader; + className?: string; +} + +export class ContentListBox extends LazyListBox { + + private readonly loader: ContentSummaryOptionDataLoader; + + constructor(options: ContentListBoxOptions) { + super('content-list-box ' + (options.className || '')); + + this.loader = options.loader; + } + + protected createItemView(item: T, readOnly: boolean): Element { + const viewer = new ContentAndStatusSelectorViewer(); + + viewer.setObject(item); + + return viewer; + } + + protected getItemId(item: ContentTreeSelectorItem): string { + return item.getId(); + } + + protected handleLazyLoad(): void { + this.loader.load(true).catch(DefaultErrorHandler.handle); + } + + protected updateItemView(itemView: Element, item: T) { + const viewer = itemView as ContentAndStatusSelectorViewer; + viewer.setObject(item); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentSelector.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentSelector.ts index 2bc3994b5e..57770affe9 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentSelector.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentSelector.ts @@ -1,19 +1,15 @@ import * as Q from 'q'; import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; import {ObjectHelper} from '@enonic/lib-admin-ui/ObjectHelper'; -import {AppHelper} from '@enonic/lib-admin-ui/util/AppHelper'; -import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; import {Input} from '@enonic/lib-admin-ui/form/Input'; import {InputTypeManager} from '@enonic/lib-admin-ui/form/inputtype/InputTypeManager'; import {Class} from '@enonic/lib-admin-ui/Class'; -import {Property} from '@enonic/lib-admin-ui/data/Property'; import {PropertyArray} from '@enonic/lib-admin-ui/data/PropertyArray'; import {Value} from '@enonic/lib-admin-ui/data/Value'; import {ValueType} from '@enonic/lib-admin-ui/data/ValueType'; import {ValueTypes} from '@enonic/lib-admin-ui/data/ValueTypes'; -import {SelectedOptionEvent} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionEvent'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; -import {ContentComboBox, ContentComboBoxBuilder, ContentSelectedOptionsView} from '../ui/selector/ContentComboBox'; +import {ContentSelectedOptionsView} from '../ui/selector/ContentComboBox'; import {ContentInputTypeManagingAdd} from '../ui/selector/ContentInputTypeManagingAdd'; import {ContentInputTypeViewContext} from '../ContentInputTypeViewContext'; import {ContentSummaryOptionDataLoader, ContentSummaryOptionDataLoaderBuilder} from '../ui/selector/ContentSummaryOptionDataLoader'; @@ -32,36 +28,36 @@ import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; import {NewContentButton} from './ui/NewContentButton'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {ContentAndStatusTreeSelectorItem} from '../../item/ContentAndStatusTreeSelectorItem'; -import {ContentSummaryAndCompareStatusFetcher} from '../../resource/ContentSummaryAndCompareStatusFetcher'; import {CompareStatus} from '../../content/CompareStatus'; import {MovedContentItem} from '../../browse/MovedContentItem'; import {ContentServerChangeItem} from '../../event/ContentServerChangeItem'; +import {ContentSelectorDropdown, ContentSelectorDropdownOptions} from './ContentSelectorDropdown'; +import {SelectionChange} from '@enonic/lib-admin-ui/util/SelectionChange'; +import {ContentListBox} from './ContentListBox'; +import {ContentTreeSelectorDropdown, ContentTreeSelectorDropdownOptions} from './ContentTreeSelectorDropdown'; export class ContentSelector extends ContentInputTypeManagingAdd { - protected contentComboBox: ContentComboBox; + protected contentSelectorDropdown: ContentSelectorDropdown; - protected comboBoxWrapper: DivEl; + protected contentSelectedOptionsView: ContentSelectedOptionsView; protected newContentButton: NewContentButton; protected treeMode: boolean; + protected initiallySelectedItems: string[]; + protected hideToggleIcon: boolean; protected contentDeletedListener: (paths: ContentServerChangeItem[], pending?: boolean) => void; - protected static contentIdBatch: ContentId[] = []; - - protected static loadSummariesResult: Q.Deferred = Q.defer(); - public static debug: boolean = false; - protected static loadSummaries: () => void = AppHelper.debounce(ContentSelector.doFetchSummaries, 10, false); - constructor(config: ContentInputTypeViewContext) { super(config, 'content-selector'); + this.initEventsListeners(); } @@ -72,71 +68,46 @@ export class ContentSelector return; } - ContentServerEventsHandler.getInstance().onContentRenamed((data: ContentSummaryAndCompareStatus[], oldPaths: ContentPath[]) => { - data.forEach((renamed: ContentSummaryAndCompareStatus, index: number) => { - this.updateSelectedItemsPathsIfParentRenamed(renamed, oldPaths[index]); - }); - }); - this.handleContentDeletedEvent(); this.handleContentUpdatedEvent(); - } - - private updateSelectedItemsPathsIfParentRenamed(renamedContent: ContentSummaryAndCompareStatus, renamedItemOldPath: ContentPath): void { - this.getSelectedOptions().forEach((selectedOption: SelectedOption) => { - const selectedOptionPath = selectedOption.getOption().getDisplayValue().getPath(); - if (selectedOptionPath?.isDescendantOf(renamedItemOldPath)) { - this.updatePathForRenamedItemDescendant(selectedOption, renamedItemOldPath, renamedContent); - } - }); + // remove missing options via removePropertyWithId } - private updatePathForRenamedItemDescendant(selectedOption: SelectedOption, renamedItemOldPath: ContentPath, - renamedAncestor: ContentSummaryAndCompareStatus) { - const selectedOptionPath = selectedOption.getOption().getDisplayValue().getPath(); - const option = selectedOption.getOption(); - const newPath = this.makeNewPathForRenamedItemDescendant(selectedOptionPath, renamedItemOldPath, renamedAncestor); - const newValue = this.makeNewItemWithUpdatedPath(option.getDisplayValue(), newPath); - option.setDisplayValue(newValue); - selectedOption.getOptionView().removeClass(ContentComboBox.NOT_FOUND_CLASS); - selectedOption.getOptionView().setOption(option); - } - - protected makeNewPathForRenamedItemDescendant(descendantItemPath: ContentPath, renamedItemOldPath: ContentPath, - renamedItem: ContentSummaryAndCompareStatus): ContentPath { - const descendantItemPathAsString = descendantItemPath.toString(); - const renamedItemOldPathAsString = renamedItemOldPath.toString(); - const newSelectedOptionPathAsString = descendantItemPathAsString.replace(renamedItemOldPathAsString, - renamedItem.getPath().toString()); - return ContentPath.create().fromString(newSelectedOptionPathAsString).build(); - } + private handleContentUpdatedEvent() { + const contentUpdatedListener = (statuses: ContentSummaryAndCompareStatus[], oldPaths?: ContentPath[]) => { + if (this.getSelectedOptions().length === 0) { + return; + } - private makeNewItemWithUpdatedPath(oldValue: ContentTreeSelectorItem, newPath: ContentPath): ContentTreeSelectorItem { - const content = oldValue.getContent(); - const newContentSummary = new ContentSummaryBuilder(content).setPath(newPath).build(); - const wrappedContent = this.wrapRenamedContentSummary(newContentSummary, oldValue); + statuses.forEach((status: ContentSummaryAndCompareStatus) => { + this.contentSelectorDropdown.updateItem(this.createSelectorItem(status)); + }); + }; - return this.createSelectorItem(wrappedContent, oldValue?.isSelectable(), oldValue?.isExpandable()); - } + const contentMovedListener = (movedItems: MovedContentItem[]) => { + if (this.getSelectedOptions().length === 0) { + return; + } - protected wrapRenamedContentSummary(newContentSummary: ContentSummary, - oldValue: ContentTreeSelectorItem): ContentSummary | ContentSummaryAndCompareStatus { - if (oldValue instanceof ContentAndStatusTreeSelectorItem) { - return ContentSummaryAndCompareStatus.fromContentAndCompareAndPublishStatus(newContentSummary, oldValue.getCompareStatus(), - oldValue.getPublishStatus()); - } + movedItems.forEach((movedItem: MovedContentItem) => { + this.contentSelectorDropdown.updateItem(this.createSelectorItem(movedItem.item)); + }); + }; - return newContentSummary; - } + const contentRenamedListener = (data: ContentSummaryAndCompareStatus[], oldPaths: ContentPath[]) => { + if (this.getSelectedOptions().length === 0) { + return; + } - private handleContentUpdatedEvent(): void { - const contentUpdatedListener = this.handleContentUpdated.bind(this); - const contentMovedListener = this.handleContentMoved.bind(this); + data.forEach((renamed: ContentSummaryAndCompareStatus, index: number) => { + this.updateSelectedItemsPathsIfParentRenamed(renamed, oldPaths[index]); + }); + }; const handler: ContentServerEventsHandler = ContentServerEventsHandler.getInstance(); handler.onContentMoved(contentMovedListener); - handler.onContentRenamed(contentUpdatedListener); + handler.onContentRenamed(contentRenamedListener); handler.onContentUpdated(contentUpdatedListener); this.onRemoved(() => { @@ -146,47 +117,26 @@ export class ContentSelector }); } - private findSelectedOptionByContentPath(contentPath: ContentPath): SelectedOption { - const selectedOptions = this.getSelectedOptions(); - for (const selectedOption of selectedOptions) { - if (contentPath.equals(this.getContentPath(selectedOption.getOption().getDisplayValue()))) { - return selectedOption; - } - } - return null; - } - private handleContentDeletedEvent() { this.contentDeletedListener = (paths: ContentServerChangeItem[], pending?: boolean) => { if (this.getSelectedOptions().length === 0) { return; } - const optionsUpdated: SelectedOption[] = []; - const selectedContentIds: string[] = []; - + let selectedContentIdsMap: object = {}; this.getSelectedOptions().forEach((selectedOption: SelectedOption) => { - const selectedOptionContentId = selectedOption.getOption().getDisplayValue()?.getContentId(); - if (selectedOptionContentId) { - selectedContentIds.push(selectedOptionContentId.toString()); + if (selectedOption.getOption().getDisplayValue()?.getContentId()) { + selectedContentIdsMap[selectedOption.getOption().getDisplayValue().getContentId().toString()] = ''; } }); - paths.filter(deletedItem => !pending && selectedContentIds.indexOf(deletedItem.getContentId().toString()) > -1) + paths.filter(deletedItem => !pending && selectedContentIdsMap.hasOwnProperty(deletedItem.getContentId().toString())) .forEach((deletedItem) => { - const selectedOption = this.getSelectedOptionsView().getById(deletedItem.getContentId().toString()); - if (selectedOption) { - const option = selectedOption.getOption(); - const newValue = this.createMissingContentItem(deletedItem.getContentId()); - option.setDisplayValue(newValue); - selectedOption.getOptionView().setOption(option); - optionsUpdated.push(selectedOption); + let selectedOption = this.getSelectedOptionsView().getById(deletedItem.getContentId().toString()); + if (selectedOption != null) { + this.handleSelectedOptionDeleted(selectedOption); } }); - - if (optionsUpdated.length > 0) { - this.handleOptionUpdated(optionsUpdated); - } }; let handler = ContentServerEventsHandler.getInstance(); @@ -197,78 +147,18 @@ export class ContentSelector }); } - protected createSelectorItem(content: ContentSummary | ContentSummaryAndCompareStatus, selectable: boolean = true, - expandable: boolean = true): ContentTreeSelectorItem { - if (content instanceof ContentSummaryAndCompareStatus) { - return new ContentAndStatusTreeSelectorItem(content, selectable, expandable); - } - - return new ContentTreeSelectorItem(content, selectable, expandable); + protected handleSelectedOptionDeleted(selectedOption: SelectedOption): void { + this.getSelectedOptionsView().removeOption(selectedOption.getOption(), false); } - protected handleContentMoved(movedItems: MovedContentItem[]): void { - if (this.getSelectedOptions().length === 0) { - return; - } - - movedItems.forEach((movedItem: MovedContentItem) => { - const selectedOption: SelectedOption = this.findSelectedOptionByContentPath(movedItem.oldPath); - - if (selectedOption) { - const contentTreeSelectorItem = this.createSelectorItem(movedItem.item); - this.getContentComboBox().updateOption(selectedOption.getOption(), contentTreeSelectorItem); - } - }); - } - - protected handleContentUpdated(updatedContents: ContentSummaryAndCompareStatus[], oldPaths?: ContentPath[]): void { - if (this.getSelectedOptions().length === 0) { - return; - } - - const optionsUpdated: SelectedOption[] = this.updateSelectedOptions(updatedContents, oldPaths); - - if (optionsUpdated.length > 0) { - this.handleOptionUpdated(optionsUpdated); + protected createSelectorItem(content: ContentSummary | ContentSummaryAndCompareStatus): ContentTreeSelectorItem { + if (content instanceof ContentSummaryAndCompareStatus) { + return new ContentAndStatusTreeSelectorItem(content); } - } - - private updateSelectedOptions(updatedContents: ContentSummaryAndCompareStatus[], oldPaths?: ContentPath[]): SelectedOption[] { - const updatedOptions: SelectedOption[] = []; - updatedContents.forEach((content, index) => { - const selectedOption = this.resolveSelectedOption(content, index, oldPaths); - if (selectedOption) { - this.updateSelectedOption(selectedOption, content); - updatedOptions.push(selectedOption); - } - }); - - return updatedOptions; - } - - private resolveSelectedOption(content: ContentSummaryAndCompareStatus, index: number, - oldPaths?: ContentPath[]): SelectedOption { - return oldPaths ? - this.findSelectedOptionByContentPath(oldPaths[index]) : - this.getSelectedOptionsView().getById(content.getContentId().toString()); - } - - private updateSelectedOption(selectedOption: SelectedOption, content: ContentSummaryAndCompareStatus): void { - const option = selectedOption.getOption(); - const oldValue = option.getDisplayValue(); - const newValue = this.createSelectorItem( - content, - oldValue?.isSelectable(), - oldValue?.isExpandable() - ); - - option.setDisplayValue(newValue); - selectedOption.getOptionView().removeClass(ContentComboBox.NOT_FOUND_CLASS); - selectedOption.getOptionView().setOption(option); + return new ContentTreeSelectorItem(content); } - protected readInputConfig(): void { const inputConfig: Record[]> = this.context.inputConfig; const isTreeModeConfig = inputConfig['treeMode'] ? inputConfig['treeMode'][0] : {}; @@ -285,12 +175,8 @@ export class ContentSelector return '${site}'; } - public getContentComboBox(): ContentComboBox { - return this.contentComboBox; - } - protected getSelectedOptionsView(): ContentSelectedOptionsView { - return this.contentComboBox.getSelectedOptionView() as ContentSelectedOptionsView; + return this.contentSelectedOptionsView; } protected getContentPath(raw: ContentTreeSelectorItem): ContentPath { @@ -317,16 +203,81 @@ export class ContentSelector } return super.layout(input, propertyArray).then(() => { - this.addContentComboBox(input, propertyArray); + this.initiallySelectedItems = this.getSelectedItemsIds(); + this.contentSelectorDropdown = this.createSelectorDropdown(input); + this.appendChild(this.contentSelectorDropdown); return this.addExtraElementsOnLayout(input, propertyArray).then(() => this.doLayout(propertyArray)); }); } - private addContentComboBox(input: Input, propertyArray: PropertyArray): void { - this.contentComboBox = this.createContentComboBox(input, propertyArray); - this.comboBoxWrapper = new DivEl('combobox-wrapper'); - this.comboBoxWrapper.appendChild(this.contentComboBox); - this.appendChild(this.comboBoxWrapper); + protected createSelectorDropdown(input: Input): ContentSelectorDropdown { + this.contentSelectedOptionsView = this.createSelectedOptionsView().setContextContent(this.context.content); + const loader = this.createLoader(); + const listBox = this.createContentListBox(loader); + + const dropdownOptions: ContentTreeSelectorDropdownOptions = { + treeMode: this.treeMode, + loader: loader, + className: this.getDropdownClassName(), + maxSelected: input.getOccurrences().getMaximum(), + selectedOptionsView: this.contentSelectedOptionsView, + getSelectedItems: this.getSelectedItemsIds.bind(this), + + }; + + const contentSelectorDropdown = this.doCreateSelectorDropdown(listBox, dropdownOptions); + + this.contentSelectedOptionsView.onOptionMoved(this.handleMoved.bind(this)); + + contentSelectorDropdown.onSelectionChanged((selectionChange: SelectionChange) => { + selectionChange.selected?.forEach((item: ContentTreeSelectorItem) => { + const contentId: ContentId = item.getContentId(); + + if (contentId) { + this.setContentIdProperty(contentId); + + this.getSelectedOptionsView().refreshSortable(); + this.updateSelectedOptionStyle(); + this.handleValueChanged(false); + } + }); + + selectionChange.deselected?.forEach((item: ContentTreeSelectorItem) => { + const property = this.getPropertyArray().getProperties().find((property) => { + const propertyValue = property.hasNonNullValue() ? property.getString() : ''; + return propertyValue === item.getId(); + }); + + if (property) { + this.handleDeselected(property.getIndex()); + this.updateSelectedOptionStyle(); + this.handleValueChanged(false); + } + }); + }); + + return contentSelectorDropdown; + } + + protected doCreateSelectorDropdown(listBox: ContentListBox, + dropdownOptions: ContentSelectorDropdownOptions): ContentSelectorDropdown { + return new ContentTreeSelectorDropdown(listBox, dropdownOptions); + } + + protected getDropdownClassName(): string { + return ''; + } + + protected getSelectedItemsIds(): string[] { + return this.getValueFromPropertyArray(this.getPropertyArray()).split(';'); + } + + protected createSelectedOptionsView(): ContentSelectedOptionsView { + return new ContentSelectedOptionsView(); + } + + protected createContentListBox(loader: ContentSummaryOptionDataLoader): ContentListBox { + return new ContentListBox({loader: loader}); } protected addExtraElementsOnLayout(input: Input, propertyArray: PropertyArray): Q.Promise { @@ -350,135 +301,42 @@ export class ContentSelector } private addNewContentButton(): void { - this.comboBoxWrapper.addClass('new-content'); - this.newContentButton = new NewContentButton( {content: this.context.content, allowedContentTypes: this.allowedContentTypes, project: this.context.project}); this.newContentButton.setTitle(i18n('action.addNew')); - this.newContentButton.onContentAdded((content: ContentSummary) => { + this.newContentButton.onContentAdded((content: ContentSummary) => { const item = ContentSummaryAndCompareStatus.fromContentAndCompareStatus(content, CompareStatus.NEW); - this.contentComboBox.select(this.createSelectorItem(item)); + this.contentSelectorDropdown.select(this.createSelectorItem(item)); }); - this.comboBoxWrapper.appendChild(this.newContentButton); + this.contentSelectorDropdown.whenRendered(() => { + this.contentSelectorDropdown.appendChild(this.newContentButton); + this.newContentButton.addClass('extra-button'); + this.contentSelectorDropdown.addClass('has-extra-button'); + }); } protected doLayout(propertyArray: PropertyArray): Q.Promise { - const contentIds: ContentId[] = []; - - propertyArray.forEach((property: Property) => { - if (property.hasNonNullValue()) { - const referenceValue: Reference = property.getReference(); + this.setLayoutInProgress(false); + this.setupSortable(); - if (ObjectHelper.iFrameSafeInstanceOf(referenceValue, Reference)) { - contentIds.push(ContentId.fromReference(referenceValue)); - } - } - }); - - return this.doLoadContent(contentIds).then((contents: ContentSummaryAndCompareStatus[]) => { - this.setupSortable(); - - //TODO: original value doesn't work because of additional request, so have to select manually - contentIds.forEach((contentId: ContentId) => { - const content = contents.find((item) => item.getContentId().equals(contentId)); - const dataItem = content ? this.createSelectorItem(content) : this.createMissingContentItem(contentId); - - this.contentComboBox.select(dataItem, undefined, true); - }); - - this.contentComboBox.resetBaseValues(); - - this.updateNewContentButton(); - - this.contentComboBox.getSelectedOptions().forEach((selectedOption: SelectedOption) => { - this.updateSelectedOptionIsEditable(selectedOption); - }); - - this.setLayoutInProgress(false); - }); + return Q.resolve(); } - protected createMissingContentItem(id: ContentId): ContentTreeSelectorItem { - const content = new ContentSummary(new ContentSummaryBuilder().setId(id.toString()).setContentId(id)); - return new ContentTreeSelectorItem(content); + protected createOptionDataLoaderBuilder(): ContentSummaryOptionDataLoaderBuilder { + return ContentSummaryOptionDataLoader.create(); } - protected createOptionDataLoaderBuilder(): ContentSummaryOptionDataLoaderBuilder { - return ContentSummaryOptionDataLoader.create() + protected createLoader(): ContentSummaryOptionDataLoader { + return this.createOptionDataLoaderBuilder() .setAllowedContentPaths(this.allowedContentPaths) .setContentTypeNames(this.allowedContentTypes) .setRelationshipType(this.relationshipType) .setContent(this.context.content) .setProject(this.context.project) - .setApplicationKey(this.context.applicationKey); - } - - protected doCreateContentComboBoxBuilder(): ContentComboBoxBuilder { - return ContentComboBox.create().setRemoveMissingSelectedOptions(false).setDisplayMissingSelectedOptions(true).setProject( - this.context.project); - } - - protected createOptionDataLoader(): ContentSummaryOptionDataLoader { - return this.createOptionDataLoaderBuilder().build(); - } - - protected createContentComboBoxBuilder(input: Input, propertyArray: PropertyArray): ContentComboBoxBuilder { - const optionDataLoader: ContentSummaryOptionDataLoader = this.createOptionDataLoader(); - const comboboxValue: string = this.getValueFromPropertyArray(propertyArray); - - return this.doCreateContentComboBoxBuilder() - .setComboBoxName(input.getName()) - .setLoader(optionDataLoader) - .setMaximumOccurrences(input.getOccurrences().getMaximum()) - .setTreegridDropdownEnabled(this.treeMode) - .setTreeModeTogglerAllowed(!this.hideToggleIcon) - .setValue(comboboxValue); - } - - protected doCreateContentComboBox(input: Input, propertyArray: PropertyArray): ContentComboBox { - return this.createContentComboBoxBuilder(input, propertyArray).build(); - } - - protected initEvents(contentComboBox: ContentComboBox) { - contentComboBox.onOptionSelected((event: SelectedOptionEvent) => { - this.fireFocusSwitchEvent(event); - this.updateNewContentButton(); - - const contentId: ContentId = event.getSelectedOption().getOption().getDisplayValue().getContentId(); - - if (contentId) { - this.setContentIdProperty(contentId); - - this.updateSelectedOptionIsEditable(event.getSelectedOption()); - this.getSelectedOptionsView().refreshSortable(); - this.updateSelectedOptionStyle(); - this.handleValueChanged(false); - this.contentComboBox.getComboBox().setIgnoreNextFocus(true); - } - - }); - - contentComboBox.onOptionDeselected((event: SelectedOptionEvent) => { - this.handleDeselected(event.getSelectedOption().getIndex()); - this.updateSelectedOptionStyle(); - this.updateNewContentButton(); - this.handleValueChanged(false); - }); - - contentComboBox.onOptionMoved(this.handleMoved.bind(this)); - - contentComboBox.onValueLoaded(() => { - this.updateNewContentButton(); - }); - } - - protected createContentComboBox(input: Input, propertyArray: PropertyArray): ContentComboBox { - const contentComboBox: ContentComboBox = this.doCreateContentComboBox(input, propertyArray); - - this.initEvents(contentComboBox); - - return contentComboBox; + .setApplicationKey(this.context.applicationKey) + .setAppendLoadResults(false) + .build(); } protected removePropertyWithId(id: string) { @@ -499,82 +357,52 @@ export class ContentSelector console.log('update(' + JSON.stringify(propertyArray.toJson()) + ')'); } + const isDirty = this.isDirty(); + return super.update(propertyArray, unchangedOnly).then(() => { - if (!unchangedOnly || !this.contentComboBox.isDirty() && this.contentComboBox.isRendered()) { - let value = this.getValueFromPropertyArray(propertyArray); - this.contentComboBox.setValue(value); - } else if (this.contentComboBox.isDirty()) { - this.resetPropertyValues(); + this.initiallySelectedItems = this.getSelectedItemsIds(); + + if (!unchangedOnly || !isDirty) { + this.contentSelectorDropdown.updateSelectedItems(); + } else if (isDirty) { + this.updateDirty(); } }); } + private isDirty(): boolean { + return !ObjectHelper.stringArrayEquals(this.initiallySelectedItems, this.getSelectedItemsIds()); + } + reset() { - this.contentComboBox.resetBaseValues(); + //this.contentComboBox.resetBaseValues(); } clear() { - this.contentComboBox.clearCombobox(); + // this.contentComboBox.clearCombobox(); } setEnabled(enable: boolean): void { super.setEnabled(enable); - this.contentComboBox.setEnabled(enable); - } - - private isResetRequired(): boolean { - const values: ContentTreeSelectorItem[] = this.contentComboBox.getSelectedDisplayValues(); - - if (this.getPropertyArray().getSize() !== values.length) { - return true; - } - - return !values.every((value: ContentTreeSelectorItem, index: number) => { - const property: Property = this.getPropertyArray().get(index); - return property?.getString() === value.getId(); - }); + this.contentSelectorDropdown.setEnabled(enable); } - resetPropertyValues() { + updateDirty() { if (ContentSelector.debug) { console.log('resetPropertyValues()'); } - if (!this.isResetRequired()) { - return; - } - - const values: ContentTreeSelectorItem[] = this.contentComboBox.getSelectedDisplayValues(); - this.ignorePropertyChange(true); - this.getPropertyArray().removeAll(true); - values.forEach(value => this.contentComboBox.deselect(value, true)); - values.forEach(value => this.contentComboBox.select(value)); - - this.ignorePropertyChange(false); - } - - private static doFetchSummaries() { - const idsToLoad = ContentSelector.contentIdBatch; - ContentSelector.contentIdBatch = []; - const promiseForIdsToLoad = ContentSelector.loadSummariesResult; - ContentSelector.loadSummariesResult = Q.defer(); - new ContentSummaryAndCompareStatusFetcher().fetchAndCompareStatus(idsToLoad).then( - (result: ContentSummaryAndCompareStatus[]) => { - promiseForIdsToLoad.resolve(result); - }); - } - - protected doLoadContent(contentIds: ContentId[]): Q.Promise { - ContentSelector.contentIdBatch = ContentSelector.contentIdBatch.concat(contentIds); - const resultPromise = ContentSelector.loadSummariesResult.promise; - ContentSelector.loadSummaries(); - return resultPromise.then((result: ContentSummaryAndCompareStatus[]) => { - const contentIdsStr: string[] = contentIds.map((id: ContentId) => id.toString()); - return result.filter(content => contentIdsStr.indexOf(content.getId()) >= 0); + this.contentSelectorDropdown.getSelectedOptions().filter((option: SelectedOption) => { + const contentId = option.getOption().getDisplayValue().getContentId(); + const reference: Reference = new Reference(contentId.toString()); + const value: Value = new Value(reference, ValueTypes.REFERENCE); + this.getPropertyArray().add(value); }); + + this.ignorePropertyChange(false); } protected setContentIdProperty(contentId: ContentId) { @@ -583,7 +411,7 @@ export class ContentSelector if (!this.getPropertyArray().containsValue(value)) { this.ignorePropertyChange(true); - if (this.contentComboBox.countSelected() === 1) { // overwrite initial value + if (this.contentSelectorDropdown.countSelected() === 1) { // overwrite initial value this.getPropertyArray().set(0, value); } else { this.getPropertyArray().add(value); @@ -612,50 +440,85 @@ export class ContentSelector protected updateSelectedOptionStyle() { if (this.getPropertyArray().getSize() > 1) { this.addClass('multiple-occurrence').removeClass('single-occurrence'); + this.contentSelectorDropdown.addClass('multiple-occurrence').removeClass('single-occurrence'); } else { this.addClass('single-occurrence').removeClass('multiple-occurrence'); + this.contentSelectorDropdown.addClass('single-occurrence').removeClass('multiple-occurrence'); } } - protected updateSelectedOptionIsEditable(selectedOption: SelectedOption) { - const selectedContentId: ContentId = selectedOption.getOption().getDisplayValue().getContentId(); - const refersToItself: boolean = selectedContentId.toString() === this.context.content?.getId(); - selectedOption.getOptionView().toggleClass('non-editable', refersToItself); + private updateSelectedItemsPathsIfParentRenamed(renamedContent: ContentSummaryAndCompareStatus, renamedItemOldPath: ContentPath): void { + this.getSelectedOptions().forEach((selectedOption: SelectedOption) => { + const selectedOptionPath = selectedOption.getOption().getDisplayValue().getPath(); + + if (selectedOptionPath?.isDescendantOf(renamedItemOldPath)) { + this.updatePathForRenamedItemDescendant(selectedOption, renamedItemOldPath, renamedContent); + } + }); } - protected getNumberOfValids(): number { - return this.getPropertyArray().getSize(); + private updatePathForRenamedItemDescendant(selectedOption: SelectedOption, renamedItemOldPath: ContentPath, + renamedAncestor: ContentSummaryAndCompareStatus) { + const selectedOptionPath = selectedOption.getOption().getDisplayValue().getPath(); + const option = selectedOption.getOption(); + const newPath = this.makeNewPathForRenamedItemDescendant(selectedOptionPath, renamedItemOldPath, renamedAncestor); + const newValue = this.makeNewItemWithUpdatedPath(option.getDisplayValue(), newPath); + option.setDisplayValue(newValue); + selectedOption.getOptionView().setOption(option); } - private updateNewContentButton(): void { - this.newContentButton?.setVisible(!this.contentComboBox.maximumOccurrencesReached()); + protected makeNewPathForRenamedItemDescendant(descendantItemPath: ContentPath, renamedItemOldPath: ContentPath, + renamedItem: ContentSummaryAndCompareStatus): ContentPath { + const descendantItemPathAsString = descendantItemPath.toString(); + const renamedItemOldPathAsString = renamedItemOldPath.toString(); + const newSelectedOptionPathAsString = descendantItemPathAsString.replace(renamedItemOldPathAsString, + renamedItem.getPath().toString()); + return ContentPath.create().fromString(newSelectedOptionPathAsString).build(); } - protected handleOptionUpdated(optionsUpdated: SelectedOption[]): void { - // + private makeNewItemWithUpdatedPath(oldValue: ContentTreeSelectorItem, newPath: ContentPath): ContentTreeSelectorItem { + const content = oldValue.getContent(); + const newContentSummary = new ContentSummaryBuilder(content).setPath(newPath).build(); + const wrappedContent = this.wrapRenamedContentSummary(newContentSummary, oldValue); + + return this.createSelectorItem(wrappedContent); + } + + protected wrapRenamedContentSummary(newContentSummary: ContentSummary, + oldValue: ContentTreeSelectorItem): ContentSummary | ContentSummaryAndCompareStatus { + if (oldValue instanceof ContentAndStatusTreeSelectorItem) { + return ContentSummaryAndCompareStatus.fromContentAndCompareAndPublishStatus(newContentSummary, oldValue.getCompareStatus(), + oldValue.getPublishStatus()); + } + + return newContentSummary; + } + + protected getNumberOfValids(): number { + return this.getPropertyArray().getSize(); } giveFocus(): boolean { - if (this.contentComboBox.maximumOccurrencesReached()) { + if (this.contentSelectorDropdown.maximumOccurrencesReached()) { return false; } - return this.contentComboBox.giveFocus(); + return this.contentSelectorDropdown.giveFocus(); } onFocus(listener: (event: FocusEvent) => void) { - this.contentComboBox.onFocus(listener); + this.contentSelectorDropdown.onFocus(listener); } unFocus(listener: (event: FocusEvent) => void) { - this.contentComboBox.unFocus(listener); + this.contentSelectorDropdown.unFocus(listener); } onBlur(listener: (event: FocusEvent) => void) { - this.contentComboBox.onBlur(listener); + this.contentSelectorDropdown.onBlur(listener); } unBlur(listener: (event: FocusEvent) => void) { - this.contentComboBox.unBlur(listener); + this.contentSelectorDropdown.unBlur(listener); } } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentSelectorDropdown.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentSelectorDropdown.ts new file mode 100644 index 0000000000..a8694408f1 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentSelectorDropdown.ts @@ -0,0 +1,171 @@ +import * as Q from 'q'; +import {ContentTreeSelectorItem} from '../../item/ContentTreeSelectorItem'; +import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; +import {ContentSummaryOptionDataHelper} from '../../util/ContentSummaryOptionDataHelper'; +import {ContentSummaryOptionDataLoader} from '../ui/selector/ContentSummaryOptionDataLoader'; +import {BaseSelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/BaseSelectedOptionsView'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; +import {ContentSummaryAndCompareStatusFetcher} from '../../resource/ContentSummaryAndCompareStatusFetcher'; +import {ContentId} from '../../content/ContentId'; +import {ContentSummary, ContentSummaryBuilder} from '../../content/ContentSummary'; +import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; +import {ContentAndStatusTreeSelectorItem} from '../../item/ContentAndStatusTreeSelectorItem'; +import {ValueChangedEvent} from '@enonic/lib-admin-ui/ValueChangedEvent'; +import {LoadedDataEvent} from '@enonic/lib-admin-ui/util/loader/event/LoadedDataEvent'; +import { + FilterableListBoxWrapperWithSelectedView, + ListBoxInputOptions +} from '@enonic/lib-admin-ui/ui/selector/list/FilterableListBoxWrapperWithSelectedView'; +import {AppHelper} from '@enonic/lib-admin-ui/util/AppHelper'; + +export interface ContentSelectorDropdownOptions extends ListBoxInputOptions { + loader: ContentSummaryOptionDataLoader; + selectedOptionsView: BaseSelectedOptionsView; + getSelectedItems: () => string[]; +} + +export class ContentSelectorDropdown + extends FilterableListBoxWrapperWithSelectedView { + + protected helper: ContentSummaryOptionDataHelper; + + protected options: ContentSelectorDropdownOptions; + + protected readonly getSelectedItemsHandler: () => string[]; + + constructor(listBox, options: ContentSelectorDropdownOptions) { + super(listBox, options); + + this.helper = new ContentSummaryOptionDataHelper(); + this.getSelectedItemsHandler = options.getSelectedItems; + this.selectedOptionsView.setOccurrencesSortable(true); + this.postInitListeners(); + } + + createSelectedOption(item: ContentTreeSelectorItem): Option { + return Option.create() + .setValue(this.helper.getDataId(item)) + .setDisplayValue(item) + .setExpandable(this.helper.isExpandable(item)) + .setSelectable(this.helper.isSelectable(item)) + .build(); + } + + protected initListeners(): void { + super.initListeners(); + + this.listBox.onItemsAdded((items: ContentTreeSelectorItem[]) => { + this.selectLoadedFlatListItems(items); + }); + + let searchValue = ''; + + const debouncedSearch = AppHelper.debounce(() => { + this.search(searchValue); + }, 300); + + this.optionFilterInput.onValueChanged((event: ValueChangedEvent) => { + searchValue = event.getNewValue(); + debouncedSearch(); + }); + } + + protected postInitListeners(): void { + this.options.loader.onLoadedData((event: LoadedDataEvent) => { + if (event.isPostLoad()) { + if (event.getData().length > 0) { + this.listBox.addItems(event.getData()); + } + } else { + this.listBox.setItems(event.getData()); + } + return Q.resolve(null); + }); + } + + protected search(value?: string): void { + this.loadMask.show(); + this.options.loader.search(value).catch(DefaultErrorHandler.handle).finally(() => this.loadMask.hide()); + } + + protected preSelectItems(): void { + const ids = this.getSelectedItemsHandler().filter(id => !StringHelper.isBlank(id)).map(id => new ContentId(id)); + + if (ids.length > 0) { + new ContentSummaryAndCompareStatusFetcher().fetchByIds(ids).then((contents) => { + const items = ids.map((id) => this.createSelectorItem(contents.find((content) => content.getId() === id.toString()), id)); + const options = items.map((item) => this.createSelectedOption(item)); + this.selectedOptionsView.addOptions(options, true, -1); + this.checkSelectionLimitReached(); + }).catch(DefaultErrorHandler.handle); + } + } + + protected createSelectorItem(content: ContentSummary | ContentSummaryAndCompareStatus, id: ContentId): ContentTreeSelectorItem { + if (!content) { // missing option + return new ContentTreeSelectorItem(new ContentSummary(new ContentSummaryBuilder().setId(id.toString()).setContentId(id))); + } + + if (content instanceof ContentSummaryAndCompareStatus) { + return new ContentAndStatusTreeSelectorItem(content); + } + + return new ContentTreeSelectorItem(content); + } + + protected selectLoadedFlatListItems(items: ContentTreeSelectorItem[]): void { + const selectedItems: string[] = this.getSelectedItemsHandler(); + + items.forEach((item: ContentTreeSelectorItem) => { + const id = item.getId(); + + if (selectedItems.indexOf(id) >= 0) { + this.select(item, true); + } + }); + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.addClass('content-selector-dropdown'); + this.preSelectItems(); + + return rendered; + }); + } + + updateSelectedItems(): void { + // unselecting all items + this.selectedOptionsView.getSelectedOptions().forEach((selectedOption) => { + this.selectedOptionsView.removeOption(selectedOption.getOption(), true); + }); + + // selecting items from property array + this.preSelectItems(); + } + + updateItem(item: ContentTreeSelectorItem): void { + super.updateItem(item); + + const existingOption = this.selectedOptionsView.getById(this.helper.getDataId(item))?.getOption(); + + if (existingOption) { + const newOption = this.createSelectedOption(item); + this.selectedOptionsView.updateOption(existingOption, newOption); + } + } + + clear(): void { + this.optionFilterInput.reset(); + } + + deselectAll(): void { + this.getSelectedOptions() + .map((option) => option.getOption().getDisplayValue()) + .filter((item) => !!item) + .forEach((item) => { + this.deselect(item); + }); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentTreeSelectionWrapper.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentTreeSelectionWrapper.ts new file mode 100644 index 0000000000..5bfb33accc --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentTreeSelectionWrapper.ts @@ -0,0 +1,85 @@ +import {SelectableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxWrapper'; +import {ContentTreeSelectorItem} from '../../item/ContentTreeSelectorItem'; +import {SelectableListBoxNavigator} from '@enonic/lib-admin-ui/ui/selector/list/SelectableListBoxNavigator'; +import {TreeListElement} from '@enonic/lib-admin-ui/ui/selector/list/TreeListBox'; + +export class ContentTreeSelectionWrapper + extends SelectableListBoxWrapper { + + private clickOutsideHandler: () => boolean; + + private enterKeyHandler: () => boolean; + + toggleItemWrapperSelected(itemId: string, isSelected: boolean) { + super.toggleItemWrapperSelected(itemId, isSelected); + } + + protected handleUserToggleAction(item: ContentTreeSelectorItem) { + if (item.isSelectable()) { + super.handleUserToggleAction(item); + } + } + + protected initListeners(): void { + super.initListeners(); + + this.addKeyNavigation(); + } + + protected createSelectionNavigator(): SelectableListBoxNavigator { + return super.createSelectionNavigator() + .setClickOutsideHandler(this.handleClickOutside.bind(this)) + .setEnterKeyHandler(this.handlerEnterPressed.bind(this)) + .setLeftKeyHandler(this.handleLeftKey.bind(this)) + .setRightKeyHandler(this.handleRightKey.bind(this)); + } + + protected handleClickOutside(): boolean { + return this.clickOutsideHandler ? this.clickOutsideHandler() : true; + } + + protected handlerEnterPressed(): boolean { + return this.enterKeyHandler ? this.enterKeyHandler(): true; + } + + setClickOutsideHandler(handler: () => boolean): this { + this.clickOutsideHandler = handler; + return this; + } + + setEnterKeyHandler(handler: () => boolean): this { + this.enterKeyHandler = handler; + return this; + } + + protected handleLeftKey(): boolean { + const focusedItem = this.selectionNavigator.getFocusedItem(); + + if (focusedItem) { + const view = this.listBox.getItemView(focusedItem); + + if (view instanceof TreeListElement) { + view.collapse(); + } + } + + return true; + } + + protected handleRightKey(): boolean { + const focusedItem = this.selectionNavigator.getFocusedItem(); + + if (focusedItem) { + const view = this.listBox.getItemView(focusedItem); + + if (view instanceof TreeListElement) { + view.expand(); + } + } + return true; + } + + getNavigator(): SelectableListBoxNavigator { + return this.selectionNavigator; + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentTreeSelectorDropdown.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentTreeSelectorDropdown.ts new file mode 100644 index 0000000000..3276d93dab --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ContentTreeSelectorDropdown.ts @@ -0,0 +1,231 @@ +import {ContentSelectorDropdown, ContentSelectorDropdownOptions} from './ContentSelectorDropdown'; +import {ContentsTreeList} from '../../browse/ContentsTreeList'; +import {ModeTogglerButton} from '../ui/selector/ModeTogglerButton'; +import {ContentTreeSelectorItem} from '../../item/ContentTreeSelectorItem'; +import * as Q from 'q'; +import {SelectionChange} from '@enonic/lib-admin-ui/util/SelectionChange'; +import {ContentTreeSelectionWrapper} from './ContentTreeSelectionWrapper'; +import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; +import {Element} from '@enonic/lib-admin-ui/dom/Element'; +import {i18n} from '@enonic/lib-admin-ui/util/Messages'; + +export interface ContentTreeSelectorDropdownOptions + extends ContentSelectorDropdownOptions { + treeMode?: boolean; +} + +export class ContentTreeSelectorDropdown + extends ContentSelectorDropdown { + + protected treeList: ContentsTreeList; + + protected treeSelectionWrapper: ContentTreeSelectionWrapper; + + protected treeMode: boolean; + + protected modeButton: ModeTogglerButton; + + protected options: ContentTreeSelectorDropdownOptions; + + constructor(listBox, options: ContentSelectorDropdownOptions) { + super(listBox, options); + } + + protected initElements(): void { + super.initElements(); + + this.modeButton = new ModeTogglerButton(); + this.modeButton.setActive(false); + this.treeList = new ContentsTreeList({loader: this.options.loader}); + this.treeList.setEmptyText(i18n('field.option.noitems')); + this.treeSelectionWrapper = new ContentTreeSelectionWrapper(this.treeList, { + maxSelected: this.options.maxSelected, + checkboxPosition: this.options.checkboxPosition, + className: 'content-tree-selector', + }); + + this.treeSelectionWrapper.hide(); + + this.treeSelectionWrapper + .setClickOutsideHandler(this.handleClickOutside.bind(this)) + .setEnterKeyHandler(this.handlerEnterPressedInTree.bind(this)); + + this.treeMode = this.options.treeMode || false; + } + + protected initListeners(): void { + super.initListeners(); + + this.listBox.whenShown(() => { + // if not empty then search will be performed after finished typing + if (StringHelper.isBlank(this.optionFilterInput.getValue())) { + this.search(this.optionFilterInput.getValue()); + } + }); + + this.treeList.onItemsAdded((items: ContentTreeSelectorItem[]) => { + this.selectLoadedTreeListItems(items); + }); + + this.modeButton.onActiveChanged((active: boolean) => { + this.treeMode = active; + + this.applyButton.hide(); + this.handleModeChanged(); + + if (!StringHelper.isBlank(this.optionFilterInput.getValue())) { + this.search(this.optionFilterInput.getValue()); + } + }); + + this.treeList.onShown(() => { + this.dropdownHandle.down(); + }); + + this.treeSelectionWrapper.onSelectionChanged((selectionChange: SelectionChange) => { + selectionChange.selected?.forEach((item: ContentTreeSelectorItem) => { + this.handleUserToggleAction(item); + }); + + selectionChange.deselected?.forEach((item: ContentTreeSelectorItem) => { + this.handleUserToggleAction(item); + }); + }); + + this.onSelectionChanged((selectionChange: SelectionChange) => { + selectionChange.selected?.forEach((item: ContentTreeSelectorItem) => { + this.treeSelectionWrapper.select(item, true); + }); + + selectionChange.deselected?.forEach((item: ContentTreeSelectorItem) => { + this.treeSelectionWrapper.deselect(item, true); + }); + }); + } + + protected postInitListeners(): void { + super.postInitListeners(); + + if (this.treeMode) { + this.modeButton.setActive(true); + this.hideDropdown(); + } + } + + protected doShowDropdown(): void { + // doing in specific order so key listeners first detached from hidden list and then attached to the shown one + if (this.treeMode) { + this.setVisibleOnDemand(this.listBox, !this.treeMode); + this.setVisibleOnDemand(this.treeSelectionWrapper, this.treeMode); + } else { + this.setVisibleOnDemand(this.treeSelectionWrapper, this.treeMode); + this.setVisibleOnDemand(this.listBox, !this.treeMode); + } + } + + protected doHideDropdown() { + this.treeSelectionWrapper.setVisible(false); + this.listBox.setVisible(false); + } + + private setVisibleOnDemand(element: Element, value: boolean): void { + if (value) { + if (!element.isVisible()) { + element.show(); + } + } else { + if (element.isVisible()) { + element.hide(); + } + } + } + + protected resetSelection(): void { + this.selectionDelta.forEach((value: boolean, id: string) => { + this.treeSelectionWrapper.toggleItemWrapperSelected(id, !value); + }); + + super.resetSelection(); + } + + protected handleModeChanged(): void { + this.options.loader.setTreeLoadMode(this.treeMode); + this.showDropdown(); + } + + protected applySelection() { + super.applySelection(); + + this.hideDropdown(); + } + + getItemById(id: string): ContentTreeSelectorItem { + return this.treeMode ? this.treeList.getItem(id) : super.getItemById(id); + } + + protected selectLoadedTreeListItems(items: ContentTreeSelectorItem[]): void { + const selectedItems: string[] = this.getSelectedItemsHandler(); + + items.forEach((item: ContentTreeSelectorItem) => { + const id = item.getId(); + + if (selectedItems.indexOf(id) >= 0) { + // Don't select item if it's unselected before loaded + this.treeSelectionWrapper.select(item, true); + } + }); + } + + protected search(value?: string) { + this.options.loader.setTreeFilterValue(value); + + if (this.treeMode) { + this.treeList.clearItems(); + this.treeList.load(); + } else { + super.search(value); + } + } + + load(): void { + this.search(this.optionFilterInput.getValue()); + } + + getTreeList(): ContentsTreeList { + return this.treeList; + } + + doRender(): Q.Promise { + return super.doRender().then((rendered: boolean) => { + this.modeButton.insertBeforeEl(this.optionFilterInput); + this.treeSelectionWrapper.addClass('filterable-listbox'); + this.modeButton.insertBeforeEl(this.optionFilterInput); + this.treeSelectionWrapper.insertBeforeEl(this.selectedOptionsView); + + this.preSelectItems(); + + return rendered; + }); + } + + protected handlerEnterPressedInTree(): boolean { + if (this.applyButton.hasFocus()) { + this.applySelection(); + return true; + } + + const focusedItem = this.treeSelectionWrapper.getNavigator().getFocusedItem(); + + if (focusedItem) { + if (this.selectionDelta.size === 0) { + this.handleUserToggleAction(focusedItem); + } + + if (this.selectionDelta.size !== 0) { + this.applySelection(); + } + } + + return true; + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageContentListBox.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageContentListBox.ts new file mode 100644 index 0000000000..c7b47ef92f --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageContentListBox.ts @@ -0,0 +1,25 @@ +import {ContentListBox, ContentListBoxOptions} from './ContentListBox'; +import {ImageSelectorViewer} from '../ui/selector/image/ImageSelectorViewer'; +import {MediaTreeSelectorItem} from '../ui/selector/media/MediaTreeSelectorItem'; +import {Element} from '@enonic/lib-admin-ui/dom/Element'; + +export class ImageContentListBox extends ContentListBox { + + constructor(options: ContentListBoxOptions) { + options.className = (options.className || '') + ' image-content-list-box'; + super(options); + } + + protected createItemView(item: MediaTreeSelectorItem, readOnly: boolean): ImageSelectorViewer { + const viewer = new ImageSelectorViewer(); + + viewer.setObject(item); + + return viewer; + } + + protected updateItemView(itemView: Element, item: MediaTreeSelectorItem): void { + const viewer = itemView as ImageSelectorViewer; + viewer.setObject(item); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageSelector.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageSelector.ts index f3777229b7..5760ab7f17 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageSelector.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageSelector.ts @@ -5,13 +5,10 @@ import {InputTypeManager} from '@enonic/lib-admin-ui/form/inputtype/InputTypeMan import {Class} from '@enonic/lib-admin-ui/Class'; import {PropertyArray} from '@enonic/lib-admin-ui/data/PropertyArray'; import {ContentTypeName} from '@enonic/lib-admin-ui/schema/content/ContentTypeName'; -import {ComboBox} from '@enonic/lib-admin-ui/ui/selector/combobox/ComboBox'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; -import {SelectedOptionEvent} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionEvent'; import {UploadFailedEvent} from '@enonic/lib-admin-ui/ui/uploader/UploadFailedEvent'; import {UploadProgressEvent} from '@enonic/lib-admin-ui/ui/uploader/UploadProgressEvent'; import {MediaSelector} from './MediaSelector'; -import {ImageContentComboBox, ImageContentComboBoxBuilder} from '../ui/selector/image/ImageContentComboBox'; import {ImageSelectorSelectedOptionsView} from '../ui/selector/image/ImageSelectorSelectedOptionsView'; import {ImageUploaderEl} from '../ui/selector/image/ImageUploaderEl'; import {ImageSelectorSelectedOptionView} from '../ui/selector/image/ImageSelectorSelectedOptionView'; @@ -19,15 +16,18 @@ import {MediaTreeSelectorItem} from '../ui/selector/media/MediaTreeSelectorItem' import {ContentInputTypeViewContext} from '../ContentInputTypeViewContext'; import {Content} from '../../content/Content'; import {GetMimeTypesByContentTypeNamesRequest} from '../../resource/GetMimeTypesByContentTypeNamesRequest'; -import {ImageOptionDataLoader} from '../ui/selector/image/ImageOptionDataLoader'; +import {ImageOptionDataLoader, ImageOptionDataLoaderBuilder} from '../ui/selector/image/ImageOptionDataLoader'; import {ContentSummaryOptionDataLoader} from '../ui/selector/ContentSummaryOptionDataLoader'; -import {ContentTreeSelectorItem} from '../../item/ContentTreeSelectorItem'; import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; import {EditContentEvent} from '../../event/EditContentEvent'; import {ContentPath} from '../../content/ContentPath'; import {UploadItem} from '@enonic/lib-admin-ui/ui/uploader/UploadItem'; import {ContentSummary} from '../../content/ContentSummary'; -import {ContentId} from '../../content/ContentId'; +import {ImageContentListBox} from './ImageContentListBox'; +import {ImageSelectorDropdown} from './ImageSelectorDropdown'; +import {ContentSelectorDropdownOptions} from './ContentSelectorDropdown'; +import {ContentListBox} from './ContentListBox'; +import {ContentTreeSelectorItem} from '../../item/ContentTreeSelectorItem'; export class ImageSelector extends MediaSelector { @@ -43,10 +43,6 @@ export class ImageSelector this.onRemoved(() => ResponsiveManager.unAvailableSizeChanged(this)); } - public getContentComboBox(): ImageContentComboBox { - return this.contentComboBox as ImageContentComboBox; - } - protected getContentPath(raw: MediaTreeSelectorItem): ContentPath { return raw.getContentSummary()?.getPath(); } @@ -55,7 +51,7 @@ export class ImageSelector return super.getSelectedOptionsView() as ImageSelectorSelectedOptionsView; } - private createSelectedOptionsView(): ImageSelectorSelectedOptionsView { + protected createSelectedOptionsView(): ImageSelectorSelectedOptionsView { let selectedOptionsView = new ImageSelectorSelectedOptionsView(); selectedOptionsView.onEditSelectedOptions((options: SelectedOption[]) => { @@ -72,9 +68,9 @@ export class ImageSelector if (item.isEmptyContent()) { selectedOptionsView.removeOption(option.getOption()); - this.handleDeselected(option.getIndex()); + // this.handleDeselected(option.getIndex()); } else { - this.contentComboBox.deselect(item); + this.contentSelectorDropdown.deselect(item); } }); @@ -84,51 +80,12 @@ export class ImageSelector return selectedOptionsView; } - protected createOptionDataLoader(): ContentSummaryOptionDataLoader { - return ImageOptionDataLoader.build(this.createOptionDataLoaderBuilder()); - } - - protected doCreateContentComboBoxBuilder(): ImageContentComboBoxBuilder { - return ImageContentComboBox.create().setProject(this.context.project); + createLoader(): ContentSummaryOptionDataLoader { + return ImageOptionDataLoader.build(this.createOptionDataLoaderBuilder().setAppendLoadResults(false)); } - protected createContentComboBoxBuilder(input: Input, propertyArray: PropertyArray): ImageContentComboBoxBuilder { - return super.createContentComboBoxBuilder(input, propertyArray) - .setSelectedOptionsView(this.createSelectedOptionsView()) - .setDisplayMissingSelectedOptions(true) - .setRemoveMissingSelectedOptions(false) as ImageContentComboBoxBuilder; - } - - protected initEvents(contentComboBox: ImageContentComboBox) { - const comboBox: ComboBox = contentComboBox.getComboBox(); - - comboBox.onOptionDeselected((event: SelectedOptionEvent) => { - // property not found. - const option = event.getSelectedOption(); - if (option.getOption().getDisplayValue().getContentSummary()) { - this.handleDeselected(option.getIndex()); - } - this.handleValueChanged(false); - }); - - comboBox.onOptionSelected((event: SelectedOptionEvent) => { - this.fireFocusSwitchEvent(event); - - if (!this.isLayoutInProgress()) { - let contentId = event.getSelectedOption().getOption().getDisplayValue().getContentId(); - if (!contentId) { - return; - } - - this.setContentIdProperty(contentId); - } - this.handleValueChanged(false); - }); - - comboBox.onOptionMoved((moved: SelectedOption, fromIndex: number) => { - this.handleMoved(moved, fromIndex); - this.handleValueChanged(false); - }); + protected createOptionDataLoaderBuilder(): ImageOptionDataLoaderBuilder { + return new ImageOptionDataLoaderBuilder(); } layout(input: Input, propertyArray: PropertyArray): Q.Promise { @@ -199,18 +156,24 @@ export class ImageSelector return new MediaTreeSelectorItem(content, selectable, expandable); } - protected createMissingContentItem(id: ContentId): MediaTreeSelectorItem { - return new MediaTreeSelectorItem().setMissingItemId(id.toString()); + protected createContentListBox(loader: ContentSummaryOptionDataLoader): ImageContentListBox { + return new ImageContentListBox({loader: loader}); } - protected updateSelectedOptionIsEditable(selectedOption: SelectedOption) { - // different behavior for image selector + protected doCreateSelectorDropdown(listBox: ContentListBox, + dropdownOptions: ContentSelectorDropdownOptions): ImageSelectorDropdown { + return new ImageSelectorDropdown(listBox, dropdownOptions); } - protected handleOptionUpdated(optionsUpdated: SelectedOption[]) { - super.handleOptionUpdated(optionsUpdated); + protected getDropdownClassName(): string { + return 'image-selector-dropdown'; + } - this.getSelectedOptionsView().updateSelectionToolbarLayout(); + protected handleSelectedOptionDeleted(selectedOption: SelectedOption): void { + const option = selectedOption.getOption(); + const newValue = new MediaTreeSelectorItem().setMissingItemId(option.getDisplayValue().getId()); + option.setDisplayValue(newValue); + selectedOption.getOptionView().setOption(option); } } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageSelectorDropdown.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageSelectorDropdown.ts new file mode 100644 index 0000000000..bf1aec2c08 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/ImageSelectorDropdown.ts @@ -0,0 +1,23 @@ +import {ContentSummary} from '../../content/ContentSummary'; +import {ContentSummaryAndCompareStatus} from '../../content/ContentSummaryAndCompareStatus'; +import {MediaTreeSelectorItem} from '../ui/selector/media/MediaTreeSelectorItem'; +import {ContentTreeSelectorDropdown} from './ContentTreeSelectorDropdown'; +import {ContentId} from '../../content/ContentId'; + +export class ImageSelectorDropdown extends ContentTreeSelectorDropdown { + + protected createSelectorItem(content: ContentSummary | ContentSummaryAndCompareStatus, id: ContentId): MediaTreeSelectorItem { + if (content instanceof ContentSummaryAndCompareStatus) { + return new MediaTreeSelectorItem(content.getContentSummary()); + } + + const mediaItem = new MediaTreeSelectorItem(content); + + if (mediaItem.isEmptyContent()) { + mediaItem.setMissingItemId(id?.toString()); + } + + return mediaItem; + } + +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/MediaSelector.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/MediaSelector.ts index 04aaca5b86..d181966ee1 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/MediaSelector.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/MediaSelector.ts @@ -4,11 +4,9 @@ import {InputTypeManager} from '@enonic/lib-admin-ui/form/inputtype/InputTypeMan import {Class} from '@enonic/lib-admin-ui/Class'; import {PropertyArray} from '@enonic/lib-admin-ui/data/PropertyArray'; import {ContentTypeName} from '@enonic/lib-admin-ui/schema/content/ContentTypeName'; -import {ComboBox} from '@enonic/lib-admin-ui/ui/selector/combobox/ComboBox'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; import {UploadedEvent} from '@enonic/lib-admin-ui/ui/uploader/UploadedEvent'; import {UploadFailedEvent} from '@enonic/lib-admin-ui/ui/uploader/UploadFailedEvent'; -import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; import {ContentSelector} from './ContentSelector'; import {ContentInputTypeViewContext} from '../ContentInputTypeViewContext'; import {MediaUploaderEl, MediaUploaderElConfig, MediaUploaderElOperation} from '../ui/upload/MediaUploaderEl'; @@ -32,9 +30,15 @@ export class MediaSelector protected addExtraElementsOnLayout(input: Input, propertyArray: PropertyArray): Q.Promise { return this.createUploader().then((mediaUploader: MediaUploaderEl) => { - this.comboBoxWrapper.appendChild(this.uploader = mediaUploader); + this.uploader = mediaUploader; - if (!this.contentComboBox.getComboBox().isVisible()) { + this.contentSelectorDropdown.whenRendered(() => { + this.contentSelectorDropdown.appendChild(this.uploader); + this.uploader.addClass('extra-button'); + this.contentSelectorDropdown.addClass('has-extra-button'); + }); + + if (!this.contentSelectorDropdown.isVisible()) { this.uploader.hide(); } }); @@ -98,12 +102,7 @@ export class MediaSelector const createdContent = event.getUploadItem().getModel(); const item = ContentSummaryAndCompareStatus.fromContentAndCompareStatus(createdContent, CompareStatus.NEW); - const option = Option.create() - .setValue(createdContent.getContentId().toString()) - .setDisplayValue(this.createSelectorItem(item)) - .build(); - - this.contentComboBox.selectOption(option); + this.contentSelectorDropdown.select(this.createSelectorItem(item)); const selectedOption = this.getSelectedOptionsView().getById(createdContent.getContentId().toString()); this.selectedOptionHandler(selectedOption); @@ -132,15 +131,13 @@ export class MediaSelector uploader.setDefaultDropzoneVisible(false); }); - const comboBox: ComboBox = this.contentComboBox.getComboBox(); - - comboBox.onHidden(() => { + this.contentSelectorDropdown.onHidden(() => { // hidden on max occurrences reached if (uploader) { uploader.hide(); } }); - comboBox.onShown(() => { + this.contentSelectorDropdown.onShown(() => { // shown on occurrences between min and max if (uploader) { uploader.show(); diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/PrincipalSelector.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/PrincipalSelector.ts index 3a5b5ff1be..4db0abeb97 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/selector/PrincipalSelector.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/selector/PrincipalSelector.ts @@ -1,15 +1,15 @@ import {PrincipalSelector as BasePrincipalSelector} from '@enonic/lib-admin-ui/form/inputtype/principal/PrincipalSelector'; -import {PrincipalLoader as BasePrincipalLoader} from '@enonic/lib-admin-ui/security/PrincipalLoader'; -import {PrincipalLoader} from '../../security/PrincipalLoader'; import {InputTypeManager} from '@enonic/lib-admin-ui/form/inputtype/InputTypeManager'; import {Class} from '@enonic/lib-admin-ui/Class'; import {InputTypeName} from '@enonic/lib-admin-ui/form/InputTypeName'; +import {PrincipalLoader} from '@enonic/lib-admin-ui/security/PrincipalLoader'; +import {CSPrincipalLoader} from '../../security/CSPrincipalLoader'; export class PrincipalSelector extends BasePrincipalSelector { - protected createLoader(): BasePrincipalLoader { - return new PrincipalLoader(); + protected createLoader(): PrincipalLoader { + return new CSPrincipalLoader(); } static getName(): InputTypeName { diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfigurator.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfigurator.ts index f68ab417f5..12c9dd1ef3 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfigurator.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfigurator.ts @@ -12,7 +12,6 @@ import {ValueTypes} from '@enonic/lib-admin-ui/data/ValueTypes'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; import {Application} from '@enonic/lib-admin-ui/application/Application'; import {ApplicationConfig} from '@enonic/lib-admin-ui/application/ApplicationConfig'; -import {SelectedOptionEvent} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionEvent'; import {ApplicationKey} from '@enonic/lib-admin-ui/application/ApplicationKey'; import {ApplicationEvent, ApplicationEventType} from '@enonic/lib-admin-ui/application/ApplicationEvent'; import {ApplicationConfigProvider} from '@enonic/lib-admin-ui/form/inputtype/appconfig/ApplicationConfigProvider'; @@ -24,6 +23,9 @@ import {BaseInputTypeManagingAdd} from '@enonic/lib-admin-ui/form/inputtype/supp import {IsAuthenticatedRequest} from '@enonic/lib-admin-ui/security/auth/IsAuthenticatedRequest'; import {LoginResult} from '@enonic/lib-admin-ui/security/auth/LoginResult'; import {ProjectHelper} from '../../settings/data/project/ProjectHelper'; +import {SelectionChange} from '@enonic/lib-admin-ui/util/SelectionChange'; +import {GetApplicationsRequest} from '../../resource/GetApplicationsRequest'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; export class SiteConfigurator extends BaseInputTypeManagingAdd { @@ -75,6 +77,8 @@ export class SiteConfigurator this.comboBox.setEnabled(!readonly); }); + this.layoutApps(propertyArray).catch(DefaultErrorHandler.handle); + this.appendChild(this.comboBox); this.comboBox.render().then(() => { @@ -85,9 +89,21 @@ export class SiteConfigurator }); } + private layoutApps(propertyArray: PropertyArray): Q.Promise { + const appKeys = this.getKeysFromPropertyArray(propertyArray); + + if (!appKeys?.length) { + return Q.resolve(); + } + + return new GetApplicationsRequest(appKeys).sendAndParse().then((apps: Application[]) => { + this.comboBox.select(apps, true); + }); + } + update(propertyArray: PropertyArray, unchangedOnly?: boolean): Q.Promise { return super.update(propertyArray, unchangedOnly).then(() => { - const optionsMissing = !!propertyArray && propertyArray.getSize() > 0 && this.comboBox.getOptions().length === 0; + const optionsMissing = !!propertyArray && propertyArray.getSize() > 0 && this.comboBox.getListSize() === 0; return optionsMissing ? this.comboBox.getLoader().preLoad() : null; }).then(() => { const ignorePropertyChange = this.isPropertyChangeIgnored(); @@ -107,17 +123,12 @@ export class SiteConfigurator return Q.all(updatePromises).then(() => { this.ignorePropertyChange(ignorePropertyChange); - if (!unchangedOnly || !this.comboBox.isDirty()) { - this.comboBox.setValue(this.getValueFromPropertyArray(propertyArray)); - } else if (this.comboBox.isDirty()) { - this.comboBox.forceChangedEvent(); - } }); }); } reset() { - this.comboBox.resetBaseValues(); + //this.comboBox.resetBaseValues(); } private static optionViewToKey(option: SiteConfiguratorSelectedOptionView): string { @@ -143,9 +154,9 @@ export class SiteConfigurator const selectedOptions: SiteConfiguratorSelectedOptionView[] = this.comboBox.getSelectedOptionViews(); const alreadySelected = selectedOptions.some(option => SiteConfigurator.optionViewToKey(option) === key); if (!alreadySelected) { - this.comboBox.selectOptionByValue(key); + this.comboBox.selectByKey(key, true); } - return this.comboBox.getSelectedOptionByValue(key); + return this.comboBox.getSelectedOptionByKey(key); } private saveToSet(siteConfig: ApplicationConfig, index: number) { @@ -171,6 +182,16 @@ export class SiteConfigurator }).join(';'); } + private getKeysFromPropertyArray(propertyArray: PropertyArray): ApplicationKey[] { + return propertyArray.getProperties() + .filter(p => p.hasNonNullValue()) + .map((property) => this.makeSiteConfigFromProperty(property).getApplicationKey()); + } + + private makeSiteConfigFromProperty(property: Property): ApplicationConfig { + return ApplicationConfig.create().fromData(property.getPropertySet()).build(); + } + private createComboBox(input: Input, siteConfigProvider: ApplicationConfigProvider): SiteConfiguratorComboBox { const value = this.getValueFromPropertyArray(this.getPropertyArray()); @@ -188,32 +209,46 @@ export class SiteConfigurator forcedValidate(); }; - comboBox.onOptionDeselected((event: SelectedOptionEvent) => { - this.ignorePropertyChange(true); + comboBox.onSelectionChanged((selectionChange: SelectionChange) => { + if (selectionChange.selected?.length > 0) { + this.ignorePropertyChange(true); - this.getPropertyArray().remove(event.getSelectedOption().getIndex()); + selectionChange.selected.forEach((selected: Application) => { + const selectedOption: SelectedOption = comboBox.getSelectedOption(selected); + const view: SiteConfiguratorSelectedOptionView = selectedOption.getOptionView() as SiteConfiguratorSelectedOptionView; - forcedValidate(); - }); + const propertyArray: PropertyArray = this.getPropertyArray(); + const configSet: PropertySet = propertyArray.get(selectedOption.getIndex()).getPropertySet().getProperty( + ApplicationConfig.PROPERTY_CONFIG).getPropertySet(); - comboBox.onOptionSelected((event: SelectedOptionEvent) => { - this.fireFocusSwitchEvent(event); - this.ignorePropertyChange(true); + view.whenRendered(() => { + view.getFormView().update(configSet, false); + }); - const selectedOption: SelectedOption = event.getSelectedOption(); - const view: SiteConfiguratorSelectedOptionView = selectedOption.getOptionView() as SiteConfiguratorSelectedOptionView; + const key = selectedOption.getOption().getDisplayValue().getApplicationKey(); + if (key) { + saveAndForceValidate(selectedOption); + } + }); + } - const propertyArray: PropertyArray = this.getPropertyArray(); - const configSet: PropertySet = propertyArray.get(selectedOption.getIndex()).getPropertySet().getProperty( - ApplicationConfig.PROPERTY_CONFIG).getPropertySet(); + if (selectionChange.deselected?.length > 0) { + this.ignorePropertyChange(true); - view.whenRendered(() => { - view.getFormView().update(configSet, false); - }); + selectionChange.deselected.forEach((deselected: Application) => { + const property = this.getPropertyArray().getProperties() + .filter(p => p.hasNonNullValue()) + .find((property) => { + const config = this.makeSiteConfigFromProperty(property); + return deselected.getApplicationKey().equals(config.getApplicationKey()); + }); + + if (property) { + this.getPropertyArray().remove(property.getIndex()); + } + }); - const key = selectedOption.getOption().getDisplayValue().getApplicationKey(); - if (key) { - saveAndForceValidate(selectedOption); + forcedValidate(); } }); diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfiguratorComboBox.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfiguratorComboBox.ts index ab763af6e5..69d651230f 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfiguratorComboBox.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfiguratorComboBox.ts @@ -1,43 +1,91 @@ import {Application} from '@enonic/lib-admin-ui/application/Application'; import {ApplicationKey} from '@enonic/lib-admin-ui/application/ApplicationKey'; -import {ApplicationViewer} from '@enonic/lib-admin-ui/application/ApplicationViewer'; import {FormView} from '@enonic/lib-admin-ui/form/FormView'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; import {ApplicationConfigProvider} from '@enonic/lib-admin-ui/form/inputtype/appconfig/ApplicationConfigProvider'; import {SiteConfiguratorSelectedOptionsView} from './SiteConfiguratorSelectedOptionsView'; import {SiteConfiguratorSelectedOptionView} from './SiteConfiguratorSelectedOptionView'; import {ContentFormContext} from '../../ContentFormContext'; -import {RichComboBox, RichComboBoxBuilder} from '@enonic/lib-admin-ui/ui/selector/combobox/RichComboBox'; import {SiteApplicationLoader} from '../../application/SiteApplicationLoader'; +import { + FilterableListBoxWrapperWithSelectedView, + ListBoxInputOptions +} from '@enonic/lib-admin-ui/ui/selector/list/FilterableListBoxWrapperWithSelectedView'; +import {SiteConfiguratorListBox} from './SiteConfiguratorListBox'; +import {LoadedDataEvent} from '@enonic/lib-admin-ui/util/loader/event/LoadedDataEvent'; +import * as Q from 'q'; +import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; +import {AppHelper} from '@enonic/lib-admin-ui/util/AppHelper'; +import {ValueChangedEvent} from '@enonic/lib-admin-ui/ValueChangedEvent'; +import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; +import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; + +interface SiteConfiguratorComboBoxOptions extends ListBoxInputOptions { + loader: SiteApplicationLoader; +} export class SiteConfiguratorComboBox - extends RichComboBox { + extends FilterableListBoxWrapperWithSelectedView { - private siteConfiguratorSelectedOptionsView: SiteConfiguratorSelectedOptionsView; + protected options: SiteConfiguratorComboBoxOptions; + + protected selectedOptionsView: SiteConfiguratorSelectedOptionsView; constructor(maxOccurrences: number, siteConfigProvider: ApplicationConfigProvider, formContext: ContentFormContext, value?: string) { - const filterObject = { - state: Application.STATE_STARTED - }; + super(new SiteConfiguratorListBox(), { + maxSelected: maxOccurrences, + selectedOptionsView: new SiteConfiguratorSelectedOptionsView(siteConfigProvider, formContext), + className: 'site-configurator-combobox', + loader: new SiteApplicationLoader({ + state: Application.STATE_STARTED + }), + } as SiteConfiguratorComboBoxOptions); + } - const builder: RichComboBoxBuilder = new RichComboBoxBuilder(); - builder - .setMaximumOccurrences(maxOccurrences) - .setIdentifierMethod('getApplicationKey') - .setComboBoxName('applicationSelector') - .setLoader(new SiteApplicationLoader(filterObject)) - .setSelectedOptionsView(new SiteConfiguratorSelectedOptionsView(siteConfigProvider, formContext)) - .setOptionDisplayValueViewer(new ApplicationViewer()).setValue(value) - .setDelayedInputValueChangedHandling(500) - .setDisplayMissingSelectedOptions(true); + protected initListeners(): void { + super.initListeners(); + + this.options.loader.onLoadedData((event: LoadedDataEvent) => { + const entries = event.getData(); + + if (event.isPostLoad()) { + this.listBox.addItems(entries); + } else { + this.listBox.setItems(entries); + } + return Q.resolve(null); + }); - super(builder); + this.listBox.whenShown(() => { + // if not empty then search will be performed after finished typing + if (StringHelper.isBlank(this.optionFilterInput.getValue())) { + this.search(this.optionFilterInput.getValue()); + } + }); + + let searchValue = ''; + + const debouncedSearch = AppHelper.debounce(() => { + this.search(searchValue); + }, 300); + + this.optionFilterInput.onValueChanged((event: ValueChangedEvent) => { + searchValue = event.getNewValue(); + debouncedSearch(); + }); + } - this.siteConfiguratorSelectedOptionsView = builder.getSelectedOptionsView() as SiteConfiguratorSelectedOptionsView; + protected search(value?: string): void { + this.options.loader.search(value).catch(DefaultErrorHandler.handle); + } - this.addClass('site-configurator-combobox'); + createSelectedOption(item: Application): Option { + return Option.create() + .setValue(item.getApplicationKey().toString()) + .setDisplayValue(item) + .build(); } getSelectedOptionViews(): SiteConfiguratorSelectedOptionView[] { @@ -49,15 +97,43 @@ export class SiteConfiguratorComboBox } getSelectedOptionsView(): SiteConfiguratorSelectedOptionsView { - return this.siteConfiguratorSelectedOptionsView; + return this.selectedOptionsView; } onSiteConfigFormDisplayed(listener: (applicationKey: ApplicationKey, formView: FormView) => void) { - this.siteConfiguratorSelectedOptionsView.onSiteConfigFormDisplayed(listener); + this.selectedOptionsView.onSiteConfigFormDisplayed(listener); } unSiteConfigFormDisplayed(listener: (applicationKey: ApplicationKey, formView: FormView) => void) { - this.siteConfiguratorSelectedOptionsView.unSiteConfigFormDisplayed(listener); + this.selectedOptionsView.unSiteConfigFormDisplayed(listener); + } + + onOptionMoved(handler: (selectedOption: SelectedOption, fromIndex: number) => void): void { + this.selectedOptionsView.onOptionMoved(handler); + } + + getLoader(): SiteApplicationLoader { + return this.options.loader; + } + + getSelectedOption(item: Application): SelectedOption { + return this.selectedOptionsView.getById(item.getApplicationKey().toString()); } + getSelectedOptionByKey(key: string): SelectedOption { + return this.selectedOptionsView.getById(key); + } + + getListSize(): number { + return this.listBox.getItemCount(); + } + + selectByKey(key: string, silent?: boolean): void { + const app = this.getLoader().getResults().find((app: Application) => app.getApplicationKey().toString() === key) || + this.listBox.getItem(key); + + if (app) { + this.select(app, silent); + } + } } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfiguratorListBox.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfiguratorListBox.ts new file mode 100644 index 0000000000..ae19831c35 --- /dev/null +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/siteconfigurator/SiteConfiguratorListBox.ts @@ -0,0 +1,26 @@ +import {LazyListBox} from '@enonic/lib-admin-ui/ui/selector/list/LazyListBox'; +import {Element} from '@enonic/lib-admin-ui/dom/Element'; +import {Application} from '@enonic/lib-admin-ui/application/Application'; +import {ApplicationViewer} from '@enonic/lib-admin-ui/application/ApplicationViewer'; + +export class SiteConfiguratorListBox + extends LazyListBox { + + constructor() { + super('site-configurator-list-box'); + } + + protected createItemView(item: Application, readOnly: boolean): ApplicationViewer { + const viewer = new ApplicationViewer(); + viewer.setObject(item); + return viewer; + } + + protected getItemId(item: Application): string { + return item.getApplicationKey().toString(); + } + + protected getScrollContainer(): Element { + return this; + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentComboBox.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentComboBox.ts index 50beff007e..c8a33c85f1 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentComboBox.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentComboBox.ts @@ -1,330 +1,32 @@ -import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; import {SpanEl} from '@enonic/lib-admin-ui/dom/SpanEl'; -import {ObjectHelper} from '@enonic/lib-admin-ui/ObjectHelper'; -import {Grid} from '@enonic/lib-admin-ui/ui/grid/Grid'; -import {GridColumn, GridColumnBuilder} from '@enonic/lib-admin-ui/ui/grid/GridColumn'; -import {ResponsiveManager} from '@enonic/lib-admin-ui/ui/responsive/ResponsiveManager'; -import {ResponsiveRanges} from '@enonic/lib-admin-ui/ui/responsive/ResponsiveRanges'; import {BaseSelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/BaseSelectedOptionsView'; -import {ComboBox, ComboBoxConfig} from '@enonic/lib-admin-ui/ui/selector/combobox/ComboBox'; -import {RichComboBox, RichComboBoxBuilder} from '@enonic/lib-admin-ui/ui/selector/combobox/RichComboBox'; import {RichSelectedOptionView, RichSelectedOptionViewBuilder} from '@enonic/lib-admin-ui/ui/selector/combobox/RichSelectedOptionView'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; -import {SelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionsView'; import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; -import {OptionDataHelper} from '@enonic/lib-admin-ui/ui/selector/OptionDataHelper'; -import {OptionDataLoader} from '@enonic/lib-admin-ui/ui/selector/OptionDataLoader'; -import {OptionsFactory} from '@enonic/lib-admin-ui/ui/selector/OptionsFactory'; -import {Viewer} from '@enonic/lib-admin-ui/ui/Viewer'; -import {AppHelper} from '@enonic/lib-admin-ui/util/AppHelper'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; -import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; -import {ValueChangedEvent} from '@enonic/lib-admin-ui/ValueChangedEvent'; import * as Q from 'q'; -import {ContentRowFormatter} from '../../../browse/ContentRowFormatter'; -import {ContentId} from '../../../content/ContentId'; import {ContentPath} from '../../../content/ContentPath'; -import {ContentSummary, ContentSummaryBuilder} from '../../../content/ContentSummary'; +import {ContentSummary} from '../../../content/ContentSummary'; import {ContentSummaryAndCompareStatus} from '../../../content/ContentSummaryAndCompareStatus'; import {EditContentEvent} from '../../../event/EditContentEvent'; import {ContentAndStatusTreeSelectorItem} from '../../../item/ContentAndStatusTreeSelectorItem'; import {ContentTreeSelectorItem} from '../../../item/ContentTreeSelectorItem'; -import {ContentTreeSelectorItemViewer} from '../../../item/ContentTreeSelectorItemViewer'; -import {ContentsExistRequest} from '../../../resource/ContentsExistRequest'; import {Project} from '../../../settings/data/project/Project'; -import {ContentSummaryOptionDataHelper} from '../../../util/ContentSummaryOptionDataHelper'; -import {ContentSummaryOptionDataLoader, ContentSummaryOptionDataLoaderBuilder} from './ContentSummaryOptionDataLoader'; -import {ModeTogglerButton} from './ModeTogglerButton'; - - -export class ContentComboBox - extends RichComboBox { - - public static NOT_FOUND_CLASS = 'content-not-found'; - - protected optionsFactory: OptionsFactory; - - protected treegridDropdownEnabled: boolean; - - protected treeModeTogglerAllowed: boolean; - - protected initialTreeEnabledState: boolean; - - protected showAfterReload: boolean; - - protected preventReload: boolean; - - protected treeModeToggler?: ModeTogglerButton; - - private statusColumn: GridColumn; - - constructor(builder: ContentComboBoxBuilder) { - super(builder); - - this.addClass('content-combo-box'); - - this.initElements(builder); - this.initListeners(); - } - - protected initElements(builder: ContentComboBoxBuilder) { - this.treegridDropdownEnabled = builder.treegridDropdownEnabled; - this.initialTreeEnabledState = this.treegridDropdownEnabled; - - this.treeModeTogglerAllowed = builder.treeModeTogglerAllowed; - if (this.treeModeTogglerAllowed) { - this.initTreeModeToggler(); - } - - this.showAfterReload = false; - this.optionsFactory = new OptionsFactory(this.getLoader(), builder.optionDataHelper); - (this.getSelectedOptionView() as ContentSelectedOptionsView).setProject(builder.project); - } - - private createStatusColumn() { - this.statusColumn = new GridColumnBuilder() - .setId('status') - .setName('Status') - .setField('displayValue') - .setFormatter(ContentRowFormatter.statusSelectorFormatter) - .setCssClass('status') - .setBoundaryWidth(75, 75) - .build(); - } - - protected initListeners() { - const debouncedHandler = AppHelper.debounce(this.handleAvailableSizeChanged.bind(this), 300); - ResponsiveManager.onAvailableSizeChanged(this, debouncedHandler); - } - - private handleAvailableSizeChanged() { - if (ResponsiveRanges._360_540.isFitOrSmaller(this.getEl().getWidth())) { - this.removeStatusColumnIfShown(); - } else { - this.addStatusColumnIfHidden(); - } - } - - private removeStatusColumnIfShown() { - if (this.isStatusColumnShown()) { - const newColumns: GridColumn[] = this.getColumnsWithoutCheckbox() - .filter((column: GridColumn) => column.id !== 'status'); - this.getDataGrid().setColumns(newColumns, true); - } - } - - private addStatusColumnIfHidden() { - if (!this.isStatusColumnShown()) { - const newColumns: GridColumn[] = [...this.getColumnsWithoutCheckbox(), this.statusColumn]; - this.getDataGrid().setColumns(newColumns, true); - } - } - - private getDataGrid(): Grid { - return this.getComboBox().getComboBoxDropdownGrid().getGrid(); - } - - private getColumnsWithoutCheckbox(): GridColumn[] { - return this.getDataGrid().getColumns().filter((column: GridColumn) => column.id !== '_checkbox_selector'); - } - - private isStatusColumnShown(): boolean { - return this.getColumnsWithoutCheckbox().some((column: GridColumn) => column.id === 'status'); - } - - protected createComboboxConfig(builder: ContentComboBoxBuilder): ComboBoxConfig { - this.prepareBuilder(builder); - const config = super.createComboboxConfig(builder); - config.treegridDropdownAllowed = builder.treegridDropdownEnabled || builder.treeModeTogglerAllowed; - - return config; - } - - protected prepareBuilder(builder: ContentComboBoxBuilder) { - this.createStatusColumn(); - - if (!builder.loader) { - builder.setLoader(this.createLoader(builder) as ContentSummaryOptionDataLoader); - } - - builder.setCreateColumns([this.statusColumn]); - - if (builder.isRequestMissingOptions) { - builder.setRequestMissingOptions((missingOptionIds: string[]) => { - return new ContentsExistRequest(missingOptionIds) - .setRequestProject(builder.project) - .sendAndParse() - .then(result => result.getContentsExistMap()); - }); - } - } - - protected createLoader(builder: ContentComboBoxBuilder): ContentSummaryOptionDataLoader { - return this.createLoaderBuilder(builder).setProject(builder.project).build(); - } - - protected createLoaderBuilder(builder: ContentComboBoxBuilder): ContentSummaryOptionDataLoaderBuilder { - return ContentSummaryOptionDataLoader.create(); - } - - getLoader(): ContentSummaryOptionDataLoader { - return super.getLoader() as ContentSummaryOptionDataLoader; - } - - getSelectedContent(): ContentSummary { - let option = this.getOptionByValue(this.getValue()); - if (option) { - return (option.getDisplayValue() as ITEM_TYPE).getContent(); - } - return null; - } - - setEnabled(enable: boolean): void { - super.setEnabled(enable); - - if (this.treeModeToggler) { - this.treeModeToggler.setEnabled(enable); - } - } - - getContent(contentId: ContentId): ContentSummary { - let option = this.getOptionByValue(contentId.toString()); - if (option) { - return (option.getDisplayValue() as ITEM_TYPE).getContent(); - } - return null; - } - - getComboBox(): ComboBox { - return super.getComboBox() as ComboBox; - } - - setContent(content: ContentSummary) { - - this.clearSelection(); - if (content) { - let optionToSelect: Option = this.getOptionByValue(content.getContentId().toString()); - if (!optionToSelect) { - optionToSelect = this.createOption(content); - this.addOption(optionToSelect); - } - this.selectOption(optionToSelect); - - } - } - - protected toggleGridOptions(_treeMode: boolean) { - // May be overridden in deriving class if the grid should - // have different settings in different modes - } - - private initTreeModeToggler() { - - this.treeModeToggler = new ModeTogglerButton(); - this.treeModeToggler.setActive(this.treegridDropdownEnabled); - this.getComboBox().prependChild(this.treeModeToggler); - - this.treeModeToggler.onActiveChanged(isActive => { - this.treegridDropdownEnabled = isActive; - this.toggleGridOptions(isActive); - if (!this.preventReload) { - this.reload(this.getComboBox().getInput().getValue()); - } - }); - - this.onLoaded(() => { - if (this.showAfterReload) { - this.getComboBox().getInput().setEnabled(true); - this.showAfterReload = false; - } - }); - - this.treeModeToggler.onClicked(() => { - this.giveFocus(); - this.showAfterReload = true; - - this.getComboBox().showDropdown(); - this.getComboBox().setEmptyDropdownText(i18n('field.search.inprogress')); - }); - - this.getComboBox().getInput().onValueChanged((event: ValueChangedEvent) => { - - if (this.initialTreeEnabledState && StringHelper.isEmpty(event.getNewValue())) { - if (!this.treeModeToggler.isActive()) { - this.preventReload = true; - this.treeModeToggler.setActive(true); - this.preventReload = false; - } - return; - } - - if (this.treeModeToggler.isActive()) { - this.preventReload = true; - this.treeModeToggler.setActive(false); - this.preventReload = false; - } - - }); - } - - protected createOptions(items: ITEM_TYPE[]): Q.Promise[]> { - return this.optionsFactory.createOptions(items); - } - - protected createOption(data: ContentSummary | ContentTreeSelectorItem, readOnly?: boolean): Option { - const item: ITEM_TYPE = ObjectHelper.iFrameSafeInstanceOf(data, ContentTreeSelectorItem) ? - data as ITEM_TYPE : - new ContentTreeSelectorItem(data as ContentSummary) as ITEM_TYPE; - - return this.optionsFactory.createOption(item, readOnly); - } - - protected reload(inputValue: string): Q.Promise { - - const deferred = Q.defer(); - - if (this.ifFlatLoadingMode(inputValue)) { - this.getLoader().search(inputValue).then(() => { - deferred.resolve(); - }).catch((reason) => { - DefaultErrorHandler.handle(reason); - }).done(); - } else { - this.getLoader().setTreeFilterValue(inputValue); - - this.getComboBox().getComboBoxDropdownGrid().reload().then(() => { - if (this.getComboBox().isDropdownShown()) { - this.getComboBox().showDropdown(); - this.getComboBox().getInput().setEnabled(true); - } - - this.notifyLoaded(this.getComboBox().getOptions().map(option => option.getDisplayValue())); - - deferred.resolve(); - }).catch((reason) => { - DefaultErrorHandler.handle(reason); - }).done(); - } - - return deferred.promise; - } - - private ifFlatLoadingMode(inputValue: string): boolean { - return !this.treegridDropdownEnabled || (!this.treeModeTogglerAllowed && !StringHelper.isEmpty(inputValue)); - } - - public static create(): ContentComboBoxBuilder { - return new ContentComboBoxBuilder(); - } -} export class ContentSelectedOptionsView extends BaseSelectedOptionsView { private project?: Project; + private contextContent: ContentSummary; + createSelectedOption(option: Option): SelectedOption { - const optionView = new ContentSelectedOptionView(option, this.project); + const optionView = new ContentSelectedOptionView(option, this.project); + + const selectedContentId = option.getDisplayValue().getId(); + const refersToItself: boolean = this.contextContent && this.contextContent.getId() === selectedContentId; + optionView.toggleClass('non-editable', !!refersToItself); + return new SelectedOption(optionView, this.count()); } @@ -333,9 +35,9 @@ export class ContentSelectedOptionsView return this; } - protected getEmptyDisplayValue(id: string): ContentTreeSelectorItem { - const content = new ContentSummary(new ContentSummaryBuilder().setId(id).setContentId(new ContentId(id))); - return new ContentTreeSelectorItem(content); + setContextContent(value: ContentSummary): this { + this.contextContent = value; + return this; } } @@ -410,7 +112,7 @@ export class ContentSelectedOptionView private updateMissingStatus(option: Option): void { this.isMissing = option.getDisplayValue() && !option.getDisplayValue().getPath(); this.setEditable(!this.isMissing); - this.toggleClass(ContentComboBox.NOT_FOUND_CLASS, this.isMissing); + this.toggleClass('content-not-found', this.isMissing); } doRender(): Q.Promise { @@ -428,122 +130,3 @@ export class ContentSelectedOptionView }); } } - -export class ContentComboBoxBuilder - extends RichComboBoxBuilder { - - comboBoxName: string = 'contentSelector'; - - selectedOptionsView: SelectedOptionsView = - new ContentSelectedOptionsView() as SelectedOptionsView; - - loader: ContentSummaryOptionDataLoader; - - optionDataHelper: OptionDataHelper = new ContentSummaryOptionDataHelper(); - - optionDisplayValueViewer: Viewer = new ContentTreeSelectorItemViewer(); - - maximumOccurrences: number = 0; - - delayedInputValueChangedHandling: number = 750; - - minWidth: number; - - value: string; - - displayMissingSelectedOptions: boolean; - - removeMissingSelectedOptions: boolean; - - treegridDropdownEnabled: boolean = false; - - treeModeTogglerAllowed: boolean = true; - - isRequestMissingOptions: boolean = true; - - project: Project; - - setTreegridDropdownEnabled(value: boolean): this { - this.treegridDropdownEnabled = value; - return this; - } - - setTreeModeTogglerAllowed(value: boolean): this { - this.treeModeTogglerAllowed = value; - return this; - } - - setMaximumOccurrences(maximumOccurrences: number): this { - super.setMaximumOccurrences(maximumOccurrences); - return this; - } - - setComboBoxName(value: string): this { - super.setComboBoxName(value); - return this; - } - - setSelectedOptionsView(selectedOptionsView: SelectedOptionsView): this { - super.setSelectedOptionsView(selectedOptionsView); - return this; - } - - setLoader(loader: OptionDataLoader): this { - super.setLoader(loader); - return this; - } - - setMinWidth(value: number): this { - super.setMinWidth(value); - return this; - } - - setValue(value: string): this { - super.setValue(value); - return this; - } - - setDelayedInputValueChangedHandling(value: number): this { - super.setDelayedInputValueChangedHandling(value ? value : 750); - return this; - } - - setDisplayMissingSelectedOptions(value: boolean): this { - super.setDisplayMissingSelectedOptions(value); - return this; - } - - setRemoveMissingSelectedOptions(value: boolean): this { - super.setRemoveMissingSelectedOptions(value); - return this; - } - - setSkipAutoDropShowOnValueChange(value: boolean): this { - super.setSkipAutoDropShowOnValueChange(value); - return this; - } - - setOptionDisplayValueViewer(value: Viewer): this { - super.setOptionDisplayValueViewer(value || new ContentTreeSelectorItemViewer()); - return this; - } - - setOptionDataHelper(value: OptionDataHelper): this { - super.setOptionDataHelper(value); - return this; - } - - setHideComboBoxWhenMaxReached(value: boolean): this { - super.setHideComboBoxWhenMaxReached(value); - return this; - } - - setProject(value: Project): this { - this.project = value; - return this; - } - - build(): ContentComboBox { - return new ContentComboBox(this); - } -} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentInputTypeManagingAdd.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentInputTypeManagingAdd.ts index 47ae1f379c..4b904f105f 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentInputTypeManagingAdd.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentInputTypeManagingAdd.ts @@ -1,5 +1,4 @@ import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; -import {RichComboBox} from '@enonic/lib-admin-ui/ui/selector/combobox/RichComboBox'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; import {SelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionsView'; import {ContentInputTypeViewContext} from '../../ContentInputTypeViewContext'; @@ -10,7 +9,7 @@ import {ApplicationKey} from '@enonic/lib-admin-ui/application/ApplicationKey'; import {ApplicationBasedName} from '@enonic/lib-admin-ui/application/ApplicationBasedName'; import {FormItem} from '@enonic/lib-admin-ui/form/FormItem'; -export class ContentInputTypeManagingAdd +export abstract class ContentInputTypeManagingAdd extends BaseInputTypeManagingAdd { protected context: ContentInputTypeViewContext; @@ -25,10 +24,6 @@ export class ContentInputTypeManagingAdd super(context, className); } - protected getContentComboBox(): RichComboBox { - throw new Error('Should be overridden by inheritor'); - } - protected getContentPath(_raw: RAW_VALUE_TYPE): ContentPath { throw new Error('Should be overridden by inheritor'); } @@ -37,9 +32,7 @@ export class ContentInputTypeManagingAdd return this.getSelectedOptionsView().getSelectedOptions(); } - protected getSelectedOptionsView(): SelectedOptionsView { - return this.getContentComboBox().getSelectedOptionView(); - } + protected abstract getSelectedOptionsView(): SelectedOptionsView; private prependApplicationName(applicationKey: ApplicationKey, name: string): string { if (!applicationKey) { diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentSummaryOptionDataLoader.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentSummaryOptionDataLoader.ts index 6670f07b62..3f06c8a548 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentSummaryOptionDataLoader.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/ContentSummaryOptionDataLoader.ts @@ -1,7 +1,6 @@ import {ApplicationKey} from '@enonic/lib-admin-ui/application/ApplicationKey'; import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; import {OptionDataLoader, OptionDataLoaderData} from '@enonic/lib-admin-ui/ui/selector/OptionDataLoader'; -import {TreeNode} from '@enonic/lib-admin-ui/ui/treegrid/TreeNode'; import * as Q from 'q'; import {ContentId} from '../../../content/ContentId'; import {ContentSummary} from '../../../content/ContentSummary'; @@ -52,7 +51,8 @@ export class ContentSummaryOptionDataLoader().setRequestProject(this.project) : new ListByIdSelectorRequest().setRequestProject(this.project); @@ -86,11 +86,6 @@ export class ContentSummaryOptionDataLoader>): Q.Promise { - this.treeRequest.setContent(node.getDataId() ? node.getData().getDisplayValue().getContent() : null); - return this.loadItems().then(items => items[0]); - } - protected sendPreLoadRequest(ids: string): Q.Promise { const contentIds = ids.split(';').map((id) => new ContentId(id)); return new GetContentSummaryByIds(contentIds).setRequestProject(this.project).sendAndParse().then(((contents: ContentSummary[]) => { @@ -136,32 +131,27 @@ export class ContentSummaryOptionDataLoader>, from: number = 0, + fetchChildren(data: Option, from: number = 0, size: number = -1): Q.Promise> { const postLoad: boolean = from > 0; - if (parentNode.getRoot().getId() === parentNode.getId()) { - this.notifyLoadingData(postLoad); - } - this.isTreeLoadMode = true; this.treeRequest.setFrom(from); this.treeRequest.setSize(size); - - this.treeRequest.setChildOrder(parentNode.getDataId() ? parentNode.getData().getDisplayValue().getContent().getChildOrder() : null); + this.treeRequest.setChildOrder(data?.getId() ? data.getDisplayValue().getContent().getChildOrder() : null); if (this.smartTreeMode) { (this.treeRequest as ContentTreeSelectorQueryRequest).setParentPath( - parentNode.getDataId() ? parentNode.getData().getDisplayValue().getContent().getPath() : null); + data?.getId() ? data.getDisplayValue().getContent().getPath() : null); } else { - this.treeRequest.setContent(parentNode.getDataId() ? parentNode.getData().getDisplayValue().getContent() : null); + this.treeRequest.setContent(data?.getDisplayValue().getContent() || null); } this.treeRequest.setSearchString(this.treeFilterValue); - const hasFakeRoot = this.fakeRoot && parentNode.getDataId() == null && !this.treeFilterValue; + const needsFakeRoot = this.fakeRoot && data?.getId() == null && !this.treeFilterValue && from === 0; return this.loadItems().then((result: DATA[]) => { result = result.filter(this.postFilterFn); @@ -169,13 +159,18 @@ export class ContentSummaryOptionDataLoader { return null; } @@ -257,6 +252,14 @@ export class ContentSummaryOptionDataLoader boolean = () => true; - setContentTypeNames(contentTypeNames: string[]): ContentSummaryOptionDataLoaderBuilder { + setContentTypeNames(contentTypeNames: string[]): this { this.contentTypeNames = contentTypeNames; return this; } - setAllowedContentPaths(allowedContentPaths: string[]): ContentSummaryOptionDataLoaderBuilder { + setAllowedContentPaths(allowedContentPaths: string[]): this { this.allowedContentPaths = allowedContentPaths; return this; } - setRelationshipType(relationshipType: string): ContentSummaryOptionDataLoaderBuilder { + setRelationshipType(relationshipType: string): this { this.relationshipType = relationshipType; return this; } - setContent(content: ContentSummary): ContentSummaryOptionDataLoaderBuilder { + setContent(content: ContentSummary): this { this.content = content; return this; } - setSmartTreeMode(smartTreeMode: boolean): ContentSummaryOptionDataLoaderBuilder { + setSmartTreeMode(smartTreeMode: boolean): this { this.smartTreeMode = smartTreeMode; return this; } - setFakeRoot(fakeRoot: ContentSummary): ContentSummaryOptionDataLoaderBuilder { + setFakeRoot(fakeRoot: ContentSummary): this { this.fakeRoot = fakeRoot; return this; } - setProject(project: Project): ContentSummaryOptionDataLoaderBuilder { + setProject(project: Project): this { this.project = project; return this; } - setApplicationKey(key: ApplicationKey): ContentSummaryOptionDataLoaderBuilder { + setApplicationKey(key: ApplicationKey): this { this.applicationKey = key; return this; } - setPostFilterFn(postFilterFn: (contentItem: ContentSummary | ContentTreeSelectorItem) => boolean): ContentSummaryOptionDataLoaderBuilder { + setPostFilterFn(postFilterFn: (contentItem: ContentSummary | ContentTreeSelectorItem) => boolean): this { this.postFilterFn = postFilterFn; return this; } + setAppendLoadResults(appendLoadResults: boolean): this { + this.appendLoadResults = appendLoadResults; + return this; + } + build(): ContentSummaryOptionDataLoader { return new ContentSummaryOptionDataLoader(this); } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/RowSelector.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/RowSelector.ts deleted file mode 100644 index 34de6e723f..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/RowSelector.ts +++ /dev/null @@ -1,209 +0,0 @@ -import {ComboBox} from '@enonic/lib-admin-ui/ui/selector/combobox/ComboBox'; -import {SelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionsView'; -import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; -import {BaseSelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/BaseSelectedOptionsView'; -import {i18n} from '@enonic/lib-admin-ui/util/Messages'; -import {Element} from '@enonic/lib-admin-ui/dom/Element'; -import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; -import {SpanEl} from '@enonic/lib-admin-ui/dom/SpanEl'; -import {DefaultOptionDisplayValueViewer} from '@enonic/lib-admin-ui/ui/selector/DefaultOptionDisplayValueViewer'; -import {SelectedOptionEvent} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionEvent'; -import {OptionFilterInputValueChangedEvent} from '@enonic/lib-admin-ui/ui/selector/OptionFilterInputValueChangedEvent'; -import {Body} from '@enonic/lib-admin-ui/dom/Body'; - -export class RowSelector - extends DivEl { - - private title: SpanEl; - - private comboBox: ComboBox; - - private selectedOptionsView: SelectedOptionsView; - - constructor(title?: string) { - super('row-selector'); - - this.initElements(title); - } - - protected initElements(title?: string) { - this.initTitle(title); - this.initCombobox(); - this.initListeners(); - } - - private initTitle(title?: string) { - this.title = new SpanEl('title'); - this.title.setHtml(title == null ? i18n('field.rowselector.title') : title); - } - - private initCombobox() { - this.selectedOptionsView = new RowSelectedOptionsView(); - this.selectedOptionsView.setEditable(false); - - this.comboBox = new ComboBox('rowSelector', { - filter: RowSelector.comboBoxFilter, - selectedOptionsView: this.selectedOptionsView, - optionDisplayValueViewer: new RowOptionDisplayValueViewer(), - hideComboBoxWhenMaxReached: false, - maximumOccurrences: 1 - }); - } - - private static comboBoxFilter(item: Option, args: { searchString: string }) { - // Do not change to one-liner `return !(...);`. Bugs expected with UglifyJs + SlickGrid filter compilation. - const isEmptyInput = args == null || args.searchString == null; - return isEmptyInput || item.getDisplayValue().toUpperCase().indexOf(args.searchString.toUpperCase()) !== -1; - } - - private initListeners() { - this.comboBox.onOptionSelected((event: SelectedOptionEvent) => { - this.comboBox.hide(); - const selectedOption = event.getSelectedOption().getOptionView().getOption(); - selectedOption.setReadOnly(true); - this.comboBox.getSelectedOptions().forEach(option => { - if (option.getValue() !== selectedOption.getValue()) { - option.setReadOnly(false); - this.deselect(option); - } - }); - }); - - this.onClicked((event: MouseEvent) => { - const target = event.target as HTMLElement; - const {classList} = target; - if (classList.contains('selected-option') || classList.contains('option-value')) { - event.stopPropagation(); - this.comboBox.show(); - this.comboBox.showDropdown(); - this.comboBox.giveInputFocus(); - } - }); - - this.comboBox.onOptionFilterInputValueChanged((event: OptionFilterInputValueChangedEvent) => { - this.comboBox.setFilterArgs({searchString: event.getNewValue()}); - }); - - this.handleClickOutside(); - } - - private handleClickOutside() { - const mouseClickListener: (event: MouseEvent) => void = (event: MouseEvent) => { - if (this.comboBox.isVisible()) { - for (let target: ParentNode = event.target as HTMLElement; target; target = target.parentNode) { - if (target === this.comboBox.getHTMLElement()) { - return; - } - } - this.comboBox.hide(); - } - }; - - this.comboBox.onRemoved(() => { - Body.get().unMouseDown(mouseClickListener); - }); - - this.comboBox.onAdded(() => { - Body.get().onMouseDown(mouseClickListener); - }); - } - - setOptions(options: Option[], saveSelection?: boolean) { - this.comboBox.setOptions(options, saveSelection); - } - - static createOptions(options: string[]): Option[] { - return options.map((displayValue: string, index: number) => { - return Option.create() - .setValue(index.toString()) - .setDisplayValue(displayValue) - .setIndices([displayValue]) - .setSelectable(true) - .build(); - }); - } - - clearSelection() { - this.comboBox.clearSelection(); - } - - select(option: Option) { - this.comboBox.selectOption(option); - } - - deselect(option: Option) { - this.comboBox.deselectOption(option); - } - - setSelection(option: Option, select: boolean = true) { - if (select) { - this.select(option); - } else { - this.deselect(option); - } - } - - isOptionSelected(option: Option): boolean { - return this.comboBox.isOptionSelected(option); - } - - isSelectionEmpty(): boolean { - return this.comboBox.countSelectedOptions() === 0; - } - - updateOptionValue(option: Option, value: string, selectable?: boolean): Option { - const newOption = Option.create() - .setValue(option.getValue()) - .setDisplayValue(value) - .setIndices([value]) - .setSelectable(selectable != null ? selectable : option.isSelectable()) - .setReadOnly(option.isReadOnly()) - .build(); - - - this.comboBox.updateOption(option, newOption); - - return newOption; - } - - onOptionSelected(listener: (event: SelectedOptionEvent) => void) { - this.comboBox.onOptionSelected(listener); - } - - onOptionDeselected(listener: (event: SelectedOptionEvent) => void) { - this.comboBox.onOptionDeselected(listener); - } - - doRender(): Q.Promise { - return super.doRender().then((rendered: boolean) => { - this.appendChildren(this.title, this.comboBox, this.selectedOptionsView); - - return rendered; - }); - } -} - -class RowSelectedOptionsView - extends BaseSelectedOptionsView { - - constructor() { - super('row-selected-options-view'); - } - - updateOption(optionToUpdate: Option, newOption: Option) { - super.updateOption(optionToUpdate, newOption); - } - - maximumOccurrencesReached(): boolean { - return false; - } -} - -class RowOptionDisplayValueViewer - extends DefaultOptionDisplayValueViewer { - setObject(displayName: string) { - const withoutNumber = !(/\(\d+\)$/.test(displayName)); - this.toggleClass('without-number', withoutNumber); - return super.setObject(displayName); - } -} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageContentComboBox.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageContentComboBox.ts index fa595a6a59..e69de29bb2 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageContentComboBox.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageContentComboBox.ts @@ -1,174 +0,0 @@ -import {ObjectHelper} from '@enonic/lib-admin-ui/ObjectHelper'; -import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; -import {SelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionsView'; -import {ContentTypeName} from '@enonic/lib-admin-ui/schema/content/ContentTypeName'; -import {OptionDataHelper} from '@enonic/lib-admin-ui/ui/selector/OptionDataHelper'; -import {ComboBox} from '@enonic/lib-admin-ui/ui/selector/combobox/ComboBox'; -import {ContentComboBox, ContentComboBoxBuilder} from '../ContentComboBox'; -import {ImageOptionDataLoader} from './ImageOptionDataLoader'; -import {ImageContentComboboxKeyEventsHandler} from './ImageContentComboboxKeyEventsHandler'; -import {ImageSelectorSelectedOptionsView} from './ImageSelectorSelectedOptionsView'; -import {ImageSelectorViewer} from './ImageSelectorViewer'; -import {MediaTreeSelectorItem} from '../media/MediaTreeSelectorItem'; -import {ContentSummaryOptionDataLoaderBuilder} from '../ContentSummaryOptionDataLoader'; -import {ResponsiveManager} from '@enonic/lib-admin-ui/ui/responsive/ResponsiveManager'; -import {ResponsiveItem} from '@enonic/lib-admin-ui/ui/responsive/ResponsiveItem'; -import {ResponsiveRanges} from '@enonic/lib-admin-ui/ui/responsive/ResponsiveRanges'; -import {GridOptions} from '@enonic/lib-admin-ui/ui/grid/GridOptions'; -import {Grid} from '@enonic/lib-admin-ui/ui/grid/Grid'; -import {ContentSummary} from '../../../../content/ContentSummary'; -import {ContentId} from '../../../../content/ContentId'; -import {ContentTreeSelectorItem} from '../../../../item/ContentTreeSelectorItem'; - -export class ImageContentComboBox - extends ContentComboBox { - - private item: ResponsiveItem; - - constructor(builder: ImageContentComboBoxBuilder) { - super(builder); - - this.addClass('image-content-combo-box'); - this.initAvailableSizeChangeListener(); - this.toggleGridOptions(builder.treegridDropdownEnabled); - this.setKeyEventsHandler(new ImageContentComboboxKeyEventsHandler(this)); - } - - private initAvailableSizeChangeListener() { - this.item = ResponsiveManager.onAvailableSizeChanged(this, (item: ResponsiveItem) => this.updateGalleryModeColumnsNumber()); - } - - private updateGalleryModeColumnsNumber() { - const options = this.getComboBox().getComboBoxDropdownGrid().getGrid().getOptions(); - - if (options.enableGalleryMode) { - const columnsFitInRow: number = this.getGalleryModeColumnsNumber(); - - if (options.galleryModeColumns !== columnsFitInRow) { - this.doToggleGridOptions(false, columnsFitInRow); - } - } - } - - protected prepareBuilder(builder: ContentComboBoxBuilder) { - super.prepareBuilder(builder); - builder.setMaxHeight(620); - - } - - protected createLoader(builder: ImageContentComboBoxBuilder): ImageOptionDataLoader { - return ImageOptionDataLoader.build(this.createLoaderBuilder(builder)); - } - - protected createLoaderBuilder(builder: ImageContentComboBoxBuilder): ContentSummaryOptionDataLoaderBuilder { - return super.createLoaderBuilder(builder) - .setContent(builder.content) - .setProject(builder.project) - .setContentTypeNames([ContentTypeName.IMAGE.toString(), ContentTypeName.MEDIA_VECTOR.toString()]); - } - - getContent(contentId: ContentId): ContentSummary { - let option = this.getOptionByValue(contentId.toString()); - if (option) { - return (option.getDisplayValue() as MediaTreeSelectorItem).getContentSummary(); - } - return null; - } - - getComboBox(): ComboBox { - return super.getComboBox(); - } - - protected toggleGridOptions(treeMode: boolean) { - const columnsFitInRow: number = treeMode ? 3 : this.getGalleryModeColumnsNumber(); - - this.doToggleGridOptions(treeMode, columnsFitInRow); - } - - private getGalleryModeColumnsNumber(): number { - if (this.item.isInRangeOrSmaller(ResponsiveRanges._240_360)) { - return 1; - } - - if (this.item.isInRangeOrSmaller(ResponsiveRanges._360_540)) { - return 2; - } - - return 3; - } - - private doToggleGridOptions(treeMode: boolean, columns: number) { - const grid = this.getComboBox().getComboBoxDropdownGrid().getGrid(); - grid.toggleClass('tree-mode', treeMode); - - grid.getOptions().setRowHeight(treeMode ? 40 : 198) - .setEnableGalleryMode(!treeMode) - .setGalleryModeColumns(columns); - - grid.invalidate(); - } - - protected createOption(data: object, readOnly?: boolean): Option { - const item: MediaTreeSelectorItem = this.dataToMediaTreeSelectorItem(data); - if (item) { - return this.optionsFactory.createOption(item, readOnly); - } - - return null; - } - - private dataToMediaTreeSelectorItem(data: object): MediaTreeSelectorItem { - if (ObjectHelper.iFrameSafeInstanceOf(data, MediaTreeSelectorItem)) { - return data as MediaTreeSelectorItem; - } - - if (ObjectHelper.iFrameSafeInstanceOf(data, ContentSummary)) { - return new MediaTreeSelectorItem(data as ContentSummary); - } - - return null; - } - - getLoader(): ImageOptionDataLoader { - return super.getLoader() as ImageOptionDataLoader; - } - - load() { - this.reload(this.getComboBox().getInput().getValue()); - } - - clear() { - super.clear(); - } - - public static create(): ImageContentComboBoxBuilder { - return new ImageContentComboBoxBuilder(); - } -} - -export class ImageContentComboBoxBuilder - extends ContentComboBoxBuilder { - - comboBoxName: string = 'imageContentSelector'; - - selectedOptionsView: SelectedOptionsView = - new ImageSelectorSelectedOptionsView() as SelectedOptionsView; - - optionDisplayValueViewer: ImageSelectorViewer = new ImageSelectorViewer(); - - loader: ImageOptionDataLoader; - - content: ContentSummary; - - isRequestMissingOptions: boolean = false; - - setContent(value: ContentSummary): ImageContentComboBoxBuilder { - this.content = value; - return this; - } - - build(): ImageContentComboBox { - return new ImageContentComboBox(this); - } - -} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageContentComboboxKeyEventsHandler.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageContentComboboxKeyEventsHandler.ts deleted file mode 100644 index 75e89d5c48..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageContentComboboxKeyEventsHandler.ts +++ /dev/null @@ -1,116 +0,0 @@ -import {KeyEventsHandler} from '@enonic/lib-admin-ui/event/KeyEventsHandler'; -import {DropdownGrid} from '@enonic/lib-admin-ui/ui/selector/DropdownGrid'; -import {ComboBoxOptionFilterInput} from '@enonic/lib-admin-ui/ui/selector/combobox/ComboBoxOptionFilterInput'; -import {ImageContentComboBox} from './ImageContentComboBox'; -import {MediaTreeSelectorItem} from '../media/MediaTreeSelectorItem'; - -export class ImageContentComboboxKeyEventsHandler - extends KeyEventsHandler { - - private static CELLS_IN_ROW: number = 3; - private input: ComboBoxOptionFilterInput; - private grid: DropdownGrid; - private lastSelectedCol: number = 0; - public static debug: boolean = false; - - constructor(comboBox: ImageContentComboBox) { - super(); - this.input = comboBox.getComboBox().getInput(); - this.grid = comboBox.getComboBox().getComboBoxDropdownGrid(); - - this.onLeft(this.handleLeft.bind(this)); - this.onUp(this.handleUp.bind(this)); - this.onRight(this.handleRight.bind(this)); - this.onDown(this.handleDown.bind(this)); - } - - private handleLeft(e: KeyboardEvent): boolean { - const activeRow = this.grid.getActiveRow(); - if (ImageContentComboboxKeyEventsHandler.debug) { - console.debug('ImageContentComboboxKeyEventsHandler.handleLeft: active row = ' + activeRow); - } - if (activeRow >= 0) { - e.stopPropagation(); - e.preventDefault(); - this.adjustCell(activeRow, -1); - return true; - } - } - - private handleUp(e: KeyboardEvent): boolean { - const activeRow = this.grid.getActiveRow(); - if (ImageContentComboboxKeyEventsHandler.debug) { - console.debug('ImageContentComboboxKeyEventsHandler.handleUp: active row = ' + activeRow); - } - if (activeRow >= 0) { - e.stopPropagation(); - e.preventDefault(); - if (this.isFirstRow(activeRow)) { - this.lastSelectedCol = activeRow; - this.grid.resetActiveSelection(); - this.input.giveFocus(); - } else { - this.adjustRow(activeRow, -1); - } - return true; - } - - if (this.grid.getGrid().hasClass('reverted')) { - this.handleDown(e); - return true; - } - } - - private handleRight(e: KeyboardEvent): boolean { - const activeRow = this.grid.getActiveRow(); - if (ImageContentComboboxKeyEventsHandler.debug) { - console.debug('ImageContentComboboxKeyEventsHandler.handleRight: active row = ' + activeRow); - } - if (activeRow >= 0) { - e.stopPropagation(); - e.preventDefault(); - this.adjustCell(activeRow, 1); - return true; - } - } - - private handleDown(e: KeyboardEvent): boolean { - if (!this.grid.isVisible()) { - // use default down handler if grid is not visible to show dropdown - return false; - } - e.stopPropagation(); - e.preventDefault(); - const activeRow = this.grid.getActiveRow(); - if (ImageContentComboboxKeyEventsHandler.debug) { - console.debug('ImageContentComboboxKeyEventsHandler.handleDown: active row = ' + activeRow); - } - if (activeRow >= 0) { - this.adjustRow(activeRow, 1); - } else { - this.grid.navigateToRow(this.lastSelectedCol); - } - return true; - } - - private isFirstRow(activeRow: number): boolean { - return activeRow / ImageContentComboboxKeyEventsHandler.CELLS_IN_ROW < 1; - } - - private adjustRow(activeRow: number, increment: number) { - const desiredRow = activeRow + increment * ImageContentComboboxKeyEventsHandler.CELLS_IN_ROW; - return this.grid.navigateToRow(Math.min(desiredRow, this.grid.getOptionCount() - 1)); - } - - private adjustCell(activeRow: number, increment: number) { - const cells = ImageContentComboboxKeyEventsHandler.CELLS_IN_ROW; - const cols = activeRow % cells; - const fullRows = activeRow - cols; - const rowLength = Math.min(this.grid.getOptionCount() - fullRows, cells); // check if row is not complete - let delta = (cols + increment) % rowLength; - if (delta < 0) { - delta += rowLength; - } - return this.grid.navigateToRow(fullRows + delta); - } -} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageOptionDataLoader.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageOptionDataLoader.ts index fcf4c2a039..a4f1fd9554 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageOptionDataLoader.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageOptionDataLoader.ts @@ -1,6 +1,5 @@ import * as Q from 'q'; import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; -import {TreeNode} from '@enonic/lib-admin-ui/ui/treegrid/TreeNode'; import {ImageContentLoader} from './ImageContentLoader'; import {MediaTreeSelectorItem} from '../media/MediaTreeSelectorItem'; import {ContentSummaryOptionDataLoader, ContentSummaryOptionDataLoaderBuilder} from '../ContentSummaryOptionDataLoader'; @@ -9,21 +8,20 @@ import {OptionDataLoaderData} from '@enonic/lib-admin-ui/ui/selector/OptionDataL import {ContentAndStatusTreeSelectorItem} from '../../../../item/ContentAndStatusTreeSelectorItem'; import {ContentSummary} from '../../../../content/ContentSummary'; import {ContentId} from '../../../../content/ContentId'; +import {ContentTypeName} from '@enonic/lib-admin-ui/schema/content/ContentTypeName'; export class ImageOptionDataLoader extends ContentSummaryOptionDataLoader { private preloadedDataListeners: ((data: MediaTreeSelectorItem[]) => void)[] = []; - fetch(node: TreeNode>): Q.Promise { - return super.fetch(node).then((data) => { - return this.wrapItem(data); - }); + constructor(builder: ImageOptionDataLoaderBuilder = new ImageOptionDataLoaderBuilder()) { + super(builder); } - fetchChildren(parentNode: TreeNode>, from: number = 0, + fetchChildren(option: Option, from: number = 0, size: number = -1): Q.Promise> { - return super.fetchChildren(parentNode, from, size).then((data: OptionDataLoaderData) => { + return super.fetchChildren(option, from, size).then((data: OptionDataLoaderData) => { return this.createOptionData(data.getData(), data.getHits(), data.getTotalHits()); } ); @@ -95,7 +93,16 @@ export class ImageOptionDataLoader return MediaTreeSelectorItem.createMediaTreeSelectorItemWithStatus(item as ContentAndStatusTreeSelectorItem); } - static build(builder: ContentSummaryOptionDataLoaderBuilder): ImageOptionDataLoader { + static build(builder: ImageOptionDataLoaderBuilder): ImageOptionDataLoader { return new ImageOptionDataLoader(builder); } } + +export class ImageOptionDataLoaderBuilder extends ContentSummaryOptionDataLoaderBuilder { + + contentTypeNames: string[] = [ContentTypeName.IMAGE.toString(), ContentTypeName.MEDIA_VECTOR.toString()]; + + build(): ImageOptionDataLoader { + return new ImageOptionDataLoader(this); + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorSelectedOptionView.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorSelectedOptionView.ts index 3d460d865c..e6231e3385 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorSelectedOptionView.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorSelectedOptionView.ts @@ -135,13 +135,13 @@ export class ImageSelectorSelectedOptionView private showSpinner() { this.progress.hide(); this.check.hide(); - this.icon.getEl().setVisibility('hidden'); + this.icon.setClass('visibility-hidden'); this.loadMask.show(); } private showResult() { this.loadMask.hide(); - this.icon.getEl().setVisibility('visible'); + this.icon.setClass('visibility-visible'); this.check.show(); this.progress.hide(); this.error.hide(); diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorSelectedOptionsView.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorSelectedOptionsView.ts index 1dfb3d192a..5d927d1e3f 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorSelectedOptionsView.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorSelectedOptionsView.ts @@ -31,7 +31,7 @@ export class ImageSelectorSelectedOptionsView readonly stickyToolbarCls: string = 'image-selector-toolbar-sticky'; constructor() { - super(); + super('image-selector-selected-options-view'); this.setOccurrencesSortable(true); @@ -326,7 +326,7 @@ export class ImageSelectorSelectedOptionsView private handleOptionViewImageLoaded(optionView: ImageSelectorSelectedOptionView) { let loadedListener = () => { - optionView.updateProportions(); + // optionView.updateProportions(); this.refreshSortable(); }; diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorViewer.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorViewer.ts index a17d78225d..1c6b13f0e0 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorViewer.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/selector/image/ImageSelectorViewer.ts @@ -29,6 +29,12 @@ export class ImageSelectorViewer return object.getPath() ? object.getPath().toString() : ''; } + doLayout(object: MediaTreeSelectorItem): void { + super.doLayout(object); + + this.namesAndIconView?.getIconImageEl()?.getEl().setAttribute('draggable', 'false'); + } + protected getHintTargetEl(): ElementHelper { return this.getEl(); } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/LinkModalDialog.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/LinkModalDialog.ts index 380799bfba..7068dfc627 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/LinkModalDialog.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/LinkModalDialog.ts @@ -9,9 +9,7 @@ import {Panel} from '@enonic/lib-admin-ui/ui/panel/Panel'; import {DockedPanel} from '@enonic/lib-admin-ui/ui/panel/DockedPanel'; import {Validators} from '@enonic/lib-admin-ui/ui/form/Validators'; import {TextInput} from '@enonic/lib-admin-ui/ui/text/TextInput'; -import {Dropdown, DropdownConfig} from '@enonic/lib-admin-ui/ui/selector/dropdown/Dropdown'; import {UploadItem} from '@enonic/lib-admin-ui/ui/uploader/UploadItem'; -import {BaseSelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/BaseSelectedOptionsView'; import {UploadStartedEvent} from '@enonic/lib-admin-ui/ui/uploader/UploadStartedEvent'; import {UploadedEvent} from '@enonic/lib-admin-ui/ui/uploader/UploadedEvent'; import {UploadFailedEvent} from '@enonic/lib-admin-ui/ui/uploader/UploadFailedEvent'; @@ -19,12 +17,9 @@ import {OverrideNativeDialog} from './OverrideNativeDialog'; import {HtmlAreaModalDialogConfig, ModalDialogFormItemBuilder} from './ModalDialog'; import {MediaTreeSelectorItem} from '../../selector/media/MediaTreeSelectorItem'; import {MediaSelectorDisplayValue} from '../../selector/media/MediaSelectorDisplayValue'; -import {ContentComboBox} from '../../selector/ContentComboBox'; +import {ContentSelectedOptionsView} from '../../selector/ContentComboBox'; import {MediaUploaderEl, MediaUploaderElOperation} from '../../upload/MediaUploaderEl'; -import { - ContentSummaryOptionDataLoader, - ContentSummaryOptionDataLoaderBuilder -} from '../../selector/ContentSummaryOptionDataLoader'; +import {ContentSummaryOptionDataLoader, ContentSummaryOptionDataLoaderBuilder} from '../../selector/ContentSummaryOptionDataLoader'; import {ContentTreeSelectorItem} from '../../../../item/ContentTreeSelectorItem'; import {Content} from '../../../../content/Content'; import {Site} from '../../../../content/Site'; @@ -45,9 +40,13 @@ import {Button} from '@enonic/lib-admin-ui/ui/button/Button'; import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; import {ValidationResult} from '@enonic/lib-admin-ui/ui/form/ValidationResult'; import {SelectedOption} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOption'; -import eventInfo = CKEDITOR.eventInfo; import {Project} from '../../../../settings/data/project/Project'; import {ContentPath} from '../../../../content/ContentPath'; +import {ContentTreeSelectorDropdown, ContentTreeSelectorDropdownOptions} from '../../../selector/ContentTreeSelectorDropdown'; +import {ContentListBox} from '../../../selector/ContentListBox'; +import {Dropdown} from '@enonic/lib-admin-ui/ui/Dropdown'; +import eventInfo = CKEDITOR.eventInfo; +import {SelectedOptionEvent} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionEvent'; export interface LinkModalDialogConfig extends HtmlAreaModalDialogConfig { @@ -224,22 +223,22 @@ export class LinkModalDialog protected setDialogInputValues() { switch (this.getOriginalLinkTypeElem().getValue()) { - case 'email': - this.link = LinkModalDialog.emailPrefix + this.getOriginalEmailElem().getValue(); - break; - case 'anchor': - this.link = LinkModalDialog.anchorPrefix + this.getOriginalAnchorElem().getValue(); - break; - case 'tel': - this.link = LinkModalDialog.telPrefix + this.getOriginalTelElem().getValue(); - break; - default: { - const val = this.getOriginalUrlElem().getValue(); - const protocol: string = this.getOriginalProtocolElem().getValue(); - this.link = StringHelper.isEmpty(val) ? - StringHelper.EMPTY_STRING : - protocol + this.getOriginalUrlElem().getValue(); - } + case 'email': + this.link = LinkModalDialog.emailPrefix + this.getOriginalEmailElem().getValue(); + break; + case 'anchor': + this.link = LinkModalDialog.anchorPrefix + this.getOriginalAnchorElem().getValue(); + break; + case 'tel': + this.link = LinkModalDialog.telPrefix + this.getOriginalTelElem().getValue(); + break; + default: { + const val = this.getOriginalUrlElem().getValue(); + const protocol: string = this.getOriginalProtocolElem().getValue(); + this.link = StringHelper.isEmpty(val) ? + StringHelper.EMPTY_STRING : + protocol + this.getOriginalUrlElem().getValue(); + } } } @@ -272,34 +271,13 @@ export class LinkModalDialog } private createContentPanel(): Panel { - const getContentId: () => string = () => { - if (!this.link) { - return StringHelper.EMPTY_STRING; - } - - if (this.isInlineLink()) { - return this.link.replace(LinkModalDialog.mediaInlinePrefix, StringHelper.EMPTY_STRING); - } - - if (this.isDownloadLink()) { - return this.link.replace(LinkModalDialog.mediaDownloadPrefix, StringHelper.EMPTY_STRING); - } - - if (this.isContentLink()) { - const regex = new RegExp(/^(.*?)(\#|\?|$)/g); - const regexResult = regex.exec(this.link); - return (regexResult.length > 1 ? regexResult[1] : this.link) - .replace(LinkModalDialog.contentPrefix, StringHelper.EMPTY_STRING); - } - - return StringHelper.EMPTY_STRING; - }; - const contentSelectorBuilder = this.createContentSelectorBuilder(this.parentSitePath); - const contentSelector = this.createContentSelector(getContentId, contentSelectorBuilder); + const loader = contentSelectorBuilder.build(); + const contentSelector: ContentTreeSelectorDropdown = this.createContentSelector(loader); const showAllContentToggler = (showAllContent: boolean) => { contentSelectorBuilder.setAllowedContentPaths([showAllContent ? '' : this.parentSitePath]); - contentSelector.getLoader().initRequests(contentSelectorBuilder); + loader.initRequests(contentSelectorBuilder); + contentSelector.load(); }; const contentPanel = this.createFormPanel([ @@ -316,6 +294,39 @@ export class LinkModalDialog return contentPanel; } + private getContentId(): string { + if (!this.link) { + return StringHelper.EMPTY_STRING; + } + + if (this.isInlineLink()) { + return this.link.replace(LinkModalDialog.mediaInlinePrefix, StringHelper.EMPTY_STRING); + } + + if (this.isDownloadLink()) { + return this.link.replace(LinkModalDialog.mediaDownloadPrefix, StringHelper.EMPTY_STRING); + } + + if (this.isContentLink()) { + const regex = new RegExp(/^(.*?)(\#|\?|$)/g); + const regexResult = regex.exec(this.link); + return (regexResult.length > 1 ? regexResult[1] : this.link) + .replace(LinkModalDialog.contentPrefix, StringHelper.EMPTY_STRING); + } + + return StringHelper.EMPTY_STRING; + } + + private getSelectedItemsHandler(): string[] { + const selectedItem = this.getContentId(); + + if (selectedItem) { + return [selectedItem]; + } + + return []; + } + private createUrlPanel(): Panel { const urlFormItem = this.createUrlFormItem('url', i18n('dialog.link.formitem.url')); @@ -359,19 +370,21 @@ export class LinkModalDialog } private createAnchorPanel(anchorList: string[]): Panel { - return this.createFormPanel([ + const anchorPanel = this.createFormPanel([ this.createAnchorDropdown(anchorList) ]); + + anchorPanel.addClass('anchor-panel'); + + return anchorPanel; } private createAnchorDropdown(anchorList: string[]): FormItem { - const dropDown = new Dropdown('anchor', {} as DropdownConfig); + const dropDown = new Dropdown('anchor'); + dropDown.addClass('anchor-dropdown'); anchorList.forEach((anchor: string) => { - dropDown.addOption(Option.create() - .setValue(LinkModalDialog.anchorPrefix + anchor) - .setDisplayValue(anchor) - .build()); + dropDown.addOption(LinkModalDialog.anchorPrefix + anchor, anchor); }); if (this.getAnchor()) { @@ -675,8 +688,8 @@ export class LinkModalDialog const urlValue: string = textInput.getValue(); const usedProtocol: UrlProtocol = this.getUsedProtocolFromValue(urlValue); const newUrlValue: string = !usedProtocol.prefix - ? prefix + urlValue - : urlValue.replace(usedProtocol.prefix, prefix); + ? prefix + urlValue + : urlValue.replace(usedProtocol.prefix, prefix); formItem.setValidator(validator); textInput.setValue(newUrlValue); @@ -876,6 +889,29 @@ export class LinkModalDialog }; } + private createContentSelector(loader: ContentSummaryOptionDataLoader): ContentTreeSelectorDropdown { + const listBox = new ContentListBox({loader: loader}); + const dropdownOptions: ContentTreeSelectorDropdownOptions = { + loader: loader, + maxSelected: 1, + selectedOptionsView: new LinkContentSelectedOptionsView(), + className: 'single-occurrence', + getSelectedItems: this.getSelectedItemsHandler.bind(this), + treeMode: true, + }; + + return new ContentTreeSelectorDropdown(listBox, dropdownOptions); + } + + private createContentSelectorBuilder(parentSitePath: string): ContentSummaryOptionDataLoaderBuilder { + return ContentSummaryOptionDataLoader + .create() + .setProject(this.config.project) + .setAppendLoadResults(false) + .setPostFilterFn((contentItem) => this.filterContentByParentPath(contentItem)) + .setAllowedContentPaths([parentSitePath ? `${parentSitePath}` : '']); + } + private filterContentByParentPath(contentItem: ContentSummary | ContentTreeSelectorItem): boolean { if (!this.parentSitePath || (this.showAllContentCheckboxFormItem.getInput() as Checkbox).isChecked()) { return true; @@ -887,32 +923,11 @@ export class LinkModalDialog return contentPath === this.parentSitePath || contentPath.startsWith(`${this.parentSitePath}/`); } - private createContentSelector(getValueFn: () => string, - loaderBuilder: ContentSummaryOptionDataLoaderBuilder - ): ContentComboBox { - const selector = ContentComboBox.create() - .setTreegridDropdownEnabled(true) - .setMaximumOccurrences(1) - .setLoader(loaderBuilder.build()) - .build(); - - selector.setValue(getValueFn.call(this)); - - return selector; - } - - private createContentSelectorBuilder(parentSitePath: string): ContentSummaryOptionDataLoaderBuilder { - return ContentSummaryOptionDataLoader - .create() - .setProject(this.config.project) - .setPostFilterFn((contentItem) => this.filterContentByParentPath(contentItem)) - .setAllowedContentPaths([parentSitePath || '']); - } - - private createSelectorFormItem(id: string, label: string, contentSelector: ContentComboBox, + private createSelectorFormItem(id: string, label: string, contentSelector: ContentTreeSelectorDropdown, addValueValidation: boolean = false): FormItem { + const formInputEl = new ContentSelectorFormInputWrapper(contentSelector); const formItemBuilder: ModalDialogFormItemBuilder = - new ModalDialogFormItemBuilder(id, label).setValidator(Validators.required).setInputEl(contentSelector); + new ModalDialogFormItemBuilder(id, label).setValidator(Validators.required).setInputEl(formInputEl); const formItem: FormItem = this.createFormItem(formItemBuilder); const mediaUploader: MediaUploaderEl = this.createMediaUploader(contentSelector); @@ -922,11 +937,13 @@ export class LinkModalDialog return formItem; } - const callHandleSelectorValueChanged = () => - this.handleSelectorValueChanged(contentSelector.getSelectedContent(), formItem); + const callHandleSelectorValueChanged = () => { + const selected = contentSelector.getSelectedOptions()[0]?.getOption().getDisplayValue()?.getContent(); + this.handleSelectorValueChanged(selected, formItem); + }; - contentSelector.onValueLoaded(callHandleSelectorValueChanged); - contentSelector.onValueChanged(callHandleSelectorValueChanged); + contentSelector.onSelectionChanged(callHandleSelectorValueChanged); + contentSelector.getSelectedOptionsView().onOptionSelected(callHandleSelectorValueChanged); return formItem; } @@ -942,6 +959,7 @@ export class LinkModalDialog this.contentTargetCheckBoxFormItem.hide(); this.anchorFormItem.hide(); this.paramsFormItem.hide(); + this.link = null; return; } @@ -962,7 +980,7 @@ export class LinkModalDialog } } - private createMediaUploader(contentSelector: ContentComboBox): MediaUploaderEl { + private createMediaUploader(contentSelector: ContentTreeSelectorDropdown): MediaUploaderEl { const mediaUploader: MediaUploaderEl = new MediaUploaderEl({ params: { parent: this.contentId?.toString() || ContentPath.getRoot().toString() @@ -980,11 +998,7 @@ export class LinkModalDialog const value: MediaTreeSelectorItem = new MediaTreeSelectorItem(null).setDisplayValue( MediaSelectorDisplayValue.fromUploadItem(uploadItem)); - const option: Option = Option.create() - .setValue(value.getId()) - .setDisplayValue(value) - .build(); - contentSelector.selectOption(option); + contentSelector.select(value); }); }); @@ -999,9 +1013,16 @@ export class LinkModalDialog const item: UploadItem = event.getUploadItem(); const createdContent: Content = item.getModel(); - const selectedOption: SelectedOption = contentSelector.getSelectedOptionView().getById(item.getId()); + const selectedOption: SelectedOption = contentSelector.getSelectedOptionsView().getById(item.getId()); const option: Option = selectedOption.getOption(); - option.setDisplayValue(new MediaTreeSelectorItem(createdContent)); + const uploadedItem = new MediaTreeSelectorItem(createdContent); + + if (contentSelector.isSelected(item.getId())) { + contentSelector.deselect(selectedOption.getOption().getDisplayValue()); + contentSelector.select(uploadedItem); + } + + option.setDisplayValue(uploadedItem); option.setValue(createdContent.getContentId().toString()); selectedOption.getOptionView().setOption(option); @@ -1009,11 +1030,10 @@ export class LinkModalDialog mediaUploader.onUploadFailed((event: UploadFailedEvent) => { const item: UploadItem = event.getUploadItem(); - const selectedOption: SelectedOption = contentSelector.getSelectedOptionView().getById(item.getId()); + const selectedOption: SelectedOption = contentSelector.getSelectedOptionsView().getById(item.getId()); if (!!selectedOption) { - (contentSelector.getSelectedOptionView() as BaseSelectedOptionsView).removeOption( - selectedOption.getOption()); + contentSelector.getSelectedOptionsView().removeOption(selectedOption.getOption()); } }); @@ -1032,14 +1052,6 @@ export class LinkModalDialog mediaUploader.setDefaultDropzoneVisible(false); }); - contentSelector.getComboBox().onHidden(() => { - mediaUploader.hide(); - }); - - contentSelector.getComboBox().onShown(() => { - mediaUploader.show(); - }); - return mediaUploader; } @@ -1106,8 +1118,8 @@ export class LinkModalDialog return this.generateUrlParamsForNonMedia(); } - private getContentIdFormItemEl(): ContentComboBox { - return this.getFieldById('contentId') as ContentComboBox; + private getContentIdFormItemEl(): ContentSelectorFormInputWrapper { + return this.getFieldById('contentId') as ContentSelectorFormInputWrapper; } private generateUrlParamsForMedia(): ContentLinkParams { @@ -1194,18 +1206,18 @@ export class LinkModalDialog this.getOriginalTitleElem().setValue(toolTip, false); switch (selectedTab.getLabel()) { - case (this.tabNames.content): - this.createContentLink(); - break; - case (this.tabNames.url): - this.createUrlLink(); - break; - case (this.tabNames.email): - this.createEmailLink(); - break; - case (this.tabNames.anchor): - this.createAnchor(); - break; + case (this.tabNames.content): + this.createContentLink(); + break; + case (this.tabNames.url): + this.createUrlLink(); + break; + case (this.tabNames.email): + this.createEmailLink(); + break; + case (this.tabNames.anchor): + this.createAnchor(); + break; } } @@ -1219,7 +1231,7 @@ export class LinkModalDialog private getOriginalUrlElem(): CKEDITOR.ui.dialog.uiElement { return (this.getElemFromOriginalDialog('info', 'urlOptions') as CKEDITOR.ui.dialog.vbox) - .getChild([0, 1]) as unknown as CKEDITOR.ui.dialog.uiElement; + .getChild([0, 1]) as unknown as CKEDITOR.ui.dialog.uiElement; } private getOriginalEmailElem(): CKEDITOR.ui.dialog.uiElement { @@ -1270,7 +1282,38 @@ export class LinkModalDialog isDirty(): boolean { return (this.textFormItem.getInput() as TextInput).isDirty() || (this.toolTipFormItem.getInput() as TextInput).isDirty() || - AppHelper.isDirty(this.dockedPanel); + AppHelper.isDirty(this.dockedPanel); + } + +} + +class ContentSelectorFormInputWrapper + extends FormInputEl { + + private contentSelector: ContentTreeSelectorDropdown; + + constructor(contentSelector: ContentTreeSelectorDropdown) { + super('div', 'content-selector-wrapper'); + + this.contentSelector = contentSelector; + this.appendChild(contentSelector); } + + getValue(): string { + return this.contentSelector.getSelectedOptions()[0]?.getOption().getDisplayValue()?.getContent()?.getId() || ''; + } +} + +class LinkContentSelectedOptionsView extends ContentSelectedOptionsView { + + addOption(option: Option, silent: boolean, keyCode: number): boolean { + const result = super.addOption(option, silent, keyCode); + + if (silent) { // forcing notify event, preselected item was selected silently + this.notifyOptionSelected(new SelectedOptionEvent(this.getSelectedOptions()[0], keyCode)); + } + + return result; + } } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/ListStyleModalDialog.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/ListStyleModalDialog.ts index cebc6f589b..c35d137d3f 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/ListStyleModalDialog.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/ListStyleModalDialog.ts @@ -4,8 +4,7 @@ import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {Action} from '@enonic/lib-admin-ui/ui/Action'; import {OverrideNativeDialog} from './OverrideNativeDialog'; import {HtmlAreaModalDialogConfig, ModalDialogFormItemBuilder} from './ModalDialog'; -import {Dropdown, DropdownConfig} from '@enonic/lib-admin-ui/ui/selector/dropdown/Dropdown'; -import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; +import {Dropdown} from '@enonic/lib-admin-ui/ui/Dropdown'; import eventInfo = CKEDITOR.eventInfo; export abstract class ListStyleModalDialog @@ -68,15 +67,12 @@ export abstract class ListStyleModalDialog return [this.typeField]; } - private initTypeDropdown(): Dropdown { - const typeDropdown: Dropdown = new Dropdown('type', {} as DropdownConfig); + private initTypeDropdown(): Dropdown { + const typeDropdown: Dropdown = new Dropdown('type'); typeDropdown.addClass('type-dropdown'); this.createTypeValuesMap().forEach((value: string, key: string) => { - typeDropdown.addOption(Option.create() - .setValue(key) - .setDisplayValue(value) - .build()); + typeDropdown.addOption(key, value); }); typeDropdown.setValue(this.getOriginalTypeFieldValue()); @@ -95,7 +91,7 @@ export abstract class ListStyleModalDialog } private getDropdownValue(): string { - const value: string = (this.typeField.getInput() as Dropdown).getValue(); + const value: string = (this.typeField.getInput() as Dropdown).getValue(); return value === 'notset' ? '' : value; } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/MacroModalDialog.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/MacroModalDialog.ts index dd5b83373f..079637b4df 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/MacroModalDialog.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/MacroModalDialog.ts @@ -6,19 +6,18 @@ import {DefaultErrorHandler} from '@enonic/lib-admin-ui/DefaultErrorHandler'; import {FormItem} from '@enonic/lib-admin-ui/ui/form/FormItem'; import {Validators} from '@enonic/lib-admin-ui/ui/form/Validators'; import {ApplicationKey} from '@enonic/lib-admin-ui/application/ApplicationKey'; -import {SelectedOptionEvent} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionEvent'; import {PropertySet} from '@enonic/lib-admin-ui/data/PropertySet'; import {HtmlAreaModalDialogConfig, ModalDialog, ModalDialogFormItemBuilder} from './ModalDialog'; import {MacroDockedPanel} from './MacroDockedPanel'; import {Action} from '@enonic/lib-admin-ui/ui/Action'; import {ContentSummary} from '../../../../content/ContentSummary'; import {MacroDescriptor} from '@enonic/lib-admin-ui/macro/MacroDescriptor'; -import {MacrosLoader} from '../../../../macro/resource/MacrosLoader'; import {GetMacrosRequest} from '../../../../macro/resource/GetMacrosRequest'; -import {MacroComboBox} from '../../../../macro/MacroComboBox'; +import {MacroComboBox, MacroFormInputElWrapper} from '../../../../macro/MacroComboBox'; import * as DOMPurify from 'dompurify'; -import {MacroDialogParams, Macro} from '../HtmlEditor'; +import {Macro, MacroDialogParams} from '../HtmlEditor'; import {HTMLAreaHelper} from '../HTMLAreaHelper'; +import {SelectionChange} from '@enonic/lib-admin-ui/util/SelectionChange'; export interface MacroModalDialogConfig extends HtmlAreaModalDialogConfig { @@ -111,27 +110,29 @@ export class MacroModalDialog } private createMacroFormItem(): FormItem { - const macroSelector: MacroComboBox = - MacroComboBox.create().setLoader(this.createMacrosLoader()).setMaximumOccurrences(1).build() as MacroComboBox; + const macroSelector: MacroComboBox = new MacroComboBox(); + macroSelector.getLoader().setApplicationKeys(this.applicationKeys); const formItemBuilder = new ModalDialogFormItemBuilder('macroId', i18n('dialog.macro.formitem.macro')).setValidator( - Validators.required).setInputEl(macroSelector); + Validators.required).setInputEl(new MacroFormInputElWrapper(macroSelector)); return this.createFormItem(formItemBuilder); } private initMacroSelectorListeners() { - (this.macroFormItem.getInput() as MacroComboBox).getComboBox().onOptionSelected((event: SelectedOptionEvent) => { - this.macroFormItem.addClass('selected-item-preview'); - this.addClass('shows-preview'); - - this.macroDockedPanel.setMacroDescriptor(event.getSelectedOption().getOption().getDisplayValue()); - }); - - (this.macroFormItem.getInput() as MacroComboBox).getComboBox().onOptionDeselected(() => { - this.macroFormItem.removeClass('selected-item-preview'); - this.removeClass('shows-preview'); - this.displayValidationErrors(false); - ResponsiveManager.fireResizeEvent(); + let firstTimeSelected = true; + + this.getMacroCombobox().onSelectionChanged((selectionChange: SelectionChange) => { + if (selectionChange.selected?.length > 0) { + this.macroFormItem.addClass('selected-item-preview'); + this.addClass('shows-preview'); + this.macroDockedPanel.setMacroDescriptor(selectionChange.selected[0], firstTimeSelected ? this.makeData() : null); + firstTimeSelected = false; + } else if (selectionChange.deselected?.length > 0) { + this.macroFormItem.removeClass('selected-item-preview'); + this.removeClass('shows-preview'); + this.displayValidationErrors(false); + ResponsiveManager.fireResizeEvent(); + } }); } @@ -145,19 +146,14 @@ export class MacroModalDialog return; } - (this.macroFormItem.getInput() as MacroComboBox).setValue(macro.getKey().getRefString()); + this.getMacroCombobox().setSelectedMacro(macro); this.macroFormItem.addClass('selected-item-preview'); this.addClass('shows-preview'); - - this.macroDockedPanel.setMacroDescriptor(macro, this.makeData()); }); } - private createMacrosLoader(): MacrosLoader { - const loader = new MacrosLoader(); - loader.setApplicationKeys(this.applicationKeys); - - return loader; + private getMacroCombobox(): MacroComboBox { + return (this.macroFormItem.getInput() as MacroFormInputElWrapper).getComboBox(); } private getSelectedMacroDescriptor(): Q.Promise { @@ -177,7 +173,7 @@ export class MacroModalDialog } private sanitize(value: string): string { - const macroName = (this.macroFormItem.getInput() as MacroComboBox).getValue().toUpperCase(); + const macroName = this.getMacroCombobox().getValue().toUpperCase(); if (macroName === 'SYSTEM:DISABLE') { return value; @@ -196,11 +192,11 @@ export class MacroModalDialog private makeData(): PropertySet { const data: PropertySet = new PropertySet(); - this.selectedMacro.attributes.forEach(item => { + this.selectedMacro?.attributes.forEach(item => { data.addString(item[0], DOMPurify.sanitize(item[1])); }); - if (this.selectedMacro.body) { + if (this.selectedMacro?.body) { data.addString('body', this.sanitize(this.selectedMacro.body)); } @@ -246,7 +242,7 @@ export class MacroModalDialog } isDirty(): boolean { - return (this.macroFormItem.getInput() as MacroComboBox).isDirty(); + return (this.macroFormItem.getInput() as MacroFormInputElWrapper).isDirty(); } open(): void { diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/ModalDialog.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/ModalDialog.ts index e68ea9716a..95cf054010 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/ModalDialog.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/ModalDialog.ts @@ -14,7 +14,6 @@ import {Panel} from '@enonic/lib-admin-ui/ui/panel/Panel'; import {ValidationResult} from '@enonic/lib-admin-ui/ui/form/ValidationResult'; import {TextInput} from '@enonic/lib-admin-ui/ui/text/TextInput'; import {InputEl} from '@enonic/lib-admin-ui/dom/InputEl'; -import {RichComboBox} from '@enonic/lib-admin-ui/ui/selector/combobox/RichComboBox'; export class ModalDialogFormItemBuilder { @@ -230,12 +229,6 @@ export abstract class ModalDialog if (ObjectHelper.iFrameSafeInstanceOf(formItemEl, TextInput)) { (formItemEl as TextInput).onValueChanged(this.onValidatedFieldValueChanged.bind(this, formItem)); } - if (ObjectHelper.iFrameSafeInstanceOf(formItemEl, RichComboBox)) { - (formItemEl as RichComboBox).onOptionSelected(this.onValidatedFieldValueChanged.bind(this, - formItem)); - (formItemEl as RichComboBox).onOptionDeselected(this.onValidatedFieldValueChanged.bind(this, - formItem)); - } } return formItem; diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/TableDialog.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/TableDialog.ts index 2797994cc3..014c98277b 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/TableDialog.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/TableDialog.ts @@ -1,13 +1,12 @@ import * as Q from 'q'; import {i18n} from '@enonic/lib-admin-ui/util/Messages'; -import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; import {OverrideNativeDialog} from './OverrideNativeDialog'; import {HtmlAreaModalDialogConfig, ModalDialogFormItemBuilder} from './ModalDialog'; import {FormItem} from '@enonic/lib-admin-ui/ui/form/FormItem'; import {NumberHelper} from '@enonic/lib-admin-ui/util/NumberHelper'; -import {Dropdown, DropdownConfig} from '@enonic/lib-admin-ui/ui/selector/dropdown/Dropdown'; import {FormInputEl} from '@enonic/lib-admin-ui/dom/FormInputEl'; import {Action} from '@enonic/lib-admin-ui/ui/Action'; +import {Dropdown} from '@enonic/lib-admin-ui/ui/Dropdown'; import eventInfo = CKEDITOR.eventInfo; enum DialogType { @@ -111,25 +110,14 @@ export class TableDialog ]; } - private createHeadersDropdown(): Dropdown { - const headerDropdown: Dropdown = new Dropdown('headers', {} as DropdownConfig); - - headerDropdown.addOption(Option.create() - .setValue('') - .setDisplayValue(i18n('dialog.table.headers.none')) - .build()); - headerDropdown.addOption(Option.create() - .setValue('row') - .setDisplayValue(i18n('dialog.table.headers.row')) - .build()); - headerDropdown.addOption(Option.create() - .setValue('col') - .setDisplayValue(i18n('dialog.table.headers.col')) - .build()); - headerDropdown.addOption(Option.create() - .setValue('both') - .setDisplayValue(i18n('dialog.table.headers.both')) - .build()); + private createHeadersDropdown(): Dropdown { + const headerDropdown: Dropdown = new Dropdown('headers'); + headerDropdown.addClass('headers-dropdown'); + + headerDropdown.addOption('', i18n('dialog.table.headers.none')); + headerDropdown.addOption('row', i18n('dialog.table.headers.row')); + headerDropdown.addOption('col', i18n('dialog.table.headers.col')); + headerDropdown.addOption('both', i18n('dialog.table.headers.both')); return headerDropdown; } @@ -139,14 +127,14 @@ export class TableDialog this.rowsField.getInput().getEl().setDisabled(this.dialogType === DialogType.TABLEPROPERTIES); this.colsField.getInput().getEl().setValue(this.getOriginalColsElem().getValue()); this.colsField.getInput().getEl().setDisabled(this.dialogType === DialogType.TABLEPROPERTIES); - (this.headersField.getInput() as Dropdown).setValue(this.getOriginalHeadersElem().getValue()); + (this.headersField.getInput() as Dropdown).setValue(this.getOriginalHeadersElem().getValue()); this.captionField.getInput().getEl().setValue(this.getOriginalCaptionElem().getValue()); } private updateOriginalDialogInputValues() { this.getOriginalRowsElem().setValue(this.rowsField.getInput().getEl().getValue(), false); this.getOriginalColsElem().setValue(this.colsField.getInput().getEl().getValue(), false); - this.getOriginalHeadersElem().setValue((this.headersField.getInput() as Dropdown).getValue(), false); + this.getOriginalHeadersElem().setValue((this.headersField.getInput() as Dropdown).getValue(), false); this.getOriginalCaptionElem().setValue(this.captionField.getInput().getEl().getValue(), false); } diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageModalDialog.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageModalDialog.ts index 97132f4f8d..42a6533df0 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageModalDialog.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageModalDialog.ts @@ -10,7 +10,6 @@ import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; import {FormItem} from '@enonic/lib-admin-ui/ui/form/FormItem'; import {Validators} from '@enonic/lib-admin-ui/ui/form/Validators'; import {Action} from '@enonic/lib-admin-ui/ui/Action'; -import {SelectedOptionEvent} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionEvent'; import {ActionButton} from '@enonic/lib-admin-ui/ui/button/ActionButton'; import {UploadedEvent} from '@enonic/lib-admin-ui/ui/uploader/UploadedEvent'; import {UploadProgressEvent} from '@enonic/lib-admin-ui/ui/uploader/UploadProgressEvent'; @@ -22,7 +21,6 @@ import {HtmlAreaModalDialogConfig, ModalDialogFormItemBuilder} from '../ModalDia import {ImageStyleSelector} from './ImageStyleSelector'; import {MediaTreeSelectorItem} from '../../../selector/media/MediaTreeSelectorItem'; import {ImageUploaderEl} from '../../../selector/image/ImageUploaderEl'; -import {ImageContentComboBox} from '../../../selector/image/ImageContentComboBox'; import {ContentSelectedOptionsView} from '../../../selector/ContentComboBox'; import {MediaUploaderElOperation} from '../../../upload/MediaUploaderEl'; import {GetContentByIdRequest} from '../../../../../resource/GetContentByIdRequest'; @@ -46,19 +44,22 @@ import {UriHelper} from '@enonic/lib-admin-ui/util/UriHelper'; import {LinkEl} from '@enonic/lib-admin-ui/dom/LinkEl'; import {ContentSummary} from '../../../../../content/ContentSummary'; import {ContentId} from '../../../../../content/ContentId'; -import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; import {Project} from '../../../../../settings/data/project/Project'; -import {SelectedOptionsView} from '@enonic/lib-admin-ui/ui/selector/combobox/SelectedOptionsView'; -import eventInfo = CKEDITOR.eventInfo; import {ContentPath} from '../../../../../content/ContentPath'; +import {ImageSelectorDropdown} from '../../../../selector/ImageSelectorDropdown'; +import {ContentSelectorDropdownOptions} from '../../../../selector/ContentSelectorDropdown'; +import {ImageContentListBox} from '../../../../selector/ImageContentListBox'; +import {ImageOptionDataLoader, ImageOptionDataLoaderBuilder} from '../../../selector/image/ImageOptionDataLoader'; +import {FormInputEl} from '@enonic/lib-admin-ui/dom/FormInputEl'; +import {SelectionChange} from '@enonic/lib-admin-ui/util/SelectionChange'; import {RadioGroup} from '@enonic/lib-admin-ui/ui/RadioGroup'; import {ValueChangedEvent} from '@enonic/lib-admin-ui/ValueChangedEvent'; import {ValidationResult} from '@enonic/lib-admin-ui/ui/form/ValidationResult'; import {Form} from '@enonic/lib-admin-ui/ui/form/Form'; import {TextInput} from '@enonic/lib-admin-ui/ui/text/TextInput'; import {StringHelper} from '@enonic/lib-admin-ui/util/StringHelper'; -import {FormInputEl} from '@enonic/lib-admin-ui/dom/FormInputEl'; import {FormView} from '@enonic/lib-admin-ui/form/FormView'; +import eventInfo = CKEDITOR.eventInfo; enum ImageAccessibilityType { DECORATIVE = 'decorative', @@ -74,8 +75,9 @@ export class ImageModalDialog private imageAltTextRadioFormItem: FormItem; private imageUploaderEl: ImageUploaderEl; private presetImageEl: HTMLElement; + private presetImageId: string; private content?: ContentSummary; - private imageSelector: ImageContentComboBox; + private imageSelector: ImageSelectorDropdown; private progress: ProgressBar; private error: DivEl; private figure: FigureEl; @@ -209,6 +211,7 @@ export class ImageModalDialog } private presetImage(presetStyles: string) { + this.presetImageId = this.extractImageId(); const altTextValue = this.getOriginalAltTextElem().getValue(); if (StringHelper.isBlank(altTextValue)) { @@ -220,14 +223,10 @@ export class ImageModalDialog const imageId: string = this.extractImageId(); - new GetContentByIdRequest(new ContentId(imageId)).setRequestProject(this.config.project).sendAndParse().then( + new GetContentByIdRequest(new ContentId(this.presetImageId)).setRequestProject(this.config.project).sendAndParse().then( (imageContent: Content) => { - this.imageSelector.setValue(imageContent.getId()); - this.imageSelector.getComboBox().onValueLoaded((options: Option[]) => { - if (options.length === 1 && options[0].getId() === imageContent.getId()) { - this.imageSelector.show(); - } - }); + this.imageSelector.updateSelectedItems(); + this.imageSelector.show(); this.previewImage(imageContent, presetStyles); this.imageSelectorFormItem.addClass('selected-item-preview'); }).catch((reason) => { @@ -280,65 +279,75 @@ export class ImageModalDialog } private createImageSelector(id: string): FormItem { - const imageSelector = (ImageContentComboBox.create() - .setProject(this.config.project) - .setMaximumOccurrences(1)) - .setContent(this.content) - .setSelectedOptionsView(new ContentSelectedOptionsView() as unknown as SelectedOptionsView) - .build(); + const loader = this.createImageLoader(); + const listBox = new ImageContentListBox({loader: loader}); + const dropdownOptions: ContentSelectorDropdownOptions = { + loader: loader, + maxSelected: 1, + selectedOptionsView: new ContentSelectedOptionsView(), + className: 'single-occurrence', + getSelectedItems: () => this.presetImageId ? [this.presetImageId] : [], + }; + + const imageSelector = new ImageSelectorDropdown(listBox, dropdownOptions); const formItemBuilder = new ModalDialogFormItemBuilder(id, i18n('dialog.image.formitem.image')).setValidator( - Validators.required).setInputEl(imageSelector); + Validators.required).setInputEl(new ImageSelectorFormInputWrapper(imageSelector)); const formItem = this.createFormItem(formItemBuilder); - const imageSelectorComboBox = imageSelector.getComboBox(); - - imageSelector.getComboBox().getInput().setPlaceholder(i18n('field.image.option.placeholder')); this.imageSelector = imageSelector; formItem.addClass('image-selector'); - imageSelectorComboBox.onOptionSelected((event: SelectedOptionEvent) => { - const imageSelectorItem: MediaTreeSelectorItem = event.getSelectedOption().getOption().getDisplayValue(); - if (!imageSelectorItem.getContentId()) { - return; - } - - this.previewImage(imageSelectorItem.getContent()); - formItem.addClass('selected-item-preview'); + this.imageSelector.onSelectionChanged((selectionChange: SelectionChange): void => { + if (selectionChange.selected?.length > 0) { + const imageSelectorItem: MediaTreeSelectorItem = selectionChange.selected[0]; + if (!imageSelectorItem.getContentId()) { + return; + } - new GetContentByIdRequest(imageSelectorItem.getContent().getContentId()).setRequestProject( - this.config.project).sendAndParse().then((content: Content) => { + this.previewImage(imageSelectorItem.getContent()); + formItem.addClass('selected-item-preview'); - const altTextValue = ImageHelper.getImageAltText(content); + new GetContentByIdRequest(imageSelectorItem.getContent().getContentId()).setRequestProject( + this.config.project).sendAndParse().then((content: Content) => { + const altTextValue = ImageHelper.getImageAltText(content); - if (!StringHelper.isBlank(altTextValue)) { - this.imageAltTextInput.setValue(altTextValue, true); - } + if (!StringHelper.isBlank(altTextValue)) { + this.imageAltTextInput.setValue(altTextValue, true); + } - this.setCaptionFieldValue(ImageHelper.getImageCaption(content)); - }).catch(DefaultErrorHandler.handle).done(); - }); + this.setCaptionFieldValue(ImageHelper.getImageCaption(content)); + }).catch(DefaultErrorHandler.handle).done(); + } - imageSelectorComboBox.onOptionDeselected(() => { - formItem.removeClass('selected-item-preview'); - this.displayValidationErrors(false); - this.removePreview(); - this.imageToolbar.unStylesChanged(); - this.imageToolbar.unPreviewSizeChanged(); - this.imageToolbar.remove(); - this.imageAltTextInput.setValue(''); - this.secondaryForm.hide(); - this.getImageAltTextRadioInput().setValue(''); - this.imageUploaderEl.show(); - this.figure.getEl().removeAttribute('style'); - ResponsiveManager.fireResizeEvent(); + if (selectionChange.deselected?.length > 0) { + formItem.removeClass('selected-item-preview'); + this.displayValidationErrors(false); + this.removePreview(); + this.imageToolbar.unStylesChanged(); + this.imageToolbar.unPreviewSizeChanged(); + this.imageToolbar.remove(); + this.imageAltTextInput.setValue(''); + this.secondaryForm.hide(); + this.getImageAltTextRadioInput().setValue(''); + this.imageUploaderEl.show(); + this.figure.getEl().removeAttribute('style'); + ResponsiveManager.fireResizeEvent(); + } }); return formItem; } + private createImageLoader(): ImageOptionDataLoader { + return new ImageOptionDataLoaderBuilder() + .setContent(this.content) + .setProject(this.config.project) + .build(); + } + private createAltTextOptionRadio(id: string): FormItem { const imageAccessibilityRadio = new RadioGroup('radio'); imageAccessibilityRadio.addClass('image-accessibility-radio'); @@ -591,7 +600,7 @@ export class ImageModalDialog const item = event.getUploadItem(); const createdContent = item.getModel(); - this.imageSelector.setContent(createdContent); + this.imageSelector.select(new MediaTreeSelectorItem(createdContent)); }); uploader.onUploadFailed(() => { @@ -706,7 +715,7 @@ export class ImageModalDialog } private updateImageSrc(imageEl: HTMLElement, width: number) { - const imageContent = this.imageSelector.getSelectedContent(); + const imageContent = this.imageSelector.getSelectedOptions()[0].getOption().getDisplayValue().getContent(); const processingStyle = this.imageToolbar.getProcessingStyle(); const imageUrlBuilder = this.createImageUrlResolver(imageContent, width, processingStyle); @@ -899,7 +908,7 @@ export class ImageDialogToolbar const imageStyleSelector: ImageStyleSelector = new ImageStyleSelector(this.contentId); this.initSelectedStyle(imageStyleSelector); - imageStyleSelector.onOptionSelected(() => { + imageStyleSelector.onSelectionChanged(() => { if (StyleHelper.isOriginalImage(this.getProcessingStyleCls())) { this.customWidthCheckbox.setChecked(false).setEnabled(false); this.rangeInputContainer.hide(); @@ -925,7 +934,7 @@ export class ImageDialogToolbar const imageStyles = Styles.getForImageAsString(this.contentId); stylesApplied.forEach(style => { if (imageStyles.indexOf(style) > -1) { - imageStyleSelector.setValue(style); + imageStyleSelector.selectStyleByName(style); return; } @@ -956,7 +965,7 @@ export class ImageDialogToolbar private getProcessingStyleCls(): string { if (this.isProcessingStyleSelected()) { - return this.imageStyleSelector.getSelectedOption().getDisplayValue().getName(); + return this.imageStyleSelector.getSelectedStyle().getName(); } return ''; @@ -970,14 +979,13 @@ export class ImageDialogToolbar } private isProcessingStyleSelected(): boolean { - return (!!this.imageStyleSelector && - !!this.imageStyleSelector.getSelectedOption() && - !this.imageStyleSelector.getSelectedOption().getDisplayValue().isEmpty()); + const selectedStyle = this.imageStyleSelector.getSelectedStyle(); + return selectedStyle && !selectedStyle.isEmpty(); } getProcessingStyle(): Style { if (this.isProcessingStyleSelected()) { - return this.imageStyleSelector.getSelectedOption().getDisplayValue().getStyle(); + return this.imageStyleSelector.getSelectedStyle(); } return; @@ -1030,3 +1038,20 @@ export class ImageDialogToolbar } } +class ImageSelectorFormInputWrapper + extends FormInputEl { + + private readonly imageSelector: ImageSelectorDropdown; + + constructor(imageSelector: ImageSelectorDropdown) { + super('div', 'content-selector-wrapper'); + + this.imageSelector = imageSelector; + this.appendChild(this.imageSelector); + } + + + getValue(): string { + return this.imageSelector.getSelectedOptions()[0]?.getOption().getDisplayValue()?.getContent()?.getId() || ''; + } +} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleNameView.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleNameView.ts deleted file mode 100644 index c7c707ce3c..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleNameView.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {StyleHelper} from '@enonic/lib-admin-ui/StyleHelper'; -import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; -import {H6El} from '@enonic/lib-admin-ui/dom/H6El'; - -export class ImageStyleNameView - extends DivEl { - - private mainNameEl: H6El; - - private addTitleAttribute: boolean; - - constructor(addTitleAttribute: boolean = true) { - super('names-view', StyleHelper.COMMON_PREFIX); - - this.addTitleAttribute = addTitleAttribute; - - this.mainNameEl = new H6El('main-name', StyleHelper.COMMON_PREFIX); - this.appendChild(this.mainNameEl); - } - - setMainName(value: string): ImageStyleNameView { - this.mainNameEl.setHtml(value); - if (this.addTitleAttribute) { - this.mainNameEl.getEl().setAttribute('title', value); - } - return this; - } -} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleOptionViewer.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleOptionViewer.ts deleted file mode 100644 index 781d47e7d8..0000000000 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleOptionViewer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {Viewer} from '@enonic/lib-admin-ui/ui/Viewer'; -import {ImageStyleOption} from './ImageStyleOptions'; -import {ImageStyleNameView} from './ImageStyleNameView'; - -export class ImageStyleOptionViewer - extends Viewer { - - private nameView: ImageStyleNameView; - - constructor() { - super(); - - this.nameView = new ImageStyleNameView(false); - this.appendChild(this.nameView); - } - - setObject(object: ImageStyleOption) { - this.nameView.setMainName(object.getDisplayName()); - - return super.setObject(object); - } - -} diff --git a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleSelector.ts b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleSelector.ts index 210322e8c2..b801d47c25 100644 --- a/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleSelector.ts +++ b/modules/lib/src/main/resources/assets/js/app/inputtype/ui/text/dialog/image/ImageStyleSelector.ts @@ -1,43 +1,154 @@ -import {i18n} from '@enonic/lib-admin-ui/util/Messages'; import {Option} from '@enonic/lib-admin-ui/ui/selector/Option'; -import {Dropdown, DropdownConfig} from '@enonic/lib-admin-ui/ui/selector/dropdown/Dropdown'; -import {OptionSelectedEvent} from '@enonic/lib-admin-ui/ui/selector/OptionSelectedEvent'; import {ImageStyleOption, ImageStyleOptions} from './ImageStyleOptions'; -import {ImageStyleOptionViewer} from './ImageStyleOptionViewer'; +import {Style} from '../../styles/Style'; +import {DivEl} from '@enonic/lib-admin-ui/dom/DivEl'; +import {ListBox} from '@enonic/lib-admin-ui/ui/selector/list/ListBox'; +import {FilterableListBoxWrapper} from '@enonic/lib-admin-ui/ui/selector/list/FilterableListBoxWrapper'; +import {WidgetView} from '../../../../../view/context/WidgetView'; export class ImageStyleSelector - extends Dropdown { + extends DivEl { private contentId: string; + private styles: Map = new Map(); + + private filterInput: StyleFilterInput; + constructor(contentId: string) { - super('imageSelector', { - optionDisplayValueViewer: new ImageStyleOptionViewer(), - inputPlaceholderText: i18n('dialog.image.style.apply'), - rowHeight: 26 - } as DropdownConfig); + super('imageSelector'); this.contentId = contentId; this.addClass('image-style-selector'); - this.initDropdown(); } - private initDropdown() { + private initDropdown(): void { + this.filterInput = new StyleFilterInput(); this.addOptions(); + } + + private addOptions() { + ImageStyleOptions.getOptions(this.contentId).forEach((option: Option) => { + this.styles.set(option.getValue(), option.getDisplayValue().getStyle()); + this.filterInput.getList().addItems(option.getDisplayValue().getStyle()) + }); + } + + getSelectedStyle(): Style { + return this.filterInput.getSelectedItems()[0]; + } + + selectStyleByName(name: string): void { + const style = this.filterInput.getList().getItem(name); + + if (style) { + this.filterInput.select(style); + } + } + + onSelectionChanged(handler: () => void): void { + this.filterInput.onSelectionChanged(handler); + } + + doRender(): Q.Promise { + return super.doRender().then((rendered) => { + this.appendChild(this.filterInput); + + return rendered; + }); + } +} + +class StyleListBox extends ListBox