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-html-editor = Toggle HTML Editor
|
||||||
editing-toggle-visual-editor = Toggle Visual Editor
|
editing-toggle-visual-editor = Toggle Visual Editor
|
||||||
editing-toggle-sticky = Toggle sticky
|
editing-toggle-sticky = Toggle sticky
|
||||||
|
editing-expand = Expand
|
||||||
|
editing-collapse = Collapse
|
||||||
editing-expand-field = Expand field
|
editing-expand-field = Expand field
|
||||||
editing-collapse-field = Collapse field
|
editing-collapse-field = Collapse field
|
||||||
editing-underline-text = Underline text
|
editing-underline-text = Underline text
|
||||||
|
|
|
@ -465,6 +465,11 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
if not self.addMode:
|
if not self.addMode:
|
||||||
self._save_current_note()
|
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:
|
elif cmd in self._links:
|
||||||
return self._links[cmd](self)
|
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),
|
not self.mw.col.get_config("closeHTMLTags", True),
|
||||||
)
|
)
|
||||||
|
|
||||||
def collapseTags(self) -> None:
|
def setTagsCollapsed(self, collapsed: bool) -> None:
|
||||||
aqt.mw.pm.set_tags_collapsed(self.editorMode, True)
|
aqt.mw.pm.set_tags_collapsed(self.editorMode, collapsed)
|
||||||
|
|
||||||
def expandTags(self) -> None:
|
|
||||||
aqt.mw.pm.set_tags_collapsed(self.editorMode, False)
|
|
||||||
|
|
||||||
# Links from HTML
|
# Links from HTML
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -1200,8 +1202,6 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
toggleMathjax=Editor.toggleMathjax,
|
toggleMathjax=Editor.toggleMathjax,
|
||||||
toggleShrinkImages=Editor.toggleShrinkImages,
|
toggleShrinkImages=Editor.toggleShrinkImages,
|
||||||
toggleCloseHTMLTags=Editor.toggleCloseHTMLTags,
|
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";
|
import { chevronDown } from "./icons";
|
||||||
|
|
||||||
export let collapsed = false;
|
export let collapsed = false;
|
||||||
export let highlighted = false;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="collapse-badge" class:collapsed class:highlighted>
|
<div class="collapse-badge" class:collapsed>
|
||||||
<Badge iconSize={80}>{@html chevronDown}</Badge>
|
<Badge iconSize={80}>{@html chevronDown}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -20,7 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
transition: opacity var(--transition) ease-in-out,
|
transition: opacity var(--transition) ease-in-out,
|
||||||
transform var(--transition) ease-in;
|
transform var(--transition) ease-in;
|
||||||
&.highlighted {
|
:global(.collapse-label:hover) & {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
&.collapsed {
|
&.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 */
|
/* make room for thicker focus border */
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
|
|
||||||
border-radius: 5px;
|
border-radius: var(--border-radius);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
|
|
||||||
@include elevation(1);
|
@include elevation(1);
|
||||||
|
|
|
@ -2,13 +2,19 @@
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
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
|
@component
|
||||||
Contains the fields. This contains the scrollable area.
|
Contains the fields. This contains the scrollable area.
|
||||||
-->
|
-->
|
||||||
<div class="fields">
|
<ScrollArea>
|
||||||
<slot />
|
<div class="fields">
|
||||||
</div>
|
<slot />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.fields {
|
.fields {
|
||||||
|
|
|
@ -4,33 +4,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as tr from "@tslib/ftl";
|
import * as tr from "@tslib/ftl";
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
|
|
||||||
import CollapseBadge from "./CollapseBadge.svelte";
|
import CollapseLabel from "./CollapseLabel.svelte";
|
||||||
|
|
||||||
export let collapsed: boolean;
|
export let collapsed: boolean;
|
||||||
let hovered = false;
|
|
||||||
|
|
||||||
$: tooltip = collapsed ? tr.editingExpandField() : tr.editingCollapseField();
|
$: tooltip = collapsed ? tr.editingExpandField() : tr.editingCollapseField();
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
dispatch("toggle");
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="label-container">
|
<div class="label-container">
|
||||||
<span
|
<CollapseLabel {collapsed} {tooltip} on:toggle>
|
||||||
class="clickable"
|
|
||||||
title={tooltip}
|
|
||||||
on:click|stopPropagation={toggle}
|
|
||||||
on:mouseenter={() => (hovered = true)}
|
|
||||||
on:mouseleave={() => (hovered = false)}
|
|
||||||
>
|
|
||||||
<CollapseBadge {collapsed} highlighted={hovered} />
|
|
||||||
<slot name="field-name" />
|
<slot name="field-name" />
|
||||||
</span>
|
</CollapseLabel>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -46,9 +31,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
|
|
||||||
.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</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 Absolute from "../components/Absolute.svelte";
|
||||||
import Badge from "../components/Badge.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 { TagEditor } from "../tag-editor";
|
||||||
import TagAddButton from "../tag-editor/tag-options-button/TagAddButton.svelte";
|
|
||||||
import { ChangeTimer } from "./change-timer";
|
import { ChangeTimer } from "./change-timer";
|
||||||
import { clearableArray } from "./destroyable";
|
import { clearableArray } from "./destroyable";
|
||||||
import DuplicateLink from "./DuplicateLink.svelte";
|
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>();
|
const tagsCollapsed = writable<boolean>();
|
||||||
export function setTagsCollapsed(collapsed: boolean): void {
|
export function setTagsCollapsed(collapsed: boolean): void {
|
||||||
$tagsCollapsed = collapsed;
|
$tagsCollapsed = collapsed;
|
||||||
if (collapsed) {
|
}
|
||||||
lowerResizer.move([tagsPane, fieldsPane], tagsPane.minHeight);
|
|
||||||
}
|
function updateTagsCollapsed(collapsed: boolean) {
|
||||||
|
$tagsCollapsed = collapsed;
|
||||||
|
bridgeCommand(`setTagsCollapsed:${$tagsCollapsed}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let noteId: number | null = null;
|
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 { wrapInternal } from "@tslib/wrap";
|
||||||
|
import Shortcut from "components/Shortcut.svelte";
|
||||||
|
|
||||||
import { mathjaxConfig } from "../editable/mathjax-element";
|
import { mathjaxConfig } from "../editable/mathjax-element";
|
||||||
|
import CollapseLabel from "./CollapseLabel.svelte";
|
||||||
import { refocusInput } from "./helpers";
|
import { refocusInput } from "./helpers";
|
||||||
import * as oldEditorAdapter from "./old-editor-adapter";
|
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);
|
setContextProperty(api);
|
||||||
setupLifecycleHooks(api);
|
setupLifecycleHooks(api);
|
||||||
|
|
||||||
let clientHeight: number;
|
|
||||||
|
|
||||||
const fieldsPane = new ResizablePane();
|
|
||||||
const tagsPane = new ResizablePane();
|
|
||||||
|
|
||||||
let lowerResizer: HorizontalResizer;
|
|
||||||
let tagEditor: TagEditor;
|
|
||||||
|
|
||||||
$: tagAmount = $tags.length;
|
$: 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>
|
</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
|
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.
|
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}>
|
<EditorToolbar {size} {wrap} api={toolbar}>
|
||||||
<slot slot="notetypeButtons" name="notetypeButtons" />
|
<slot slot="notetypeButtons" name="notetypeButtons" />
|
||||||
</EditorToolbar>
|
</EditorToolbar>
|
||||||
|
@ -435,223 +402,166 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||||
</Absolute>
|
</Absolute>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Pane
|
<Fields>
|
||||||
bind:this={fieldsPane.resizable}
|
{#each fieldsData as field, index}
|
||||||
on:resize={(e) => {
|
{@const content = fieldStores[index]}
|
||||||
fieldsPane.height = e.detail.height;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PaneContent>
|
|
||||||
<Fields>
|
|
||||||
{#each fieldsData as field, index}
|
|
||||||
{@const content = fieldStores[index]}
|
|
||||||
|
|
||||||
<EditorField
|
<EditorField
|
||||||
{field}
|
{field}
|
||||||
{content}
|
{content}
|
||||||
flipInputs={plainTextDefaults[index]}
|
flipInputs={plainTextDefaults[index]}
|
||||||
api={fields[index]}
|
api={fields[index]}
|
||||||
on:focusin={() => {
|
on:focusin={() => {
|
||||||
$focusedField = fields[index];
|
$focusedField = fields[index];
|
||||||
bridgeCommand(`focus:${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();
|
|
||||||
}}
|
}}
|
||||||
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()}` : ""}
|
<svelte:fragment slot="field-label">
|
||||||
</TagAddButton>
|
<LabelContainer
|
||||||
</div>
|
collapsed={fieldsCollapsed[index]}
|
||||||
</HorizontalResizer>
|
on:toggle={async () => {
|
||||||
|
fieldsCollapsed[index] = !fieldsCollapsed[index];
|
||||||
|
|
||||||
<Pane
|
const defaultInput = !plainTextDefaults[index]
|
||||||
bind:this={tagsPane.resizable}
|
? richTextInputs[index]
|
||||||
on:resize={(e) => {
|
: plainTextInputs[index];
|
||||||
tagsPane.height = e.detail.height;
|
|
||||||
if (tagsPane.maxHeight > 0) {
|
if (!fieldsCollapsed[index]) {
|
||||||
snapTags = tagsPane.height < tagsPane.maxHeight / 2;
|
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) {
|
<CollapseLabel
|
||||||
return 1;
|
collapsed={$tagsCollapsed}
|
||||||
} else {
|
tooltip={$tagsCollapsed ? tr.editingExpand() : tr.editingCollapse()}
|
||||||
return snapTags ? tagsPane.height / tagsPane.maxHeight : 1;
|
on:toggle={() => updateTagsCollapsed(!$tagsCollapsed)}
|
||||||
}
|
|
||||||
})()}
|
|
||||||
>
|
>
|
||||||
<PaneContent scroll={false}>
|
{@html `${tagAmount > 0 ? tagAmount : ""} ${tr.editingTags()}`}
|
||||||
<TagEditor
|
</CollapseLabel>
|
||||||
{tags}
|
<Collapsible toggleDisplay collapse={$tagsCollapsed}>
|
||||||
--button-opacity={snapTags ? 0 : 1}
|
<TagEditor {tags} on:tagsupdate={saveTags} />
|
||||||
bind:this={tagEditor}
|
</Collapsible>
|
||||||
on:tagsupdate={saveTags}
|
|
||||||
on:tagsFocused={() => {
|
|
||||||
expandTags();
|
|
||||||
$tagsCollapsed = false;
|
|
||||||
}}
|
|
||||||
on:heightChange={(e) => {
|
|
||||||
tagsPane.maxHeight = e.detail.height;
|
|
||||||
if (!$tagsCollapsed) {
|
|
||||||
expandTags();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PaneContent>
|
|
||||||
</Pane>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<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);
|
$: assumedRows = Math.floor(height / badgeHeight);
|
||||||
$: shortenTags = shortenTags || assumedRows > 2;
|
$: shortenTags = shortenTags || assumedRows > 2;
|
||||||
$: anyTagsSelected = tagTypes.some((tag) => tag.selected);
|
$: anyTagsSelected = tagTypes.some((tag) => tag.selected);
|
||||||
|
|
||||||
$: dispatch("heightChange", { height: height + 1 });
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if anyTagsSelected}
|
{#if anyTagsSelected}
|
||||||
|
@ -506,11 +504,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.tag-editor {
|
.tag-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
background: var(--canvas-inset);
|
background: var(--canvas-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
margin: 1px;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline-offset: -1px;
|
||||||
|
outline: 2px solid var(--border-focus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-relative {
|
.tag-relative {
|
||||||
|
|
Loading…
Reference in a new issue