Fix outstanding tag editor issues (#1717)
* Start using WithFloating for SelectedTagBadge * Adjust arrow on WithFloating for all directions * Move TagOptionsBadge to its own sub directory * Show autocomplete menu via WithFloating * Have WithFloating return asReference instead of initializing its own reference element * Add html: overflow: hidden for editor * Replace ButtonToolbar with generic div * Move scroll logic into autocomplete item + restrict Popover width to 95vw * Fix autocomplete menu after pressing enter after selecting - should not trigger an autocomplete choose * Overlap TagInput perfectly with Tag * Satisfy formatter * Fix autocompletion item scrolling too much * Remove unused Tag.svelte focusable prop * Remove console.log * Fix floating arrow is a diamond in dark mode * Set autocompletion menu to 80vw
This commit is contained in:
parent
2d6dd0630f
commit
1ec3741934
@ -41,6 +41,10 @@ editing-show-duplicates = Show Duplicates
|
||||
editing-subscript = Subscript
|
||||
editing-superscript = Superscript
|
||||
editing-tags = Tags
|
||||
editing-tags-add = Add tag
|
||||
editing-tags-copy = Copy tags
|
||||
editing-tags-remove = Remove tags
|
||||
editing-tags-select-all = Select all tags
|
||||
editing-text-color = Text color
|
||||
editing-text-highlight-color = Text highlight color
|
||||
editing-to-make-a-cloze-deletion-on = To make a cloze deletion on an existing note, you need to change it to a cloze type first, via 'Notes>Change Note Type'
|
||||
|
@ -17,6 +17,7 @@ Alternative to DropdownMenu that avoids Bootstrap
|
||||
border-radius: 5px;
|
||||
background-color: var(--frame-bg);
|
||||
min-width: 1rem;
|
||||
max-width: 95vw;
|
||||
|
||||
padding: 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
|
@ -11,15 +11,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import isClosingKeyup from "../sveltelib/closing-keyup";
|
||||
import { documentClick, documentKeyup } from "../sveltelib/event-store";
|
||||
import portal from "../sveltelib/portal";
|
||||
import type { PositionArgs } from "../sveltelib/position";
|
||||
import position from "../sveltelib/position";
|
||||
import subscribeTrigger from "../sveltelib/subscribe-trigger";
|
||||
import { pageTheme } from "../sveltelib/theme";
|
||||
import toggleable from "../sveltelib/toggleable";
|
||||
|
||||
/** TODO at the moment we only dropdowns which are placed actually below the reference */
|
||||
const placement: Placement = "bottom";
|
||||
|
||||
export let placement: Placement = "bottom";
|
||||
export let closeOnInsideClick = false;
|
||||
export let keepOnKeyup = false;
|
||||
|
||||
/** This may be passed in for more fine-grained control */
|
||||
export let show = writable(false);
|
||||
@ -30,45 +30,61 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
const { toggle, on, off } = toggleable(show);
|
||||
|
||||
onMount(() =>
|
||||
subscribeTrigger(
|
||||
show,
|
||||
let args: PositionArgs;
|
||||
$: args = {
|
||||
floating: $show ? floating : null,
|
||||
placement,
|
||||
arrow,
|
||||
};
|
||||
|
||||
let update: (args: PositionArgs) => void;
|
||||
$: update?.(args);
|
||||
|
||||
function asReference(element: HTMLElement) {
|
||||
const pos = position(element, args);
|
||||
reference = element;
|
||||
update = pos.update;
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
pos.destroy();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const triggers = [
|
||||
isClosingClick(documentClick, {
|
||||
reference,
|
||||
floating,
|
||||
inside: closeOnInsideClick,
|
||||
outside: true,
|
||||
}),
|
||||
isClosingKeyup(documentKeyup, {
|
||||
reference,
|
||||
floating,
|
||||
}),
|
||||
),
|
||||
);
|
||||
];
|
||||
|
||||
if (!keepOnKeyup) {
|
||||
triggers.push(
|
||||
isClosingKeyup(documentKeyup, {
|
||||
reference,
|
||||
floating,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
subscribeTrigger(show, ...triggers);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={reference}
|
||||
class="reference"
|
||||
use:position={{ floating: $show ? floating : null, placement, arrow }}
|
||||
>
|
||||
<slot name="reference" {show} {toggle} {on} {off} />
|
||||
</div>
|
||||
<slot name="reference" {show} {toggle} {on} {off} {asReference} />
|
||||
|
||||
<div bind:this={floating} class="floating" hidden={!$show} use:portal>
|
||||
<slot name="floating" />
|
||||
|
||||
<div bind:this={arrow} class="arrow" class:dark={$pageTheme.isDark} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass/elevation" as elevation;
|
||||
|
||||
.reference {
|
||||
/* TODO This should not be necessary */
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.floating {
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
@ -82,7 +98,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
background-color: var(--frame-bg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
transform: rotate(45deg);
|
||||
z-index: 60;
|
||||
|
||||
/* outer border */
|
||||
@ -92,16 +107,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
border-color: #060606;
|
||||
}
|
||||
|
||||
/* These are dependant on which edge the arrow is supposed to be */
|
||||
/* Rotate the box to indicate the different directions */
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
|
||||
/* inner border */
|
||||
box-shadow: inset 1px 1px 0 0 #eeeeee;
|
||||
/* lightmode box-shadow: inset 1px 1px 0 0 #eee; */
|
||||
|
||||
&.dark {
|
||||
box-shadow: inset 0 0 0 1px #565656;
|
||||
box-shadow: inset 1px 1px 0 0 #565656;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -354,7 +354,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
</Fields>
|
||||
</FieldsEditor>
|
||||
|
||||
<TagEditor {size} {wrap} {tags} on:tagsupdate={saveTags} />
|
||||
<TagEditor {tags} on:tagsupdate={saveTags} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -8,3 +8,7 @@ $btn-disabled-opacity: 0.4;
|
||||
@import "sass/bootstrap/scss/button-group";
|
||||
@import "sass/bootstrap/scss/dropdown";
|
||||
@import "sass/bootstrap-tooltip";
|
||||
|
||||
html {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
@ -70,10 +70,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
</script>
|
||||
|
||||
<WithFloating show={showDropdown} closeOnInsideClick let:toggle>
|
||||
<IconButton slot="reference" {disabled} on:click={toggle}>
|
||||
{@html functionIcon}
|
||||
</IconButton>
|
||||
<WithFloating show={showDropdown} closeOnInsideClick>
|
||||
<span
|
||||
class="latex-button"
|
||||
slot="reference"
|
||||
let:asReference
|
||||
use:asReference
|
||||
let:toggle
|
||||
>
|
||||
<IconButton slot="reference" {disabled} on:click={toggle}>
|
||||
{@html functionIcon}
|
||||
</IconButton>
|
||||
</span>
|
||||
|
||||
<Popover slot="floating">
|
||||
{#each dropdownItems as [callback, keyCombination, label]}
|
||||
@ -89,6 +97,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
</WithFloating>
|
||||
|
||||
<style lang="scss">
|
||||
.latex-button {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
font: Verdana;
|
||||
}
|
||||
|
@ -3,35 +3,27 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
import { pageTheme } from "../../sveltelib/theme";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
export let selected = false;
|
||||
export let active = false;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
export function scroll() {
|
||||
/* TODO will not work on Gecko */
|
||||
(buttonRef as any)?.scrollIntoViewIfNeeded(false);
|
||||
|
||||
$: if (selected && buttonRef) {
|
||||
/* buttonRef.scrollIntoView({ behavior: "smooth", block: "start" }); */
|
||||
/* TODO will not work on Gecko */
|
||||
(buttonRef as any).scrollIntoViewIfNeeded({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||
</script>
|
||||
|
||||
<button
|
||||
bind:this={buttonRef}
|
||||
{id}
|
||||
tabindex="-1"
|
||||
class="autocomplete-item btn {className}"
|
||||
class="autocomplete-item btn"
|
||||
class:btn-day={!$pageTheme.isDark}
|
||||
class:btn-night={$pageTheme.isDark}
|
||||
class:selected
|
||||
@ -52,6 +44,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
text-align: start;
|
||||
white-space: nowrap;
|
||||
flex-grow: 1;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
|
@ -1,80 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import Badge from "../../components/Badge.svelte";
|
||||
import DropdownItem from "../../components/DropdownItem.svelte";
|
||||
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
||||
import { withSpan } from "../../components/helpers";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { dotsIcon } from "./icons";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const allLabel = "Select all tags";
|
||||
const allShortcut = "Control+A";
|
||||
const copyLabel = "Copy tags";
|
||||
const copyShortcut = "Control+C";
|
||||
const removeLabel = "Remove tags";
|
||||
const removeShortcut = "Backspace";
|
||||
</script>
|
||||
|
||||
<WithDropdown let:createDropdown>
|
||||
<div class="more-icon">
|
||||
<Badge class="me-1" on:mount={withSpan(createDropdown)}>{@html dotsIcon}</Badge>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownItem
|
||||
on:click={(event) => {
|
||||
dispatch("tagselectall");
|
||||
event.stopImmediatePropagation();
|
||||
}}>{allLabel} ({getPlatformString(allShortcut)})</DropdownItem
|
||||
>
|
||||
<Shortcut
|
||||
keyCombination={allShortcut}
|
||||
on:action={(event) => {
|
||||
dispatch("tagselectall");
|
||||
event.stopImmediatePropagation();
|
||||
}}
|
||||
/>
|
||||
|
||||
<DropdownItem on:click={() => dispatch("tagcopy")}
|
||||
>{copyLabel} ({getPlatformString(copyShortcut)})</DropdownItem
|
||||
>
|
||||
<Shortcut
|
||||
keyCombination={copyShortcut}
|
||||
on:action={() => dispatch("tagcopy")}
|
||||
/>
|
||||
|
||||
<DropdownItem on:click={() => dispatch("tagdelete")}
|
||||
>{removeLabel} ({getPlatformString(removeShortcut)})</DropdownItem
|
||||
>
|
||||
<Shortcut
|
||||
keyCombination={removeShortcut}
|
||||
on:action={() => dispatch("tagdelete")}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</WithDropdown>
|
||||
|
||||
<style lang="scss">
|
||||
.more-icon {
|
||||
line-height: 1;
|
||||
|
||||
:global(svg) {
|
||||
padding-bottom: 2px;
|
||||
cursor: pointer;
|
||||
fill: currentColor;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
:global(svg:hover) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -11,7 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export { className as class };
|
||||
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let selected: boolean = false;
|
||||
export let selected = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
@ -5,15 +5,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||
import StickyContainer from "../../components/StickyContainer.svelte";
|
||||
import { Tags, tags as tagsService } from "../../lib/proto";
|
||||
import { execCommand } from "../helpers";
|
||||
import Tag from "./Tag.svelte";
|
||||
import { TagOptionsButton } from "./tag-options-button";
|
||||
import TagEditMode from "./TagEditMode.svelte";
|
||||
import TagInput from "./TagInput.svelte";
|
||||
import TagOptionsBadge from "./TagOptionsBadge.svelte";
|
||||
import type { Tag as TagType } from "./tags";
|
||||
import {
|
||||
attachId,
|
||||
@ -21,10 +20,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
replaceWithColons,
|
||||
replaceWithUnicodeSeparator,
|
||||
} from "./tags";
|
||||
import TagSpacer from "./TagSpacer.svelte";
|
||||
import WithAutocomplete from "./WithAutocomplete.svelte";
|
||||
|
||||
export let size: number;
|
||||
export let wrap: boolean;
|
||||
export let tags: Writable<string[]>;
|
||||
|
||||
let tagTypes: TagType[];
|
||||
@ -36,6 +34,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
$: tagsToTagTypes($tags);
|
||||
|
||||
const show = writable(false);
|
||||
const dispatch = createEventDispatcher();
|
||||
const noSuggestions = Promise.resolve([]);
|
||||
let suggestionsPromise: Promise<string[]> = noSuggestions;
|
||||
@ -145,19 +144,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
return true;
|
||||
}
|
||||
|
||||
async function enterBehavior(
|
||||
index: number,
|
||||
start: number,
|
||||
end: number,
|
||||
): Promise<void> {
|
||||
if (autocomplete.hasSelected()) {
|
||||
autocomplete.chooseSelected();
|
||||
await tick();
|
||||
}
|
||||
|
||||
splitTag(index, start, end);
|
||||
}
|
||||
|
||||
async function splitTag(index: number, start: number, end: number): Promise<void> {
|
||||
const current = activeName.slice(0, start);
|
||||
const splitOff = activeName.slice(end);
|
||||
@ -279,7 +265,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
break;
|
||||
|
||||
case "Tab":
|
||||
if (event.shiftKey) {
|
||||
if (!$show) {
|
||||
break;
|
||||
} else if (event.shiftKey) {
|
||||
autocomplete.selectPrevious();
|
||||
} else {
|
||||
autocomplete.selectNext();
|
||||
@ -294,12 +282,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyup(): void {
|
||||
if (activeName.length === 0) {
|
||||
autocomplete.hide();
|
||||
}
|
||||
}
|
||||
|
||||
let selectionAnchor: number | null = null;
|
||||
let selectionFocus: number | null = null;
|
||||
|
||||
@ -389,6 +371,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
// typically correct for rows < 7
|
||||
$: assumedRows = Math.floor(height / badgeHeight);
|
||||
$: shortenTags = shortenTags || assumedRows > 2;
|
||||
$: anyTagsSelected = tagTypes.some((tag) => tag.selected);
|
||||
</script>
|
||||
|
||||
<StickyContainer
|
||||
@ -397,36 +380,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
bind:height
|
||||
class="d-flex"
|
||||
>
|
||||
{#if !wrap}
|
||||
<TagOptionsBadge
|
||||
showSelectionsOptions={tagTypes.some((tag) => tag.selected)}
|
||||
<div class="tag-editor-area" on:focusout={deselectIfLeave}>
|
||||
<TagOptionsButton
|
||||
bind:badgeHeight
|
||||
tagsSelected={anyTagsSelected}
|
||||
on:tagselectall={selectAllTags}
|
||||
on:tagcopy={copySelectedTags}
|
||||
on:tagdelete={deleteSelectedTags}
|
||||
on:tagappend={appendEmptyTag}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ButtonToolbar
|
||||
class="d-flex align-items-center w-100 px-1"
|
||||
{size}
|
||||
{wrap}
|
||||
on:focusout={deselectIfLeave}
|
||||
>
|
||||
{#if wrap}
|
||||
<TagOptionsBadge
|
||||
showSelectionsOptions={tagTypes.some((tag) => tag.selected)}
|
||||
bind:badgeHeight
|
||||
on:tagselectall={selectAllTags}
|
||||
on:tagcopy={copySelectedTags}
|
||||
on:tagdelete={deleteSelectedTags}
|
||||
on:tagappend={appendEmptyTag}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#each tagTypes as tag, index (tag.id)}
|
||||
<div class="position-relative" class:hide-tag={index === active}>
|
||||
<div class="tag-relative" class:hide-tag={index === active}>
|
||||
<TagEditMode
|
||||
class="ms-0"
|
||||
name={index === active ? activeName : tag.name}
|
||||
@ -449,90 +414,76 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
/>
|
||||
|
||||
{#if index === active}
|
||||
<div class="adjust-position">
|
||||
<WithAutocomplete
|
||||
drop="up"
|
||||
class="d-flex flex-column cap-items"
|
||||
{suggestionsPromise}
|
||||
on:update={updateSuggestions}
|
||||
on:select={({ detail }) => onAutocomplete(detail.selected)}
|
||||
on:choose={({ detail }) => onAutocomplete(detail.chosen)}
|
||||
let:createAutocomplete
|
||||
>
|
||||
<TagInput
|
||||
id={tag.id}
|
||||
class="position-absolute start-0 top-0 bottom-0 ps-2 py-0"
|
||||
disabled={autocompleteDisabled}
|
||||
bind:name={activeName}
|
||||
bind:input={activeInput}
|
||||
on:focus={() => {
|
||||
activeName = tag.name;
|
||||
autocomplete = createAutocomplete(activeInput);
|
||||
}}
|
||||
on:keydown={onKeydown}
|
||||
on:keyup={onKeyup}
|
||||
on:taginput={() => updateTagName(tag)}
|
||||
on:tagsplit={({ detail }) =>
|
||||
enterBehavior(index, detail.start, detail.end)}
|
||||
on:tagadd={() => insertTagKeepFocus(index)}
|
||||
on:tagdelete={() => deleteTagAt(index)}
|
||||
on:tagjoinprevious={() => joinWithPreviousTag(index)}
|
||||
on:tagjoinnext={() => joinWithNextTag(index)}
|
||||
on:tagmoveprevious={() => moveToPreviousTag(index)}
|
||||
on:tagmovenext={() => moveToNextTag(index)}
|
||||
on:tagaccept={() => {
|
||||
deleteTagIfNotUnique(tag, index);
|
||||
if (tag) {
|
||||
updateTagName(tag);
|
||||
}
|
||||
saveTags();
|
||||
decideNextActive();
|
||||
}}
|
||||
/>
|
||||
</WithAutocomplete>
|
||||
</div>
|
||||
<WithAutocomplete
|
||||
{suggestionsPromise}
|
||||
{show}
|
||||
on:update={updateSuggestions}
|
||||
on:select={({ detail }) => onAutocomplete(detail.selected)}
|
||||
on:choose={({ detail }) => {
|
||||
onAutocomplete(detail.chosen);
|
||||
splitTag(index, detail.chosen.length, detail.chosen.length);
|
||||
}}
|
||||
let:createAutocomplete
|
||||
let:hide
|
||||
>
|
||||
<TagInput
|
||||
id={tag.id}
|
||||
class="position-absolute start-0 top-0 bottom-0 ps-2 py-0"
|
||||
disabled={autocompleteDisabled}
|
||||
bind:name={activeName}
|
||||
bind:input={activeInput}
|
||||
on:focus={() => {
|
||||
activeName = tag.name;
|
||||
autocomplete = createAutocomplete();
|
||||
}}
|
||||
on:keydown={onKeydown}
|
||||
on:keyup={() => {
|
||||
if (activeName.length === 0) {
|
||||
hide?.();
|
||||
}
|
||||
}}
|
||||
on:taginput={() => updateTagName(tag)}
|
||||
on:tagsplit={({ detail }) =>
|
||||
splitTag(index, detail.start, detail.end)}
|
||||
on:tagadd={() => insertTagKeepFocus(index)}
|
||||
on:tagdelete={() => deleteTagAt(index)}
|
||||
on:tagjoinprevious={() => joinWithPreviousTag(index)}
|
||||
on:tagjoinnext={() => joinWithNextTag(index)}
|
||||
on:tagmoveprevious={() => moveToPreviousTag(index)}
|
||||
on:tagmovenext={() => moveToNextTag(index)}
|
||||
on:tagaccept={() => {
|
||||
deleteTagIfNotUnique(tag, index);
|
||||
if (tag) {
|
||||
updateTagName(tag);
|
||||
}
|
||||
saveTags();
|
||||
decideNextActive();
|
||||
}}
|
||||
/>
|
||||
</WithAutocomplete>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div
|
||||
class="tag-spacer flex-grow-1 align-self-stretch"
|
||||
on:click={appendEmptyTag}
|
||||
/>
|
||||
|
||||
<div class="position-relative hide-tag zero-width-tag">
|
||||
<!-- makes sure footer does not resize when adding first tag -->
|
||||
<Tag>SPACER</Tag>
|
||||
</div>
|
||||
</ButtonToolbar>
|
||||
<TagSpacer on:click={appendEmptyTag} />
|
||||
</div>
|
||||
</StickyContainer>
|
||||
|
||||
<style lang="scss">
|
||||
.tag-spacer {
|
||||
cursor: text;
|
||||
.tag-editor-area {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: 0 1px 1px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.tag-relative {
|
||||
position: relative;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
.hide-tag :global(.tag) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.zero-width-tag :global(.tag) {
|
||||
width: 0;
|
||||
pointer-events: none;
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.adjust-position {
|
||||
:global(.tag-input) {
|
||||
/* recreates positioning of Tag component
|
||||
* so that the text does not move when accepting */
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
|
||||
:global(.cap-items) {
|
||||
max-height: 7rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -265,4 +265,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tag-input {
|
||||
/* recreates positioning of Tag component
|
||||
* so that the text does not move when accepting */
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,24 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import AddTagBadge from "./AddTagBadge.svelte";
|
||||
import SelectedTagBadge from "./SelectedTagBadge.svelte";
|
||||
|
||||
export let badgeHeight: number;
|
||||
export let showSelectionsOptions: boolean;
|
||||
</script>
|
||||
|
||||
<div class="gap" bind:offsetHeight={badgeHeight} on:mousedown|preventDefault>
|
||||
{#if showSelectionsOptions}
|
||||
<SelectedTagBadge
|
||||
--badge-align="-webkit-baseline-middle"
|
||||
on:tagselectall
|
||||
on:tagcopy
|
||||
on:tagdelete
|
||||
/>
|
||||
{:else}
|
||||
<AddTagBadge --badge-align="-webkit-baseline-middle" on:tagappend />
|
||||
{/if}
|
||||
</div>
|
13
ts/editor/tag-editor/TagSpacer.svelte
Normal file
13
ts/editor/tag-editor/TagSpacer.svelte
Normal file
@ -0,0 +1,13 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<div class="tag-spacer" on:click />
|
||||
|
||||
<style lang="scss">
|
||||
.tag-spacer {
|
||||
cursor: text;
|
||||
flex-grow: 1;
|
||||
align-self: stretch;
|
||||
}
|
||||
</style>
|
@ -1,15 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Badge from "../../components/Badge.svelte";
|
||||
import { deleteIcon } from "./icons";
|
||||
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<Badge class="d-flex align-items-center ms-1 {className}" on:click iconSize={80}
|
||||
>{@html deleteIcon}</Badge
|
||||
>
|
@ -3,42 +3,35 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type Dropdown from "bootstrap/js/dist/dropdown";
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||
import Popover from "../../components/Popover.svelte";
|
||||
import WithFloating from "../../components/WithFloating.svelte";
|
||||
import AutocompleteItem from "./AutocompleteItem.svelte";
|
||||
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
|
||||
export let drop: "down" | "up" = "down";
|
||||
export let suggestionsPromise: Promise<string[]>;
|
||||
|
||||
let dropdown: Dropdown;
|
||||
let show = false;
|
||||
export let show: Writable<boolean>;
|
||||
|
||||
let suggestionsItems: string[] = [];
|
||||
$: suggestionsPromise.then((items) => {
|
||||
show = items.length > 0;
|
||||
|
||||
if (show) {
|
||||
dropdown.show();
|
||||
} else {
|
||||
dropdown.hide();
|
||||
}
|
||||
|
||||
show.set(items.length > 0);
|
||||
suggestionsItems = items;
|
||||
});
|
||||
|
||||
let selected: number | null = null;
|
||||
let active: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const dispatch = createEventDispatcher<{
|
||||
update: void;
|
||||
/* Selected should be displayed to the user, but it is not accepted */
|
||||
select: { selected: string };
|
||||
/* Autocompletion action should finish with "chosen" */
|
||||
choose: { chosen: string };
|
||||
}>();
|
||||
|
||||
/**
|
||||
* select as currently highlighted item
|
||||
* Select as currently highlighted item
|
||||
*/
|
||||
function incrementSelected(): void {
|
||||
if (selected === null) {
|
||||
@ -62,8 +55,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
async function updateSelected(): Promise<void> {
|
||||
dispatch("select", { selected: suggestionsItems[selected ?? -1] });
|
||||
await tick();
|
||||
dropdown.update();
|
||||
}
|
||||
|
||||
async function selectNext(): Promise<void> {
|
||||
@ -77,20 +68,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
|
||||
/**
|
||||
* choose as accepted suggestion
|
||||
* Choose as accepted suggestion
|
||||
*/
|
||||
async function chooseSelected() {
|
||||
active = true;
|
||||
dispatch("choose", { chosen: suggestionsItems[selected ?? -1] });
|
||||
|
||||
await tick();
|
||||
show = false;
|
||||
show.set(false);
|
||||
}
|
||||
|
||||
async function update() {
|
||||
dropdown.update();
|
||||
await tick();
|
||||
|
||||
dispatch("update");
|
||||
}
|
||||
|
||||
@ -98,26 +87,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
return selected !== null;
|
||||
}
|
||||
|
||||
const createAutocomplete =
|
||||
(createDropdown: (element: HTMLElement) => Dropdown) =>
|
||||
(element: HTMLElement): any => {
|
||||
dropdown = createDropdown(element);
|
||||
|
||||
const api = {
|
||||
hide: dropdown.hide,
|
||||
show: dropdown.show,
|
||||
toggle: dropdown.toggle,
|
||||
isVisible: (dropdown as any).isVisible,
|
||||
selectPrevious,
|
||||
selectNext,
|
||||
chooseSelected,
|
||||
update,
|
||||
hasSelected,
|
||||
};
|
||||
|
||||
return api;
|
||||
function createAutocomplete() {
|
||||
const api = {
|
||||
selectPrevious,
|
||||
selectNext,
|
||||
chooseSelected,
|
||||
update,
|
||||
hasSelected,
|
||||
};
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
function setSelected(index: number): void {
|
||||
selected = index;
|
||||
active = true;
|
||||
@ -137,44 +118,73 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
setSelected(index);
|
||||
}
|
||||
}
|
||||
|
||||
let scroll: () => void;
|
||||
|
||||
$: if (scroll) {
|
||||
scroll();
|
||||
}
|
||||
</script>
|
||||
|
||||
<WithDropdown {drop} toggleOpen={false} let:createDropdown align="start">
|
||||
<slot createAutocomplete={createAutocomplete(createDropdown)} />
|
||||
<WithFloating keepOnKeyup {show} placement="top-start" let:toggle let:hide let:show>
|
||||
<span
|
||||
class="autocomplete-reference"
|
||||
slot="reference"
|
||||
let:asReference
|
||||
use:asReference
|
||||
>
|
||||
<slot {createAutocomplete} {toggle} {hide} {show} />
|
||||
</span>
|
||||
|
||||
<DropdownMenu class={className} {show}>
|
||||
{#each suggestionsItems as suggestion, index}
|
||||
{#if index === selected}
|
||||
<AutocompleteItem
|
||||
bind:scroll
|
||||
selected
|
||||
{active}
|
||||
on:mousedown={() => setSelectedAndActive(index)}
|
||||
on:mouseup={() => {
|
||||
selectIndex(index);
|
||||
chooseSelected();
|
||||
}}
|
||||
on:mouseenter={(event) => selectIfMousedown(event, index)}
|
||||
on:mouseleave={() => (active = false)}
|
||||
>{suggestion}</AutocompleteItem
|
||||
>
|
||||
{:else}
|
||||
<AutocompleteItem
|
||||
on:mousedown={() => setSelectedAndActive(index)}
|
||||
on:mouseup={() => {
|
||||
selectIndex(index);
|
||||
chooseSelected();
|
||||
}}
|
||||
on:mouseenter={(event) => selectIfMousedown(event, index)}
|
||||
>{suggestion}</AutocompleteItem
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</DropdownMenu>
|
||||
</WithDropdown>
|
||||
<Popover slot="floating">
|
||||
<div class="autocomplete-menu">
|
||||
{#each suggestionsItems as suggestion, index}
|
||||
{#if index === selected}
|
||||
<AutocompleteItem
|
||||
selected
|
||||
{active}
|
||||
on:mousedown={() => setSelectedAndActive(index)}
|
||||
on:mouseup={() => {
|
||||
selectIndex(index);
|
||||
chooseSelected();
|
||||
}}
|
||||
on:mouseenter={(event) => selectIfMousedown(event, index)}
|
||||
on:mouseleave={() => (active = false)}
|
||||
>{suggestion}</AutocompleteItem
|
||||
>
|
||||
{:else}
|
||||
<AutocompleteItem
|
||||
on:mousedown={() => setSelectedAndActive(index)}
|
||||
on:mouseup={() => {
|
||||
selectIndex(index);
|
||||
chooseSelected();
|
||||
}}
|
||||
on:mouseenter={(event) => selectIfMousedown(event, index)}
|
||||
>{suggestion}</AutocompleteItem
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
|
||||
<style lang="scss">
|
||||
.autocomplete-reference {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
/* Make sure that text in TagInput perfectly overlaps with Tag */
|
||||
border-left: 1px solid transparent;
|
||||
border-top: 2px solid transparent;
|
||||
}
|
||||
|
||||
.autocomplete-menu {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
width: 80vw;
|
||||
max-height: 7rem;
|
||||
|
||||
font-size: 11px;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
@ -3,7 +3,4 @@
|
||||
|
||||
/// <reference types="../../lib/image-import" />
|
||||
|
||||
export { default as dotsIcon } from "@mdi/svg/svg/dots-vertical.svg";
|
||||
export { default as tagIcon } from "@mdi/svg/svg/tag.svg";
|
||||
export { default as addTagIcon } from "@mdi/svg/svg/tag-plus.svg";
|
||||
export { default as deleteIcon } from "bootstrap-icons/icons/x.svg";
|
||||
|
@ -5,35 +5,32 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import Badge from "../../components/Badge.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import IconConstrain from "../../../components/IconConstrain.svelte";
|
||||
import Shortcut from "../../../components/Shortcut.svelte";
|
||||
import * as tr from "../../../lib/ftl";
|
||||
import { getPlatformString } from "../../../lib/shortcuts";
|
||||
import { addTagIcon, tagIcon } from "./icons";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const tooltip = "Add tag";
|
||||
const keyCombination = "Control+Shift+T";
|
||||
const dispatch = createEventDispatcher<{ tagappend: CustomEvent<void> }>();
|
||||
|
||||
function appendTag(): void {
|
||||
dispatch("tagappend");
|
||||
}
|
||||
const keyCombination = "Control+Shift+T";
|
||||
</script>
|
||||
|
||||
<div class="add-icon">
|
||||
<Badge
|
||||
class="d-flex me-1"
|
||||
tooltip="{tooltip} ({getPlatformString(keyCombination)})"
|
||||
on:click={appendTag}
|
||||
>
|
||||
<div
|
||||
class="tag-add-button"
|
||||
title="{tr.editingTagsAdd()} ({getPlatformString(keyCombination)})"
|
||||
on:click={() => dispatch("tagappend")}
|
||||
>
|
||||
<IconConstrain>
|
||||
{@html tagIcon}
|
||||
{@html addTagIcon}
|
||||
</Badge>
|
||||
|
||||
<Shortcut {keyCombination} on:action={appendTag} />
|
||||
</IconConstrain>
|
||||
</div>
|
||||
|
||||
<Shortcut {keyCombination} on:action={() => dispatch("tagappend")} />
|
||||
|
||||
<style lang="scss">
|
||||
.add-icon {
|
||||
.tag-add-button {
|
||||
line-height: 1;
|
||||
|
||||
:global(svg:last-child) {
|
@ -0,0 +1,29 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import TagAddButton from "./TagAddButton.svelte";
|
||||
import TagsSelectedButton from "./TagsSelectedButton.svelte";
|
||||
|
||||
export let badgeHeight: number;
|
||||
export let tagsSelected: boolean;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="tag-options-button gap"
|
||||
bind:offsetHeight={badgeHeight}
|
||||
on:mousedown|preventDefault
|
||||
>
|
||||
{#if tagsSelected}
|
||||
<TagsSelectedButton on:tagselectall on:tagcopy on:tagdelete />
|
||||
{:else}
|
||||
<TagAddButton on:tagappend />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.tag-options-button {
|
||||
padding: 3px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,77 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import DropdownItem from "../../../components/DropdownItem.svelte";
|
||||
import IconConstrain from "../../../components/IconConstrain.svelte";
|
||||
import Popover from "../../../components/Popover.svelte";
|
||||
import Shortcut from "../../../components/Shortcut.svelte";
|
||||
import WithFloating from "../../../components/WithFloating.svelte";
|
||||
import * as tr from "../../../lib/ftl";
|
||||
import { getPlatformString } from "../../../lib/shortcuts";
|
||||
import { dotsIcon } from "./icons";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const allShortcut = "Control+A";
|
||||
const copyShortcut = "Control+C";
|
||||
const removeShortcut = "Backspace";
|
||||
</script>
|
||||
|
||||
<WithFloating placement="top">
|
||||
<div
|
||||
class="tags-selected-button"
|
||||
slot="reference"
|
||||
let:asReference
|
||||
use:asReference
|
||||
let:toggle
|
||||
on:click={toggle}
|
||||
>
|
||||
<IconConstrain>{@html dotsIcon}</IconConstrain>
|
||||
</div>
|
||||
|
||||
<Popover slot="floating">
|
||||
<DropdownItem on:click={() => dispatch("tagselectall")}>
|
||||
{tr.editingTagsSelectAll()} ({getPlatformString(allShortcut)})
|
||||
</DropdownItem>
|
||||
<Shortcut
|
||||
keyCombination={allShortcut}
|
||||
on:action={() => dispatch("tagselectall")}
|
||||
/>
|
||||
|
||||
<DropdownItem on:click={() => dispatch("tagcopy")}
|
||||
>{tr.editingTagsCopy()} ({getPlatformString(copyShortcut)})</DropdownItem
|
||||
>
|
||||
<Shortcut keyCombination={copyShortcut} on:action={() => dispatch("tagcopy")} />
|
||||
|
||||
<DropdownItem on:click={() => dispatch("tagdelete")}
|
||||
>{tr.editingTagsRemove()} ({getPlatformString(
|
||||
removeShortcut,
|
||||
)})</DropdownItem
|
||||
>
|
||||
<Shortcut
|
||||
keyCombination={removeShortcut}
|
||||
on:action={() => dispatch("tagdelete")}
|
||||
/>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
|
||||
<style lang="scss">
|
||||
.tags-selected-button {
|
||||
line-height: 1;
|
||||
|
||||
:global(svg) {
|
||||
padding-bottom: 2px;
|
||||
cursor: pointer;
|
||||
fill: currentColor;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
:global(svg:hover) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
8
ts/editor/tag-editor/tag-options-button/icons.ts
Normal file
8
ts/editor/tag-editor/tag-options-button/icons.ts
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/// <reference types="../../../lib/image-import" />
|
||||
|
||||
export { default as dotsIcon } from "@mdi/svg/svg/dots-vertical.svg";
|
||||
export { default as tagIcon } from "@mdi/svg/svg/tag.svg";
|
||||
export { default as addTagIcon } from "@mdi/svg/svg/tag-plus.svg";
|
4
ts/editor/tag-editor/tag-options-button/index.ts
Normal file
4
ts/editor/tag-editor/tag-options-button/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export { default as TagOptionsButton } from "./TagOptionsButton.svelte";
|
@ -7,7 +7,8 @@
|
||||
"plain-text-input/*",
|
||||
"rich-text-input/*",
|
||||
"editor-toolbar/*",
|
||||
"tag-editor/*"
|
||||
"tag-editor/*",
|
||||
"tag-editor/tag-options-button/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../components" },
|
||||
|
@ -2,9 +2,16 @@
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { Placement } from "@floating-ui/dom";
|
||||
import { arrow, autoUpdate, computePosition, offset, shift } from "@floating-ui/dom";
|
||||
import {
|
||||
arrow,
|
||||
autoUpdate,
|
||||
computePosition,
|
||||
inline,
|
||||
offset,
|
||||
shift,
|
||||
} from "@floating-ui/dom";
|
||||
|
||||
interface PositionArgs {
|
||||
export interface PositionArgs {
|
||||
/**
|
||||
* The floating element which is positioned relative to `reference`.
|
||||
*/
|
||||
@ -25,19 +32,41 @@ function position(
|
||||
args.floating!,
|
||||
{
|
||||
middleware: [
|
||||
inline(),
|
||||
offset(5),
|
||||
shift({ padding: 5 }),
|
||||
arrow({ element: args.arrow }),
|
||||
arrow({ element: args.arrow, padding: 5 }),
|
||||
],
|
||||
placement: args.placement,
|
||||
},
|
||||
);
|
||||
|
||||
const arrowX = middlewareData.arrow?.x ?? "";
|
||||
let rotation: number;
|
||||
let arrowX: number | undefined;
|
||||
let arrowY: number | undefined;
|
||||
|
||||
if (args.placement.startsWith("bottom")) {
|
||||
rotation = 45;
|
||||
arrowX = middlewareData.arrow?.x;
|
||||
arrowY = -5;
|
||||
} else if (args.placement.startsWith("left")) {
|
||||
rotation = 135;
|
||||
arrowX = args.floating!.offsetWidth - 5;
|
||||
arrowY = middlewareData.arrow?.y;
|
||||
} else if (args.placement.startsWith("top")) {
|
||||
rotation = 225;
|
||||
arrowX = middlewareData.arrow?.x;
|
||||
arrowY = args.floating!.offsetHeight - 5;
|
||||
} /* if (args.placement.startsWith("right")) */ else {
|
||||
rotation = 315;
|
||||
arrowX = -5;
|
||||
arrowY = middlewareData.arrow?.y;
|
||||
}
|
||||
|
||||
Object.assign(args.arrow.style, {
|
||||
left: `${arrowX}px`,
|
||||
top: `-5px`,
|
||||
left: arrowX ? `${arrowX}px` : "",
|
||||
top: arrowY ? `${arrowY}px` : "",
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
});
|
||||
|
||||
Object.assign(args.floating!.style, {
|
||||
|
Loading…
Reference in New Issue
Block a user