Add a legacy switch to NoteEditor

This commit is contained in:
Abdo 2025-07-24 13:14:27 +03:00
parent a05f3dd38a
commit 63052e7fb9
11 changed files with 208 additions and 94 deletions

View file

@ -146,7 +146,10 @@ fn build_css(build: &mut Build) -> Result<()> {
},
)?;
}
let other_ts_css = build.inputs_with_suffix(inputs![":ts:editor", ":ts:reviewer:reviewer.css"], ".css");
let other_ts_css = build.inputs_with_suffix(
inputs![":ts:editor", ":ts:editable", ":ts:reviewer:reviewer.css"],
".css",
);
build.add_action(
"qt:aqt:data:web:css",
CopyFiles {
@ -187,8 +190,15 @@ fn build_js(build: &mut Build) -> Result<()> {
inputs: files,
},
)?;
let files_from_ts =
build.inputs_with_suffix(inputs![":ts:editor", ":ts:reviewer:reviewer.js", ":ts:mathjax"], ".js");
let files_from_ts = build.inputs_with_suffix(
inputs![
":ts:editor",
":ts:editable",
":ts:reviewer:reviewer.js",
":ts:mathjax"
],
".js",
);
build.add_action(
"qt:aqt:data:web:js",
CopyFiles {

View file

@ -203,16 +203,23 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> {
Ok(())
};
// we use the generated .css file separately in the legacy editor
build_page(
"editable",
false,
inputs![
":ts:lib",
":ts:components",
":ts:domlib",
":ts:sveltelib",
":sass",
":sveltekit",
],
)?;
build_page(
"congrats",
true,
inputs![
//
":ts:lib",
":ts:components",
":sass",
":sveltekit"
],
inputs![":ts:lib", ":ts:components", ":sass", ":sveltekit"],
)?;
Ok(())

View file

@ -91,4 +91,3 @@ class EditCurrent(QMainWindow):
self.editor.call_after_note_saved(callback)
onReset = on_operation_did_execute
onReset = on_operation_did_execute

View file

@ -187,7 +187,7 @@ class Editor:
context=self,
default_css=False,
)
self.web.eval(f"setupEditor('{mode}')")
self.web.eval(f"setupEditor('{mode}', true)")
self.web.show()
lefttopbtns: list[str] = []
@ -563,9 +563,9 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
if not self.note:
return
data = [
(fld, self.mw.col.media.escape_media_filenames(val))
for fld, val in self.note.items()
field_names = self.note.keys()
field_values = [
self.mw.col.media.escape_media_filenames(val) for val in self.note.values()
]
note_type = self.note_type()
@ -600,7 +600,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
js = f"""
saveSession();
setFields({json.dumps(data)});
setFields({json.dumps(field_names)}, {json.dumps(field_values)});
setIsImageOcclusion({json.dumps(self.current_notetype_is_image_occlusion())});
setNotetypeMeta({json.dumps(notetype_meta)});
setCollapsed({json.dumps(collapsed)});
@ -1787,4 +1787,3 @@ gui_hooks.editor_will_use_font_for_field.append(fontMungeHack)
gui_hooks.editor_will_munge_html.append(munge_html) # type: ignore
gui_hooks.editor_will_munge_html.append(remove_null_bytes) # type: ignore
gui_hooks.editor_will_munge_html.append(reverse_url_quoting) # type: ignore
gui_hooks.editor_will_munge_html.append(reverse_url_quoting) # type: ignore

View file

@ -4,3 +4,6 @@
import "./editable-base.scss";
import "./content-editable.scss";
import "./mathjax.scss";
/* only imported for the CSS in the legacy editor */
import "./ContentEditable.svelte";
import "./Mathjax.svelte";

View file

@ -215,18 +215,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
async function toggleStickyAll() {
const values: boolean[] = [];
const notetype = await getNotetype({ ntid: notetypeMeta.id });
const anySticky = notetype.fields.some((f) => f.config!.sticky);
for (const field of notetype.fields) {
const sticky = field.config!.sticky;
if (!anySticky || sticky) {
field.config!.sticky = !sticky;
if (isLegacy) {
bridgeCommand(
"toggleStickyAll",
(values: boolean[]) => (stickies = values),
);
} else {
const values: boolean[] = [];
const notetype = await getNotetype({ ntid: notetypeMeta.id });
const anySticky = notetype.fields.some((f) => f.config!.sticky);
for (const field of notetype.fields) {
const sticky = field.config!.sticky;
if (!anySticky || sticky) {
field.config!.sticky = !sticky;
}
values.push(field.config!.sticky);
}
values.push(field.config!.sticky);
await updateEditorNotetype(notetype);
setSticky(values);
}
await updateEditorNotetype(notetype);
setSticky(values);
}
let deregisterSticky: () => void;
@ -262,9 +269,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
await setMeta(tagsCollapsedMetaKey, collapsed);
}
let note: Note;
export function setNote(n: Note): void {
note = n;
function clearCodeMirrorHistory() {
// TODO this is a hack, because it requires the NoteEditor to know implementation details of the PlainTextInput.
// It should be refactored once we work on our own Undo stack
for (const pi of plainTextInputs) {
@ -272,6 +277,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
}
let noteId: number | null = null;
export function setNoteId(ntid: number): void {
clearCodeMirrorHistory();
noteId = ntid;
}
function getNoteId(): number | null {
return noteId;
}
let note: Note;
export function setNote(n: Note): void {
note = n;
clearCodeMirrorHistory();
}
let notetypeMeta: NotetypeIdAndModTime;
function setNotetypeMeta(notetype: Notetype): void {
notetypeMeta = { id: notetype.id, modTime: notetype.mtimeSecs };
@ -330,13 +351,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
async function transformContentBeforeSave(content: string): Promise<string> {
content = content.replace(/ data-editor-shrink="(true|false)"/g, "");
// misbehaving apps may include a null byte in the text
content = content.replaceAll("\0", "");
// reverse the url quoting we added to get images to display
content = (await decodeIriPaths({ val: content })).val;
if (!isLegacy) {
// misbehaving apps may include a null byte in the text
content = content.replaceAll("\0", "");
// reverse the url quoting we added to get images to display
content = (await decodeIriPaths({ val: content })).val;
if (["<br>", "<div><br></div>"].includes(content)) {
return "";
if (["<br>", "<div><br></div>"].includes(content)) {
return "";
}
}
return content;
}
@ -352,10 +375,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
async function updateField(index: number, content: string): Promise<void> {
fieldSave.schedule(async () => {
bridgeCommand(`key:${index}`);
note!.fields[index] = await transformContentBeforeSave(content);
await updateCurrentNote();
await updateDuplicateDisplay();
if (isLegacy) {
bridgeCommand(
`key:${index}:${getNoteId()}:${transformContentBeforeSave(
content,
)}`,
);
} else {
bridgeCommand(`key:${index}`);
note!.fields[index] = await transformContentBeforeSave(content);
await updateCurrentNote();
await updateDuplicateDisplay();
}
}, 600);
}
@ -640,16 +671,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
async function pickIOImage() {
imageOcclusionMode = undefined;
const filename = await openFilePickerForImageOcclusion();
if (!filename) {
return;
if (isLegacy) {
bridgeCommand("addImageForOcclusion");
} else {
const filename = await openFilePickerForImageOcclusion();
if (!filename) {
return;
}
setupMaskEditor(filename);
}
setupMaskEditor(filename);
}
async function pickIOImageFromClipboard() {
imageOcclusionMode = undefined;
await setupMaskEditorFromClipboard();
if (isLegacy) {
bridgeCommand("addImageForOcclusionFromClipboard");
} else {
await setupMaskEditorFromClipboard();
}
}
async function handlePickerDrop(event: DragEvent) {
@ -928,6 +967,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
await loadNote(note!.id, notetypeMeta.id, 0, null);
}
function checkNonLegacy(value: any): any | undefined {
if (isLegacy) {
return value;
}
return undefined;
}
function preventDefaultIfNonLegacy(event: Event) {
if (!isLegacy) {
event.preventDefault();
}
}
$: signalEditorState($editorState);
$: $editorState = getEditorState(
@ -976,6 +1028,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
saveNow,
closeAddCards,
focusIfField,
getNoteId,
setNoteId,
setNotetypeMeta,
wrap,
setMathjaxEnabled,
@ -1036,6 +1090,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let uiResolve: (api: NoteEditorAPI) => void;
export let mode: EditorMode;
export let isLegacy: boolean;
$: if (noteEditor) {
uiResolve(api as NoteEditorAPI);
@ -1044,9 +1099,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<!-- Block Qt's default drag & drop behavior -->
<svelte:body
on:dragenter|preventDefault
on:dragover|preventDefault
on:drop|preventDefault
on:dragenter={preventDefaultIfNonLegacy}
on:dragover={preventDefaultIfNonLegacy}
on:drop={preventDefaultIfNonLegacy}
/>
<!--
@ -1059,11 +1114,13 @@ components and functionality for general note editing.
role="presentation"
bind:this={noteEditor}
on:contextmenu={(event) => {
contextMenuInput = $focusedInput;
onContextMenu(event, api, $focusedInput, contextMenu);
if (!isLegacy) {
contextMenuInput = $focusedInput;
onContextMenu(event, api, $focusedInput, contextMenu);
}
}}
on:dragover|preventDefault
on:drop={handlePickerDrop}
on:dragover={preventDefaultIfNonLegacy}
on:drop={checkNonLegacy(handlePickerDrop)}
>
<EditorToolbar {size} {wrap} api={toolbar}>
<svelte:fragment slot="notetypeButtons">
@ -1122,12 +1179,20 @@ components and functionality for general note editing.
on:focusout={async () => {
$focusedField = null;
setAddonButtonsDisabled(true);
bridgeCommand(`blur:${index}`);
note!.fields[index] = await transformContentBeforeSave(
get(content),
);
await updateCurrentNote();
await updateDuplicateDisplay();
if (isLegacy) {
bridgeCommand(
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
get(content),
)}`,
);
} else {
bridgeCommand(`blur:${index}`);
note!.fields[index] = await transformContentBeforeSave(
get(content),
);
await updateCurrentNote();
await updateDuplicateDisplay();
}
}}
on:mouseenter={() => {
$hoveredField = fields[index];
@ -1160,6 +1225,7 @@ components and functionality for general note editing.
bind:active={stickies[index]}
{index}
{note}
{isLegacy}
show={fields[index] === $hoveredField ||
fields[index] === $focusedField}
/>
@ -1192,6 +1258,7 @@ components and functionality for general note editing.
>
<RichTextInput
{hidden}
{isLegacy}
on:focusout={() => {
saveFieldNow();
$focusedInput = null;
@ -1241,21 +1308,25 @@ components and functionality for general note editing.
<Collapsible toggleDisplay collapse={$tagsCollapsed}>
<TagEditor {tags} on:tagsupdate={saveTags} />
</Collapsible>
<ActionButtons {mode} {onClose} {onAdd} />
{#if !isLegacy}
<ActionButtons {mode} {onClose} {onAdd} />
{/if}
{/if}
<ContextMenu bind:this={contextMenu}>
{#each contextMenuItems as item}
<Item
click={() => {
item.action();
contextMenuInput?.focus();
}}
>
{item.label}
</Item>
{/each}
</ContextMenu>
{#if !isLegacy}
<ContextMenu bind:this={contextMenu}>
{#each contextMenuItems as item}
<Item
click={() => {
item.action();
contextMenuInput?.focus();
}}
>
{item.label}
</Item>
{/each}
</ContextMenu>
{/if}
</div>
<style lang="scss">

View file

@ -16,11 +16,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { context as editorFieldContext } from "./EditorField.svelte";
import type { Note } from "@generated/anki/notes_pb";
import { getNotetype, updateEditorNotetype } from "@generated/backend";
import { bridgeCommand } from "@tslib/bridgecommand";
const animated = !document.body.classList.contains("reduce-motion");
export let active: boolean;
export let show: boolean;
export let isLegacy: boolean;
const editorField = editorFieldContext.get();
const keyCombination = "F9";
@ -29,10 +31,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let note: Note;
async function toggle() {
active = !active;
const notetype = await getNotetype({ ntid: note.notetypeId });
notetype.fields[index].config!.sticky = active;
await updateEditorNotetype(notetype);
if (isLegacy) {
bridgeCommand(`toggleSticky:${index}`, (value: boolean) => {
active = value;
});
} else {
active = !active;
const notetype = await getNotetype({ ntid: note.notetypeId });
notetype.fields[index].config!.sticky = active;
await updateEditorNotetype(notetype);
}
}
function shortcut(target: HTMLElement): () => void {

View file

@ -53,11 +53,11 @@ export const components = {
export { editorToolbar } from "./editor-toolbar";
export async function setupEditor(mode: EditorMode) {
export async function setupEditor(mode: EditorMode, isLegacy = false) {
if (!["add", "browser", "current"].includes(mode)) {
alert("unexpected editor type");
return;
}
await setupI18n({ modules: editorModules });
mount(NoteEditor, { target: document.body, props: { uiResolve, mode } });
mount(NoteEditor, { target: document.body, props: { uiResolve, mode, isLegacy } });
}

View file

@ -37,6 +37,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
Object.assign(api, {
setColorButtons,
});
// For legacy editor
Object.assign(globalThis, { setColorButtons });
</script>
<DynamicallySlottable slotHost={Item} {api}>

View file

@ -84,6 +84,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { fragmentToStored, storedToFragment } from "./transform";
export let hidden = false;
export let isLegacy = false;
export const focusFlag = new Flag();
export let isClozeField: boolean;
@ -231,6 +232,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<div class="rich-text-input" on:focusin={setFocus} on:focusout={removeFocus} {hidden}>
<RichTextStyles
{isLegacy}
color={$pageTheme.isDark ? "white" : "black"}
fontFamily={$fontFamily}
fontSize={$fontSize}

View file

@ -13,6 +13,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { mount } from "svelte";
export let isLegacy = false;
export let callback: (styles: Record<string, any>) => void;
const [userBaseStyle, userBaseResolve] = promiseWithResolver<StyleObject>();
@ -47,23 +49,34 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: setStyling("fontSize", fontSize + "px");
$: setStyling("direction", direction);
const styles: StyleLinkType[] = [
{
id: "editableBaseStyle",
type: "link",
href: editableBaseCSS,
},
{
id: "contentEditableStyle",
type: "link",
href: contentEditableCSS,
},
{
id: "mathjaxStyle",
type: "link",
href: mathjaxCSS,
},
];
let styles: StyleLinkType[];
if (isLegacy) {
styles = [
{
id: "rootStyle",
type: "link",
href: "./_anki/css/editable.css",
},
];
} else {
styles = [
{
id: "editableBaseStyle",
type: "link",
href: editableBaseCSS,
},
{
id: "contentEditableStyle",
type: "link",
href: contentEditableCSS,
},
{
id: "mathjaxStyle",
type: "link",
href: mathjaxCSS,
},
];
}
function attachToShadow(element: Element) {
const customStyles = mount(CustomStyles, {