Port most components from first tageditor PR

This commit is contained in:
Henrik Giesel 2021-06-24 00:12:20 +02:00
parent 1026d26793
commit 392bdf6184
8 changed files with 331 additions and 16 deletions

View File

@ -4,20 +4,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="typescript">
import Badge from "components/Badge.svelte";
import TagInputEdit from "./TagInputEdit.svelte";
import { deleteIcon } from "./icons";
let active = false;
export let name: string;
function activate(): void {
active = true;
}
let active = false;
</script>
{#if active}
<span>active!</span>
<TagInputEdit bind:name on:focusout={() => (active = false)} />
{:else}
<span class="tag text-nowrap bg-secondary rounded p-1 me-2" on:click={activate}>
<slot />
<span
class="tag text-nowrap bg-secondary rounded p-1 me-2"
on:click|stopPropagation={() => (active = true)}
>
<span>{name}</span>
<Badge class="delete-icon">{@html deleteIcon}</Badge>
</span>
{/if}

View File

@ -0,0 +1,98 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="typescript">
import { createEventDispatcher, onMount, onDestroy } from "svelte";
import Dropdown from "bootstrap/js/dist/dropdown";
export let name: string;
const dispatch = createEventDispatcher();
const triggerId = "tagLabel" + String(Math.random()).slice(2);
const triggerClass = "dropdown-toggle";
let originalName = name;
let menu: HTMLDivElement;
let dropdown;
let activeItem = -1;
const tagSuggestions = ["en::idioms", "anki::functionality", "math"];
$: tagValues = [...tagSuggestions, originalName];
onMount(() => {
const toggle = menu.querySelector(`#${triggerId}`)!;
dropdown = new Dropdown(toggle, {
reference: "parent",
});
});
function onItemClick(event: Event) {
dispatch("nameChosen", { name: event.currentTarget!.innerText });
event.stopPropagation();
event.preventDefault();
}
function onKeydown(event: KeyboardEvent) {
if (event.code === "ArrowUp") {
activeItem = activeItem === tagValues.length - 1 ? 0 : ++activeItem;
name = tagValues[activeItem];
event.preventDefault();
} else if (event.code === "ArrowDown") {
activeItem = activeItem === 0 ? tagValues.length - 1 : --activeItem;
name = tagValues[activeItem];
event.preventDefault();
} else if (event.code === "Enter") {
const dropdownActive = dropdown._element.classList.contains("show");
if (dropdownActive) {
if (typeof activeItem === "number") {
name = tagValues[activeItem];
activeItem = null;
}
dropdown.hide();
} else {
dispatch("accept");
}
}
}
</script>
<div class="dropdown" bind:this={menu} on:keydown={onKeydown}>
<slot {triggerId} {triggerClass} {dropdown} />
<ul class="dropdown-menu" aria-labelledby={triggerId}>
{#each tagSuggestions as tag, index}
<li>
<a
href="#/"
class="dropdown-item"
class:dropdown-item-active={activeItem === index}
on:click={onItemClick}
>
{tag}
</a>
</li>
{/each}
</ul>
</div>
<style lang="scss">
:global(.show).dropdown-menu {
display: flex;
flex-direction: column-reverse;
}
.dropdown-item {
padding: 0rem 0.3rem;
font-size: smaller;
&:focus {
outline: none;
}
}
.dropdown-item:hover {
background-color: #c3c5c7;
}
.dropdown-item-active {
color: #1e2125;
background-color: #c3c5c7;
}
</style>

View File

@ -6,24 +6,37 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import StickyBottom from "components/StickyBottom.svelte";
import Badge from "components/Badge.svelte";
import Tag from "./Tag.svelte";
import TagInputNew from "./TagInputNew.svelte";
import { tagIcon } from "./icons";
export let tags = ["en::foobar", "zh::あっちこっち"];
let tagInputNew: HTMLInputElement;
let inputNew = false;
function focusTagInputNew(): void {
inputNew = true;
tagInputNew.focus();
}
</script>
<StickyBottom>
<div>
<div class="d-flex flex-wrap" on:click={focusTagInputNew}>
<Badge class="me-1">{@html tagIcon}</Badge>
<Tag>en::foobar</Tag>
<Tag>zh::あちこ</Tag>
{#each tags as tag}
<Tag name={tag} />
{/each}
{#if inputNew}
<TagInputNew bind:input={tagInputNew} on:blur={() => (inputNew = false)} />
{/if}
</div>
</StickyBottom>
<style lang="scss">
div {
display: flex;
:global(#mdi-tag-outline) {
fill: currentColor;
:global(#mdi-tag-outline) {
height: 100%;
}
height: 100%;
}
</style>

108
ts/editor/TagInput.svelte Normal file
View File

@ -0,0 +1,108 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="typescript">
import { createEventDispatcher } from "svelte";
import TagAutocomplete from "./TagAutocomplete.svelte";
import { normalizeTagname } from "./tags";
import type Dropdown from "bootstrap/js/dist/dropdown";
export let name: string;
export let input: HTMLInputElement;
const dispatch = createEventDispatcher();
function onFocus(event: FocusEvent, dropdown: Dropdown): void {
dropdown.show();
}
function onAccept(event: Event): void {
dispatch("update", { tagname: normalizeTagname(name) });
}
function dropdownBlur(event: Event, dropdown: Dropdown): void {
onAccept(event);
dropdown.hide();
}
function onKeydown(event: KeyboardEvent, dropdown: Dropdown): void {
if (event.code === "Space") {
name += "::";
event.preventDefault();
} else if (event.code === "Backspace" && name.endsWith("::")) {
name = name.slice(0, -2);
event.preventDefault();
}
}
function onPaste({ clipboardData }: ClipboardEvent): void {
const pasted = name + clipboardData!.getData("text/plain");
const splitted = pasted.split(" ");
const last = splitted.pop();
for (const token of splitted) {
const tagname = normalizeTagname(token);
if (tagname) {
dispatch("add", { tagname });
}
}
name = last!;
}
function setTagname({ detail }: CustomEvent): void {
name = detail.name;
}
</script>
<TagAutocomplete
bind:name
let:triggerId
let:triggerClass
let:dropdown
on:nameChosen={setTagname}
on:accept={onAccept}
>
<label data-value={name} id={triggerId} class={triggerClass}>
<input
type="text"
size="1"
bind:this={input}
bind:value={name}
on:focus={(event) => onFocus(event, dropdown)}
on:blur={(event) => dropdownBlur(event, dropdown)}
on:focusout
on:keydown={(event) => onKeydown(event, dropdown)}
on:paste={onPaste}
on:click
/>
</label>
</TagAutocomplete>
<style lang="scss">
label {
display: inline-grid;
&::after,
input {
color: var(--text-fg);
background: none;
resize: none;
appearance: none;
width: auto;
grid-area: 1 / 1;
font: inherit;
outline: none;
border: none;
margin: 0;
padding: 0;
}
&::after {
/* 8 spaces to minimize reflow on clicking tag */
content: attr(data-value) " ";
visibility: hidden;
white-space: pre-wrap;
position: relative;
top: -2rem;
}
}
</style>

View File

@ -0,0 +1,42 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="typescript">
import { onMount } from "svelte";
import TagInput from "./TagInput.svelte";
export let name: string;
let input: HTMLInputElement;
function moveCursorToEnd(element: HTMLInputElement): void {
element.selectionStart = element.selectionEnd = element.value.length;
}
onMount(() => {
// Make sure Autocomplete was fully mounted
setTimeout(() => {
moveCursorToEnd(input);
input.focus();
}, 0);
});
function onKeydown(): void {
console.log("onkeydown");
}
function stopPropagation(event: Event): void {
event.stopPropagation();
}
</script>
<TagInput
bind:name
bind:input
on:keydown={onKeydown}
on:click={stopPropagation}
on:focusout
on:update
on:add
/>

View File

@ -0,0 +1,35 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="typescript">
import { createEventDispatcher } from "svelte";
import TagInput from "./TagInput.svelte";
export let input: HTMLInputElement;
let name = "";
let active = true;
const dispatch = createEventDispatcher();
function onKeydown(): void {
console.log("onkeydown");
}
function translateToAdd({ detail }: CustomEvent): void {
if (name) {
dispatch("add", detail);
name = "";
}
}
</script>
<TagInput
bind:name
bind:input
on:keydown={onKeydown}
on:add
on:update={translateToAdd}
on:blur
/>

16
ts/editor/tags.ts Normal file
View File

@ -0,0 +1,16 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export function normalizeTagname(tagname: string): string {
let trimmed = tagname.trim();
while (true) {
if (trimmed.startsWith("::")) {
trimmed = trimmed.slice(2).trimStart();
} else if (trimmed.endsWith("::")) {
trimmed = trimmed.slice(0, -2).trimEnd();
} else {
return trimmed;
}
}
}

View File

@ -8,6 +8,7 @@
"es2019.array",
"es2018.promise",
"es2020.promise",
"es2019.string",
"dom",
"dom.iterable"
],