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

View file

@ -203,16 +203,23 @@ fn build_and_check_pages(build: &mut Build) -> Result<()> {
Ok(()) 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( build_page(
"congrats", "congrats",
true, true,
inputs![ inputs![":ts:lib", ":ts:components", ":sass", ":sveltekit"],
//
":ts:lib",
":ts:components",
":sass",
":sveltekit"
],
)?; )?;
Ok(()) Ok(())

View file

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

View file

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

View file

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

View file

@ -215,6 +215,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
async function toggleStickyAll() { async function toggleStickyAll() {
if (isLegacy) {
bridgeCommand(
"toggleStickyAll",
(values: boolean[]) => (stickies = values),
);
} else {
const values: boolean[] = []; const values: boolean[] = [];
const notetype = await getNotetype({ ntid: notetypeMeta.id }); const notetype = await getNotetype({ ntid: notetypeMeta.id });
const anySticky = notetype.fields.some((f) => f.config!.sticky); const anySticky = notetype.fields.some((f) => f.config!.sticky);
@ -228,6 +234,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
await updateEditorNotetype(notetype); await updateEditorNotetype(notetype);
setSticky(values); setSticky(values);
} }
}
let deregisterSticky: () => void; 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); await setMeta(tagsCollapsedMetaKey, collapsed);
} }
let note: Note; function clearCodeMirrorHistory() {
export function setNote(n: Note): void {
note = n;
// TODO this is a hack, because it requires the NoteEditor to know implementation details of the PlainTextInput. // 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 // It should be refactored once we work on our own Undo stack
for (const pi of plainTextInputs) { 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; let notetypeMeta: NotetypeIdAndModTime;
function setNotetypeMeta(notetype: Notetype): void { function setNotetypeMeta(notetype: Notetype): void {
notetypeMeta = { id: notetype.id, modTime: notetype.mtimeSecs }; notetypeMeta = { id: notetype.id, modTime: notetype.mtimeSecs };
@ -330,6 +351,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
async function transformContentBeforeSave(content: string): Promise<string> { async function transformContentBeforeSave(content: string): Promise<string> {
content = content.replace(/ data-editor-shrink="(true|false)"/g, ""); content = content.replace(/ data-editor-shrink="(true|false)"/g, "");
if (!isLegacy) {
// misbehaving apps may include a null byte in the text // misbehaving apps may include a null byte in the text
content = content.replaceAll("\0", ""); content = content.replaceAll("\0", "");
// reverse the url quoting we added to get images to display // reverse the url quoting we added to get images to display
@ -338,6 +360,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
if (["<br>", "<div><br></div>"].includes(content)) { if (["<br>", "<div><br></div>"].includes(content)) {
return ""; return "";
} }
}
return content; 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> { async function updateField(index: number, content: string): Promise<void> {
fieldSave.schedule(async () => { fieldSave.schedule(async () => {
if (isLegacy) {
bridgeCommand(
`key:${index}:${getNoteId()}:${transformContentBeforeSave(
content,
)}`,
);
} else {
bridgeCommand(`key:${index}`); bridgeCommand(`key:${index}`);
note!.fields[index] = await transformContentBeforeSave(content); note!.fields[index] = await transformContentBeforeSave(content);
await updateCurrentNote(); await updateCurrentNote();
await updateDuplicateDisplay(); await updateDuplicateDisplay();
}
}, 600); }, 600);
} }
@ -640,17 +671,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
async function pickIOImage() { async function pickIOImage() {
imageOcclusionMode = undefined; imageOcclusionMode = undefined;
if (isLegacy) {
bridgeCommand("addImageForOcclusion");
} else {
const filename = await openFilePickerForImageOcclusion(); const filename = await openFilePickerForImageOcclusion();
if (!filename) { if (!filename) {
return; return;
} }
setupMaskEditor(filename); setupMaskEditor(filename);
} }
}
async function pickIOImageFromClipboard() { async function pickIOImageFromClipboard() {
imageOcclusionMode = undefined; imageOcclusionMode = undefined;
if (isLegacy) {
bridgeCommand("addImageForOcclusionFromClipboard");
} else {
await setupMaskEditorFromClipboard(); await setupMaskEditorFromClipboard();
} }
}
async function handlePickerDrop(event: DragEvent) { async function handlePickerDrop(event: DragEvent) {
if ($editorState === EditorState.ImageOcclusionPicker) { if ($editorState === EditorState.ImageOcclusionPicker) {
@ -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); 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); $: signalEditorState($editorState);
$: $editorState = getEditorState( $: $editorState = getEditorState(
@ -976,6 +1028,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
saveNow, saveNow,
closeAddCards, closeAddCards,
focusIfField, focusIfField,
getNoteId,
setNoteId,
setNotetypeMeta, setNotetypeMeta,
wrap, wrap,
setMathjaxEnabled, 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 uiResolve: (api: NoteEditorAPI) => void;
export let mode: EditorMode; export let mode: EditorMode;
export let isLegacy: boolean;
$: if (noteEditor) { $: if (noteEditor) {
uiResolve(api as NoteEditorAPI); 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 --> <!-- Block Qt's default drag & drop behavior -->
<svelte:body <svelte:body
on:dragenter|preventDefault on:dragenter={preventDefaultIfNonLegacy}
on:dragover|preventDefault on:dragover={preventDefaultIfNonLegacy}
on:drop|preventDefault on:drop={preventDefaultIfNonLegacy}
/> />
<!-- <!--
@ -1059,11 +1114,13 @@ components and functionality for general note editing.
role="presentation" role="presentation"
bind:this={noteEditor} bind:this={noteEditor}
on:contextmenu={(event) => { on:contextmenu={(event) => {
if (!isLegacy) {
contextMenuInput = $focusedInput; contextMenuInput = $focusedInput;
onContextMenu(event, api, $focusedInput, contextMenu); onContextMenu(event, api, $focusedInput, contextMenu);
}
}} }}
on:dragover|preventDefault on:dragover={preventDefaultIfNonLegacy}
on:drop={handlePickerDrop} on:drop={checkNonLegacy(handlePickerDrop)}
> >
<EditorToolbar {size} {wrap} api={toolbar}> <EditorToolbar {size} {wrap} api={toolbar}>
<svelte:fragment slot="notetypeButtons"> <svelte:fragment slot="notetypeButtons">
@ -1122,12 +1179,20 @@ components and functionality for general note editing.
on:focusout={async () => { on:focusout={async () => {
$focusedField = null; $focusedField = null;
setAddonButtonsDisabled(true); setAddonButtonsDisabled(true);
if (isLegacy) {
bridgeCommand(
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
get(content),
)}`,
);
} else {
bridgeCommand(`blur:${index}`); bridgeCommand(`blur:${index}`);
note!.fields[index] = await transformContentBeforeSave( note!.fields[index] = await transformContentBeforeSave(
get(content), get(content),
); );
await updateCurrentNote(); await updateCurrentNote();
await updateDuplicateDisplay(); await updateDuplicateDisplay();
}
}} }}
on:mouseenter={() => { on:mouseenter={() => {
$hoveredField = fields[index]; $hoveredField = fields[index];
@ -1160,6 +1225,7 @@ components and functionality for general note editing.
bind:active={stickies[index]} bind:active={stickies[index]}
{index} {index}
{note} {note}
{isLegacy}
show={fields[index] === $hoveredField || show={fields[index] === $hoveredField ||
fields[index] === $focusedField} fields[index] === $focusedField}
/> />
@ -1192,6 +1258,7 @@ components and functionality for general note editing.
> >
<RichTextInput <RichTextInput
{hidden} {hidden}
{isLegacy}
on:focusout={() => { on:focusout={() => {
saveFieldNow(); saveFieldNow();
$focusedInput = null; $focusedInput = null;
@ -1241,9 +1308,12 @@ components and functionality for general note editing.
<Collapsible toggleDisplay collapse={$tagsCollapsed}> <Collapsible toggleDisplay collapse={$tagsCollapsed}>
<TagEditor {tags} on:tagsupdate={saveTags} /> <TagEditor {tags} on:tagsupdate={saveTags} />
</Collapsible> </Collapsible>
{#if !isLegacy}
<ActionButtons {mode} {onClose} {onAdd} /> <ActionButtons {mode} {onClose} {onAdd} />
{/if} {/if}
{/if}
{#if !isLegacy}
<ContextMenu bind:this={contextMenu}> <ContextMenu bind:this={contextMenu}>
{#each contextMenuItems as item} {#each contextMenuItems as item}
<Item <Item
@ -1256,6 +1326,7 @@ components and functionality for general note editing.
</Item> </Item>
{/each} {/each}
</ContextMenu> </ContextMenu>
{/if}
</div> </div>
<style lang="scss"> <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 { context as editorFieldContext } from "./EditorField.svelte";
import type { Note } from "@generated/anki/notes_pb"; import type { Note } from "@generated/anki/notes_pb";
import { getNotetype, updateEditorNotetype } from "@generated/backend"; import { getNotetype, updateEditorNotetype } from "@generated/backend";
import { bridgeCommand } from "@tslib/bridgecommand";
const animated = !document.body.classList.contains("reduce-motion"); const animated = !document.body.classList.contains("reduce-motion");
export let active: boolean; export let active: boolean;
export let show: boolean; export let show: boolean;
export let isLegacy: boolean;
const editorField = editorFieldContext.get(); const editorField = editorFieldContext.get();
const keyCombination = "F9"; const keyCombination = "F9";
@ -29,11 +31,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let note: Note; export let note: Note;
async function toggle() { async function toggle() {
if (isLegacy) {
bridgeCommand(`toggleSticky:${index}`, (value: boolean) => {
active = value;
});
} else {
active = !active; active = !active;
const notetype = await getNotetype({ ntid: note.notetypeId }); const notetype = await getNotetype({ ntid: note.notetypeId });
notetype.fields[index].config!.sticky = active; notetype.fields[index].config!.sticky = active;
await updateEditorNotetype(notetype); await updateEditorNotetype(notetype);
} }
}
function shortcut(target: HTMLElement): () => void { function shortcut(target: HTMLElement): () => void {
return registerShortcut(toggle, keyCombination, { target }); return registerShortcut(toggle, keyCombination, { target });

View file

@ -53,11 +53,11 @@ export const components = {
export { editorToolbar } from "./editor-toolbar"; 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)) { if (!["add", "browser", "current"].includes(mode)) {
alert("unexpected editor type"); alert("unexpected editor type");
return; return;
} }
await setupI18n({ modules: editorModules }); 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, { Object.assign(api, {
setColorButtons, setColorButtons,
}); });
// For legacy editor
Object.assign(globalThis, { setColorButtons });
</script> </script>
<DynamicallySlottable slotHost={Item} {api}> <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"; import { fragmentToStored, storedToFragment } from "./transform";
export let hidden = false; export let hidden = false;
export let isLegacy = false;
export const focusFlag = new Flag(); export const focusFlag = new Flag();
export let isClozeField: boolean; 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}> <div class="rich-text-input" on:focusin={setFocus} on:focusout={removeFocus} {hidden}>
<RichTextStyles <RichTextStyles
{isLegacy}
color={$pageTheme.isDark ? "white" : "black"} color={$pageTheme.isDark ? "white" : "black"}
fontFamily={$fontFamily} fontFamily={$fontFamily}
fontSize={$fontSize} 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"; import { mount } from "svelte";
export let isLegacy = false;
export let callback: (styles: Record<string, any>) => void; export let callback: (styles: Record<string, any>) => void;
const [userBaseStyle, userBaseResolve] = promiseWithResolver<StyleObject>(); const [userBaseStyle, userBaseResolve] = promiseWithResolver<StyleObject>();
@ -47,7 +49,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
$: setStyling("fontSize", fontSize + "px"); $: setStyling("fontSize", fontSize + "px");
$: setStyling("direction", direction); $: setStyling("direction", direction);
const styles: StyleLinkType[] = [ let styles: StyleLinkType[];
if (isLegacy) {
styles = [
{
id: "rootStyle",
type: "link",
href: "./_anki/css/editable.css",
},
];
} else {
styles = [
{ {
id: "editableBaseStyle", id: "editableBaseStyle",
type: "link", type: "link",
@ -64,6 +76,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
href: mathjaxCSS, href: mathjaxCSS,
}, },
]; ];
}
function attachToShadow(element: Element) { function attachToShadow(element: Element) {
const customStyles = mount(CustomStyles, { const customStyles = mount(CustomStyles, {