Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pat-contentbrowser fixes #1395

Merged
merged 14 commits into from
Sep 27, 2024
Merged
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
109 changes: 77 additions & 32 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,7 +679,7 @@
min-height: 2rem;
}
.contentItem:focus-visible {
outline:none;
outline: none;
}
.contentItem.even {
background-color: var(--bs-secondary-bg);
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
Loading