Fix sticky field labels - Make editor toolbar+tag editor non sticky in editor (#2058)

* Add back overflow:hidden to field descriptions

* Add explaining comment

* Put back overflow:hidden in FieldsEditor

* Move inline padding from Fields component but EditorField+LabelContainer

* Simplify editor design by making editor toolbar not sticky

* Make tag editor in note editor non-sticky as well

* Fix merge mess

* The floating elements were portaled because I passed in undefined and they have a default argument

- Fix unrelated to PR
This commit is contained in:
Henrik Giesel 2022-09-12 11:22:22 +02:00 committed by GitHub
parent 8f8f3bd465
commit 1a87937973
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 205 additions and 201 deletions

View file

@ -25,7 +25,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import subscribeToUpdates from "../sveltelib/subscribe-updates"; import subscribeToUpdates from "../sveltelib/subscribe-updates";
import FloatingArrow from "./FloatingArrow.svelte"; import FloatingArrow from "./FloatingArrow.svelte";
export let portalTarget: HTMLElement | undefined = undefined; export let portalTarget: HTMLElement | null = null;
export let placement: Placement | Placement[] | "auto" = "bottom"; export let placement: Placement | Placement[] | "auto" = "bottom";
export let offset = 5; export let offset = 5;

View file

@ -117,6 +117,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<style lang="scss"> <style lang="scss">
.editor-field { .editor-field {
position: relative; position: relative;
padding: 0 3px;
--border-color: var(--border); --border-color: var(--border);
} }
</style> </style>

View file

@ -43,5 +43,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/* Stay a on single line */ /* Stay a on single line */
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
/* The field description is placed absolutely on top of the editor field */
/* So we need to make sure it does not escape the editor field if the */
/* description is too long */
overflow: hidden;
} }
</style> </style>

View file

@ -2,6 +2,10 @@
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
--> -->
<!--
@component
Contains the fields. This contains the scrollable area.
-->
<div class="fields"> <div class="fields">
<slot /> <slot />
</div> </div>
@ -12,6 +16,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
grid-auto-rows: min-content; grid-auto-rows: min-content;
grid-gap: 6px; grid-gap: 6px;
padding: 0 3px 5px; /* Add space after the last field and the start of the tag editor */
padding-bottom: 5px;
/* Move the scrollbar for the NoteEditor into this element */
overflow-y: auto;
/* Push the tag editor to the bottom of the note editor */
flex-grow: 1;
} }
</style> </style>

View file

@ -1,16 +0,0 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<div class="fields-editor">
<slot />
</div>
<style lang="scss">
.fields-editor {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
}
</style>

View file

@ -37,16 +37,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<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: 0 3px 1px;
padding-bottom: 1px;
/* slightly wider than EditingArea position: sticky;
to cover field borders on scroll */ top: 0;
left: -1px;
right: -1px;
z-index: 10; z-index: 10;
background: var(--label-color); background: var(--label-color);
.clickable { .clickable {

View file

@ -44,7 +44,6 @@ 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 StickyContainer from "../components/StickyContainer.svelte";
import { bridgeCommand } from "../lib/bridgecommand"; import { bridgeCommand } from "../lib/bridgecommand";
import { TagEditor } from "../tag-editor"; import { TagEditor } from "../tag-editor";
import { ChangeTimer } from "./change-timer"; import { ChangeTimer } from "./change-timer";
@ -56,7 +55,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import EditorField from "./EditorField.svelte"; import EditorField from "./EditorField.svelte";
import FieldDescription from "./FieldDescription.svelte"; import FieldDescription from "./FieldDescription.svelte";
import Fields from "./Fields.svelte"; import Fields from "./Fields.svelte";
import FieldsEditor from "./FieldsEditor.svelte";
import FrameElement from "./FrameElement.svelte"; import FrameElement from "./FrameElement.svelte";
import { alertIcon } from "./icons"; import { alertIcon } from "./icons";
import ImageHandle from "./image-overlay"; import ImageHandle from "./image-overlay";
@ -321,185 +319,187 @@ Functionality exclusive to specifc 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"> <div class="note-editor">
<FieldsEditor> <EditorToolbar {size} {wrap} api={toolbar}>
<EditorToolbar {size} {wrap} api={toolbar}> <slot slot="notetypeButtons" name="notetypeButtons" />
<slot slot="notetypeButtons" name="notetypeButtons" /> </EditorToolbar>
</EditorToolbar>
{#if hint} {#if hint}
<Absolute bottom right --margin="10px"> <Absolute bottom right --margin="10px">
<Notification> <Notification>
<Badge --badge-color="tomato" --icon-align="top" <Badge --badge-color="tomato" --icon-align="top"
>{@html alertIcon}</Badge >{@html alertIcon}</Badge
> >
<span>{@html hint}</span> <span>{@html hint}</span>
</Notification> </Notification>
</Absolute> </Absolute>
{/if} {/if}
<Fields> <Fields>
<DecoratedElements> <DecoratedElements>
{#each fieldsData as field, index} {#each fieldsData as field, index}
{@const content = fieldStores[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={() => { on:focusout={() => {
$focusedField = null; $focusedField = null;
bridgeCommand( bridgeCommand(`blur:${index}:${getNoteId()}:${get(content)}`);
`blur:${index}:${getNoteId()}:${get(content)}`, }}
); on:mouseenter={() => {
}} $hoveredField = fields[index];
on:mouseenter={() => { }}
$hoveredField = fields[index]; on:mouseleave={() => {
}} $hoveredField = null;
on:mouseleave={() => { }}
$hoveredField = null; collapsed={fieldsCollapsed[index]}
}} --label-color={cols[index] === "dupe"
collapsed={fieldsCollapsed[index]} ? "var(--flag1-bg)"
--label-color={cols[index] === "dupe" : "var(--window-bg)"}
? "var(--flag1-bg)" >
: "var(--window-bg)"} <svelte:fragment slot="field-label">
> <LabelContainer
<svelte:fragment slot="field-label"> collapsed={fieldsCollapsed[index]}
<LabelContainer on:toggle={async () => {
collapsed={fieldsCollapsed[index]} fieldsCollapsed[index] = !fieldsCollapsed[index];
on:toggle={async () => {
fieldsCollapsed[index] = !fieldsCollapsed[index];
const defaultInput = !plainTextDefaults[index] const defaultInput = !plainTextDefaults[index]
? richTextInputs[index] ? richTextInputs[index]
: plainTextInputs[index]; : plainTextInputs[index];
if (!fieldsCollapsed[index]) { if (!fieldsCollapsed[index]) {
refocusInput(defaultInput.api); refocusInput(defaultInput.api);
} else if (!plainTextDefaults[index]) { } else if (!plainTextDefaults[index]) {
plainTextsHidden[index] = true; plainTextsHidden[index] = true;
} else { } else {
richTextsHidden[index] = true; richTextsHidden[index] = true;
} }
}} }}
> >
<svelte:fragment slot="field-name"> <svelte:fragment slot="field-name">
<LabelName> <LabelName>
{field.name} {field.name}
</LabelName> </LabelName>
</svelte:fragment> </svelte:fragment>
<FieldState> <FieldState>
{#if cols[index] === "dupe"} {#if cols[index] === "dupe"}
<DuplicateLink /> <DuplicateLink />
{/if} {/if}
{#if plainTextDefaults[index]} {#if plainTextDefaults[index]}
<RichTextBadge <RichTextBadge
visible={!fieldsCollapsed[index] && visible={!fieldsCollapsed[index] &&
(fields[index] === $hoveredField || (fields[index] === $hoveredField ||
fields[index] === $focusedField)} fields[index] === $focusedField)}
bind:off={richTextsHidden[index]} bind:off={richTextsHidden[index]}
on:toggle={async () => { on:toggle={async () => {
richTextsHidden[index] = richTextsHidden[index] =
!richTextsHidden[index]; !richTextsHidden[index];
if (!richTextsHidden[index]) { if (!richTextsHidden[index]) {
refocusInput( refocusInput(richTextInputs[index].api);
richTextInputs[index].api, }
); }}
}
}}
/>
{:else}
<PlainTextBadge
visible={!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}
visible={fields[index] === $hoveredField ||
fields[index] === $focusedField}
/> />
</FieldState> {:else}
</LabelContainer> <PlainTextBadge
</svelte:fragment> visible={!fieldsCollapsed[index] &&
<svelte:fragment slot="rich-text-input"> (fields[index] === $hoveredField ||
<Collapsible fields[index] === $focusedField)}
collapse={richTextsHidden[index]} bind:off={plainTextsHidden[index]}
let:collapsed={hidden} on:toggle={async () => {
> plainTextsHidden[index] =
<RichTextInput !plainTextsHidden[index];
{hidden}
on:focusout={() => { if (!plainTextsHidden[index]) {
saveFieldNow(); refocusInput(
$focusedInput = null; plainTextInputs[index].api,
}} );
bind:this={richTextInputs[index]} }
> }}
<ImageHandle maxWidth={250} maxHeight={125} /> />
<MathjaxHandle /> {/if}
{#if insertSymbols} <slot
<SymbolsOverlay /> name="field-state"
{/if} {field}
<FieldDescription> {index}
{field.description} visible={fields[index] === $hoveredField ||
</FieldDescription> fields[index] === $focusedField}
</RichTextInput>
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="plain-text-input">
<Collapsible
collapse={plainTextsHidden[index]}
let:collapsed={hidden}
>
<PlainTextInput
{hidden}
isDefault={plainTextDefaults[index]}
richTextHidden={richTextsHidden[index]}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={plainTextInputs[index]}
/> />
</Collapsible> </FieldState>
</svelte:fragment> </LabelContainer>
</EditorField> </svelte:fragment>
{/each} <svelte:fragment slot="rich-text-input">
<Collapsible
collapse={richTextsHidden[index]}
let:collapsed={hidden}
>
<RichTextInput
{hidden}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={richTextInputs[index]}
>
<ImageHandle maxWidth={250} maxHeight={125} />
<MathjaxHandle />
{#if insertSymbols}
<SymbolsOverlay />
{/if}
<FieldDescription>
{field.description}
</FieldDescription>
</RichTextInput>
</Collapsible>
</svelte:fragment>
<svelte:fragment slot="plain-text-input">
<Collapsible
collapse={plainTextsHidden[index]}
let:collapsed={hidden}
>
<PlainTextInput
{hidden}
isDefault={plainTextDefaults[index]}
richTextHidden={richTextsHidden[index]}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
}}
bind:this={plainTextInputs[index]}
/>
</Collapsible>
</svelte:fragment>
</EditorField>
{/each}
<MathjaxElement /> <MathjaxElement />
<FrameElement /> <FrameElement />
</DecoratedElements> </DecoratedElements>
</Fields> </Fields>
</FieldsEditor>
<StickyContainer --gutter-block="0.1rem" --sticky-borders="1px 0 0" class="d-flex"> <div class="note-editor-tag-editor">
<TagEditor {tags} on:tagsupdate={saveTags} /> <TagEditor {tags} on:tagsupdate={saveTags} />
</StickyContainer> </div>
</div> </div>
<style lang="scss"> <style lang="scss">
.note-editor { .note-editor {
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%;
}
.note-editor-tag-editor {
padding: 2px 0 0;
border-width: thin 0 0;
border-style: solid;
border-color: var(--medium-border);
} }
</style> </style>

View file

@ -53,7 +53,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import ButtonToolbar from "../../components/ButtonToolbar.svelte"; import ButtonToolbar from "../../components/ButtonToolbar.svelte";
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte"; import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
import Item from "../../components/Item.svelte"; import Item from "../../components/Item.svelte";
import StickyContainer from "../../components/StickyContainer.svelte";
import BlockButtons from "./BlockButtons.svelte"; import BlockButtons from "./BlockButtons.svelte";
import InlineButtons from "./InlineButtons.svelte"; import InlineButtons from "./InlineButtons.svelte";
import NotetypeButtons from "./NotetypeButtons.svelte"; import NotetypeButtons from "./NotetypeButtons.svelte";
@ -85,7 +84,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
setContextProperty(api); setContextProperty(api);
</script> </script>
<StickyContainer --gutter-block="0.1rem" --sticky-borders="0 0 1px"> <div class="editor-toolbar">
<ButtonToolbar {size} {wrap}> <ButtonToolbar {size} {wrap}>
<DynamicallySlottable slotHost={Item} api={toolbar}> <DynamicallySlottable slotHost={Item} api={toolbar}>
<Item id="notetype"> <Item id="notetype">
@ -111,4 +110,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</Item> </Item>
</DynamicallySlottable> </DynamicallySlottable>
</ButtonToolbar> </ButtonToolbar>
</StickyContainer> </div>
<style lang="scss">
.editor-toolbar {
padding: 0 0 2px;
border-width: 0 0 thin;
border-style: solid;
border-color: var(--medium-border);
}
</style>

View file

@ -7,11 +7,11 @@
*/ */
function portal( function portal(
element: HTMLElement, element: HTMLElement,
targetElement: Element = document.body, targetElement: Element | null = document.body,
): { update(target: Element): void; destroy(): void } { ): { update(target: Element): void; destroy(): void } {
let target: Element; let target: Element | null;
async function update(newTarget: Element) { async function update(newTarget: Element | null) {
target = newTarget; target = newTarget;
if (!target) { if (!target) {

View file

@ -382,7 +382,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: anyTagsSelected = tagTypes.some((tag) => tag.selected); $: anyTagsSelected = tagTypes.some((tag) => tag.selected);
</script> </script>
<div class="tag-editor-area" on:focusout={deselectIfLeave} bind:offsetHeight={height}> <div class="tag-editor" on:focusout={deselectIfLeave} bind:offsetHeight={height}>
<TagOptionsButton <TagOptionsButton
bind:badgeHeight bind:badgeHeight
tagsSelected={anyTagsSelected} tagsSelected={anyTagsSelected}
@ -483,13 +483,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</div> </div>
<style lang="scss"> <style lang="scss">
.tag-editor-area { .tag-editor {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
align-items: flex-end; align-items: flex-end;
padding: 0 1px 1px;
overflow: hidden;
margin-bottom: 3px;
} }
.tag-relative { .tag-relative {