Make tags editor resizable using Henrik's components (#2046)

* Make tags editor resizable using Henrik's components

All credit for the components goes to Henrik. I just tweaked the design a bit and implemented them in NoteEditor.

Co-Authored-By: Henrik Giesel <hengiesel@gmail.com>

* Remove PaneContent padding

Co-Authored-By: Henrik Giesel <hengiesel@gmail.com>

* Add responsive box-shadows on scroll/resize

only shown when content overflows in the respective direction.

* Remove comment

* Fix overflow calculations and shadow mix-up

This happened when I switched from using scrolledToX to overflowX booleans.

* Simplify overflow calculations

* Make drag handles 0 height/width

The remaining height requirement comes from a margin set on NoteEditor.

* Run eslint on components

* Split editor into three panes: Toolbar, Fields, Tags

* Remove upper split for now

to unblock 2.1.55 beta

* Move panes.scss to sass folder

* Use single type for resizable panes

* Implement collapsed state toggled with click on resizer

* Add button to uncollapse tags pane and focus input

* Add indicator for # of tags

* Use dbclick to prevent interference with resize state

* Add utility functions for expand/collapse

* Meddle around with types and formatting

* Fix collapsed state being forgotten on second browser open (dae)

* Fix typecheck (dae)

Our tooling generates .d.ts files from the Svelte files, but it doesn't
expect variables to be exported. By changing them into functions, they
get included in .bazel/bin/ts/components/Pane.svelte.d.ts

* Remove an unnecessary bridgeCommand (dae)

* Fix the bottom of tags getting cut off (dae)

Not sure why offsetHeight is inaccurate in this case.

* Add missing header (dae)

Co-authored-by: Henrik Giesel <hengiesel@gmail.com>
This commit is contained in:
Matthias Metelka 2022-09-28 06:02:32 +02:00 committed by GitHub
parent a54b815c7b
commit f72570c604
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 801 additions and 167 deletions

View File

@ -533,6 +533,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
setNoteId({});
setColorButtons({});
setTags({});
setTagsCollapsed({});
setMathjaxEnabled({});
setShrinkImages({});
""".format(
@ -545,6 +546,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
json.dumps(self.note.id),
json.dumps([text_color, highlight_color]),
json.dumps(self.note.tags),
json.dumps(self.mw.pm.tags_collapsed(self.editorMode)),
json.dumps(self.mw.col.get_config("renderMathjax", True)),
json.dumps(self.mw.col.get_config("shrinkEditorImages", True)),
)
@ -1167,6 +1169,12 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
not self.mw.col.get_config("shrinkEditorImages", True),
)
def collapseTags(self) -> None:
aqt.mw.pm.set_tags_collapsed(self.editorMode, True)
def expandTags(self) -> None:
aqt.mw.pm.set_tags_collapsed(self.editorMode, False)
# Links from HTML
######################################################################
@ -1195,6 +1203,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
mathjaxChemistry=Editor.insertMathjaxChemistry,
toggleMathjax=Editor.toggleMathjax,
toggleShrinkImages=Editor.toggleShrinkImages,
expandTags=Editor.expandTags,
collapseTags=Editor.collapseTags,
)

View File

@ -28,6 +28,7 @@ from aqt.utils import disable_help_button, send_to_trash, showWarning, tr
if TYPE_CHECKING:
from aqt.browser.layout import BrowserLayout
from aqt.editor import EditorMode
# Profile handling
@ -553,6 +554,21 @@ create table if not exists profiles
def set_browser_layout(self, layout: BrowserLayout) -> None:
self.meta["browser_layout"] = layout.value
def editor_key(self, mode: EditorMode) -> str:
from aqt.editor import EditorMode
return {
EditorMode.ADD_CARDS: "add",
EditorMode.BROWSER: "browser",
EditorMode.EDIT_CURRENT: "current",
}[mode]
def tags_collapsed(self, mode: EditorMode) -> bool:
return self.meta.get(f"{self.editor_key(mode)}TagsCollapsed", False)
def set_tags_collapsed(self, mode: EditorMode, collapsed: bool) -> None:
self.meta[f"{self.editor_key(mode)}TagsCollapsed"] = collapsed
def legacy_import_export(self) -> bool:
return self.meta.get("legacy_import", False)

View File

@ -59,6 +59,14 @@ sass_library(
visibility = ["//visibility:public"],
)
sass_library(
name = "panes_lib",
srcs = [
"panes.scss",
],
visibility = ["//visibility:public"],
)
sass_library(
name = "breakpoints_lib",
srcs = [

29
sass/panes.scss Normal file
View File

@ -0,0 +1,29 @@
/* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
@mixin resizable($direction, $width-resizable, $height-resizable) {
display: flex;
flex-flow: #{$direction} nowrap;
flex-basis: 0;
flex-grow: var(--pane-size);
overflow: hidden;
overflow-y: auto;
&.resize {
flex-basis: auto;
@if $width-resizable {
&.resize-width {
width: var(--resized-width);
}
}
@if $height-resizable {
&.resize-height {
height: var(--resized-height);
}
}
}
}

View File

@ -41,6 +41,7 @@ svelte_check(
"//sass:base_lib",
"//sass:button_mixins_lib",
"//sass:scrollbar_lib",
"//sass:panes_lib",
"//sass:breakpoints_lib",
"//sass:elevation_lib",
"//sass/bootstrap",

View File

@ -0,0 +1,117 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { on } from "../lib/events";
import { Callback, singleCallback } from "../lib/typing";
import IconConstrain from "./IconConstrain.svelte";
import { horizontalHandle } from "./icons";
import type { ResizablePane } from "./types";
export let panes: ResizablePane[];
export let index = 0;
export let tip = "";
export let clientHeight: number;
let destroy: Callback;
let before: ResizablePane;
let after: ResizablePane;
$: resizerAmount = panes.length - 1;
$: componentsHeight = clientHeight - resizerHeight * resizerAmount;
export function move(targets: ResizablePane[], targetHeight: number): void {
const [resizeTarget, resizePartner] = targets;
if (targetHeight <= resizeTarget.maxHeight) {
resizeTarget.resizable.getHeightResizer().setSize(targetHeight);
resizePartner.resizable
.getHeightResizer()
.setSize(componentsHeight - targetHeight);
}
}
function onMove(this: Window, { movementY }: PointerEvent): void {
if (movementY < 0) {
if (after.height - movementY <= after.maxHeight) {
const resized = before.resizable.getHeightResizer().resize(movementY);
after.resizable.getHeightResizer().resize(-resized);
} else {
const resized = before.resizable
.getHeightResizer()
.resize(after.height - after.maxHeight);
after.resizable.getHeightResizer().resize(-resized);
}
} else if (before.height + movementY <= before.maxHeight) {
const resized = after.resizable.getHeightResizer().resize(-movementY);
before.resizable.getHeightResizer().resize(-resized);
} else {
const resized = after.resizable
.getHeightResizer()
.resize(before.height - before.maxHeight);
before.resizable.getHeightResizer().resize(-resized);
}
}
let resizerHeight: number;
function releasePointer(this: Window): void {
destroy();
document.exitPointerLock();
for (const pane of panes) {
pane.resizable.getHeightResizer().stop(componentsHeight, panes.length);
}
}
function lockPointer(this: HTMLDivElement) {
this.requestPointerLock();
before = panes[index];
after = panes[index + 1];
for (const pane of panes) {
pane.resizable.getHeightResizer().start();
}
destroy = singleCallback(
on(window, "pointermove", onMove),
on(window, "pointerup", releasePointer),
);
}
</script>
<div
class="horizontal-resizer"
title={tip}
bind:clientHeight={resizerHeight}
on:pointerdown|preventDefault={lockPointer}
on:dblclick
>
<div class="drag-handle">
<IconConstrain iconSize={80}>{@html horizontalHandle}</IconConstrain>
</div>
</div>
<style lang="scss">
.horizontal-resizer {
width: 100%;
cursor: row-resize;
position: relative;
height: 10px;
border-top: 1px solid var(--border);
z-index: 20;
.drag-handle {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
opacity: 0.4;
}
&:hover .drag-handle {
opacity: 0.8;
}
}
</style>

63
ts/components/Pane.svelte Normal file
View File

@ -0,0 +1,63 @@
<!--
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 { writable } from "svelte/store";
import { resizable, Resizer } from "./resizable";
export let baseSize = 600;
const resizes = writable(false);
const paneSize = writable(baseSize);
const [
{ resizesDimension: resizesWidth, resizedDimension: resizedWidth },
widthAction,
widthResizer,
] = resizable(baseSize, resizes, paneSize);
const [
{ resizesDimension: resizesHeight, resizedDimension: resizedHeight },
heightAction,
heightResizer,
] = resizable(baseSize, resizes, paneSize);
const dispatch = createEventDispatcher();
$: resizeArgs = { width: $resizedWidth, height: $resizedHeight };
$: dispatch("resize", resizeArgs);
export function getHeightResizer(): Resizer {
return heightResizer;
}
export function getWidthResizer(): Resizer {
return widthResizer;
}
</script>
<div
class="pane"
class:resize={$resizes}
class:resize-width={$resizesWidth}
class:resize-height={$resizesHeight}
style:--pane-size={$paneSize}
style:--resized-width="{$resizedWidth}px"
style:--resized-height="{$resizedHeight}px"
on:focusin
on:pointerdown
use:widthAction={(element) => element.offsetWidth}
use:heightAction={(element) => element.offsetHeight}
>
<slot />
</div>
<style lang="scss">
@use "sass/panes" as panes;
.pane {
@include panes.resizable(column, true, true);
}
</style>

View File

@ -0,0 +1,81 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { promiseWithResolver } from "../lib/promise";
export let scroll = true;
const [element, elementResolve] = promiseWithResolver<HTMLElement>();
let clientWidth = 0;
let clientHeight = 0;
let scrollWidth = 0;
let scrollHeight = 0;
let scrollTop = 0;
let scrollLeft = 0;
$: overflowTop = scrollTop > 0;
$: overflowBottom = scrollTop < scrollHeight - clientHeight;
$: overflowLeft = scrollLeft > 0;
$: overflowRight = scrollLeft < scrollWidth - clientWidth;
$: shadows = {
top: overflowTop ? "0 5px" : null,
bottom: overflowBottom ? "0 -5px" : null,
left: overflowLeft ? "5px 0" : null,
right: overflowRight ? "-5px 0" : null,
};
const rest = "5px -5px var(--shadow)";
$: shadow = Array.from(
Object.values(shadows).filter((v) => v != null),
(v) => `inset ${v} ${rest}`,
).join(", ");
async function updateScrollState(): Promise<void> {
const el = await element;
scrollHeight = el.scrollHeight;
scrollWidth = el.scrollWidth;
scrollTop = el.scrollTop;
scrollLeft = el.scrollLeft;
}
</script>
<div
class="pane-content"
class:scroll
style:--box-shadow={shadow}
style:--client-height="{clientHeight}px"
use:elementResolve
bind:clientHeight
bind:clientWidth
on:scroll={updateScrollState}
on:resize={updateScrollState}
>
<slot />
</div>
<style lang="scss">
.pane-content {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
&.scroll {
overflow: auto;
}
/* force box-shadow to be rendered above children */
&::before {
content: "";
position: fixed;
pointer-events: none;
left: 0;
right: 0;
z-index: 4;
height: var(--client-height);
box-shadow: var(--box-shadow);
transition: box-shadow 0.1s ease-in-out;
}
}
</style>

View File

@ -0,0 +1,101 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { on } from "../lib/events";
import { Callback, singleCallback } from "../lib/typing";
import IconConstrain from "./IconConstrain.svelte";
import { verticalHandle } from "./icons";
import type { ResizablePane } from "./types";
export let components: ResizablePane[];
export let index = 0;
export let clientWidth: number;
let destroy: Callback;
let before: ResizablePane;
let after: ResizablePane;
function onMove(this: Window, { movementX }: PointerEvent): void {
if (movementX < 0) {
const resized = before.resizable.getWidthResizer().resize(movementX);
after.resizable.getWidthResizer().resize(-resized);
} else if (movementX > 0) {
const resized = after.resizable.getWidthResizer().resize(-movementX);
before.resizable.getWidthResizer().resize(-resized);
}
}
let minWidth: number;
function releasePointer(this: Window): void {
destroy();
document.exitPointerLock();
const resizerAmount = components.length - 1;
const componentsWidth = clientWidth - minWidth * resizerAmount;
for (const component of components) {
component.resizable
.getWidthResizer()
.stop(componentsWidth, components.length);
}
}
function lockPointer(this: HTMLHRElement) {
this.requestPointerLock();
before = components[index];
after = components[index + 1];
for (const component of components) {
component.resizable.getWidthResizer().start();
}
destroy = singleCallback(
on(window, "pointermove", onMove),
on(window, "pointerup", releasePointer),
);
}
</script>
<div
bind:clientWidth={minWidth}
class="vertical-resizer"
on:pointerdown|preventDefault={lockPointer}
>
<div class="drag-handle">
<IconConstrain iconSize={80}>{@html verticalHandle}</IconConstrain>
</div>
</div>
<style lang="scss">
.vertical-resizer {
height: 100%;
cursor: col-resize;
position: relative;
z-index: 20;
.drag-handle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0.4;
}
&::before {
content: "";
position: absolute;
width: 10px;
left: -5px;
top: 0;
height: 100%;
}
&:hover .drag-handle {
opacity: 0.8;
}
}
</style>

View File

@ -3,6 +3,10 @@
/// <reference types="../lib/image-import" />
export { default as hsplitIcon } from "@mdi/svg/svg/arrow-split-horizontal.svg";
export { default as vsplitIcon } from "@mdi/svg/svg/arrow-split-vertical.svg";
export { default as chevronDown } from "@mdi/svg/svg/chevron-down.svg";
export { default as chevronLeft } from "@mdi/svg/svg/chevron-left.svg";
export { default as chevronRight } from "@mdi/svg/svg/chevron-right.svg";
export { default as horizontalHandle } from "@mdi/svg/svg/drag-horizontal.svg";
export { default as verticalHandle } from "@mdi/svg/svg/drag-vertical.svg";

View File

@ -0,0 +1,87 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { Writable } from "svelte/store";
import { writable } from "svelte/store";
export interface Resizer {
start(): void;
/**
* @returns Actually applied resize. If the resizedWidth is too small,
* no resize can be applied anymore.
*/
resize(increment: number): number;
setSize(size: number): void;
stop(fullWidth: number, amount: number): void;
}
interface ResizedStores {
resizesDimension: Writable<boolean>;
resizedDimension: Writable<number>;
}
type ResizableResult = [
ResizedStores,
(element: HTMLElement, getter: (element: HTMLElement) => number) => void,
Resizer,
];
export function resizable(
baseSize: number,
resizes: Writable<boolean>,
paneSize: Writable<number>,
): ResizableResult {
const resizesDimension = writable(false);
const resizedDimension = writable(0);
let pane: HTMLElement;
let getter: (element: HTMLElement) => number;
let dimension = 0;
function resizeAction(
element: HTMLElement,
getValue: (element: HTMLElement) => number,
): void {
pane = element;
getter = getValue;
}
function start() {
resizes.set(true);
resizesDimension.set(true);
dimension = getter(pane);
resizedDimension.set(dimension);
}
function resize(increment = 0): number {
if (dimension + increment < 0) {
const applied = -dimension;
dimension = 0;
resizedDimension.set(dimension);
return applied;
}
dimension += increment;
resizedDimension.set(dimension);
return increment;
}
function setSize(size = 0): void {
paneSize.set(size);
}
function stop(fullDimension: number, amount: number): void {
paneSize.set((dimension / fullDimension) * amount * baseSize);
resizesDimension.set(false);
resizes.set(false);
}
return [
{ resizesDimension, resizedDimension },
resizeAction,
{ start, resize, setSize, stop },
];
}

View File

@ -1,5 +1,14 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type Pane from "./Pane.svelte";
export type Size = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
export type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl" | "xxl";
export class ResizablePane {
resizable = {} as Pane;
height = 0;
minHeight = 0;
maxHeight = Infinity;
}

View File

@ -20,9 +20,6 @@ Contains the fields. This contains the scrollable area.
/* Add space after the last field and the start of the tag editor */
padding-bottom: 5px;
/* Move the scrollbar for the NoteEditor into this element */
overflow-y: auto;
/* Push the tag editor to the bottom of the note editor */
flex-grow: 1;
}

View File

@ -44,8 +44,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import Absolute from "../components/Absolute.svelte";
import Badge from "../components/Badge.svelte";
import HorizontalResizer from "../components/HorizontalResizer.svelte";
import Pane from "../components/Pane.svelte";
import PaneContent from "../components/PaneContent.svelte";
import { ResizablePane } from "../components/types";
import { bridgeCommand } from "../lib/bridgecommand";
import { TagEditor } from "../tag-editor";
import TagAddButton from "../tag-editor/tag-options-button/TagAddButton.svelte";
import { ChangeTimer } from "./change-timer";
import DecoratedElements from "./DecoratedElements.svelte";
import { clearableArray } from "./destroyable";
@ -165,6 +170,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$tags = ts;
}
const tagsCollapsed = writable<boolean>();
export function setTagsCollapsed(collapsed: boolean): void {
$tagsCollapsed = collapsed;
if (collapsed) {
lowerResizer.move([tagsPane, fieldsPane], tagsPane.minHeight);
}
}
let noteId: number | null = null;
export function setNoteId(ntid: number): void {
// TODO this is a hack, because it requires the NoteEditor to know implementation details of the PlainTextInput.
@ -206,6 +219,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
})) as FieldData[];
function saveTags({ detail }: CustomEvent): void {
tagAmount = detail.tags.filter((tag: string) => tag != "").length;
bridgeCommand(`saveTags:${JSON.stringify(detail.tags)}`);
}
@ -288,6 +302,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setFonts,
focusField,
setTags,
setTagsCollapsed,
setBackgrounds,
setClozeHint,
saveNow: saveFieldNow,
@ -323,6 +338,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setContextProperty(api);
setupLifecycleHooks(api);
let clientHeight: number;
const fieldsPane = new ResizablePane();
const tagsPane = new ResizablePane();
let lowerResizer: HorizontalResizer;
let tagEditor: TagEditor;
$: tagAmount = $tags.length;
function collapseTags(): void {
lowerResizer.move([tagsPane, fieldsPane], tagsPane.minHeight);
}
function expandTags(): void {
lowerResizer.move([tagsPane, fieldsPane], tagsPane.maxHeight);
}
</script>
<!--
@ -333,7 +366,7 @@ components and functionality for general note editing.
Functionality exclusive to specifc note-editing views (e.g. in the browser or
the AddCards dialog) should be implemented in the user of this component.
-->
<div class="note-editor">
<div class="note-editor" bind:clientHeight>
<EditorToolbar {size} {wrap} api={toolbar}>
<slot slot="notetypeButtons" name="notetypeButtons" />
</EditorToolbar>
@ -341,7 +374,7 @@ the AddCards dialog) should be implemented in the user of this component.
{#if hint}
<Absolute bottom right --margin="10px">
<Notification>
<Badge --badge-color="var(--accent-danger)" --icon-align="top"
<Badge --badge-color="tomato" --icon-align="top"
>{@html alertIcon}</Badge
>
<span>{@html hint}</span>
@ -349,162 +382,227 @@ the AddCards dialog) should be implemented in the user of this component.
</Absolute>
{/if}
<Fields>
<DecoratedElements>
{#each fieldsData as field, index}
{@const content = fieldStores[index]}
<Pane
bind:this={fieldsPane.resizable}
on:resize={(e) => (fieldsPane.height = e.detail.height)}
>
<PaneContent>
<Fields>
<DecoratedElements>
{#each fieldsData as field, index}
{@const content = fieldStores[index]}
<EditorField
{field}
{content}
flipInputs={plainTextDefaults[index]}
api={fields[index]}
on:focusin={() => {
$focusedField = fields[index];
bridgeCommand(`focus:${index}`);
}}
on:focusout={() => {
$focusedField = null;
bridgeCommand(
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
get(content),
)}`,
);
}}
on:mouseenter={() => {
$hoveredField = fields[index];
}}
on:mouseleave={() => {
$hoveredField = null;
}}
collapsed={fieldsCollapsed[index]}
--label-color={cols[index] === "dupe"
? "palette-of(flag-1)"
: "palette-of(canvas)"}
>
<svelte:fragment slot="field-label">
<LabelContainer
collapsed={fieldsCollapsed[index]}
on:toggle={async () => {
fieldsCollapsed[index] = !fieldsCollapsed[index];
const defaultInput = !plainTextDefaults[index]
? richTextInputs[index]
: plainTextInputs[index];
if (!fieldsCollapsed[index]) {
refocusInput(defaultInput.api);
} else if (!plainTextDefaults[index]) {
plainTextsHidden[index] = true;
} else {
richTextsHidden[index] = true;
}
<EditorField
{field}
{content}
flipInputs={plainTextDefaults[index]}
api={fields[index]}
on:focusin={() => {
$focusedField = fields[index];
bridgeCommand(`focus:${index}`);
}}
on:focusout={() => {
$focusedField = null;
bridgeCommand(
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
get(content),
)}`,
);
}}
on:mouseenter={() => {
$hoveredField = fields[index];
}}
on:mouseleave={() => {
$hoveredField = null;
}}
collapsed={fieldsCollapsed[index]}
--label-color={cols[index] === "dupe"
? "palette-of(flag-1)"
: "palette-of(canvas)"}
>
<svelte:fragment slot="field-name">
<LabelName>
{field.name}
</LabelName>
<svelte:fragment slot="field-label">
<LabelContainer
collapsed={fieldsCollapsed[index]}
on:toggle={async () => {
fieldsCollapsed[index] =
!fieldsCollapsed[index];
const defaultInput = !plainTextDefaults[index]
? richTextInputs[index]
: plainTextInputs[index];
if (!fieldsCollapsed[index]) {
refocusInput(defaultInput.api);
} else if (!plainTextDefaults[index]) {
plainTextsHidden[index] = true;
} else {
richTextsHidden[index] = true;
}
}}
>
<svelte:fragment slot="field-name">
<LabelName>
{field.name}
</LabelName>
</svelte:fragment>
<FieldState>
{#if cols[index] === "dupe"}
<DuplicateLink />
{/if}
{#if plainTextDefaults[index]}
<RichTextBadge
visible={!fieldsCollapsed[index] &&
(fields[index] === $hoveredField ||
fields[index] ===
$focusedField)}
bind:off={richTextsHidden[index]}
on:toggle={async () => {
richTextsHidden[index] =
!richTextsHidden[index];
if (!richTextsHidden[index]) {
refocusInput(
richTextInputs[index].api,
);
}
}}
/>
{:else}
<PlainTextBadge
visible={!fieldsCollapsed[index] &&
(fields[index] === $hoveredField ||
fields[index] ===
$focusedField)}
bind:off={plainTextsHidden[index]}
on:toggle={async () => {
plainTextsHidden[index] =
!plainTextsHidden[index];
if (!plainTextsHidden[index]) {
refocusInput(
plainTextInputs[index].api,
);
}
}}
/>
{/if}
<slot
name="field-state"
{field}
{index}
visible={fields[index] === $hoveredField ||
fields[index] === $focusedField}
/>
</FieldState>
</LabelContainer>
</svelte:fragment>
<FieldState>
{#if cols[index] === "dupe"}
<DuplicateLink />
{/if}
{#if plainTextDefaults[index]}
<RichTextBadge
visible={!fieldsCollapsed[index] &&
(fields[index] === $hoveredField ||
fields[index] === $focusedField)}
bind:off={richTextsHidden[index]}
on:toggle={async () => {
richTextsHidden[index] =
!richTextsHidden[index];
if (!richTextsHidden[index]) {
refocusInput(richTextInputs[index].api);
}
<svelte:fragment slot="rich-text-input">
<Collapsible
collapse={richTextsHidden[index]}
let:collapsed={hidden}
>
<RichTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
/>
{:else}
<PlainTextBadge
visible={!fieldsCollapsed[index] &&
(fields[index] === $hoveredField ||
fields[index] === $focusedField)}
bind:off={plainTextsHidden[index]}
on:toggle={async () => {
plainTextsHidden[index] =
!plainTextsHidden[index];
if (!plainTextsHidden[index]) {
refocusInput(
plainTextInputs[index].api,
);
}
bind:this={richTextInputs[index]}
>
<ImageHandle maxWidth={250} maxHeight={125} />
<MathjaxHandle />
{#if insertSymbols}
<SymbolsOverlay />
{/if}
<FieldDescription>
{field.description}
</FieldDescription>
</RichTextInput>
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="plain-text-input">
<Collapsible
collapse={plainTextsHidden[index]}
let:collapsed={hidden}
>
<PlainTextInput
{hidden}
isDefault={plainTextDefaults[index]}
richTextHidden={richTextsHidden[index]}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={plainTextInputs[index]}
/>
{/if}
<slot
name="field-state"
{field}
{index}
visible={fields[index] === $hoveredField ||
fields[index] === $focusedField}
/>
</FieldState>
</LabelContainer>
</svelte:fragment>
<svelte:fragment slot="rich-text-input">
<Collapsible
collapse={richTextsHidden[index]}
let:collapsed={hidden}
>
<RichTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={richTextInputs[index]}
>
<ImageHandle maxWidth={250} maxHeight={125} />
<MathjaxHandle />
{#if insertSymbols}
<SymbolsOverlay />
{/if}
<FieldDescription>
{field.description}
</FieldDescription>
</RichTextInput>
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="plain-text-input">
<Collapsible
collapse={plainTextsHidden[index]}
let:collapsed={hidden}
>
<PlainTextInput
{hidden}
isDefault={plainTextDefaults[index]}
richTextHidden={richTextsHidden[index]}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={plainTextInputs[index]}
/>
</Collapsible>
</svelte:fragment>
</EditorField>
{/each}
</Collapsible>
</svelte:fragment>
</EditorField>
{/each}
<MathjaxElement />
<FrameElement />
</DecoratedElements>
</Fields>
<MathjaxElement />
<FrameElement />
</DecoratedElements>
</Fields>
</PaneContent>
</Pane>
<div class="note-editor-tag-editor">
<TagEditor {tags} on:tagsupdate={saveTags} />
</div>
{#if $tagsCollapsed}
<div class="tags-expander">
<TagAddButton
on:tagappend={() => {
tagEditor.appendEmptyTag();
}}
keyCombination="Control+Shift+T"
>
{@html tagAmount > 0 ? `${tagAmount} Tags` : ""}
</TagAddButton>
</div>
{/if}
<HorizontalResizer
panes={[fieldsPane, tagsPane]}
tip={`Double click to ${$tagsCollapsed ? "expand" : "collapse"} tag editor`}
{clientHeight}
bind:this={lowerResizer}
on:dblclick={() => {
if ($tagsCollapsed) {
expandTags();
bridgeCommand("expandTags");
$tagsCollapsed = false;
} else {
collapseTags();
bridgeCommand("collapseTags");
$tagsCollapsed = true;
}
}}
/>
<Pane
bind:this={tagsPane.resizable}
on:resize={(e) => {
tagsPane.height = e.detail.height;
$tagsCollapsed = tagsPane.height == 0;
}}
>
<PaneContent scroll={false}>
<TagEditor
{tags}
bind:this={tagEditor}
on:tagsupdate={saveTags}
on:tagsFocused={() => {
expandTags();
$tagsCollapsed = false;
}}
on:heightChange={(e) => {
tagsPane.maxHeight = e.detail.height;
if (!$tagsCollapsed) {
expandTags();
}
}}
/>
</PaneContent>
</Pane>
</div>
<style lang="scss">
@ -513,12 +611,7 @@ the AddCards dialog) should be implemented in the user of this component.
flex-direction: column;
height: 100%;
}
.note-editor-tag-editor {
padding: 2px 0 0;
border-width: thin 0 0;
border-style: solid;
border-color: var(--border-subtle);
.tags-expander {
margin-top: 0.5rem;
}
</style>

View File

@ -9,6 +9,7 @@ $btn-disabled-opacity: 0.4;
@import "sass/bootstrap/scss/dropdown";
@import "sass/bootstrap-tooltip";
html {
html,
body {
overflow: hidden;
}

View File

@ -48,6 +48,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</script>
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { writable } from "svelte/store";
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
@ -83,9 +84,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} as EditorToolbarAPI);
setContextProperty(api);
const dispatch = createEventDispatcher();
let clientHeight: number;
$: dispatch("heightChange", { height: clientHeight });
</script>
<div class="editor-toolbar">
<div class="editor-toolbar" bind:clientHeight>
<ButtonToolbar {size} {wrap}>
<DynamicallySlottable slotHost={Item} api={toolbar}>
<Item id="notetype">
@ -119,10 +125,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<style lang="scss">
.editor-toolbar {
padding: 0 0 2px;
border-width: 0 0 thin;
border-style: solid;
border-color: var(--border-subtle);
padding: 0 0 4px;
border-bottom: 1px solid var(--border);
}
</style>

View File

@ -111,7 +111,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
}
function appendEmptyTag(): void {
export function appendEmptyTag(): void {
// used by tag badge and tag spacer
deselect();
const lastTag = tagTypes[tagTypes.length - 1];
@ -380,6 +380,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: assumedRows = Math.floor(height / badgeHeight);
$: shortenTags = shortenTags || assumedRows > 2;
$: anyTagsSelected = tagTypes.some((tag) => tag.selected);
$: dispatch("heightChange", { height: height * 1.15 });
</script>
<div class="tag-editor" on:focusout={deselectIfLeave} bind:offsetHeight={height}>
@ -435,6 +437,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
bind:name={activeName}
bind:input={activeInput}
on:focus={() => {
dispatch("tagsFocused");
activeName = tag.name;
autocomplete = createAutocomplete();
}}

View File

@ -31,6 +31,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
{@html tagIcon}
{@html addTagIcon}
</IconConstrain>
<span class="tags-info">
<slot />
</span>
</div>
<Shortcut {keyCombination} on:action={() => dispatch("tagappend")} />
@ -63,5 +66,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
:global(svg:hover) {
opacity: 1;
}
.tags-info {
cursor: pointer;
color: var(--fg-subtle);
margin-left: 0.75rem;
}
}
:global([dir="rtl"]) .tags-info {
margin-right: 0.75rem;
}
</style>