Simplify NoteEditor by replacing Pane components with Collapsible (#2395)

* Remove Pane components and use Collapsible for TagEditor

* Update translations

* Give TagEditor border and focus outline

* Use ScrollArea from #2248 for fields

* Refactor ScrollArea

* Fix error caused by calling bridgeCommand when it's not available

* Make sure tag editor fills whole width of container

which is important for the CSV import page.

* Update NoteEditor.svelte

* Add back removed ftl strings

* Fix tests (dae)
This commit is contained in:
Matthias Metelka 2023-02-27 07:23:19 +01:00 committed by GitHub
parent d849abace2
commit b12476de9a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 371 additions and 689 deletions

View file

@ -56,6 +56,8 @@ editing-to-make-a-cloze-deletion-on = To make a cloze deletion on an existing no
editing-toggle-html-editor = Toggle HTML Editor
editing-toggle-visual-editor = Toggle Visual Editor
editing-toggle-sticky = Toggle sticky
editing-expand = Expand
editing-collapse = Collapse
editing-expand-field = Expand field
editing-collapse-field = Collapse field
editing-underline-text = Underline text

View file

@ -465,6 +465,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
if not self.addMode:
self._save_current_note()
elif cmd.startswith("setTagsCollapsed"):
(type, collapsed_string) = cmd.split(":", 1)
collapsed = collapsed_string == "true"
self.setTagsCollapsed(collapsed)
elif cmd in self._links:
return self._links[cmd](self)
@ -1165,11 +1170,8 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
not self.mw.col.get_config("closeHTMLTags", 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)
def setTagsCollapsed(self, collapsed: bool) -> None:
aqt.mw.pm.set_tags_collapsed(self.editorMode, collapsed)
# Links from HTML
######################################################################
@ -1200,8 +1202,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
toggleMathjax=Editor.toggleMathjax,
toggleShrinkImages=Editor.toggleShrinkImages,
toggleCloseHTMLTags=Editor.toggleCloseHTMLTags,
expandTags=Editor.expandTags,
collapseTags=Editor.collapseTags,
)

View file

@ -1,149 +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 { on } from "@tslib/events";
import type { Callback } from "@tslib/typing";
import { singleCallback } from "@tslib/typing";
import { createEventDispatcher } from "svelte";
import { fly } from "svelte/transition";
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 showIndicator = false;
export let clientHeight: number;
const rtl = window.getComputedStyle(document.body).direction == "rtl";
const dispatch = createEventDispatcher();
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.call(window);
dispatch("release");
}),
);
}
</script>
<div
class="horizontal-resizer"
class:rtl
title={tip}
bind:clientHeight={resizerHeight}
on:pointerdown|preventDefault={lockPointer}
on:dblclick|preventDefault
>
{#if showIndicator}
<div
class="resize-indicator"
transition:fly={{ x: rtl ? 25 : -25, duration: 200 }}
>
<slot />
</div>
{/if}
<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: 25px;
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;
}
.resize-indicator {
position: absolute;
font-size: small;
bottom: 0;
}
&.rtl .resize-indicator {
padding: 0.5rem 0 0 0.5rem;
right: 0;
}
}
</style>

View file

@ -1,65 +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 { writable } from "svelte/store";
import type { Resizer } from "./resizable";
import { resizable } 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);
opacity: var(--opacity, 1);
}
</style>

View file

@ -1,81 +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 { promiseWithResolver } from "@tslib/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 var(--transition) ease-in-out;
}
}
</style>

View file

@ -0,0 +1,147 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
let className: string = "";
export { className as class };
export let scrollX = false;
export let scrollY = false;
let scrollBarWidth = 0;
let scrollBarHeight = 0;
let measuring = true;
const scrollStates = {
top: false,
right: false,
bottom: false,
left: false,
};
function measureScrollbar(el: HTMLDivElement) {
scrollBarWidth = el.offsetWidth - el.clientWidth;
scrollBarHeight = el.offsetHeight - el.clientHeight;
measuring = false;
}
const callback = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
scrollStates[entry.target.getAttribute("data-edge")!] =
!entry.isIntersecting;
});
};
let observer: IntersectionObserver;
function initObserver(el: HTMLDivElement) {
observer = new IntersectionObserver(callback, { root: el });
for (const edge of el.getElementsByClassName("scroll-edge")) {
observer.observe(edge);
}
}
</script>
<div class="scroll-area-relative">
<div class="scroll-area-wrapper {className}">
<div
class="scroll-area"
class:measuring
class:scroll-x={scrollX}
class:scroll-y={scrollY}
style:--scrollbar-height="{scrollBarHeight}px"
use:measureScrollbar
use:initObserver
>
<div class="d-flex flex-column flex-grow-1">
<div class="scroll-edge" data-edge="top" />
<div class="d-flex flex-row flex-grow-1">
<div class="scroll-edge" data-edge="left" />
<div class="scroll-content flex-grow-1">
<slot />
</div>
<div class="scroll-edge" data-edge="right" />
</div>
<div class="scroll-edge" data-edge="bottom" />
</div>
</div>
{#if scrollStates.top} <div class="scroll-shadow top-0" /> {/if}
{#if scrollStates.bottom} <div class="scroll-shadow bottom-0" /> {/if}
{#if scrollStates.left} <div class="scroll-shadow start-0" /> {/if}
{#if scrollStates.right} <div class="scroll-shadow end-0" /> {/if}
</div>
</div>
<style lang="scss">
$shadow-top: inset 0 5px 5px -5px var(--shadow);
$shadow-bottom: inset 0 -5px 5px -5px var(--shadow);
$shadow-left: inset 5px 0 5px -5px var(--shadow);
$shadow-right: inset -5px 0 5px -5px var(--shadow);
.scroll-area-relative {
height: calc(var(--height) + var(--scrollbar-height));
flex-grow: 1;
position: relative;
}
.scroll-area {
position: absolute;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
overscroll-behavior: none;
overflow: auto;
&.scroll-x {
overflow-x: auto;
overflow-y: hidden;
overscroll-behavior-y: auto;
}
&.scroll-y {
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior-x: none;
}
&.measuring {
visibility: hidden;
overflow: scroll;
}
}
.scroll-edge {
&[data-edge="top"],
&[data-edge="bottom"] {
height: 1px;
}
&[data-edge="left"],
&[data-edge="right"] {
width: 1px;
}
}
.scroll-shadow {
position: absolute;
pointer-events: none;
// z-index between LabelContainer (editor) and FloatingArrow
z-index: 55;
&.top-0,
&.bottom-0 {
left: 0;
right: 0;
height: 5px;
}
&.start-0,
&.end-0 {
top: 0;
bottom: 0;
width: 5px;
}
&.top-0 {
box-shadow: $shadow-top;
}
&.bottom-0 {
box-shadow: $shadow-bottom;
}
&.start-0 {
box-shadow: $shadow-left;
}
&.end-0 {
box-shadow: $shadow-right;
}
}
</style>

View file

@ -1,103 +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 { on } from "@tslib/events";
import type { Callback } from "@tslib/typing";
import { singleCallback } from "@tslib/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

@ -7,10 +7,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { chevronDown } from "./icons";
export let collapsed = false;
export let highlighted = false;
</script>
<div class="collapse-badge" class:collapsed class:highlighted>
<div class="collapse-badge" class:collapsed>
<Badge iconSize={80}>{@html chevronDown}</Badge>
</div>
@ -20,7 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
opacity: 0.4;
transition: opacity var(--transition) ease-in-out,
transform var(--transition) ease-in;
&.highlighted {
:global(.collapse-label:hover) & {
opacity: 1;
}
&.collapsed {

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
-->
<script lang="ts">
import { createEventDispatcher } from "svelte";
import CollapseBadge from "./CollapseBadge.svelte";
export let collapsed: boolean;
export let tooltip: string;
const dispatch = createEventDispatcher();
function toggle() {
dispatch("toggle");
}
</script>
<span class="collapse-label" title={tooltip} on:click|stopPropagation={toggle}>
<CollapseBadge {collapsed} />
<slot />
</span>
<style lang="scss">
.collapse-label {
cursor: pointer;
}
</style>

View file

@ -132,7 +132,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/* make room for thicker focus border */
margin: 1px;
border-radius: 5px;
border-radius: var(--border-radius);
border: 1px solid var(--border);
@include elevation(1);

View file

@ -2,13 +2,19 @@
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import ScrollArea from "components/ScrollArea.svelte";
</script>
<!--
@component
Contains the fields. This contains the scrollable area.
-->
<div class="fields">
<slot />
</div>
<ScrollArea>
<div class="fields">
<slot />
</div>
</ScrollArea>
<style lang="scss">
.fields {

View file

@ -4,33 +4,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import * as tr from "@tslib/ftl";
import { createEventDispatcher } from "svelte";
import CollapseBadge from "./CollapseBadge.svelte";
import CollapseLabel from "./CollapseLabel.svelte";
export let collapsed: boolean;
let hovered = false;
$: tooltip = collapsed ? tr.editingExpandField() : tr.editingCollapseField();
const dispatch = createEventDispatcher();
function toggle() {
dispatch("toggle");
}
</script>
<div class="label-container">
<span
class="clickable"
title={tooltip}
on:click|stopPropagation={toggle}
on:mouseenter={() => (hovered = true)}
on:mouseleave={() => (hovered = false)}
>
<CollapseBadge {collapsed} highlighted={hovered} />
<CollapseLabel {collapsed} {tooltip} on:toggle>
<slot name="field-name" />
</span>
</CollapseLabel>
<slot />
</div>
@ -46,9 +31,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
position: sticky;
top: 0;
z-index: 50;
.clickable {
cursor: pointer;
}
}
</style>

View file

@ -47,12 +47,7 @@ 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 { TagEditor } from "../tag-editor";
import TagAddButton from "../tag-editor/tag-options-button/TagAddButton.svelte";
import { ChangeTimer } from "./change-timer";
import { clearableArray } from "./destroyable";
import DuplicateLink from "./DuplicateLink.svelte";
@ -197,9 +192,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const tagsCollapsed = writable<boolean>();
export function setTagsCollapsed(collapsed: boolean): void {
$tagsCollapsed = collapsed;
if (collapsed) {
lowerResizer.move([tagsPane, fieldsPane], tagsPane.minHeight);
}
}
function updateTagsCollapsed(collapsed: boolean) {
$tagsCollapsed = collapsed;
bridgeCommand(`setTagsCollapsed:${$tagsCollapsed}`);
}
let noteId: number | null = null;
@ -312,8 +309,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
import { wrapInternal } from "@tslib/wrap";
import Shortcut from "components/Shortcut.svelte";
import { mathjaxConfig } from "../editable/mathjax-element";
import CollapseLabel from "./CollapseLabel.svelte";
import { refocusInput } from "./helpers";
import * as oldEditorAdapter from "./old-editor-adapter";
@ -376,39 +375,7 @@ 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;
let snapTags = $tagsCollapsed;
function collapseTags(): void {
lowerResizer.move([tagsPane, fieldsPane], tagsPane.minHeight);
$tagsCollapsed = snapTags = true;
}
function expandTags(): void {
lowerResizer.move([tagsPane, fieldsPane], tagsPane.maxHeight);
$tagsCollapsed = snapTags = false;
}
window.addEventListener("resize", () => snapResizer(snapTags));
function snapResizer(collapse: boolean): void {
if (collapse) {
collapseTags();
bridgeCommand("collapseTags");
} else {
expandTags();
bridgeCommand("expandTags");
}
}
</script>
<!--
@ -419,7 +386,7 @@ components and functionality for general note editing.
Functionality exclusive to specific 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" bind:clientHeight>
<div class="note-editor">
<EditorToolbar {size} {wrap} api={toolbar}>
<slot slot="notetypeButtons" name="notetypeButtons" />
</EditorToolbar>
@ -435,223 +402,166 @@ the AddCards dialog) should be implemented in the user of this component.
</Absolute>
{/if}
<Pane
bind:this={fieldsPane.resizable}
on:resize={(e) => {
fieldsPane.height = e.detail.height;
}}
>
<PaneContent>
<Fields>
{#each fieldsData as field, index}
{@const content = fieldStores[index]}
<Fields>
{#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]}
dupe={cols[index] === "dupe"}
--description-font-size="{field.fontSize}px"
--description-content={`"${field.description}"`}
>
<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;
}
}}
--icon-align="bottom"
>
<svelte:fragment slot="field-name">
<LabelName>
{field.name}
</LabelName>
</svelte:fragment>
<FieldState>
{#if cols[index] === "dupe"}
<DuplicateLink />
{/if}
{#if plainTextDefaults[index]}
<RichTextBadge
show={!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
show={!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}
show={fields[index] === $hoveredField ||
fields[index] === $focusedField}
/>
</FieldState>
</LabelContainer>
</svelte:fragment>
<svelte:fragment slot="rich-text-input">
<Collapsible
collapse={richTextsHidden[index]}
let:collapsed={hidden}
toggleDisplay
>
<RichTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={richTextInputs[index]}
/>
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="plain-text-input">
<Collapsible
collapse={plainTextsHidden[index]}
let:collapsed={hidden}
toggleDisplay
>
<PlainTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={plainTextInputs[index]}
/>
</Collapsible>
</svelte:fragment>
</EditorField>
{/each}
<MathjaxOverlay />
<ImageOverlay maxWidth={250} maxHeight={125} />
{#if insertSymbols}
<SymbolsOverlay />
{/if}
</Fields>
</PaneContent>
</Pane>
<HorizontalResizer
panes={[fieldsPane, tagsPane]}
showIndicator={$tagsCollapsed || snapTags}
tip={$tagsCollapsed
? tr.editingDoubleClickToExpand()
: tr.editingDoubleClickToCollapse()}
{clientHeight}
bind:this={lowerResizer}
on:dblclick={() => snapResizer(!$tagsCollapsed)}
on:release={() => {
snapResizer(snapTags);
}}
>
<div class="tags-expander">
<TagAddButton
on:tagappend={() => {
tagEditor.appendEmptyTag();
<EditorField
{field}
{content}
flipInputs={plainTextDefaults[index]}
api={fields[index]}
on:focusin={() => {
$focusedField = fields[index];
bridgeCommand(`focus:${index}`);
}}
keyCombination="Control+Shift+T"
on:focusout={() => {
$focusedField = null;
bridgeCommand(
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
get(content),
)}`,
);
}}
on:mouseenter={() => {
$hoveredField = fields[index];
}}
on:mouseleave={() => {
$hoveredField = null;
}}
collapsed={fieldsCollapsed[index]}
dupe={cols[index] === "dupe"}
--description-font-size="{field.fontSize}px"
--description-content={`"${field.description}"`}
>
{@html tagAmount > 0 ? `${tagAmount} ${tr.editingTags()}` : ""}
</TagAddButton>
</div>
</HorizontalResizer>
<svelte:fragment slot="field-label">
<LabelContainer
collapsed={fieldsCollapsed[index]}
on:toggle={async () => {
fieldsCollapsed[index] = !fieldsCollapsed[index];
<Pane
bind:this={tagsPane.resizable}
on:resize={(e) => {
tagsPane.height = e.detail.height;
if (tagsPane.maxHeight > 0) {
snapTags = tagsPane.height < tagsPane.maxHeight / 2;
}
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;
}
}}
--icon-align="bottom"
>
<svelte:fragment slot="field-name">
<LabelName>
{field.name}
</LabelName>
</svelte:fragment>
<FieldState>
{#if cols[index] === "dupe"}
<DuplicateLink />
{/if}
{#if plainTextDefaults[index]}
<RichTextBadge
show={!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
show={!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}
show={fields[index] === $hoveredField ||
fields[index] === $focusedField}
/>
</FieldState>
</LabelContainer>
</svelte:fragment>
<svelte:fragment slot="rich-text-input">
<Collapsible
collapse={richTextsHidden[index]}
let:collapsed={hidden}
toggleDisplay
>
<RichTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={richTextInputs[index]}
/>
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="plain-text-input">
<Collapsible
collapse={plainTextsHidden[index]}
let:collapsed={hidden}
toggleDisplay
>
<PlainTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={plainTextInputs[index]}
/>
</Collapsible>
</svelte:fragment>
</EditorField>
{/each}
<MathjaxOverlay />
<ImageOverlay maxWidth={250} maxHeight={125} />
{#if insertSymbols}
<SymbolsOverlay />
{/if}
</Fields>
<Shortcut
keyCombination="Control+Shift+T"
on:action={() => {
updateTagsCollapsed(false);
}}
--opacity={(() => {
if (!$tagsCollapsed) {
return 1;
} else {
return snapTags ? tagsPane.height / tagsPane.maxHeight : 1;
}
})()}
/>
<CollapseLabel
collapsed={$tagsCollapsed}
tooltip={$tagsCollapsed ? tr.editingExpand() : tr.editingCollapse()}
on:toggle={() => updateTagsCollapsed(!$tagsCollapsed)}
>
<PaneContent scroll={false}>
<TagEditor
{tags}
--button-opacity={snapTags ? 0 : 1}
bind:this={tagEditor}
on:tagsupdate={saveTags}
on:tagsFocused={() => {
expandTags();
$tagsCollapsed = false;
}}
on:heightChange={(e) => {
tagsPane.maxHeight = e.detail.height;
if (!$tagsCollapsed) {
expandTags();
}
}}
/>
</PaneContent>
</Pane>
{@html `${tagAmount > 0 ? tagAmount : ""} ${tr.editingTags()}`}
</CollapseLabel>
<Collapsible toggleDisplay collapse={$tagsCollapsed}>
<TagEditor {tags} on:tagsupdate={saveTags} />
</Collapsible>
</div>
<style lang="scss">

View file

@ -391,8 +391,6 @@ 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 });
</script>
{#if anyTagsSelected}
@ -506,11 +504,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<style lang="scss">
.tag-editor {
display: flex;
flex-grow: 1;
flex-flow: row wrap;
align-items: flex-end;
background: var(--canvas-inset);
background: var(--canvas-elevated);
border: 1px solid var(--border);
border-radius: var(--border-radius);
padding: 6px;
margin: 1px;
&:focus-within {
outline-offset: -1px;
outline: 2px solid var(--border-focus);
}
}
.tag-relative {