From f72570c604ebd6c949b3145ac7ef76a5c609db09 Mon Sep 17 00:00:00 2001 From: Matthias Metelka <62722460+kleinerpirat@users.noreply.github.com> Date: Wed, 28 Sep 2022 06:02:32 +0200 Subject: [PATCH] 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 * Remove PaneContent padding Co-Authored-By: Henrik Giesel * 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 --- qt/aqt/editor.py | 10 + qt/aqt/profiles.py | 16 + sass/BUILD.bazel | 8 + sass/panes.scss | 29 ++ ts/components/BUILD.bazel | 1 + ts/components/HorizontalResizer.svelte | 117 +++++ ts/components/Pane.svelte | 63 +++ ts/components/PaneContent.svelte | 81 ++++ ts/components/VerticalResizer.svelte | 101 +++++ ts/components/icons.ts | 4 + ts/components/resizable.ts | 87 ++++ ts/components/types.ts | 9 + ts/editor/Fields.svelte | 3 - ts/editor/NoteEditor.svelte | 411 +++++++++++------- ts/editor/editor-base.scss | 3 +- ts/editor/editor-toolbar/EditorToolbar.svelte | 15 +- ts/tag-editor/TagEditor.svelte | 5 +- .../tag-options-button/TagAddButton.svelte | 11 + 18 files changed, 804 insertions(+), 170 deletions(-) create mode 100644 sass/panes.scss create mode 100644 ts/components/HorizontalResizer.svelte create mode 100644 ts/components/Pane.svelte create mode 100644 ts/components/PaneContent.svelte create mode 100644 ts/components/VerticalResizer.svelte create mode 100644 ts/components/resizable.ts diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index d386dd1b244..715cf9f5159 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -533,6 +533,7 @@ def oncallback(arg: Any) -> None: setNoteId({}); setColorButtons({}); setTags({}); + setTagsCollapsed({}); setMathjaxEnabled({}); setShrinkImages({}); """.format( @@ -545,6 +546,7 @@ def oncallback(arg: Any) -> None: 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 @@ def toggleShrinkImages(self) -> None: 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 @@ def _init_links(self) -> None: mathjaxChemistry=Editor.insertMathjaxChemistry, toggleMathjax=Editor.toggleMathjax, toggleShrinkImages=Editor.toggleShrinkImages, + expandTags=Editor.expandTags, + collapseTags=Editor.collapseTags, ) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 1e1a4c284ad..8fea6442fd9 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -28,6 +28,7 @@ if TYPE_CHECKING: from aqt.browser.layout import BrowserLayout + from aqt.editor import EditorMode # Profile handling @@ -553,6 +554,21 @@ def browser_layout(self) -> BrowserLayout: 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) diff --git a/sass/BUILD.bazel b/sass/BUILD.bazel index 85b3b57c22c..9171d5f9da7 100644 --- a/sass/BUILD.bazel +++ b/sass/BUILD.bazel @@ -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 = [ diff --git a/sass/panes.scss b/sass/panes.scss new file mode 100644 index 00000000000..713555fb51d --- /dev/null +++ b/sass/panes.scss @@ -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); + } + } + } +} diff --git a/ts/components/BUILD.bazel b/ts/components/BUILD.bazel index 1e47ed39fe5..c457e8de5c7 100644 --- a/ts/components/BUILD.bazel +++ b/ts/components/BUILD.bazel @@ -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", diff --git a/ts/components/HorizontalResizer.svelte b/ts/components/HorizontalResizer.svelte new file mode 100644 index 00000000000..c43af360ea1 --- /dev/null +++ b/ts/components/HorizontalResizer.svelte @@ -0,0 +1,117 @@ + + + +
+
+ {@html horizontalHandle} +
+
+ + diff --git a/ts/components/Pane.svelte b/ts/components/Pane.svelte new file mode 100644 index 00000000000..c291947eea4 --- /dev/null +++ b/ts/components/Pane.svelte @@ -0,0 +1,63 @@ + + + +
element.offsetWidth} + use:heightAction={(element) => element.offsetHeight} +> + +
+ + diff --git a/ts/components/PaneContent.svelte b/ts/components/PaneContent.svelte new file mode 100644 index 00000000000..7a9c4a90355 --- /dev/null +++ b/ts/components/PaneContent.svelte @@ -0,0 +1,81 @@ + + + +
+ +
+ + diff --git a/ts/components/VerticalResizer.svelte b/ts/components/VerticalResizer.svelte new file mode 100644 index 00000000000..f85267061be --- /dev/null +++ b/ts/components/VerticalResizer.svelte @@ -0,0 +1,101 @@ + + + +
+
+ {@html verticalHandle} +
+
+ + diff --git a/ts/components/icons.ts b/ts/components/icons.ts index 66fb4539c5d..b26924bf95f 100644 --- a/ts/components/icons.ts +++ b/ts/components/icons.ts @@ -3,6 +3,10 @@ /// +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"; diff --git a/ts/components/resizable.ts b/ts/components/resizable.ts new file mode 100644 index 00000000000..13ee8b3313c --- /dev/null +++ b/ts/components/resizable.ts @@ -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; + resizedDimension: Writable; +} + +type ResizableResult = [ + ResizedStores, + (element: HTMLElement, getter: (element: HTMLElement) => number) => void, + Resizer, +]; + +export function resizable( + baseSize: number, + resizes: Writable, + paneSize: Writable, +): 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 }, + ]; +} diff --git a/ts/components/types.ts b/ts/components/types.ts index 220585537a3..bd29121ea3b 100644 --- a/ts/components/types.ts +++ b/ts/components/types.ts @@ -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; +} diff --git a/ts/editor/Fields.svelte b/ts/editor/Fields.svelte index a065914e59c..e12f46eaed5 100644 --- a/ts/editor/Fields.svelte +++ b/ts/editor/Fields.svelte @@ -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; } diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte index bd8ed502935..8b016687361 100644 --- a/ts/editor/NoteEditor.svelte +++ b/ts/editor/NoteEditor.svelte @@ -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(); + 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); + } -
+
@@ -341,7 +374,7 @@ the AddCards dialog) should be implemented in the user of this component. {#if hint} - {@html alertIcon} {@html hint} @@ -349,162 +382,227 @@ the AddCards dialog) should be implemented in the user of this component. {/if} - - - {#each fieldsData as field, index} - {@const content = fieldStores[index]} - - { - $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)"} - > - - { - 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; - } + (fieldsPane.height = e.detail.height)} + > + + + + {#each fieldsData as field, index} + {@const content = fieldStores[index]} + + { + $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)"} > - - - {field.name} - + + { + 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; + } + }} + > + + + {field.name} + + + + {#if cols[index] === "dupe"} + + {/if} + {#if plainTextDefaults[index]} + { + richTextsHidden[index] = + !richTextsHidden[index]; + + if (!richTextsHidden[index]) { + refocusInput( + richTextInputs[index].api, + ); + } + }} + /> + {:else} + { + plainTextsHidden[index] = + !plainTextsHidden[index]; + + if (!plainTextsHidden[index]) { + refocusInput( + plainTextInputs[index].api, + ); + } + }} + /> + {/if} + + + - - {#if cols[index] === "dupe"} - - {/if} - {#if plainTextDefaults[index]} - { - richTextsHidden[index] = - !richTextsHidden[index]; - - if (!richTextsHidden[index]) { - refocusInput(richTextInputs[index].api); - } + + + { + saveFieldNow(); + $focusedInput = null; }} - /> - {:else} - { - plainTextsHidden[index] = - !plainTextsHidden[index]; - - if (!plainTextsHidden[index]) { - refocusInput( - plainTextInputs[index].api, - ); - } + bind:this={richTextInputs[index]} + > + + + {#if insertSymbols} + + {/if} + + {field.description} + + + + + + + { + saveFieldNow(); + $focusedInput = null; }} + bind:this={plainTextInputs[index]} /> - {/if} - - - - - - - { - saveFieldNow(); - $focusedInput = null; - }} - bind:this={richTextInputs[index]} - > - - - {#if insertSymbols} - - {/if} - - {field.description} - - - - - - - { - saveFieldNow(); - $focusedInput = null; - }} - bind:this={plainTextInputs[index]} - /> - - - - {/each} - - - - - - -
- -
+ + + + {/each} + + + + + + + + + {#if $tagsCollapsed} +
+ { + tagEditor.appendEmptyTag(); + }} + keyCombination="Control+Shift+T" + > + {@html tagAmount > 0 ? `${tagAmount} Tags` : ""} + +
+ {/if} + + { + if ($tagsCollapsed) { + expandTags(); + bridgeCommand("expandTags"); + $tagsCollapsed = false; + } else { + collapseTags(); + bridgeCommand("collapseTags"); + $tagsCollapsed = true; + } + }} + /> + + { + tagsPane.height = e.detail.height; + $tagsCollapsed = tagsPane.height == 0; + }} + > + + { + expandTags(); + $tagsCollapsed = false; + }} + on:heightChange={(e) => { + tagsPane.maxHeight = e.detail.height; + if (!$tagsCollapsed) { + expandTags(); + } + }} + /> + +
diff --git a/ts/editor/editor-base.scss b/ts/editor/editor-base.scss index 1e942aa3e05..8bda4c2aa0d 100644 --- a/ts/editor/editor-base.scss +++ b/ts/editor/editor-base.scss @@ -9,6 +9,7 @@ $btn-disabled-opacity: 0.4; @import "sass/bootstrap/scss/dropdown"; @import "sass/bootstrap-tooltip"; -html { +html, +body { overflow: hidden; } diff --git a/ts/editor/editor-toolbar/EditorToolbar.svelte b/ts/editor/editor-toolbar/EditorToolbar.svelte index 09ab00ecf75..b00c78dfc92 100644 --- a/ts/editor/editor-toolbar/EditorToolbar.svelte +++ b/ts/editor/editor-toolbar/EditorToolbar.svelte @@ -48,6 +48,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -
+
@@ -119,10 +125,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html diff --git a/ts/tag-editor/TagEditor.svelte b/ts/tag-editor/TagEditor.svelte index 9438395ec6a..f78797812ec 100644 --- a/ts/tag-editor/TagEditor.svelte +++ b/ts/tag-editor/TagEditor.svelte @@ -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 });
@@ -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(); }} diff --git a/ts/tag-editor/tag-options-button/TagAddButton.svelte b/ts/tag-editor/tag-options-button/TagAddButton.svelte index 95d5d58e625..e66feb8659b 100644 --- a/ts/tag-editor/tag-options-button/TagAddButton.svelte +++ b/ts/tag-editor/tag-options-button/TagAddButton.svelte @@ -31,6 +31,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {@html tagIcon} {@html addTagIcon} + + +
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; }