mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
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:
parent
d849abace2
commit
b12476de9a
14 changed files with 371 additions and 689 deletions
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
147
ts/components/ScrollArea.svelte
Normal file
147
ts/components/ScrollArea.svelte
Normal 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>
|
|
@ -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>
|
|
@ -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 {
|
||||
|
|
29
ts/editor/CollapseLabel.svelte
Normal file
29
ts/editor/CollapseLabel.svelte
Normal 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>
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue