diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py
index d386dd1b2..715cf9f51 100644
--- a/qt/aqt/editor.py
+++ b/qt/aqt/editor.py
@@ -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,
)
diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py
index 1e1a4c284..8fea6442f 100644
--- a/qt/aqt/profiles.py
+++ b/qt/aqt/profiles.py
@@ -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)
diff --git a/sass/BUILD.bazel b/sass/BUILD.bazel
index 85b3b57c2..9171d5f9d 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 000000000..713555fb5
--- /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 1e47ed39f..c457e8de5 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 000000000..c43af360e
--- /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 000000000..c291947ee
--- /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 000000000..7a9c4a903
--- /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 000000000..f85267061
--- /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 66fb4539c..b26924bf9 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 000000000..13ee8b331
--- /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 220585537..bd29121ea 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 a065914e5..e12f46eae 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 bd8ed5029..8b0166873 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]}
+ (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)"}
- >
-
- {
- 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;
- }
+ {
+ $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 1e942aa3e..8bda4c2aa 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 09ab00ecf..b00c78dfc 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
-