mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
feat: proper select search positioning & keyboard controls
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
export let childStart = false;
|
||||
export let noStyle = false;
|
||||
export let fullWidth = false;
|
||||
export let fixed = false;
|
||||
|
||||
let element: HTMLDivElement;
|
||||
let tooltip: HTMLDivElement;
|
||||
@@ -21,6 +22,7 @@
|
||||
onMount(() => {
|
||||
instance = createPopper(element, tooltip, {
|
||||
placement,
|
||||
strategy: fixed ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'arrow',
|
||||
@@ -39,6 +41,20 @@
|
||||
options: {
|
||||
fallbackPlacements: ['bottom-start', 'bottom-end', 'top-start', 'top-end']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'sameWidth',
|
||||
enabled: fixed,
|
||||
phase: 'beforeWrite',
|
||||
requires: ['computeStyles'],
|
||||
fn: ({ state }) => {
|
||||
state.styles.popper.width = `${state.rects.reference.width}px`;
|
||||
},
|
||||
effect: ({ state }) => {
|
||||
state.elements.popper.style.width = `${
|
||||
(state.elements.reference as HTMLElement)?.offsetWidth
|
||||
}px`;
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
export let show = false;
|
||||
export let placement: Placement = 'bottom-start';
|
||||
export let fixed = false;
|
||||
export let scrollable = false;
|
||||
export let childStart = false;
|
||||
export let noArrow = false;
|
||||
@@ -13,7 +14,7 @@
|
||||
export let position: 'relative' | 'static' = 'relative';
|
||||
</script>
|
||||
|
||||
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth}>
|
||||
<Drop bind:show {placement} {childStart} {noArrow} {noStyle} {fullWidth} {fixed}>
|
||||
<slot />
|
||||
<svelte:fragment slot="list">
|
||||
<div
|
||||
|
||||
@@ -77,7 +77,8 @@
|
||||
class="modal"
|
||||
class:is-small={size === 'small'}
|
||||
class:is-big={size === 'big'}
|
||||
bind:this={dialog}>
|
||||
bind:this={dialog}
|
||||
on:cancel|preventDefault>
|
||||
{#if show}
|
||||
<!-- svelte-ignore a11y-no-redundant-roles -->
|
||||
<form class="modal-form" role="form" on:submit|preventDefault>
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
export let label: string;
|
||||
export let optionalText: string | undefined = undefined;
|
||||
export let showLabel = true;
|
||||
export let value: string | number | boolean;
|
||||
export let search: string = null;
|
||||
export let placeholder = '';
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
@@ -18,10 +16,19 @@
|
||||
value: string | boolean | number;
|
||||
label: string;
|
||||
}[];
|
||||
// Input value
|
||||
export let search: string = null;
|
||||
// The actual selected value
|
||||
export let value: string | number | boolean;
|
||||
|
||||
let element: HTMLInputElement;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
let hasFocus = false;
|
||||
let selected: number = null;
|
||||
|
||||
$: if (!hasFocus) {
|
||||
selected = null;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (element && autofocus) {
|
||||
@@ -29,17 +36,46 @@
|
||||
}
|
||||
});
|
||||
|
||||
const valueChange = (event: Event) => {
|
||||
hasFocus = false;
|
||||
|
||||
function handleInput(event: Event) {
|
||||
hasFocus = true;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
search = target.value;
|
||||
}, debounce);
|
||||
};
|
||||
}
|
||||
|
||||
$: console.log(options);
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
hasFocus = false;
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (selected === null) {
|
||||
selected = 0;
|
||||
} else {
|
||||
selected = (selected + 1) % options.length;
|
||||
}
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (selected === null) {
|
||||
selected = options.length - 1;
|
||||
} else {
|
||||
selected = (selected - 1 + options.length) % options.length;
|
||||
}
|
||||
break;
|
||||
case 'Enter':
|
||||
if (selected !== null) {
|
||||
event.preventDefault();
|
||||
value = options[selected].value;
|
||||
search = options[selected].label;
|
||||
hasFocus = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<li class="u-position-relative form-item">
|
||||
@@ -50,7 +86,8 @@
|
||||
scrollable
|
||||
placement="bottom-end"
|
||||
position="static"
|
||||
fullWidth={true}>
|
||||
fullWidth={true}
|
||||
fixed>
|
||||
<Label {required} {optionalText} hide={!showLabel} for={id}>
|
||||
{label}
|
||||
</Label>
|
||||
@@ -66,7 +103,9 @@
|
||||
bind:value={search}
|
||||
bind:this={element}
|
||||
on:focus={() => (hasFocus = true)}
|
||||
on:input={valueChange} />
|
||||
on:click={() => (hasFocus = true)}
|
||||
on:input={handleInput}
|
||||
on:keydown={handleKeydown} />
|
||||
|
||||
<div class="options-list">
|
||||
<button
|
||||
@@ -74,7 +113,8 @@
|
||||
aria-label="clear field"
|
||||
type="button"
|
||||
on:click|preventDefault={() => {
|
||||
element.value = null;
|
||||
search = '';
|
||||
value = null;
|
||||
}}>
|
||||
<span class="icon-x" aria-hidden="true" />
|
||||
</button>
|
||||
@@ -85,26 +125,37 @@
|
||||
</div>
|
||||
</div>
|
||||
<svelte:fragment slot="list">
|
||||
{#if options?.length}
|
||||
{#each options as option}
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
class="drop-button"
|
||||
type="button"
|
||||
on:click|preventDefault={() => {
|
||||
value = option.value;
|
||||
search = option.label;
|
||||
hasFocus = false;
|
||||
}}>
|
||||
<span class="text">{option.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{#each options as option, i}
|
||||
<li class="drop-list-item">
|
||||
<button
|
||||
class="drop-button"
|
||||
class:is-selected={selected === i}
|
||||
type="button"
|
||||
on:click|preventDefault={() => {
|
||||
value = option.value;
|
||||
search = option.label;
|
||||
hasFocus = false;
|
||||
}}>
|
||||
<span class="text">{option.label}</span>
|
||||
</button>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="drop-list-item">
|
||||
<span class="text">There are no documents that match your search</span>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</svelte:fragment>
|
||||
</DropList>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
.form-item :global(.drop) {
|
||||
translate: 0 4px;
|
||||
}
|
||||
|
||||
.form-item :global(.drop-section) {
|
||||
width: 100%;
|
||||
margin-inline: initial;
|
||||
max-inline-size: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
+8
-8
@@ -1,23 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { DropList, DropListItem, Empty, Heading } from '$lib/components';
|
||||
import { Pill } from '$lib/elements';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCellHead,
|
||||
TableCellText,
|
||||
TableCell
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '$lib/elements/table';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { DropList, DropListItem, Empty, Heading } from '$lib/components';
|
||||
import { attributes, type Attributes } from '../store';
|
||||
import { Container } from '$lib/layout';
|
||||
import { Pill } from '$lib/elements';
|
||||
import Create from '../createAttribute.svelte';
|
||||
import CreateIndex from '../indexes/createIndex.svelte';
|
||||
import { attributes, type Attributes } from '../store';
|
||||
import Delete from './deleteAttribute.svelte';
|
||||
import { options } from './store';
|
||||
import Edit from './edit.svelte';
|
||||
import { options } from './store';
|
||||
|
||||
let showCreateDropdown = false;
|
||||
let showDropdown = [];
|
||||
|
||||
+35
-20
@@ -45,14 +45,25 @@
|
||||
import arrowOne from './arrow-one.svg';
|
||||
import arrowTwo from './arrow-two.svg';
|
||||
|
||||
// Props
|
||||
export let selectedAttribute: Models.AttributeString;
|
||||
export let data: Partial<Models.AttributeString> = {
|
||||
|
||||
type Data = Partial<Models.AttributeString> & {
|
||||
relation: 'one' | 'many';
|
||||
related?: string | number | boolean;
|
||||
keyRelated?: string;
|
||||
del?: string | number | boolean;
|
||||
};
|
||||
|
||||
export let data: Data = {
|
||||
required: false,
|
||||
size: 0,
|
||||
default: null,
|
||||
array: false
|
||||
array: false,
|
||||
relation: 'one'
|
||||
};
|
||||
|
||||
// Constants
|
||||
const databaseId = $page.params.database;
|
||||
const oneWay = [
|
||||
{ value: 'one', label: 'One to one' },
|
||||
@@ -70,16 +81,13 @@
|
||||
{ value: 'null', label: 'Null' },
|
||||
{ value: 'restrict', label: 'Restrict' }
|
||||
];
|
||||
|
||||
// Variables
|
||||
let search: string = null;
|
||||
let collectionList: Models.CollectionList;
|
||||
|
||||
let way = 'one';
|
||||
|
||||
onMount(async () => {
|
||||
collectionList = await getCollections();
|
||||
data.relation = 'one';
|
||||
});
|
||||
|
||||
// Lifecycle hooks
|
||||
async function getCollections(search: string = null) {
|
||||
if (search) {
|
||||
const collections = await sdkForProject.databases.listCollections(
|
||||
@@ -87,24 +95,31 @@
|
||||
[Query.orderDesc('$createdAt')],
|
||||
search
|
||||
);
|
||||
collectionList = collections;
|
||||
return collections;
|
||||
} else {
|
||||
const collections = await sdkForProject.databases.listCollections(databaseId);
|
||||
collectionList = collections;
|
||||
return collections;
|
||||
}
|
||||
}
|
||||
|
||||
$: getCollections(search);
|
||||
onMount(async () => {
|
||||
collectionList = await getCollections();
|
||||
});
|
||||
|
||||
// Reactive statements
|
||||
$: getCollections(search).then((res) => (collectionList = res));
|
||||
$: collections = collectionList?.collections?.filter((n) => n.$id !== $collection.$id) ?? [];
|
||||
|
||||
$: if (selectedAttribute) {
|
||||
({
|
||||
required: data.required,
|
||||
array: data.array,
|
||||
size: data.size,
|
||||
default: data.default
|
||||
} = selectedAttribute);
|
||||
data = {
|
||||
...data,
|
||||
required: selectedAttribute.required,
|
||||
array: selectedAttribute.array,
|
||||
size: selectedAttribute.size,
|
||||
default: selectedAttribute.default
|
||||
};
|
||||
}
|
||||
|
||||
$: if (data.required || data.array) {
|
||||
data.default = null;
|
||||
}
|
||||
@@ -137,7 +152,7 @@
|
||||
options={collectionList?.collections?.map((n) => ({ value: n.$id, label: n.name })) ?? []} />
|
||||
|
||||
{#if data?.related}
|
||||
{@const selectedCol = collections.find((n) => n.$id === data.related)}
|
||||
{@const selectedCol = collections?.find((n) => n.$id === data.related)}
|
||||
<div>
|
||||
<InputText
|
||||
id="key"
|
||||
@@ -193,12 +208,12 @@
|
||||
{:else}
|
||||
<img src={arrowTwo} alt={'Two way relationship'} />
|
||||
{/if}
|
||||
<span>{selectedCol.name}</span>
|
||||
<span>{selectedCol?.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="u-text-center ">
|
||||
<b> {$collection.name}</b> has {data.relation === 'one' ? 'one' : 'many'}
|
||||
<b>{selectedCol.name}</b>
|
||||
<b>{selectedCol?.name}</b>
|
||||
</p>
|
||||
|
||||
<InputSelect
|
||||
|
||||
Reference in New Issue
Block a user