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-subscript = Subscript
|
||||||
editing-superscript = Superscript
|
editing-superscript = Superscript
|
||||||
editing-tags = Tags
|
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-color = Text color
|
||||||
editing-text-highlight-color = Text highlight 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'
|
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;
|
border-radius: 5px;
|
||||||
background-color: var(--frame-bg);
|
background-color: var(--frame-bg);
|
||||||
min-width: 1rem;
|
min-width: 1rem;
|
||||||
|
max-width: 95vw;
|
||||||
|
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
font-size: 1rem;
|
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 isClosingKeyup from "../sveltelib/closing-keyup";
|
||||||
import { documentClick, documentKeyup } from "../sveltelib/event-store";
|
import { documentClick, documentKeyup } from "../sveltelib/event-store";
|
||||||
import portal from "../sveltelib/portal";
|
import portal from "../sveltelib/portal";
|
||||||
|
import type { PositionArgs } from "../sveltelib/position";
|
||||||
import position from "../sveltelib/position";
|
import position from "../sveltelib/position";
|
||||||
import subscribeTrigger from "../sveltelib/subscribe-trigger";
|
import subscribeTrigger from "../sveltelib/subscribe-trigger";
|
||||||
import { pageTheme } from "../sveltelib/theme";
|
import { pageTheme } from "../sveltelib/theme";
|
||||||
import toggleable from "../sveltelib/toggleable";
|
import toggleable from "../sveltelib/toggleable";
|
||||||
|
|
||||||
/** TODO at the moment we only dropdowns which are placed actually below the reference */
|
export let placement: Placement = "bottom";
|
||||||
const placement: Placement = "bottom";
|
|
||||||
|
|
||||||
export let closeOnInsideClick = false;
|
export let closeOnInsideClick = false;
|
||||||
|
export let keepOnKeyup = false;
|
||||||
|
|
||||||
/** This may be passed in for more fine-grained control */
|
/** This may be passed in for more fine-grained control */
|
||||||
export let show = writable(false);
|
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);
|
const { toggle, on, off } = toggleable(show);
|
||||||
|
|
||||||
onMount(() =>
|
let args: PositionArgs;
|
||||||
subscribeTrigger(
|
$: args = {
|
||||||
show,
|
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, {
|
isClosingClick(documentClick, {
|
||||||
reference,
|
reference,
|
||||||
floating,
|
floating,
|
||||||
inside: closeOnInsideClick,
|
inside: closeOnInsideClick,
|
||||||
outside: true,
|
outside: true,
|
||||||
}),
|
}),
|
||||||
isClosingKeyup(documentKeyup, {
|
];
|
||||||
reference,
|
|
||||||
floating,
|
if (!keepOnKeyup) {
|
||||||
}),
|
triggers.push(
|
||||||
),
|
isClosingKeyup(documentKeyup, {
|
||||||
);
|
reference,
|
||||||
|
floating,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeTrigger(show, ...triggers);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<slot name="reference" {show} {toggle} {on} {off} {asReference} />
|
||||||
bind:this={reference}
|
|
||||||
class="reference"
|
|
||||||
use:position={{ floating: $show ? floating : null, placement, arrow }}
|
|
||||||
>
|
|
||||||
<slot name="reference" {show} {toggle} {on} {off} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div bind:this={floating} class="floating" hidden={!$show} use:portal>
|
<div bind:this={floating} class="floating" hidden={!$show} use:portal>
|
||||||
<slot name="floating" />
|
<slot name="floating" />
|
||||||
|
|
||||||
<div bind:this={arrow} class="arrow" class:dark={$pageTheme.isDark} />
|
<div bind:this={arrow} class="arrow" class:dark={$pageTheme.isDark} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use "sass/elevation" as elevation;
|
@use "sass/elevation" as elevation;
|
||||||
|
|
||||||
.reference {
|
|
||||||
/* TODO This should not be necessary */
|
|
||||||
line-height: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating {
|
.floating {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 5px;
|
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);
|
background-color: var(--frame-bg);
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
transform: rotate(45deg);
|
|
||||||
z-index: 60;
|
z-index: 60;
|
||||||
|
|
||||||
/* outer border */
|
/* outer border */
|
||||||
@ -92,16 +107,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
border-color: #060606;
|
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-right: none;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|
||||||
/* inner border */
|
/* inner border */
|
||||||
box-shadow: inset 1px 1px 0 0 #eeeeee;
|
box-shadow: inset 1px 1px 0 0 #eeeeee;
|
||||||
/* lightmode box-shadow: inset 1px 1px 0 0 #eee; */
|
|
||||||
|
|
||||||
&.dark {
|
&.dark {
|
||||||
box-shadow: inset 0 0 0 1px #565656;
|
box-shadow: inset 1px 1px 0 0 #565656;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -354,7 +354,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
</Fields>
|
</Fields>
|
||||||
</FieldsEditor>
|
</FieldsEditor>
|
||||||
|
|
||||||
<TagEditor {size} {wrap} {tags} on:tagsupdate={saveTags} />
|
<TagEditor {tags} on:tagsupdate={saveTags} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -8,3 +8,7 @@ $btn-disabled-opacity: 0.4;
|
|||||||
@import "sass/bootstrap/scss/button-group";
|
@import "sass/bootstrap/scss/button-group";
|
||||||
@import "sass/bootstrap/scss/dropdown";
|
@import "sass/bootstrap/scss/dropdown";
|
||||||
@import "sass/bootstrap-tooltip";
|
@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>
|
</script>
|
||||||
|
|
||||||
<WithFloating show={showDropdown} closeOnInsideClick let:toggle>
|
<WithFloating show={showDropdown} closeOnInsideClick>
|
||||||
<IconButton slot="reference" {disabled} on:click={toggle}>
|
<span
|
||||||
{@html functionIcon}
|
class="latex-button"
|
||||||
</IconButton>
|
slot="reference"
|
||||||
|
let:asReference
|
||||||
|
use:asReference
|
||||||
|
let:toggle
|
||||||
|
>
|
||||||
|
<IconButton slot="reference" {disabled} on:click={toggle}>
|
||||||
|
{@html functionIcon}
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
|
||||||
<Popover slot="floating">
|
<Popover slot="floating">
|
||||||
{#each dropdownItems as [callback, keyCombination, label]}
|
{#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>
|
</WithFloating>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.latex-button {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.shortcut {
|
.shortcut {
|
||||||
font: Verdana;
|
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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, onMount } from "svelte";
|
|
||||||
|
|
||||||
import { pageTheme } from "../../sveltelib/theme";
|
import { pageTheme } from "../../sveltelib/theme";
|
||||||
|
|
||||||
export let id: string | undefined = undefined;
|
|
||||||
let className = "";
|
|
||||||
export { className as class };
|
|
||||||
|
|
||||||
export let selected = false;
|
export let selected = false;
|
||||||
export let active = false;
|
export let active = false;
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
let buttonRef: HTMLButtonElement;
|
let buttonRef: HTMLButtonElement;
|
||||||
|
|
||||||
export function scroll() {
|
$: if (selected && buttonRef) {
|
||||||
/* TODO will not work on Gecko */
|
|
||||||
(buttonRef as any)?.scrollIntoViewIfNeeded(false);
|
|
||||||
|
|
||||||
/* buttonRef.scrollIntoView({ behavior: "smooth", block: "start" }); */
|
/* 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>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
bind:this={buttonRef}
|
bind:this={buttonRef}
|
||||||
{id}
|
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
class="autocomplete-item btn {className}"
|
class="autocomplete-item btn"
|
||||||
class:btn-day={!$pageTheme.isDark}
|
class:btn-day={!$pageTheme.isDark}
|
||||||
class:btn-night={$pageTheme.isDark}
|
class:btn-night={$pageTheme.isDark}
|
||||||
class:selected
|
class:selected
|
||||||
@ -52,6 +44,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
text-align: start;
|
text-align: start;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
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 { className as class };
|
||||||
|
|
||||||
export let tooltip: string | undefined = undefined;
|
export let tooltip: string | undefined = undefined;
|
||||||
export let selected: boolean = false;
|
export let selected = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -5,15 +5,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, tick } from "svelte";
|
import { createEventDispatcher, tick } from "svelte";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
|
||||||
import StickyContainer from "../../components/StickyContainer.svelte";
|
import StickyContainer from "../../components/StickyContainer.svelte";
|
||||||
import { Tags, tags as tagsService } from "../../lib/proto";
|
import { Tags, tags as tagsService } from "../../lib/proto";
|
||||||
import { execCommand } from "../helpers";
|
import { execCommand } from "../helpers";
|
||||||
import Tag from "./Tag.svelte";
|
import { TagOptionsButton } from "./tag-options-button";
|
||||||
import TagEditMode from "./TagEditMode.svelte";
|
import TagEditMode from "./TagEditMode.svelte";
|
||||||
import TagInput from "./TagInput.svelte";
|
import TagInput from "./TagInput.svelte";
|
||||||
import TagOptionsBadge from "./TagOptionsBadge.svelte";
|
|
||||||
import type { Tag as TagType } from "./tags";
|
import type { Tag as TagType } from "./tags";
|
||||||
import {
|
import {
|
||||||
attachId,
|
attachId,
|
||||||
@ -21,10 +20,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
replaceWithColons,
|
replaceWithColons,
|
||||||
replaceWithUnicodeSeparator,
|
replaceWithUnicodeSeparator,
|
||||||
} from "./tags";
|
} from "./tags";
|
||||||
|
import TagSpacer from "./TagSpacer.svelte";
|
||||||
import WithAutocomplete from "./WithAutocomplete.svelte";
|
import WithAutocomplete from "./WithAutocomplete.svelte";
|
||||||
|
|
||||||
export let size: number;
|
|
||||||
export let wrap: boolean;
|
|
||||||
export let tags: Writable<string[]>;
|
export let tags: Writable<string[]>;
|
||||||
|
|
||||||
let tagTypes: TagType[];
|
let tagTypes: TagType[];
|
||||||
@ -36,6 +34,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
$: tagsToTagTypes($tags);
|
$: tagsToTagTypes($tags);
|
||||||
|
|
||||||
|
const show = writable(false);
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
const noSuggestions = Promise.resolve([]);
|
const noSuggestions = Promise.resolve([]);
|
||||||
let suggestionsPromise: Promise<string[]> = noSuggestions;
|
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;
|
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> {
|
async function splitTag(index: number, start: number, end: number): Promise<void> {
|
||||||
const current = activeName.slice(0, start);
|
const current = activeName.slice(0, start);
|
||||||
const splitOff = activeName.slice(end);
|
const splitOff = activeName.slice(end);
|
||||||
@ -279,7 +265,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "Tab":
|
case "Tab":
|
||||||
if (event.shiftKey) {
|
if (!$show) {
|
||||||
|
break;
|
||||||
|
} else if (event.shiftKey) {
|
||||||
autocomplete.selectPrevious();
|
autocomplete.selectPrevious();
|
||||||
} else {
|
} else {
|
||||||
autocomplete.selectNext();
|
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 selectionAnchor: number | null = null;
|
||||||
let selectionFocus: 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
|
// typically correct for rows < 7
|
||||||
$: assumedRows = Math.floor(height / badgeHeight);
|
$: assumedRows = Math.floor(height / badgeHeight);
|
||||||
$: shortenTags = shortenTags || assumedRows > 2;
|
$: shortenTags = shortenTags || assumedRows > 2;
|
||||||
|
$: anyTagsSelected = tagTypes.some((tag) => tag.selected);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<StickyContainer
|
<StickyContainer
|
||||||
@ -397,36 +380,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
bind:height
|
bind:height
|
||||||
class="d-flex"
|
class="d-flex"
|
||||||
>
|
>
|
||||||
{#if !wrap}
|
<div class="tag-editor-area" on:focusout={deselectIfLeave}>
|
||||||
<TagOptionsBadge
|
<TagOptionsButton
|
||||||
showSelectionsOptions={tagTypes.some((tag) => tag.selected)}
|
|
||||||
bind:badgeHeight
|
bind:badgeHeight
|
||||||
|
tagsSelected={anyTagsSelected}
|
||||||
on:tagselectall={selectAllTags}
|
on:tagselectall={selectAllTags}
|
||||||
on:tagcopy={copySelectedTags}
|
on:tagcopy={copySelectedTags}
|
||||||
on:tagdelete={deleteSelectedTags}
|
on:tagdelete={deleteSelectedTags}
|
||||||
on:tagappend={appendEmptyTag}
|
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)}
|
{#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
|
<TagEditMode
|
||||||
class="ms-0"
|
class="ms-0"
|
||||||
name={index === active ? activeName : tag.name}
|
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}
|
{#if index === active}
|
||||||
<div class="adjust-position">
|
<WithAutocomplete
|
||||||
<WithAutocomplete
|
{suggestionsPromise}
|
||||||
drop="up"
|
{show}
|
||||||
class="d-flex flex-column cap-items"
|
on:update={updateSuggestions}
|
||||||
{suggestionsPromise}
|
on:select={({ detail }) => onAutocomplete(detail.selected)}
|
||||||
on:update={updateSuggestions}
|
on:choose={({ detail }) => {
|
||||||
on:select={({ detail }) => onAutocomplete(detail.selected)}
|
onAutocomplete(detail.chosen);
|
||||||
on:choose={({ detail }) => onAutocomplete(detail.chosen)}
|
splitTag(index, detail.chosen.length, detail.chosen.length);
|
||||||
let:createAutocomplete
|
}}
|
||||||
>
|
let:createAutocomplete
|
||||||
<TagInput
|
let:hide
|
||||||
id={tag.id}
|
>
|
||||||
class="position-absolute start-0 top-0 bottom-0 ps-2 py-0"
|
<TagInput
|
||||||
disabled={autocompleteDisabled}
|
id={tag.id}
|
||||||
bind:name={activeName}
|
class="position-absolute start-0 top-0 bottom-0 ps-2 py-0"
|
||||||
bind:input={activeInput}
|
disabled={autocompleteDisabled}
|
||||||
on:focus={() => {
|
bind:name={activeName}
|
||||||
activeName = tag.name;
|
bind:input={activeInput}
|
||||||
autocomplete = createAutocomplete(activeInput);
|
on:focus={() => {
|
||||||
}}
|
activeName = tag.name;
|
||||||
on:keydown={onKeydown}
|
autocomplete = createAutocomplete();
|
||||||
on:keyup={onKeyup}
|
}}
|
||||||
on:taginput={() => updateTagName(tag)}
|
on:keydown={onKeydown}
|
||||||
on:tagsplit={({ detail }) =>
|
on:keyup={() => {
|
||||||
enterBehavior(index, detail.start, detail.end)}
|
if (activeName.length === 0) {
|
||||||
on:tagadd={() => insertTagKeepFocus(index)}
|
hide?.();
|
||||||
on:tagdelete={() => deleteTagAt(index)}
|
}
|
||||||
on:tagjoinprevious={() => joinWithPreviousTag(index)}
|
}}
|
||||||
on:tagjoinnext={() => joinWithNextTag(index)}
|
on:taginput={() => updateTagName(tag)}
|
||||||
on:tagmoveprevious={() => moveToPreviousTag(index)}
|
on:tagsplit={({ detail }) =>
|
||||||
on:tagmovenext={() => moveToNextTag(index)}
|
splitTag(index, detail.start, detail.end)}
|
||||||
on:tagaccept={() => {
|
on:tagadd={() => insertTagKeepFocus(index)}
|
||||||
deleteTagIfNotUnique(tag, index);
|
on:tagdelete={() => deleteTagAt(index)}
|
||||||
if (tag) {
|
on:tagjoinprevious={() => joinWithPreviousTag(index)}
|
||||||
updateTagName(tag);
|
on:tagjoinnext={() => joinWithNextTag(index)}
|
||||||
}
|
on:tagmoveprevious={() => moveToPreviousTag(index)}
|
||||||
saveTags();
|
on:tagmovenext={() => moveToNextTag(index)}
|
||||||
decideNextActive();
|
on:tagaccept={() => {
|
||||||
}}
|
deleteTagIfNotUnique(tag, index);
|
||||||
/>
|
if (tag) {
|
||||||
</WithAutocomplete>
|
updateTagName(tag);
|
||||||
</div>
|
}
|
||||||
|
saveTags();
|
||||||
|
decideNextActive();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WithAutocomplete>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<div
|
<TagSpacer on:click={appendEmptyTag} />
|
||||||
class="tag-spacer flex-grow-1 align-self-stretch"
|
</div>
|
||||||
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>
|
|
||||||
</StickyContainer>
|
</StickyContainer>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.tag-spacer {
|
.tag-editor-area {
|
||||||
cursor: text;
|
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) {
|
.hide-tag :global(.tag) {
|
||||||
opacity: 0;
|
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>
|
</style>
|
||||||
|
@ -265,4 +265,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-input {
|
||||||
|
/* recreates positioning of Tag component
|
||||||
|
* so that the text does not move when accepting */
|
||||||
|
border-left: 1px solid transparent;
|
||||||
|
}
|
||||||
</style>
|
</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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type Dropdown from "bootstrap/js/dist/dropdown";
|
|
||||||
import { createEventDispatcher, tick } from "svelte";
|
import { createEventDispatcher, tick } from "svelte";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
import Popover from "../../components/Popover.svelte";
|
||||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
import WithFloating from "../../components/WithFloating.svelte";
|
||||||
import AutocompleteItem from "./AutocompleteItem.svelte";
|
import AutocompleteItem from "./AutocompleteItem.svelte";
|
||||||
|
|
||||||
let className: string = "";
|
|
||||||
export { className as class };
|
|
||||||
|
|
||||||
export let drop: "down" | "up" = "down";
|
|
||||||
export let suggestionsPromise: Promise<string[]>;
|
export let suggestionsPromise: Promise<string[]>;
|
||||||
|
export let show: Writable<boolean>;
|
||||||
let dropdown: Dropdown;
|
|
||||||
let show = false;
|
|
||||||
|
|
||||||
let suggestionsItems: string[] = [];
|
let suggestionsItems: string[] = [];
|
||||||
$: suggestionsPromise.then((items) => {
|
$: suggestionsPromise.then((items) => {
|
||||||
show = items.length > 0;
|
show.set(items.length > 0);
|
||||||
|
|
||||||
if (show) {
|
|
||||||
dropdown.show();
|
|
||||||
} else {
|
|
||||||
dropdown.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
suggestionsItems = items;
|
suggestionsItems = items;
|
||||||
});
|
});
|
||||||
|
|
||||||
let selected: number | null = null;
|
let selected: number | null = null;
|
||||||
let active: boolean = false;
|
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 {
|
function incrementSelected(): void {
|
||||||
if (selected === null) {
|
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> {
|
async function updateSelected(): Promise<void> {
|
||||||
dispatch("select", { selected: suggestionsItems[selected ?? -1] });
|
dispatch("select", { selected: suggestionsItems[selected ?? -1] });
|
||||||
await tick();
|
|
||||||
dropdown.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectNext(): Promise<void> {
|
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() {
|
async function chooseSelected() {
|
||||||
active = true;
|
active = true;
|
||||||
dispatch("choose", { chosen: suggestionsItems[selected ?? -1] });
|
dispatch("choose", { chosen: suggestionsItems[selected ?? -1] });
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
show = false;
|
show.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function update() {
|
async function update() {
|
||||||
dropdown.update();
|
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
dispatch("update");
|
dispatch("update");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,26 +87,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
return selected !== null;
|
return selected !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAutocomplete =
|
function createAutocomplete() {
|
||||||
(createDropdown: (element: HTMLElement) => Dropdown) =>
|
const api = {
|
||||||
(element: HTMLElement): any => {
|
selectPrevious,
|
||||||
dropdown = createDropdown(element);
|
selectNext,
|
||||||
|
chooseSelected,
|
||||||
const api = {
|
update,
|
||||||
hide: dropdown.hide,
|
hasSelected,
|
||||||
show: dropdown.show,
|
|
||||||
toggle: dropdown.toggle,
|
|
||||||
isVisible: (dropdown as any).isVisible,
|
|
||||||
selectPrevious,
|
|
||||||
selectNext,
|
|
||||||
chooseSelected,
|
|
||||||
update,
|
|
||||||
hasSelected,
|
|
||||||
};
|
|
||||||
|
|
||||||
return api;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
function setSelected(index: number): void {
|
function setSelected(index: number): void {
|
||||||
selected = index;
|
selected = index;
|
||||||
active = true;
|
active = true;
|
||||||
@ -137,44 +118,73 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
setSelected(index);
|
setSelected(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let scroll: () => void;
|
|
||||||
|
|
||||||
$: if (scroll) {
|
|
||||||
scroll();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithDropdown {drop} toggleOpen={false} let:createDropdown align="start">
|
<WithFloating keepOnKeyup {show} placement="top-start" let:toggle let:hide let:show>
|
||||||
<slot createAutocomplete={createAutocomplete(createDropdown)} />
|
<span
|
||||||
|
class="autocomplete-reference"
|
||||||
|
slot="reference"
|
||||||
|
let:asReference
|
||||||
|
use:asReference
|
||||||
|
>
|
||||||
|
<slot {createAutocomplete} {toggle} {hide} {show} />
|
||||||
|
</span>
|
||||||
|
|
||||||
<DropdownMenu class={className} {show}>
|
<Popover slot="floating">
|
||||||
{#each suggestionsItems as suggestion, index}
|
<div class="autocomplete-menu">
|
||||||
{#if index === selected}
|
{#each suggestionsItems as suggestion, index}
|
||||||
<AutocompleteItem
|
{#if index === selected}
|
||||||
bind:scroll
|
<AutocompleteItem
|
||||||
selected
|
selected
|
||||||
{active}
|
{active}
|
||||||
on:mousedown={() => setSelectedAndActive(index)}
|
on:mousedown={() => setSelectedAndActive(index)}
|
||||||
on:mouseup={() => {
|
on:mouseup={() => {
|
||||||
selectIndex(index);
|
selectIndex(index);
|
||||||
chooseSelected();
|
chooseSelected();
|
||||||
}}
|
}}
|
||||||
on:mouseenter={(event) => selectIfMousedown(event, index)}
|
on:mouseenter={(event) => selectIfMousedown(event, index)}
|
||||||
on:mouseleave={() => (active = false)}
|
on:mouseleave={() => (active = false)}
|
||||||
>{suggestion}</AutocompleteItem
|
>{suggestion}</AutocompleteItem
|
||||||
>
|
>
|
||||||
{:else}
|
{:else}
|
||||||
<AutocompleteItem
|
<AutocompleteItem
|
||||||
on:mousedown={() => setSelectedAndActive(index)}
|
on:mousedown={() => setSelectedAndActive(index)}
|
||||||
on:mouseup={() => {
|
on:mouseup={() => {
|
||||||
selectIndex(index);
|
selectIndex(index);
|
||||||
chooseSelected();
|
chooseSelected();
|
||||||
}}
|
}}
|
||||||
on:mouseenter={(event) => selectIfMousedown(event, index)}
|
on:mouseenter={(event) => selectIfMousedown(event, index)}
|
||||||
>{suggestion}</AutocompleteItem
|
>{suggestion}</AutocompleteItem
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</DropdownMenu>
|
</div>
|
||||||
</WithDropdown>
|
</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" />
|
/// <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";
|
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">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
import Badge from "../../components/Badge.svelte";
|
import IconConstrain from "../../../components/IconConstrain.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../../components/Shortcut.svelte";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import * as tr from "../../../lib/ftl";
|
||||||
|
import { getPlatformString } from "../../../lib/shortcuts";
|
||||||
import { addTagIcon, tagIcon } from "./icons";
|
import { addTagIcon, tagIcon } from "./icons";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher<{ tagappend: CustomEvent<void> }>();
|
||||||
const tooltip = "Add tag";
|
|
||||||
const keyCombination = "Control+Shift+T";
|
|
||||||
|
|
||||||
function appendTag(): void {
|
const keyCombination = "Control+Shift+T";
|
||||||
dispatch("tagappend");
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="add-icon">
|
<div
|
||||||
<Badge
|
class="tag-add-button"
|
||||||
class="d-flex me-1"
|
title="{tr.editingTagsAdd()} ({getPlatformString(keyCombination)})"
|
||||||
tooltip="{tooltip} ({getPlatformString(keyCombination)})"
|
on:click={() => dispatch("tagappend")}
|
||||||
on:click={appendTag}
|
>
|
||||||
>
|
<IconConstrain>
|
||||||
{@html tagIcon}
|
{@html tagIcon}
|
||||||
{@html addTagIcon}
|
{@html addTagIcon}
|
||||||
</Badge>
|
</IconConstrain>
|
||||||
|
|
||||||
<Shortcut {keyCombination} on:action={appendTag} />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Shortcut {keyCombination} on:action={() => dispatch("tagappend")} />
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.add-icon {
|
.tag-add-button {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
:global(svg:last-child) {
|
: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/*",
|
"plain-text-input/*",
|
||||||
"rich-text-input/*",
|
"rich-text-input/*",
|
||||||
"editor-toolbar/*",
|
"editor-toolbar/*",
|
||||||
"tag-editor/*"
|
"tag-editor/*",
|
||||||
|
"tag-editor/tag-options-button/*"
|
||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../components" },
|
{ "path": "../components" },
|
||||||
|
@ -2,9 +2,16 @@
|
|||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import type { Placement } from "@floating-ui/dom";
|
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`.
|
* The floating element which is positioned relative to `reference`.
|
||||||
*/
|
*/
|
||||||
@ -25,19 +32,41 @@ function position(
|
|||||||
args.floating!,
|
args.floating!,
|
||||||
{
|
{
|
||||||
middleware: [
|
middleware: [
|
||||||
|
inline(),
|
||||||
offset(5),
|
offset(5),
|
||||||
shift({ padding: 5 }),
|
shift({ padding: 5 }),
|
||||||
arrow({ element: args.arrow }),
|
arrow({ element: args.arrow, padding: 5 }),
|
||||||
],
|
],
|
||||||
placement: args.placement,
|
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, {
|
Object.assign(args.arrow.style, {
|
||||||
left: `${arrowX}px`,
|
left: arrowX ? `${arrowX}px` : "",
|
||||||
top: `-5px`,
|
top: arrowY ? `${arrowY}px` : "",
|
||||||
|
transform: `rotate(${rotation}deg)`,
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.assign(args.floating!.style, {
|
Object.assign(args.floating!.style, {
|
||||||
|
Loading…
Reference in New Issue
Block a user