mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
Field redesign (#2002)
* Adjust size of legacy buttons * Revert "Adjust size of legacy buttons" This reverts commitfb888fe1db
. * Remove unused function from #1476 * Use outline version for tag icon * Add chevron icons * Remove code icons, keep one pin icon version * Add code-bg color * Redesign fields * Remove unused import * Fix imports * Move PlainTextBadge between editing inputs where it belongs :) * Make whole separator line clickable * Fix transition and format * Don't show toggle when field is collapsed * Show toggle only on hover for mobile I'd like to implement a swipe mechanism. * Use tweened SVG for triangle instead of CSS hack * Implement more obvious HTML toggle on bottom right * Reduce field height by a few pixels * Reduce field height by two pixels * Show HTML toggle when PlainTextInput is active, regardless of hover/focus * Remove RichTextBadge.svelte * Create separate collapsed field state this means users can collapse fields with the HTML editor open and it will stay open when the field is expanded again. * Add slide out animation to EditingArea, RichTextInput and PlainTextInput only for collapsing, because it is choppy on expansion (common issue with Svelte transitions). * Fix aliasing issue on focused field corners * Make StickyBadge feel more responsive * Move StickyBadge closer to field border * Adjust field gutter/margins * Make LabelContainer sticky to make field operations accessible on fields with a lot of content. * Add back html icons, remove visual editor icons * Revert "Add code-bg color" This reverts commit4200f35419
. * Add rich text icon, remove strikethrough code icon * Revert PlainTextBadge to original position * Adjust margins in FieldState * Rename PlainTextBadge to SecondaryInputBadge in preparation for #1987 * Run eslint and prettier * Make whole LabelContainer clickable area for collapse/expand * Revert "Add slide out animation to EditingArea, RichTextInput and PlainTextInput" This reverts commit9a2b3410d0
. * Fix error on collapse/expansion this was caused by the {#if} blocks, which resulted in the deletion of original EditingAreas. * Refocus when toggling chevron and secondary input badge * Revert "Revert "Add code-bg color"" This reverts commit1cfd3bda65
. * Use single rotating chevron icon and make it RTL-compatible * Remove redundant CSS transition rule * Introduce animated Collapsible component and fix refocus on toggle * Do not try to force repaint, as it is not required * Remove RTL store from LabelContainer the direction is already applied globally. * Collapse secondary input with field * Add focusedField to NoteEditorAPI * Replace :global CSS selector with class .visible thus removing the assumption that the component is used inside an EditorField. https://github.com/ankitects/anki/pull/2002#discussion_r944876448 * Use named function syntax instead of function expressions * Add explanation comment * Remove unnecessary :bind directive * Create CollapseBadge component * Move :global selector into .plain-text-input * Add comment explaining box-shadow pseudo-element * Move Collapsible from EditingArea, PlainTextInput and RichTextInput into user components * Rename SecondaryInputBadge to PlainTextBadge and remove generalization logic I kept the rich text icon inside icons.ts for future use. * Sort imports * Fix background-color for duplicates not showing with yet another pseudo-element :) The pseudo-element that covers up field borders on scroll caused this issue. Fighting fire with fire here. * Increase size of plain text toggle to original value again This makes the clickable area a bit bigger and looks slightly more consistent with StickyBadge. * Scrap pseudo-element mess in LabelContainer and tackle the actual issue * Add class .visible to StickyBadge too This introduces a peculiar bug: The active prop of StickyBadge resets to false when the mouse leaves the field - regardless of the actual back-end value. * Fix sticky badge resetting on mouseleave/blur * Apply overflow: hidden only during transition fixes MathJax handle getting cut off by fields * Remove unused variable * Fix visual bug caused by overflow:hidden not applying in time I tried several asynchronous approaches, but they all caused issues: either they prevented the CSS transition or they made field inputs lose focus. In the end I resorted to direct, synchronous DOM-manipulation and added an explanatory comment. * Decrease Collapsible load time by blocking first transition I noticed the sliding animation has a hefty performance impact when a large number of fields is loaded simultaneously. Blocking the first transition (which isn't even visible) results in a big boost in load time. * Replace usages of gap with margins for children * Revert unnecessary removal of grid-gap definition * Correct comments about flex-gap property mistook that for grid-gap. * Resolve style issues * Add minimum targets to gap comment Co-authored-by: Henrik Giesel <hengiesel@gmail.com>
This commit is contained in:
parent
19deb7ad25
commit
5f6ac1a916
20 changed files with 375 additions and 189 deletions
|
@ -54,7 +54,8 @@ editing-text-highlight-color = Text highlight color
|
||||||
editing-to-make-a-cloze-deletion-on = To make a cloze deletion on an existing note, you need to change it to a cloze type first, via 'Notes>Change Note Type'
|
editing-to-make-a-cloze-deletion-on = To make a cloze deletion on an existing note, you need to change it to a cloze type first, via 'Notes>Change Note Type'
|
||||||
editing-toggle-html-editor = Toggle HTML Editor
|
editing-toggle-html-editor = Toggle HTML Editor
|
||||||
editing-toggle-sticky = Toggle sticky
|
editing-toggle-sticky = Toggle sticky
|
||||||
editing-toggle-visual-editor = Toggle Visual Editor
|
editing-expand-field = Expand field
|
||||||
|
editing-collapse-field = Collapse field
|
||||||
editing-underline-text = Underline text
|
editing-underline-text = Underline text
|
||||||
editing-unordered-list = Unordered list
|
editing-unordered-list = Unordered list
|
||||||
editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will not work until you switch the type at the top to Cloze.
|
editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will not work until you switch the type at the top to Cloze.
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
--tooltip-bg: #fcfcfc;
|
--tooltip-bg: #fcfcfc;
|
||||||
--focus-border: #0969da;
|
--focus-border: #0969da;
|
||||||
--focus-shadow: rgba(9 105 218 / 0.3);
|
--focus-shadow: rgba(9 105 218 / 0.3);
|
||||||
|
--code-bg: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[class*="night-mode"] {
|
:root[class*="night-mode"] {
|
||||||
|
@ -77,5 +78,6 @@
|
||||||
--tooltip-bg: #272727;
|
--tooltip-bg: #272727;
|
||||||
--focus-border: #316dca;
|
--focus-border: #316dca;
|
||||||
--focus-shadow: #194380;
|
--focus-shadow: #194380;
|
||||||
|
--code-bg: #272822;
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
}
|
}
|
||||||
|
|
100
ts/components/Collapsible.svelte
Normal file
100
ts/components/Collapsible.svelte
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<!--
|
||||||
|
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 "../lib/promise";
|
||||||
|
|
||||||
|
export let id: string | undefined = undefined;
|
||||||
|
let className: string = "";
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
export let collapsed = false;
|
||||||
|
|
||||||
|
const [outerPromise, outerResolve] = promiseWithResolver<HTMLElement>();
|
||||||
|
const [innerPromise, innerResolve] = promiseWithResolver<HTMLElement>();
|
||||||
|
|
||||||
|
let isCollapsed = false;
|
||||||
|
|
||||||
|
let style: string;
|
||||||
|
function setStyle(height: number, duration: number) {
|
||||||
|
style = `--collapse-height: -${height}px; --duration: ${duration}ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The following two functions use synchronous DOM-manipulation,
|
||||||
|
because Editor field inputs would lose focus when using tick() */
|
||||||
|
|
||||||
|
function getRequiredHeight(el: HTMLElement): number {
|
||||||
|
el.style.setProperty("position", "absolute");
|
||||||
|
el.style.setProperty("visibility", "hidden");
|
||||||
|
el.removeAttribute("hidden");
|
||||||
|
|
||||||
|
const height = el.clientHeight;
|
||||||
|
|
||||||
|
el.setAttribute("hidden", "");
|
||||||
|
el.style.removeProperty("position");
|
||||||
|
el.style.removeProperty("visibility");
|
||||||
|
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transition(collapse: boolean) {
|
||||||
|
const outer = await outerPromise;
|
||||||
|
const inner = await innerPromise;
|
||||||
|
|
||||||
|
outer.style.setProperty("overflow", "hidden");
|
||||||
|
isCollapsed = true;
|
||||||
|
|
||||||
|
const height = collapse ? inner.clientHeight : getRequiredHeight(inner);
|
||||||
|
const duration = Math.sqrt(height * 80);
|
||||||
|
|
||||||
|
setStyle(height, duration);
|
||||||
|
|
||||||
|
if (!collapse) {
|
||||||
|
inner.removeAttribute("hidden");
|
||||||
|
isCollapsed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inner.addEventListener(
|
||||||
|
"transitionend",
|
||||||
|
() => {
|
||||||
|
inner.toggleAttribute("hidden", collapse);
|
||||||
|
outer.style.removeProperty("overflow");
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* prevent transition on mount for performance reasons */
|
||||||
|
let blockTransition = true;
|
||||||
|
|
||||||
|
$: if (blockTransition) {
|
||||||
|
blockTransition = false;
|
||||||
|
} else {
|
||||||
|
transition(collapsed);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {id} class="collapsible-container {className}" use:outerResolve>
|
||||||
|
<div
|
||||||
|
class="collapsible-inner"
|
||||||
|
class:collapsed={isCollapsed}
|
||||||
|
use:innerResolve
|
||||||
|
{style}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.collapsible-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.collapsible-inner {
|
||||||
|
transition: margin-top var(--duration) ease-in;
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
margin-top: var(--collapse-height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
35
ts/editor/CollapseBadge.svelte
Normal file
35
ts/editor/CollapseBadge.svelte
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import Badge from "../components/Badge.svelte";
|
||||||
|
import { chevronDown } from "./icons";
|
||||||
|
|
||||||
|
export let collapsed = false;
|
||||||
|
export let highlighted = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="collapse-badge" class:collapsed class:highlighted>
|
||||||
|
<Badge iconSize={80} --icon-align="text-bottom">{@html chevronDown}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.collapse-badge {
|
||||||
|
display: inline-block;
|
||||||
|
opacity: 0.4;
|
||||||
|
transition: opacity 0.2s ease-in-out, transform 80ms ease-in;
|
||||||
|
&.highlighted {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&.collapsed {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global([dir="rtl"]) {
|
||||||
|
.collapse-badge.collapsed {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -189,10 +189,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--frame-bg);
|
background: var(--frame-bg);
|
||||||
border-radius: 0 0 5px 5px;
|
border-radius: 5px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
|
||||||
&:focus {
|
box-shadow: 0px 0px 2px 0px var(--border);
|
||||||
|
transition: box-shadow 80ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
||||||
|
/* This pseudo-element is required to display
|
||||||
|
the inset box-shadow above field contents */
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -1px;
|
||||||
|
right: -1px;
|
||||||
|
bottom: -1px;
|
||||||
|
left: -1px;
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: inset 0 0 0 2px var(--focus-border);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -45,22 +45,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import { directionKey } from "../lib/context-keys";
|
import Collapsible from "../components/Collapsible.svelte";
|
||||||
|
import { collapsedKey, directionKey } from "../lib/context-keys";
|
||||||
import { promiseWithResolver } from "../lib/promise";
|
import { promiseWithResolver } from "../lib/promise";
|
||||||
import type { Destroyable } from "./destroyable";
|
import type { Destroyable } from "./destroyable";
|
||||||
import EditingArea from "./EditingArea.svelte";
|
import EditingArea from "./EditingArea.svelte";
|
||||||
import FieldState from "./FieldState.svelte";
|
|
||||||
import LabelContainer from "./LabelContainer.svelte";
|
|
||||||
import LabelName from "./LabelName.svelte";
|
|
||||||
|
|
||||||
export let content: Writable<string>;
|
export let content: Writable<string>;
|
||||||
export let field: FieldData;
|
export let field: FieldData;
|
||||||
|
export let collapsed = false;
|
||||||
|
|
||||||
const directionStore = writable<"ltr" | "rtl">();
|
const directionStore = writable<"ltr" | "rtl">();
|
||||||
setContext(directionKey, directionStore);
|
setContext(directionKey, directionStore);
|
||||||
|
|
||||||
$: $directionStore = field.direction;
|
$: $directionStore = field.direction;
|
||||||
|
|
||||||
|
const collapsedStore = writable<boolean>();
|
||||||
|
setContext(collapsedKey, collapsedStore);
|
||||||
|
|
||||||
|
$: $collapsedStore = collapsed;
|
||||||
|
|
||||||
const editingArea: Partial<EditingAreaAPI> = {};
|
const editingArea: Partial<EditingAreaAPI> = {};
|
||||||
const [element, elementResolve] = promiseWithResolver<HTMLElement>();
|
const [element, elementResolve] = promiseWithResolver<HTMLElement>();
|
||||||
|
|
||||||
|
@ -85,15 +89,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
on:focusin
|
on:focusin
|
||||||
on:focusout
|
on:focusout
|
||||||
on:click={() => editingArea.focus?.()}
|
on:click={() => editingArea.focus?.()}
|
||||||
|
on:mouseenter
|
||||||
|
on:mouseleave
|
||||||
>
|
>
|
||||||
<LabelContainer>
|
<slot name="field-label" />
|
||||||
<span>
|
|
||||||
<LabelName>
|
<Collapsible {collapsed}>
|
||||||
{field.name}
|
|
||||||
</LabelName>
|
|
||||||
</span>
|
|
||||||
<FieldState><slot name="field-state" /></FieldState>
|
|
||||||
</LabelContainer>
|
|
||||||
<EditingArea
|
<EditingArea
|
||||||
{content}
|
{content}
|
||||||
fontFamily={field.fontFamily}
|
fontFamily={field.fontFamily}
|
||||||
|
@ -102,20 +103,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
>
|
>
|
||||||
<slot name="editing-inputs" />
|
<slot name="editing-inputs" />
|
||||||
</EditingArea>
|
</EditingArea>
|
||||||
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.editor-field {
|
.editor-field {
|
||||||
|
position: relative;
|
||||||
--border-color: var(--border);
|
--border-color: var(--border);
|
||||||
|
|
||||||
border-radius: 5px;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
--border-color: var(--focus-border);
|
|
||||||
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px var(--focus-shadow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,8 +12,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
justify-content: flex;
|
justify-content: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
& > :global(*) {
|
/* replace with "gap: 5px" once it's available
|
||||||
margin-left: 2px;
|
- required: Chromium 84 (Qt6 only) and iOS 14.1 */
|
||||||
|
> :global(*) {
|
||||||
|
margin: 0 3px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:global([dir="rtl"]) .field-state > :global(*) {
|
||||||
|
margin: 0 3px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
&:first-child {
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -10,9 +10,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
.fields {
|
.fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-rows: min-content;
|
grid-auto-rows: min-content;
|
||||||
grid-gap: 4px;
|
grid-gap: 6px;
|
||||||
|
|
||||||
padding: 5px 3px 0;
|
padding: 0 3px;
|
||||||
/* set height to 100% for rich text widgets */
|
/* set height to 100% for rich text widgets */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
|
|
@ -13,5 +13,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
/* replace with "gap: 5px" once it's available
|
||||||
|
- required: Chromium 84 (Qt6 only) and iOS 14.1 */
|
||||||
|
> :global(*) {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,38 +3,54 @@ 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">
|
<script lang="ts">
|
||||||
import { getContext } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import type { Readable } from "svelte/store";
|
|
||||||
|
|
||||||
import { directionKey } from "../lib/context-keys";
|
import * as tr from "../lib/ftl";
|
||||||
|
import CollapseBadge from "./CollapseBadge.svelte";
|
||||||
|
|
||||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
export let collapsed: boolean;
|
||||||
|
let hovered = false;
|
||||||
|
|
||||||
|
$: tooltip = collapsed ? tr.editingExpandField() : tr.editingCollapseField();
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
dispatch("toggle");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div class="label-container" on:mousedown|preventDefault>
|
||||||
class="label-container"
|
<span
|
||||||
class:rtl={$direction === "rtl"}
|
class="clickable"
|
||||||
on:mousedown|preventDefault
|
title={tooltip}
|
||||||
|
on:click|stopPropagation={toggle}
|
||||||
|
on:mouseenter={() => (hovered = true)}
|
||||||
|
on:mouseleave={() => (hovered = false)}
|
||||||
>
|
>
|
||||||
|
<CollapseBadge {collapsed} highlighted={hovered} />
|
||||||
|
<slot name="field-name" />
|
||||||
|
</span>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.label-container {
|
.label-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: sticky;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
top: 0;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
|
||||||
background-color: var(--label-color, transparent);
|
/* slightly wider than EditingArea
|
||||||
|
to cover field borders on scroll */
|
||||||
|
left: -1px;
|
||||||
|
right: -1px;
|
||||||
|
z-index: 3;
|
||||||
|
background: var(--label-color);
|
||||||
|
|
||||||
border-width: 0 0 1px;
|
.clickable {
|
||||||
border-style: dashed;
|
cursor: pointer;
|
||||||
border-color: var(--border-color);
|
|
||||||
border-radius: 5px 5px 0 0;
|
|
||||||
|
|
||||||
padding: 0px 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rtl {
|
|
||||||
direction: rtl;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -45,7 +45,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<NoteEditor bind:this={noteEditor} {api}>
|
<NoteEditor bind:this={noteEditor} {api}>
|
||||||
<svelte:fragment slot="field-state" let:index>
|
<svelte:fragment slot="field-state" let:index let:visible>
|
||||||
<StickyBadge active={stickies[index]} {index} />
|
<StickyBadge bind:active={stickies[index]} {index} {visible} />
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</NoteEditor>
|
</NoteEditor>
|
||||||
|
|
|
@ -5,12 +5,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
|
|
||||||
|
import Collapsible from "../components/Collapsible.svelte";
|
||||||
import type { EditingInputAPI } from "./EditingArea.svelte";
|
import type { EditingInputAPI } from "./EditingArea.svelte";
|
||||||
import type { EditorToolbarAPI } from "./editor-toolbar";
|
import type { EditorToolbarAPI } from "./editor-toolbar";
|
||||||
import type { EditorFieldAPI } from "./EditorField.svelte";
|
import type { EditorFieldAPI } from "./EditorField.svelte";
|
||||||
|
import FieldState from "./FieldState.svelte";
|
||||||
|
import LabelContainer from "./LabelContainer.svelte";
|
||||||
|
import LabelName from "./LabelName.svelte";
|
||||||
|
|
||||||
export interface NoteEditorAPI {
|
export interface NoteEditorAPI {
|
||||||
fields: EditorFieldAPI[];
|
fields: EditorFieldAPI[];
|
||||||
|
hoveredField: Writable<EditorFieldAPI | null>;
|
||||||
focusedField: Writable<EditorFieldAPI | null>;
|
focusedField: Writable<EditorFieldAPI | null>;
|
||||||
focusedInput: Writable<EditingInputAPI | null>;
|
focusedInput: Writable<EditingInputAPI | null>;
|
||||||
toolbar: EditorToolbarAPI;
|
toolbar: EditorToolbarAPI;
|
||||||
|
@ -61,7 +66,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import PlainTextInput from "./plain-text-input";
|
import PlainTextInput from "./plain-text-input";
|
||||||
import PlainTextBadge from "./PlainTextBadge.svelte";
|
import PlainTextBadge from "./PlainTextBadge.svelte";
|
||||||
import RichTextInput, { editingInputIsRichText } from "./rich-text-input";
|
import RichTextInput, { editingInputIsRichText } from "./rich-text-input";
|
||||||
import RichTextBadge from "./RichTextBadge.svelte";
|
|
||||||
|
|
||||||
function quoteFontFamily(fontFamily: string): string {
|
function quoteFontFamily(fontFamily: string): string {
|
||||||
// generic families (e.g. sans-serif) must not be quoted
|
// generic families (e.g. sans-serif) must not be quoted
|
||||||
|
@ -128,10 +132,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
let fonts: [string, number, boolean][] = [];
|
let fonts: [string, number, boolean][] = [];
|
||||||
|
let fieldsCollapsed: boolean[] = [];
|
||||||
|
|
||||||
const fields = clearableArray<EditorFieldAPI>();
|
const fields = clearableArray<EditorFieldAPI>();
|
||||||
|
|
||||||
export function setFonts(fs: [string, number, boolean][]): void {
|
export function setFonts(fs: [string, number, boolean][]): void {
|
||||||
fonts = fs;
|
fonts = fs;
|
||||||
|
fieldsCollapsed = fonts.map((_, index) => fieldsCollapsed[index] ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusField(index: number | null): void {
|
export function focusField(index: number | null): void {
|
||||||
|
@ -272,11 +279,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let apiPartial: Partial<NoteEditorAPI> = {};
|
let apiPartial: Partial<NoteEditorAPI> = {};
|
||||||
export { apiPartial as api };
|
export { apiPartial as api };
|
||||||
|
|
||||||
|
const hoveredField: NoteEditorAPI["hoveredField"] = writable(null);
|
||||||
const focusedField: NoteEditorAPI["focusedField"] = writable(null);
|
const focusedField: NoteEditorAPI["focusedField"] = writable(null);
|
||||||
const focusedInput: NoteEditorAPI["focusedInput"] = writable(null);
|
const focusedInput: NoteEditorAPI["focusedInput"] = writable(null);
|
||||||
|
|
||||||
const api: NoteEditorAPI = {
|
const api: NoteEditorAPI = {
|
||||||
...apiPartial,
|
...apiPartial,
|
||||||
|
hoveredField,
|
||||||
focusedField,
|
focusedField,
|
||||||
focusedInput,
|
focusedInput,
|
||||||
toolbar: toolbar as EditorToolbarAPI,
|
toolbar: toolbar as EditorToolbarAPI,
|
||||||
|
@ -331,41 +340,69 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||||
`blur:${index}:${getNoteId()}:${get(content)}`,
|
`blur:${index}:${getNoteId()}:${get(content)}`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
on:mouseenter={() => {
|
||||||
|
$hoveredField = fields[index];
|
||||||
|
}}
|
||||||
|
on:mouseleave={() => {
|
||||||
|
$hoveredField = null;
|
||||||
|
}}
|
||||||
|
collapsed={fieldsCollapsed[index]}
|
||||||
--label-color={cols[index] === "dupe"
|
--label-color={cols[index] === "dupe"
|
||||||
? "var(--flag1-bg)"
|
? "var(--flag1-bg)"
|
||||||
: "transparent"}
|
: "var(--window-bg)"}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="field-state">
|
<svelte:fragment slot="field-label">
|
||||||
|
<LabelContainer
|
||||||
|
collapsed={fieldsCollapsed[index]}
|
||||||
|
on:toggle={async () => {
|
||||||
|
fieldsCollapsed[index] = !fieldsCollapsed[index];
|
||||||
|
|
||||||
|
if (!fieldsCollapsed[index]) {
|
||||||
|
await tick();
|
||||||
|
richTextInputs[index].api.refocus();
|
||||||
|
} else {
|
||||||
|
plainTextsHidden[index] = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="field-name">
|
||||||
|
<LabelName>
|
||||||
|
{field.name}
|
||||||
|
</LabelName>
|
||||||
|
</svelte:fragment>
|
||||||
|
<FieldState>
|
||||||
{#if cols[index] === "dupe"}
|
{#if cols[index] === "dupe"}
|
||||||
<DuplicateLink />
|
<DuplicateLink />
|
||||||
{/if}
|
{/if}
|
||||||
<RichTextBadge
|
|
||||||
bind:off={richTextsHidden[index]}
|
|
||||||
on:toggle={() => {
|
|
||||||
richTextsHidden[index] = !richTextsHidden[index];
|
|
||||||
|
|
||||||
if (!richTextsHidden[index]) {
|
|
||||||
richTextInputs[index].api.refocus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<PlainTextBadge
|
<PlainTextBadge
|
||||||
|
visible={!fieldsCollapsed[index] &&
|
||||||
|
(fields[index] === $hoveredField ||
|
||||||
|
fields[index] === $focusedField)}
|
||||||
bind:off={plainTextsHidden[index]}
|
bind:off={plainTextsHidden[index]}
|
||||||
on:toggle={() => {
|
on:toggle={async () => {
|
||||||
plainTextsHidden[index] = !plainTextsHidden[index];
|
plainTextsHidden[index] =
|
||||||
|
!plainTextsHidden[index];
|
||||||
|
|
||||||
if (!plainTextsHidden[index]) {
|
if (!plainTextsHidden[index]) {
|
||||||
|
await tick();
|
||||||
plainTextInputs[index].api.refocus();
|
plainTextInputs[index].api.refocus();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<slot
|
||||||
<slot name="field-state" {field} {index} />
|
name="field-state"
|
||||||
|
{field}
|
||||||
|
{index}
|
||||||
|
visible={fields[index] === $hoveredField ||
|
||||||
|
fields[index] === $focusedField}
|
||||||
|
/>
|
||||||
|
</FieldState>
|
||||||
|
</LabelContainer>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="editing-inputs">
|
<svelte:fragment slot="editing-inputs">
|
||||||
|
<Collapsible collapsed={richTextsHidden[index]}>
|
||||||
<RichTextInput
|
<RichTextInput
|
||||||
hidden={richTextsHidden[index]}
|
bind:hidden={richTextsHidden[index]}
|
||||||
on:focusout={() => {
|
on:focusout={() => {
|
||||||
saveFieldNow();
|
saveFieldNow();
|
||||||
$focusedInput = null;
|
$focusedInput = null;
|
||||||
|
@ -378,15 +415,18 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||||
{field.description}
|
{field.description}
|
||||||
</FieldDescription>
|
</FieldDescription>
|
||||||
</RichTextInput>
|
</RichTextInput>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
|
<Collapsible collapsed={plainTextsHidden[index]}>
|
||||||
<PlainTextInput
|
<PlainTextInput
|
||||||
hidden={plainTextsHidden[index]}
|
bind:hidden={plainTextsHidden[index]}
|
||||||
on:focusout={() => {
|
on:focusout={() => {
|
||||||
saveFieldNow();
|
saveFieldNow();
|
||||||
$focusedInput = null;
|
$focusedInput = null;
|
||||||
}}
|
}}
|
||||||
bind:this={plainTextInputs[index]}
|
bind:this={plainTextInputs[index]}
|
||||||
/>
|
/>
|
||||||
|
</Collapsible>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</EditorField>
|
</EditorField>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -9,16 +9,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { getPlatformString, registerShortcut } from "../lib/shortcuts";
|
import { getPlatformString, registerShortcut } from "../lib/shortcuts";
|
||||||
import { context as editorFieldContext } from "./EditorField.svelte";
|
import { context as editorFieldContext } from "./EditorField.svelte";
|
||||||
import { htmlOff, htmlOn } from "./icons";
|
import { plainTextIcon } from "./icons";
|
||||||
|
|
||||||
const editorField = editorFieldContext.get();
|
const editorField = editorFieldContext.get();
|
||||||
const keyCombination = "Control+Shift+X";
|
const keyCombination = "Control+Shift+X";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let visible = false;
|
||||||
export let off = false;
|
export let off = false;
|
||||||
|
|
||||||
$: icon = off ? htmlOff : htmlOn;
|
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
dispatch("toggle");
|
dispatch("toggle");
|
||||||
}
|
}
|
||||||
|
@ -32,25 +31,29 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="plain-text-badge"
|
class="plain-text-badge"
|
||||||
|
class:visible
|
||||||
class:highlighted={!off}
|
class:highlighted={!off}
|
||||||
on:click|stopPropagation={toggle}
|
on:click|stopPropagation={toggle}
|
||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
tooltip="{tr.editingToggleHtmlEditor()} ({getPlatformString(keyCombination)})"
|
tooltip="{tr.editingToggleHtmlEditor()} ({getPlatformString(keyCombination)})"
|
||||||
iconSize={80}
|
iconSize={80}>{@html plainTextIcon}</Badge
|
||||||
--icon-align="text-top">{@html icon}</Badge
|
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
span {
|
span {
|
||||||
opacity: 0.4;
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
&.highlighted {
|
&.visible {
|
||||||
opacity: 1;
|
opacity: 0.4;
|
||||||
}
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.highlighted {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,42 +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 Badge from "../components/Badge.svelte";
|
|
||||||
import * as tr from "../lib/ftl";
|
|
||||||
import { richTextOff, richTextOn } from "./icons";
|
|
||||||
|
|
||||||
export let off: boolean;
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
dispatch("toggle");
|
|
||||||
}
|
|
||||||
|
|
||||||
$: icon = off ? richTextOff : richTextOn;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span class="rich-text-badge" class:highlighted={off} on:click|stopPropagation={toggle}>
|
|
||||||
<Badge
|
|
||||||
tooltip={tr.editingToggleVisualEditor()}
|
|
||||||
iconSize={80}
|
|
||||||
--icon-align="text-top">{@html icon}</Badge
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
span {
|
|
||||||
opacity: 0.4;
|
|
||||||
|
|
||||||
&.highlighted {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -10,11 +10,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { getPlatformString, registerShortcut } from "../lib/shortcuts";
|
import { getPlatformString, registerShortcut } from "../lib/shortcuts";
|
||||||
import { context as editorFieldContext } from "./EditorField.svelte";
|
import { context as editorFieldContext } from "./EditorField.svelte";
|
||||||
import { stickyOff, stickyOn } from "./icons";
|
import { stickyIcon } from "./icons";
|
||||||
|
|
||||||
export let active: boolean;
|
export let active: boolean;
|
||||||
|
export let visible: boolean;
|
||||||
$: icon = active ? stickyOn : stickyOff;
|
|
||||||
|
|
||||||
const editorField = editorFieldContext.get();
|
const editorField = editorFieldContext.get();
|
||||||
const keyCombination = "F9";
|
const keyCombination = "F9";
|
||||||
|
@ -34,23 +33,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
onMount(() => editorField.element.then(shortcut));
|
onMount(() => editorField.element.then(shortcut));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class:highlighted={active} on:click|stopPropagation={toggle}>
|
<span class:highlighted={active} class:visible on:click|stopPropagation={toggle}>
|
||||||
<Badge
|
<Badge
|
||||||
tooltip="{tr.editingToggleSticky()} ({getPlatformString(keyCombination)})"
|
tooltip="{tr.editingToggleSticky()} ({getPlatformString(keyCombination)})"
|
||||||
widthMultiplier={0.7}
|
widthMultiplier={0.7}>{@html stickyIcon}</Badge
|
||||||
--icon-align="text-top">{@html icon}</Badge
|
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
span {
|
span {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
&.visible {
|
||||||
|
transition: none;
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
|
|
||||||
&.highlighted {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.highlighted {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,10 +5,9 @@
|
||||||
|
|
||||||
export { default as incrementClozeIcon } from "../icons/contain-plus.svg";
|
export { default as incrementClozeIcon } from "../icons/contain-plus.svg";
|
||||||
export { default as alertIcon } from "@mdi/svg/svg/alert.svg";
|
export { default as alertIcon } from "@mdi/svg/svg/alert.svg";
|
||||||
export { default as htmlOn } from "@mdi/svg/svg/code-tags.svg";
|
export { default as chevronDown } from "@mdi/svg/svg/chevron-down.svg";
|
||||||
|
export { default as chevronUp } from "@mdi/svg/svg/chevron-up.svg";
|
||||||
|
export { default as plainTextIcon } from "@mdi/svg/svg/code-tags.svg";
|
||||||
export { default as clozeIcon } from "@mdi/svg/svg/contain.svg";
|
export { default as clozeIcon } from "@mdi/svg/svg/contain.svg";
|
||||||
export { default as richTextOff } from "@mdi/svg/svg/eye-off-outline.svg";
|
export { default as richTextIcon } from "@mdi/svg/svg/format-font.svg";
|
||||||
export { default as richTextOn } from "@mdi/svg/svg/eye-outline.svg";
|
export { default as stickyIcon } from "@mdi/svg/svg/pin-outline.svg";
|
||||||
export { default as stickyOff } from "@mdi/svg/svg/pin-off-outline.svg";
|
|
||||||
export { default as stickyOn } from "@mdi/svg/svg/pin-outline.svg";
|
|
||||||
export { default as htmlOff } from "@mdi/svg/svg/xml.svg";
|
|
||||||
|
|
|
@ -143,7 +143,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<div
|
<div
|
||||||
class="plain-text-input"
|
class="plain-text-input"
|
||||||
class:light-theme={!$pageTheme.isDark}
|
class:light-theme={!$pageTheme.isDark}
|
||||||
class:hidden
|
|
||||||
on:focusin={() => ($focusedInput = api)}
|
on:focusin={() => ($focusedInput = api)}
|
||||||
>
|
>
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
|
@ -161,18 +160,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
:global(.CodeMirror) {
|
:global(.CodeMirror) {
|
||||||
border-radius: 0 0 5px 5px;
|
border-radius: 0 0 5px 5px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
background: var(--code-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.CodeMirror-lines) {
|
:global(.CodeMirror-lines) {
|
||||||
padding: 6px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-theme :global(.CodeMirror) {
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -211,7 +211,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
setupLifecycleHooks(api);
|
setupLifecycleHooks(api);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rich-text-input" {hidden} on:focusin={setFocus} on:focusout={removeFocus}>
|
<div class="rich-text-input" on:focusin={setFocus} on:focusout={removeFocus}>
|
||||||
<RichTextStyles
|
<RichTextStyles
|
||||||
color={$pageTheme.isDark ? "white" : "black"}
|
color={$pageTheme.isDark ? "white" : "black"}
|
||||||
fontFamily={$fontFamily}
|
fontFamily={$fontFamily}
|
||||||
|
@ -238,6 +238,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
</RichTextStyles>
|
</RichTextStyles>
|
||||||
|
<slot name="plain-text-badge" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -5,3 +5,4 @@ export const fontFamilyKey = Symbol("fontFamily");
|
||||||
export const fontSizeKey = Symbol("fontSize");
|
export const fontSizeKey = Symbol("fontSize");
|
||||||
export const directionKey = Symbol("direction");
|
export const directionKey = Symbol("direction");
|
||||||
export const descriptionKey = Symbol("description");
|
export const descriptionKey = Symbol("description");
|
||||||
|
export const collapsedKey = Symbol("collapsed");
|
||||||
|
|
|
@ -4,5 +4,5 @@
|
||||||
/// <reference types="../../lib/image-import" />
|
/// <reference types="../../lib/image-import" />
|
||||||
|
|
||||||
export { default as dotsIcon } from "@mdi/svg/svg/dots-vertical.svg";
|
export { default as dotsIcon } from "@mdi/svg/svg/dots-vertical.svg";
|
||||||
export { default as tagIcon } from "@mdi/svg/svg/tag.svg";
|
export { default as tagIcon } from "@mdi/svg/svg/tag-outline.svg";
|
||||||
export { default as addTagIcon } from "@mdi/svg/svg/tag-plus.svg";
|
export { default as addTagIcon } from "@mdi/svg/svg/tag-plus-outline.svg";
|
||||||
|
|
Loading…
Reference in a new issue