initial v0.8 pre-release
This commit is contained in:
@@ -10,10 +10,13 @@
|
||||
let classes = "";
|
||||
export { classes as class }; // export reserved keyword
|
||||
|
||||
export let draggable = false;
|
||||
export let active = false;
|
||||
export let interactive = true;
|
||||
export let single = false; // ensures that only one accordion is expanded in its given parent container
|
||||
|
||||
let isDragOver = false;
|
||||
|
||||
$: if (active) {
|
||||
clearTimeout(expandTimeoutId);
|
||||
expandTimeoutId = setTimeout(() => {
|
||||
@@ -49,10 +52,10 @@
|
||||
}
|
||||
|
||||
export function collapseSiblings() {
|
||||
if (single && accordionElem.parentElement) {
|
||||
const handlers = accordionElem.parentElement.querySelectorAll(
|
||||
".accordion.active .accordion-header.interactive"
|
||||
);
|
||||
if (single && accordionElem.closest(".accordions")) {
|
||||
const handlers = accordionElem
|
||||
.closest(".accordions")
|
||||
.querySelectorAll(".accordion.active .accordion-header.interactive");
|
||||
for (const handler of handlers) {
|
||||
handler.click(); // @todo consider using store or other more reliable approach
|
||||
}
|
||||
@@ -78,14 +81,36 @@
|
||||
<div
|
||||
bind:this={accordionElem}
|
||||
tabindex={interactive ? 0 : -1}
|
||||
class="accordion {classes}"
|
||||
class="accordion {isDragOver ? 'drag-over' : ''} {classes}"
|
||||
class:active
|
||||
on:keydown|self={keyToggle}
|
||||
>
|
||||
<header
|
||||
class="accordion-header"
|
||||
{draggable}
|
||||
class:interactive
|
||||
on:click|preventDefault={() => interactive && toggle()}
|
||||
on:drop|preventDefault={(e) => {
|
||||
if (draggable) {
|
||||
isDragOver = false;
|
||||
collapseSiblings();
|
||||
dispatch("drop", e);
|
||||
}
|
||||
}}
|
||||
on:dragstart={(e) => draggable && dispatch("dragstart", e)}
|
||||
on:dragenter={(e) => {
|
||||
if (draggable) {
|
||||
isDragOver = true;
|
||||
dispatch("dragenter", e);
|
||||
}
|
||||
}}
|
||||
on:dragleave={(e) => {
|
||||
if (draggable) {
|
||||
isDragOver = false;
|
||||
dispatch("dragleave", e);
|
||||
}
|
||||
}}
|
||||
on:dragover|preventDefault
|
||||
>
|
||||
<slot name="header" {active} />
|
||||
</header>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script>
|
||||
// @todo consider replacing with readonly CodeEditor
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/plugins/normalize-whitespace/prism-normalize-whitespace.js";
|
||||
import "prismjs/components/prism-dart.js";
|
||||
@@ -40,7 +39,7 @@
|
||||
code {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--xsSpacing);
|
||||
padding: 10px 15px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
|
||||
let confirmationPopup;
|
||||
let isConfirmationBusy = false;
|
||||
let confirmed = false;
|
||||
|
||||
$: if ($confirmation?.text) {
|
||||
confirmed = false;
|
||||
confirmationPopup?.show();
|
||||
}
|
||||
</script>
|
||||
@@ -19,10 +21,11 @@
|
||||
btnClose={false}
|
||||
popup
|
||||
on:hide={async () => {
|
||||
if ($confirmation?.noCallback) {
|
||||
if (!confirmed && $confirmation?.noCallback) {
|
||||
$confirmation.noCallback();
|
||||
}
|
||||
await tick();
|
||||
confirmed = false;
|
||||
resetConfirmation();
|
||||
}}
|
||||
>
|
||||
@@ -36,9 +39,7 @@
|
||||
class="btn btn-secondary btn-expanded-sm"
|
||||
disabled={isConfirmationBusy}
|
||||
on:click={() => {
|
||||
if ($confirmation?.noCallback) {
|
||||
$confirmation.noCallback();
|
||||
}
|
||||
confirmed = false;
|
||||
confirmationPopup?.hide();
|
||||
}}
|
||||
>
|
||||
@@ -55,6 +56,7 @@
|
||||
await Promise.resolve($confirmation.yesCallback());
|
||||
isConfirmationBusy = false;
|
||||
}
|
||||
confirmed = true;
|
||||
confirmationPopup?.hide();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -185,17 +185,23 @@
|
||||
prefix + "updated",
|
||||
];
|
||||
|
||||
if (collection.isAuth) {
|
||||
result.push(prefix + "username");
|
||||
result.push(prefix + "email");
|
||||
result.push(prefix + "emailVisibility");
|
||||
result.push(prefix + "verified");
|
||||
}
|
||||
|
||||
for (const field of collection.schema) {
|
||||
const key = prefix + field.name;
|
||||
|
||||
result.push(key);
|
||||
|
||||
if (field.type === "relation" && field.options.collectionId) {
|
||||
const subKeys = getCollectionFieldKeys(field.options.collectionId, key + ".", level + 1);
|
||||
if (subKeys.length) {
|
||||
result = result.concat(subKeys);
|
||||
} else {
|
||||
result.push(key);
|
||||
}
|
||||
} else {
|
||||
result.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,28 +223,27 @@
|
||||
result.push("@request.method");
|
||||
result.push("@request.query.");
|
||||
result.push("@request.data.");
|
||||
result.push("@request.user.id");
|
||||
result.push("@request.user.email");
|
||||
result.push("@request.user.verified");
|
||||
result.push("@request.user.created");
|
||||
result.push("@request.user.updated");
|
||||
result.push("@request.auth.");
|
||||
result.push("@request.auth.id");
|
||||
result.push("@request.auth.collectionId");
|
||||
result.push("@request.auth.collectionName");
|
||||
result.push("@request.auth.username");
|
||||
result.push("@request.auth.email");
|
||||
result.push("@request.auth.emailVisibility");
|
||||
result.push("@request.auth.verified");
|
||||
result.push("@request.auth.created");
|
||||
result.push("@request.auth.updated");
|
||||
}
|
||||
|
||||
// add @collections and @request.user.profile keys
|
||||
// add @collections.* keys
|
||||
if (includeRequestKeys || includeIndirectCollectionsKeys) {
|
||||
for (const collection of mergedCollections) {
|
||||
let prefix = "";
|
||||
if (collection.name === import.meta.env.PB_PROFILE_COLLECTION) {
|
||||
if (!includeRequestKeys) {
|
||||
continue;
|
||||
}
|
||||
prefix = "@request.user.profile.";
|
||||
} else {
|
||||
if (!includeIndirectCollectionsKeys) {
|
||||
continue;
|
||||
}
|
||||
prefix = "@collection." + collection.name + ".";
|
||||
|
||||
if (!includeIndirectCollectionsKeys) {
|
||||
continue;
|
||||
}
|
||||
prefix = "@collection." + collection.name + ".";
|
||||
|
||||
const keys = getCollectionFieldKeys(collection.name, prefix);
|
||||
for (const key of keys) {
|
||||
@@ -258,8 +263,8 @@
|
||||
|
||||
// Returns object with all the completions matching the context.
|
||||
function completions(context) {
|
||||
let word = context.matchBefore(/[\@\w\.]*/);
|
||||
if (word.from == word.to && !context.explicit) {
|
||||
let word = context.matchBefore(/[\'\"\@\w\.]*/);
|
||||
if (word && word.from == word.to && !context.explicit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -269,18 +274,8 @@
|
||||
options.push({ label: "@collection.*", apply: "@collection." });
|
||||
}
|
||||
|
||||
const skipFields = [
|
||||
"@request.user.profile.userId",
|
||||
"@request.user.profile.created",
|
||||
"@request.user.profile.updated",
|
||||
];
|
||||
|
||||
const keys = getAllKeys(!disableRequestKeys, !disableRequestKeys && word.text.startsWith("@c"));
|
||||
for (const key of keys) {
|
||||
if (skipFields.includes(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
options.push({
|
||||
label: key.endsWith(".") ? key + "*" : key,
|
||||
apply: key,
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
<script>
|
||||
export let date = "";
|
||||
|
||||
// note: manual trim the ms without converting to DateTime
|
||||
// to help improving the rendering performance in large data sets
|
||||
$: shortDate = date.length > 19 ? date.substring(0, 19) : date;
|
||||
$: dateOnly = date ? date.substring(0, 10) : null;
|
||||
|
||||
$: timeOnly = date ? date.substring(10, 19) : null;
|
||||
</script>
|
||||
|
||||
{#if date}
|
||||
<span class="txt">{shortDate} UTC</span>
|
||||
<div class="datetime">
|
||||
<div class="date">{dateOnly}</div>
|
||||
<div class="time">{timeOnly} UTC</div>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="txt txt-hint">N/A</span>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.datetime {
|
||||
width: 100%;
|
||||
display: block;
|
||||
line-height: var(--smLineHeight);
|
||||
}
|
||||
.time {
|
||||
font-size: var(--smFontSize);
|
||||
color: var(--txtHintColor);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let classes = "";
|
||||
export { classes as class }; // export reserved keyword
|
||||
|
||||
let wrapper = null;
|
||||
let scrollClasses = "";
|
||||
let scrollTimeoutId = null;
|
||||
let observer;
|
||||
|
||||
export function refresh() {
|
||||
if (!wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(scrollTimeoutId);
|
||||
|
||||
scrollTimeoutId = setTimeout(() => {
|
||||
const offsetWidth = wrapper.offsetWidth;
|
||||
const scrollWidth = wrapper.scrollWidth;
|
||||
|
||||
if (scrollWidth - offsetWidth) {
|
||||
scrollClasses = "scrollable";
|
||||
if (wrapper.scrollLeft === 0) {
|
||||
scrollClasses += " scroll-start";
|
||||
} else if (wrapper.scrollLeft + offsetWidth == scrollWidth) {
|
||||
scrollClasses += " scroll-end";
|
||||
}
|
||||
} else {
|
||||
scrollClasses = "";
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
refresh();
|
||||
|
||||
observer = new MutationObserver(() => {
|
||||
refresh();
|
||||
});
|
||||
|
||||
observer.observe(wrapper, {
|
||||
attributeFilter: ["width"],
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
return () => {
|
||||
observer?.disconnect();
|
||||
clearTimeout(scrollTimeoutId);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={refresh} />
|
||||
|
||||
<div class="horizontal-scroller-wrapper">
|
||||
<slot name="before" />
|
||||
|
||||
<div bind:this={wrapper} class="horizontal-scroller {classes} {scrollClasses}" on:scroll={refresh}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<slot name="after" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.horizontal-scroller {
|
||||
width: auto;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.horizontal-scroller-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
:global(.horizontal-scroller-wrapper .columns-dropdown) {
|
||||
top: 40px;
|
||||
z-index: 100;
|
||||
max-height: 340px;
|
||||
}
|
||||
</style>
|
||||
@@ -24,7 +24,7 @@
|
||||
passwordConfirm,
|
||||
});
|
||||
|
||||
await ApiClient.admins.authViaEmail(email, password);
|
||||
await ApiClient.admins.authWithPassword(email, password);
|
||||
|
||||
dispatch("submit");
|
||||
} catch (err) {
|
||||
|
||||
@@ -86,10 +86,12 @@
|
||||
oldFocusedElem = document.activeElement;
|
||||
wrapper?.focus();
|
||||
dispatch("show");
|
||||
document.body.classList.add("overlay-active");
|
||||
} else {
|
||||
clearTimeout(contentScrollThrottle);
|
||||
oldFocusedElem?.focus();
|
||||
dispatch("hide");
|
||||
document.body.classList.remove("overlay-active");
|
||||
}
|
||||
|
||||
await tick();
|
||||
@@ -179,13 +181,17 @@
|
||||
// ensures that no artifacts remains
|
||||
// (currently there is a bug with svelte transition)
|
||||
wrapper?.classList?.add("hidden");
|
||||
|
||||
setTimeout(() => {
|
||||
wrapper?.remove();
|
||||
}, 0);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={handleResize} on:keydown={handleEscPress} />
|
||||
|
||||
<div class="overlay-panel-wrapper" bind:this={wrapper}>
|
||||
<div bind:this={wrapper} class="overlay-panel-wrapper">
|
||||
{#if active}
|
||||
<div class="overlay-panel-container" class:padded={popup} class:active>
|
||||
<div
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
href={import.meta.env.PB_RELEASES}
|
||||
class="inline-flex flex-gap-5"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
title="Releases"
|
||||
>
|
||||
<span class="txt">PocketBase {import.meta.env.PB_VERSION}</span>
|
||||
|
||||
@@ -21,15 +21,21 @@
|
||||
|
||||
<OverlayPanel bind:this={panel} class="image-preview" btnClose={false} popup on:show on:hide>
|
||||
<svelte:fragment slot="header">
|
||||
<div class="overlay-close" on:click|preventDefault={hide}>
|
||||
<button type="button" class="overlay-close" on:click|preventDefault={hide}>
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
|
||||
<img src={url} alt="Preview {url}" />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<a href={url} title="Download" class="link-hint txt-ellipsis">
|
||||
<a
|
||||
href={url}
|
||||
title="Download"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
class="link-hint txt-ellipsis"
|
||||
>
|
||||
{url.substring(url.lastIndexOf("/") + 1)}
|
||||
</a>
|
||||
<div class="flex-fill" />
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
refreshTimeoutId = setTimeout(() => {
|
||||
refreshTimeoutId = null;
|
||||
tooltipData = oldTooltipData;
|
||||
}, 200);
|
||||
}, 150);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -45,6 +45,6 @@
|
||||
}
|
||||
}
|
||||
.btn.refreshing i {
|
||||
animation: refresh 200ms ease-out;
|
||||
animation: refresh 150ms ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="searchbar-wrapper" on:click|stopPropagation>
|
||||
<form class="searchbar" on:submit|preventDefault={submit}>
|
||||
<div class="searchbar-wrapper">
|
||||
<form class="searchbar" on:click|stopPropagation on:submit|preventDefault={submit}>
|
||||
<label for={uniqueId} class="m-l-10 txt-xl">
|
||||
<i class="ri-search-line" />
|
||||
</label>
|
||||
|
||||
@@ -194,13 +194,15 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="select {classes}" class:multiple class:disabled bind:this={container}>
|
||||
<div tabindex={disabled ? "-1" : "0"} class="selected-container" class:disabled bind:this={labelDiv}>
|
||||
<div bind:this={container} class="select {classes}" class:multiple class:disabled>
|
||||
<div bind:this={labelDiv} tabindex={disabled ? "-1" : "0"} class="selected-container" class:disabled>
|
||||
{#each CommonHelper.toArray(selected) as item}
|
||||
<div class="option">
|
||||
{#if labelComponent}
|
||||
<svelte:component this={labelComponent} {item} {...labelComponentProps} />
|
||||
{:else}<span class="txt">{item}</span>{/if}
|
||||
{:else}
|
||||
<span class="txt">{item}</span>
|
||||
{/if}
|
||||
|
||||
{#if multiple || toggle}
|
||||
<span
|
||||
@@ -213,17 +215,19 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="txt-placeholder">{selectPlaceholder}</div>
|
||||
<div class="block txt-placeholder" class:link-hint={!disabled}>
|
||||
{selectPlaceholder}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if !disabled}
|
||||
<Toggler
|
||||
bind:this={toggler}
|
||||
class="dropdown dropdown-block options-dropdown dropdown-left"
|
||||
trigger={labelDiv}
|
||||
on:show={onDropdownShow}
|
||||
on:hide
|
||||
bind:this={toggler}
|
||||
>
|
||||
{#if searchable}
|
||||
<div class="form-field form-field-sm options-search">
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
|
||||
<div class="content">{toast.message}</div>
|
||||
|
||||
<div class="close" on:click|preventDefault={() => removeToast(toast)}>
|
||||
<button type="button" class="close" on:click|preventDefault={() => removeToast(toast)}>
|
||||
<i class="ri-close-line" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -9,15 +9,20 @@
|
||||
let classes = "";
|
||||
export { classes as class }; // export reserved keyword
|
||||
|
||||
let container;
|
||||
let container = undefined;
|
||||
let activeTrigger = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: if (container) {
|
||||
bindTrigger(trigger);
|
||||
}
|
||||
|
||||
$: if (active) {
|
||||
trigger?.classList?.add("active");
|
||||
activeTrigger?.classList?.add("active");
|
||||
dispatch("show");
|
||||
} else {
|
||||
trigger?.classList?.remove("active");
|
||||
activeTrigger?.classList?.remove("active");
|
||||
dispatch("hide");
|
||||
}
|
||||
|
||||
@@ -42,7 +47,7 @@
|
||||
!container ||
|
||||
elem.classList.contains(closableClass) ||
|
||||
// is the trigger itself (or a direct child)
|
||||
(trigger?.contains(elem) && !container.contains(elem)) ||
|
||||
(activeTrigger?.contains(elem) && !container.contains(elem)) ||
|
||||
// is closable toggler child
|
||||
(container.contains(elem) && elem.closest && elem.closest("." + closableClass))
|
||||
);
|
||||
@@ -51,6 +56,8 @@
|
||||
function handleClickToggle(e) {
|
||||
if (!active || isClosable(e.target)) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
@@ -67,13 +74,13 @@
|
||||
}
|
||||
|
||||
function handleOutsideClick(e) {
|
||||
if (active && !container?.contains(e.target) && !trigger?.contains(e.target)) {
|
||||
if (active && !container?.contains(e.target) && !activeTrigger?.contains(e.target)) {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscPress(e) {
|
||||
if (active && escClose && e.code == "Escape") {
|
||||
if (active && escClose && e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
hide();
|
||||
}
|
||||
@@ -83,16 +90,34 @@
|
||||
return handleOutsideClick(e);
|
||||
}
|
||||
|
||||
function bindTrigger(newTrigger) {
|
||||
cleanup();
|
||||
|
||||
activeTrigger = newTrigger || container?.parentNode;
|
||||
|
||||
if (!activeTrigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
container?.addEventListener("click", handleClickToggle);
|
||||
activeTrigger.addEventListener("click", handleClickToggle);
|
||||
activeTrigger.addEventListener("keydown", handleKeydownToggle);
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (!activeTrigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
container?.removeEventListener("click", handleClickToggle);
|
||||
activeTrigger.removeEventListener("click", handleClickToggle);
|
||||
activeTrigger.removeEventListener("keydown", handleKeydownToggle);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
trigger = trigger || container.parentNode;
|
||||
bindTrigger();
|
||||
|
||||
trigger.addEventListener("click", handleClickToggle);
|
||||
trigger.addEventListener("keydown", handleKeydownToggle);
|
||||
|
||||
return () => {
|
||||
trigger.removeEventListener("click", handleClickToggle);
|
||||
trigger.removeEventListener("keydown", handleKeydownToggle);
|
||||
};
|
||||
return () => cleanup();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user