Skip to content

Commit

Permalink
Merge pull request #1395 from plone/petschki-contentbrowser-enhancements
Browse files Browse the repository at this point in the history
`pat-contentbrowser` fixes
  • Loading branch information
petschki authored Sep 27, 2024
2 parents 9e88e9a + 3c5be50 commit fff6fc1
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 99 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ But to resemble the CSS syntax, new patterns should instead separate each word w
- [pat-preventdoublesubmit](src/pat/preventdoublesubmit/README.md): Prevent multiple submissions of the same forn.
- [pat-querystring](src/pat/querystring/README.md): Show the querystring selection app.
- [pat-recurrence](src/pat/recurrence/README.md): Show the recurrence widget.
- [pat-relateditems](src/pat/relateditems/README.md): Show a widget to select related items. (deprecated: use `pat-contentbrowser` instead)
- [pat-select2](src/pat/select2/README.md): Show a widget which enhances dropdown selections with automatic suggestions, search and tagging functionality.
- [pat-sortable](src/pat/sortable/README.md): A pattern to make listings sortable.
- [pat-structure](src/pat/structure/README.md): Plone's folder contents app.
Expand All @@ -111,6 +110,7 @@ But to resemble the CSS syntax, new patterns should instead separate each word w

Deprecated patterns:

- [pat-relateditems](src/pat/relateditems/README.md) (_deprecated_): Show a widget to select related items. (use `pat-contentbrowser` instead)
- [pat-backdrop](src/pat/backdrop/README.md) (_deprecated_): Renders a dark background.
- [pat-contentloader](src/pat/contentloader/README.md) (_deprecated_): Load remote or local content into a target.
- [pat-texteditor](src/pat/texteditor/README.md) (_deprecated_): Show a code editor.
Expand Down
4 changes: 3 additions & 1 deletion src/pat/contentbrowser/contentbrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ parser.addArgument(
"review_state",
], null, true
);
parser.addArgument("width");
parser.addArgument("max-depth");
parser.addArgument("base-path");
parser.addArgument("context-path");
Expand All @@ -33,6 +34,7 @@ parser.addArgument("selection-template");
parser.addArgument("favorites");
parser.addArgument("recently-used");
parser.addArgument("recently-used-key");
parser.addArgument("recently-used-max-items", 20);
parser.addArgument("b-size");

class Pattern extends BasePattern {
Expand All @@ -43,7 +45,7 @@ class Pattern extends BasePattern {
async init() {
this.el.setAttribute('style', 'display: none');

// ensure an id on our elemen (TinyMCE doesn't have one)
// ensure an id on our element (TinyMCE doesn't have one)
let nodeId = this.el.getAttribute("id");
if (!nodeId) {
nodeId = utils.generateId();
Expand Down
4 changes: 4 additions & 0 deletions src/pat/contentbrowser/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
} from "./stores";
export let maxDepth;
export let width;
export let attributes;
export let contextPath;
export let vocabularyUrl;
Expand All @@ -29,6 +30,7 @@
export let favorites;
export let recentlyUsed;
export let recentlyUsedKey;
export let recentlyUsedMaxItems;
export let bSize = 20;
const log = logger.getLogger("pat-contentbrowser");
Expand Down Expand Up @@ -58,6 +60,7 @@
attributes: attributes,
contextPath: contextPath,
vocabularyUrl: vocabularyUrl,
width: width,
maxDepth: maxDepth,
basePath: basePath,
selectableTypes: selectableTypes,
Expand All @@ -70,6 +73,7 @@
favorites: favorites,
recentlyUsed: recentlyUsed,
recentlyUsedKey: recentlyUsedKey,
recentlyUsedMaxItems: recentlyUsedMaxItems,
base_url: base_url,
pageSize: bSize,
};
Expand Down
111 changes: 78 additions & 33 deletions src/pat/contentbrowser/src/ContentBrowser.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
import _t from "../../../core/i18n-wrapper";
import Upload from "../../upload/upload";
import contentStore from "./ContentStore";
import { clickOutside, get_items_from_uids, resolveIcon } from "./utils";
import {
clickOutside,
get_items_from_uids,
request,
resolveIcon,
updateRecentlyUsed,
} from "./utils";
import Favorites from "./Favorites.svelte";
import RecentlyUsed from "./RecentlyUsed.svelte";
animateScroll.setGlobalOptions({
scrollX: true,
Expand Down Expand Up @@ -117,7 +125,7 @@
const levelWrapper = e.currentTarget.closest(".levelItems");
const prevSelection = levelWrapper.querySelectorAll(".selectedItem");
if (prevSelection.length) {
if (prevSelection.length && $config.maximumSelectionSize != 1) {
// check for pressed shift or ctrl/meta key for multiselection
if (shiftKey || e?.shiftKey) {
Expand All @@ -143,7 +151,7 @@
action: select ? "add" : "remove",
});
}
} else if (e?.metaKey || e?.ctrlKey) {
} else if (e?.metaKey || e?.ctrlKey) {
// de/select multiple single items
// NOTE: only for mouse click event
updatePreview({
Expand All @@ -155,12 +163,11 @@
[...prevSelection].map((el) => el.classList.remove("selectedItem"));
changePath(item, e);
}
} else {
changePath(item, e);
}
e.currentTarget.focus(); // needed for keyboard navigation
e.currentTarget.focus(); // needed for keyboard navigation
e.currentTarget.classList.add("selectedItem");
}
Expand All @@ -170,10 +177,10 @@
return;
}
const possibleFocusEls = [
...document.querySelectorAll(".levelColumn .inPath"), // previously selected folder
...document.querySelectorAll(".levelColumn .selectedItem"), // previously selected item
document.querySelector(".levelColumn .contentItem"), // default first item
]
...document.querySelectorAll(".levelColumn .inPath"), // previously selected folder
...document.querySelectorAll(".levelColumn .selectedItem"), // previously selected item
document.querySelector(".levelColumn .contentItem"), // default first item
];
if (possibleFocusEls.length) {
keyboardNavInitialized = true;
possibleFocusEls[0].focus();
Expand Down Expand Up @@ -220,14 +227,24 @@
}
if (e.key == "Enter") {
if (isSelectable(item)) {
addSelectedItems(item);
if ($config.maximumSelectionSize == 1) {
addItem(item);
} else {
addSelectedItems();
}
}
}
}
async function addItem(item) {
selectedItems.update((n) => [...n, item]);
selectedUids.update(() => $selectedItems.map((x) => x.UID));
if ($config.maximumSelectionSize == 1) {
selectedItems.set([item]);
selectedUids.set([item.UID]);
} else {
selectedItems.update((n) => [...n, item]);
selectedUids.update(() => $selectedItems.map((x) => x.UID));
}
updateRecentlyUsed(item, $config);
updatePreview({ action: "clear" });
$showContentBrowser = false;
keyboardNavInitialized = false;
Expand All @@ -248,6 +265,29 @@
keyboardNavInitialized = false;
}
function selectRecentlyUsed(event) {
addItem(event.detail.item);
}
async function selectFavorite(event) {
const path = event.detail.item.path;
const response = await request({
vocabularyUrl: $config.vocabularyUrl,
attributes: $config.attributes,
levelInfoPath: path,
});
if (!response.total) {
alert(`${path} not found!`);
return;
}
const item = response.results[0];
if (!item.path) {
// fix for Plone Site
item.path = "/";
}
changePath(item);
}
function cancelSelection() {
$showContentBrowser = false;
keyboardNavInitialized = false;
Expand Down Expand Up @@ -325,32 +365,39 @@
<nav
class="content-browser"
transition:fly={{ x: (vw / 100) * 94, opacity: 1 }}
on:introend={() => { scrollToRight(); initKeyboardNav() }}
on:introend={() => {
scrollToRight();
initKeyboardNav();
}}
use:clickOutside
on:click_outside={cancelSelection}
>
<div class="toolBar navbar">
<div class="filter">
<div class="filter me-3">
<input type="text" name="filter" on:input={filterItems} />
<label for="filter"
><svg use:resolveIcon={{ iconName: "search" }} /></label
>
</div>
<RecentlyUsed on:selectItem={selectRecentlyUsed} />
<Favorites on:selectItem={selectFavorite} />
{#if $config.uploadEnabled}
<button
type="button"
class="upload btn btn-secondary btn-sm"
tabindex="0"
on:keydown={upload}
on:click={upload}
><svg use:resolveIcon={{ iconName: "upload" }} />
{_t("upload to ${current_path}", {
current_path: $currentPath,
})}</button
>
<div class="ms-2">
<button
type="button"
class="upload btn btn-outline-light btn-sm"
tabindex="0"
on:keydown={upload}
on:click={upload}
><svg use:resolveIcon={{ iconName: "upload" }} />
{_t("upload to ${current_path}", {
current_path: $currentPath,
})}</button
>
</div>
{/if}
<button
class="btn btn-link text-white"
class="btn btn-link text-white ms-auto"
tabindex="0"
on:click|preventDefault={() => cancelSelection()}
><svg use:resolveIcon={{ iconName: "x-circle" }} /></button
Expand Down Expand Up @@ -427,7 +474,8 @@
role="button"
tabindex={n}
data-uuid={item.UID}
on:keydown|preventDefault={(e) => keyboardNavigation(item, e)}
on:keydown|preventDefault={(e) =>
keyboardNavigation(item, e)}
on:click={(e) => clickItem(item, e)}
>
{#if level.gridView}
Expand Down Expand Up @@ -583,10 +631,7 @@
color: var(--bs-light);
width: 100%;
display: flex;
justify-content: space-between;
}
.toolBar > .upload {
margin: 0 1rem 0 auto;
justify-content: start;
}
.toolBar :global(svg) {
vertical-align: -0.125em;
Expand Down Expand Up @@ -634,10 +679,10 @@
min-height: 2rem;
}
.contentItem:focus-visible {
outline:none;
outline: none;
}
.contentItem.even {
background-color: var(--bs-secondary-bg);
background-color: rgba(var(--bs-secondary-bg-rgb), .4);
}
.contentItem.inPath,
.contentItem:focus {
Expand Down
2 changes: 2 additions & 0 deletions src/pat/contentbrowser/src/ContentStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export default function (config, pathCache) {
if (levelInfo.total) {
level.UID = levelInfo.results[0].UID;
level.Title = levelInfo.results[0].Title;
level.portal_type = levelInfo.results[0].portal_type;
level.getIcon = levelInfo.results[0].getIcon;
// check if level is selectable (config.selectableTypes)
level.selectable = (!config.selectableTypes.length || config.selectableTypes.indexOf(levelInfo.results[0].portal_type) != -1);
}
Expand Down
36 changes: 36 additions & 0 deletions src/pat/contentbrowser/src/Favorites.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script>
import { resolveIcon } from "./utils";
import _t from "../../../core/i18n-wrapper";
import { createEventDispatcher, getContext } from "svelte";
const config = getContext("config");
const dispatch = createEventDispatcher();
function select(item) {
dispatch("selectItem", {
item: item,
});
}
</script>

{#if $config?.favorites}
<div class="favorites dropdown dropdown-menu-end ms-2">
<button
type="button"
class="favorites dropdown-toggle btn btn-outline-light btn-sm"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<svg use:resolveIcon={{ iconName: "star-fill" }} />
{_t("Favorites")}
</button>
<ul class="dropdown-menu">
{#each $config.favorites as favorite}
<li>
<a class="dropdown-item" href="{favorite.path}" on:click|preventDefault={() => select(favorite)}>{favorite.title}</a>
</li>
{/each}
</ul>
</div>
{/if}
49 changes: 49 additions & 0 deletions src/pat/contentbrowser/src/RecentlyUsed.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script>
import { resolveIcon, recentlyUsedItems } from "./utils";
import _t from "../../../core/i18n-wrapper";
import { createEventDispatcher, getContext } from "svelte";
const config = getContext("config");
const items = recentlyUsedItems(true, $config);
const dispatch = createEventDispatcher();
function select(item) {
dispatch("selectItem", {
item: item,
});
}
</script>

{#if $config.recentlyUsed && items.length}
<div class="recentlyUsed dropdown ms-2">
<button
type="button"
class="recentlyUsed dropdown-toggle btn btn-outline-light btn-sm"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
<svg use:resolveIcon={{ iconName: "grid-fill" }} />
{_t("Recently Used")}
</button>
<ul class="dropdown-menu">
{#each items.reverse() as recentlyUsed}
<li>
<a
href={recentlyUsed.getURL}
on:click|preventDefault={() => select(recentlyUsed)}
class="dropdown-item"
>
<svg
on:error={console.log(recentlyUsed)}
use:resolveIcon={{
iconName: `contenttype/${recentlyUsed?.portal_type.toLowerCase().replace(/\.| /g, "-")}`,
}}
/>
{recentlyUsed.Title}
</a>
</li>
{/each}
</ul>
</div>
{/if}
Loading

0 comments on commit fff6fc1

Please sign in to comment.