From d110c4916cf1d83fbeae48ae891515c79a412018 Mon Sep 17 00:00:00 2001 From: Matthias Metelka <62722460+kleinerpirat@users.noreply.github.com> Date: Wed, 31 Aug 2022 15:34:39 +0200 Subject: [PATCH] Introduce setting to collapse field by default (#1990) * Introduce setting to collapse field by default * Fix schema order * Change wording from adjective to imperative sounds a bit less clunky * Update rslib/src/notetype/schema11.rs (dae) * Keep settings in single column * Add back Toggle Visual Editor string * Add RichTextBadge component and show it conditionally * Reverse input order depending on default setting * Make PlainTextInput border-radius responsive to toggle states * Prevent first Collapsible transition differently * Focus inputs after Collapsible transition The double tick calls are just a temporary solution until I find the exact moment an input is focusable again. * Use requestAnimationFrame to await focusable state Note: Svelte tick doesn't seem to work in this scenario. --- ftl/core/editing.ftl | 1 + ftl/core/fields.ftl | 1 + proto/anki/notetypes.proto | 1 + qt/aqt/editor.py | 3 + qt/aqt/fields.py | 4 + qt/aqt/forms/fields.ui | 96 ++++++++++--------- rslib/src/notetype/fields.rs | 1 + rslib/src/notetype/schema11.rs | 8 +- ts/components/Collapsible.svelte | 18 ++-- ts/editor/EditorField.svelte | 10 +- ts/editor/NoteEditor.svelte | 93 ++++++++++++------ ts/editor/RichTextBadge.svelte | 59 ++++++++++++ ts/editor/helpers.ts | 15 +++ .../plain-text-input/PlainTextInput.svelte | 21 +++- 14 files changed, 248 insertions(+), 83 deletions(-) create mode 100644 ts/editor/RichTextBadge.svelte diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl index 244872282..f5bccd201 100644 --- a/ftl/core/editing.ftl +++ b/ftl/core/editing.ftl @@ -53,6 +53,7 @@ editing-text-color = Text color 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-toggle-html-editor = Toggle HTML Editor +editing-toggle-visual-editor = Toggle Visual Editor editing-toggle-sticky = Toggle sticky editing-expand-field = Expand field editing-collapse-field = Collapse field diff --git a/ftl/core/fields.ftl b/ftl/core/fields.ftl index dfcb6f3f0..50a86087f 100644 --- a/ftl/core/fields.ftl +++ b/ftl/core/fields.ftl @@ -10,6 +10,7 @@ fields-font = Font: fields-new-position-1 = New position (1...{ $val }): fields-notes-require-at-least-one-field = Notes require at least one field. fields-reverse-text-direction-rtl = Reverse text direction (RTL) +fields-collapse-by-default = Collapse by default fields-html-by-default = Use HTML editor by default fields-size = Size: fields-sort-by-this-field-in-the = Sort by this field in the browser diff --git a/proto/anki/notetypes.proto b/proto/anki/notetypes.proto index ff9f0ee0c..3d8ae7483 100644 --- a/proto/anki/notetypes.proto +++ b/proto/anki/notetypes.proto @@ -73,6 +73,7 @@ message Notetype { uint32 font_size = 4; string description = 5; bool plain_text = 6; + bool collapsed = 7; bytes other = 255; } diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 1fcc086bf..f55eef4e3 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -501,6 +501,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too ] flds = self.note.note_type()["flds"] + collapsed = [fld["collapsed"] for fld in flds] plain_texts = [fld.get("plainText", False) for fld in flds] descriptions = [fld.get("description", "") for fld in flds] @@ -524,6 +525,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too js = """ setFields({}); + setCollapsed({}); setPlainTexts({}); setDescriptions({}); setFonts({}); @@ -534,6 +536,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too setMathjaxEnabled({}); """.format( json.dumps(data), + json.dumps(collapsed), json.dumps(plain_texts), json.dumps(descriptions), json.dumps(self.fonts()), diff --git a/qt/aqt/fields.py b/qt/aqt/fields.py index 7bbddfe7b..5de5f4643 100644 --- a/qt/aqt/fields.py +++ b/qt/aqt/fields.py @@ -244,6 +244,7 @@ class FieldDialog(QDialog): f.sortField.setChecked(self.model["sortf"] == fld["ord"]) f.rtl.setChecked(fld["rtl"]) f.plainTextByDefault.setChecked(fld["plainText"]) + f.collapseByDefault.setChecked(fld["collapsed"]) f.fieldDescription.setText(fld.get("description", "")) def saveField(self) -> None: @@ -269,6 +270,9 @@ class FieldDialog(QDialog): if fld["plainText"] != plain_text: fld["plainText"] = plain_text self.change_tracker.mark_basic() + collapsed = f.collapseByDefault.isChecked() + if fld["collapsed"] != collapsed: + fld["collapsed"] = collapsed desc = f.fieldDescription.text() if fld.get("description", "") != desc: fld["description"] = desc diff --git a/qt/aqt/forms/fields.ui b/qt/aqt/forms/fields.ui index cb2a04b1b..4b7ad1599 100644 --- a/qt/aqt/forms/fields.ui +++ b/qt/aqt/forms/fields.ui @@ -6,8 +6,8 @@ 0 0 - 598 - 378 + 567 + 438 @@ -84,44 +84,6 @@ - - - - fields_reverse_text_direction_rtl - - - - - - - - 0 - 25 - - - - - - - - fields_editing_font - - - - - - - fields_description_placeholder - - - - - - - actions_options - - - @@ -135,6 +97,13 @@ + + + + fields_editing_font + + + @@ -145,10 +114,27 @@ - - + + - fields_sort_by_this_field_in_the + actions_options + + + + + + + + 0 + 25 + + + + + + + + fields_reverse_text_direction_rtl @@ -162,6 +148,30 @@ + + + + fields_description_placeholder + + + + + + + fields_sort_by_this_field_in_the + + + + + + + true + + + fields_collapse_by_default + + + diff --git a/rslib/src/notetype/fields.rs b/rslib/src/notetype/fields.rs index ca1908e3d..88586a156 100644 --- a/rslib/src/notetype/fields.rs +++ b/rslib/src/notetype/fields.rs @@ -46,6 +46,7 @@ impl NoteField { font_name: "Arial".into(), font_size: 20, description: "".into(), + collapsed: false, other: vec![], }, } diff --git a/rslib/src/notetype/schema11.rs b/rslib/src/notetype/schema11.rs index 0fab7e560..c3c24dafa 100644 --- a/rslib/src/notetype/schema11.rs +++ b/rslib/src/notetype/schema11.rs @@ -161,7 +161,7 @@ impl From for NotetypeSchema11 { /// See [crate::deckconfig::schema11::clear_other_duplicates()]. fn clear_other_field_duplicates(other: &mut HashMap) { - for key in &["description", "plainText"] { + for key in &["description", "plainText", "collapsed"] { other.remove(*key); } } @@ -215,6 +215,9 @@ pub struct NoteFieldSchema11 { #[serde(default, deserialize_with = "default_on_invalid")] pub(crate) plain_text: bool, + #[serde(default, deserialize_with = "default_on_invalid")] + pub(crate) collapsed: bool, + #[serde(flatten)] pub(crate) other: HashMap, } @@ -230,6 +233,7 @@ impl Default for NoteFieldSchema11 { font: "Arial".to_string(), size: 20, description: String::new(), + collapsed: false, other: Default::default(), } } @@ -247,6 +251,7 @@ impl From for NoteField { font_name: f.font, font_size: f.size as u32, description: f.description, + collapsed: f.collapsed, other: other_to_bytes(&f.other), }, } @@ -269,6 +274,7 @@ impl From for NoteFieldSchema11 { font: conf.font_name, size: conf.font_size as u16, description: conf.description, + collapsed: conf.collapsed, other, } } diff --git a/ts/components/Collapsible.svelte b/ts/components/Collapsible.svelte index 3a116aa53..1b19f28c9 100644 --- a/ts/components/Collapsible.svelte +++ b/ts/components/Collapsible.svelte @@ -10,12 +10,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export { className as class }; export let collapsed = false; + let isCollapsed = false; + let hidden = collapsed; const [outerPromise, outerResolve] = promiseWithResolver(); const [innerPromise, innerResolve] = promiseWithResolver(); - let isCollapsed = false; - let style: string; function setStyle(height: number, duration: number) { style = `--collapse-height: -${height}px; --duration: ${duration}ms`; @@ -60,18 +60,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html () => { inner.toggleAttribute("hidden", collapse); outer.style.removeProperty("overflow"); + hidden = collapse; }, { once: true }, ); } /* prevent transition on mount for performance reasons */ - let blockTransition = true; + let firstTransition = true; - $: if (blockTransition) { - blockTransition = false; - } else { + $: { transition(collapsed); + firstTransition = false; } @@ -79,10 +79,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
- +
@@ -96,5 +97,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html &.collapsed { margin-top: var(--collapse-height); } + &.no-transition { + transition: none; + } } diff --git a/ts/editor/EditorField.svelte b/ts/editor/EditorField.svelte index bd3f1983d..9f903c824 100644 --- a/ts/editor/EditorField.svelte +++ b/ts/editor/EditorField.svelte @@ -14,6 +14,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html direction: "ltr" | "rtl"; plainText: boolean; description: string; + collapsed: boolean; } export interface EditorFieldAPI { @@ -54,6 +55,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let content: Writable; export let field: FieldData; export let collapsed = false; + export let flipInputs = false; const directionStore = writable<"ltr" | "rtl">(); setContext(directionKey, directionStore); @@ -101,7 +103,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html fontSize={field.fontSize} api={editingArea} > - + {#if flipInputs} + + + {:else} + + + {/if} diff --git a/ts/editor/NoteEditor.svelte b/ts/editor/NoteEditor.svelte index 9f942f3ba..7aaa1d01f 100644 --- a/ts/editor/NoteEditor.svelte +++ b/ts/editor/NoteEditor.svelte @@ -66,6 +66,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import PlainTextInput from "./plain-text-input"; import PlainTextBadge from "./PlainTextBadge.svelte"; import RichTextInput, { editingInputIsRichText } from "./rich-text-input"; + import RichTextBadge from "./RichTextBadge.svelte"; function quoteFontFamily(fontFamily: string): string { // generic families (e.g. sans-serif) must not be quoted @@ -113,13 +114,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html fieldNames = newFieldNames; } - let plainTexts: boolean[] = []; + let fieldsCollapsed: boolean[] = []; + export function setCollapsed(fs: boolean[]): void { + fieldsCollapsed = fs; + } + let richTextsHidden: boolean[] = []; let plainTextsHidden: boolean[] = []; + let plainTextDefaults: boolean[] = []; export function setPlainTexts(fs: boolean[]): void { - richTextsHidden = plainTexts = fs; + richTextsHidden = fs; plainTextsHidden = Array.from(fs, (v) => !v); + plainTextDefaults = [...richTextsHidden]; } function setMathjaxEnabled(enabled: boolean): void { @@ -132,13 +139,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html } let fonts: [string, number, boolean][] = []; - let fieldsCollapsed: boolean[] = []; const fields = clearableArray(); export function setFonts(fs: [string, number, boolean][]): void { fonts = fs; - fieldsCollapsed = fonts.map((_, index) => fieldsCollapsed[index] ?? false); } export function focusField(index: number | null): void { @@ -186,11 +191,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html $: fieldsData = fieldNames.map((name, index) => ({ name, - plainText: plainTexts[index], + plainText: plainTextDefaults[index], description: fieldDescriptions[index], fontFamily: quoteFontFamily(fonts[index][0]), fontSize: fonts[index][1], direction: fonts[index][2] ? "rtl" : "ltr", + collapsed: fieldsCollapsed[index], })) as FieldData[]; function saveTags({ detail }: CustomEvent): void { @@ -241,6 +247,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { mathjaxConfig } from "../editable/mathjax-element"; import { wrapInternal } from "../lib/wrap"; + import { refocusInput } from "./helpers"; import * as oldEditorAdapter from "./old-editor-adapter"; onMount(() => { @@ -256,6 +263,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html Object.assign(globalThis, { setFields, + setCollapsed, setPlainTexts, setDescriptions, setFonts, @@ -329,6 +337,7 @@ the AddCards dialog) should be implemented in the user of this component. { $focusedField = fields[index]; @@ -357,11 +366,16 @@ the AddCards dialog) should be implemented in the user of this component. on:toggle={async () => { fieldsCollapsed[index] = !fieldsCollapsed[index]; + const defaultInput = !plainTextDefaults[index] + ? richTextInputs[index] + : plainTextInputs[index]; + if (!fieldsCollapsed[index]) { - await tick(); - richTextInputs[index].api.refocus(); - } else { + refocusInput(defaultInput.api); + } else if (!plainTextDefaults[index]) { plainTextsHidden[index] = true; + } else { + richTextsHidden[index] = true; } }} > @@ -374,21 +388,41 @@ the AddCards dialog) should be implemented in the user of this component. {#if cols[index] === "dupe"} {/if} - { - plainTextsHidden[index] = - !plainTextsHidden[index]; + {#if plainTextDefaults[index]} + { + richTextsHidden[index] = + !richTextsHidden[index]; - if (!plainTextsHidden[index]) { - await tick(); - plainTextInputs[index].api.refocus(); - } - }} - /> + if (!richTextsHidden[index]) { + refocusInput( + richTextInputs[index].api, + ); + } + }} + /> + {:else} + { + plainTextsHidden[index] = + !plainTextsHidden[index]; + + if (!plainTextsHidden[index]) { + refocusInput( + plainTextInputs[index].api, + ); + } + }} + /> + {/if} - - + + { saveFieldNow(); $focusedInput = null; @@ -416,10 +450,13 @@ the AddCards dialog) should be implemented in the user of this component. - - + + + { saveFieldNow(); $focusedInput = null; diff --git a/ts/editor/RichTextBadge.svelte b/ts/editor/RichTextBadge.svelte new file mode 100644 index 000000000..180925395 --- /dev/null +++ b/ts/editor/RichTextBadge.svelte @@ -0,0 +1,59 @@ + + + + + {@html richTextIcon} + + + diff --git a/ts/editor/helpers.ts b/ts/editor/helpers.ts index 98fe4f892..53aeee6d4 100644 --- a/ts/editor/helpers.ts +++ b/ts/editor/helpers.ts @@ -1,6 +1,9 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +import type { PlainTextInputAPI } from "./plain-text-input"; +import type { RichTextInputAPI } from "./rich-text-input"; + function isFontElement(element: Element): element is HTMLFontElement { return element.tagName === "FONT"; } @@ -19,3 +22,15 @@ export function withFontColor( return false; } + +/*** + * Required for field inputs wrapped in Collapsible + */ +export async function refocusInput( + api: RichTextInputAPI | PlainTextInputAPI, +): Promise { + do { + await new Promise(window.requestAnimationFrame); + } while (!api.focusable); + api.refocus(); +} diff --git a/ts/editor/plain-text-input/PlainTextInput.svelte b/ts/editor/plain-text-input/PlainTextInput.svelte index 67d4f8049..d3479f418 100644 --- a/ts/editor/plain-text-input/PlainTextInput.svelte +++ b/ts/editor/plain-text-input/PlainTextInput.svelte @@ -39,7 +39,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import removeProhibitedTags from "./remove-prohibited"; import { storedToUndecorated, undecoratedToStored } from "./transform"; + export let isDefault: boolean; export let hidden: boolean; + export let richTextHidden: boolean; const configuration = { mode: htmlanki, @@ -143,6 +145,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
($focusedInput = api)} > .plain-text-input { - overflow-x: hidden; + overflow: hidden; + + border-top: 1px solid var(--border); + border-radius: 0 0 5px 5px; + + &.is-default { + border-top: none; + border-bottom: 1px solid var(--border); + border-radius: 5px 5px 0 0; + } + &.alone { + border: none; + border-radius: 5px; + } :global(.CodeMirror) { - border-radius: 0 0 5px 5px; - border-top: 1px solid var(--border); background: var(--code-bg); } :global(.CodeMirror-lines) {