Split/Merge editor.py for its three use cases (#1581)

* Forbid inserting object and iframe tags via PlainTextInput

* Add optional browserMode parameter to Editor

* Create new ts modules for three editor instances

- note-creator for AddCards
- browser-editor for the editor in the Browser
- reviewer-editor for the EditCurrent

* Revert "Forbid inserting object and iframe tags via PlainTextInput"

This reverts commit ab90ae8194494d883a1863126496e2d8f332509e.

* Refactor browserMode to editorMode

* Move new editor variants inside /ts/editor directory

* Fix typo
This commit is contained in:
Henrik Giesel 2022-01-12 05:51:43 +01:00 committed by GitHub
parent 489eadb352
commit 3beea5e1e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 286 additions and 100 deletions

View file

@ -71,7 +71,12 @@ class AddCards(QMainWindow):
self.setAndFocusNote(new_note) self.setAndFocusNote(new_note)
def setupEditor(self) -> None: def setupEditor(self) -> None:
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self, True) self.editor = aqt.editor.Editor(
self.mw,
self.form.fieldsArea,
self,
editorMode=aqt.editor.EditorMode.ADD_CARDS,
)
self.editor.web.eval("noteEditorPromise.then(() => activateStickyShortcuts());") self.editor.web.eval("noteEditorPromise.then(() => activateStickyShortcuts());")
def setup_choosers(self) -> None: def setup_choosers(self) -> None:

View file

@ -418,7 +418,12 @@ class Browser(QMainWindow):
) )
gui_hooks.editor_did_init.append(add_preview_button) gui_hooks.editor_did_init.append(add_preview_button)
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) self.editor = aqt.editor.Editor(
self.mw,
self.form.fieldsArea,
self,
editorMode=aqt.editor.EditorMode.BROWSER,
)
gui_hooks.editor_did_init.remove(add_preview_button) gui_hooks.editor_did_init.remove(add_preview_button)
@ensure_editor_saved @ensure_editor_saved

View file

@ -15,14 +15,6 @@ compile_sass(
], ],
) )
copy_files_into_group(
name = "editor",
srcs = [
"editor.css",
],
package = "//ts/editor",
)
copy_files_into_group( copy_files_into_group(
name = "editable", name = "editable",
srcs = [ srcs = [
@ -31,6 +23,16 @@ copy_files_into_group(
package = "//ts/editable", package = "//ts/editable",
) )
copy_files_into_group(
name = "editor",
srcs = [
"browser_editor.css",
"reviewer_editor.css",
"note_creator.css",
],
package = "//ts/editor",
)
copy_files_into_group( copy_files_into_group(
name = "reviewer", name = "reviewer",
srcs = [ srcs = [

View file

@ -26,7 +26,9 @@ typescript(
copy_files_into_group( copy_files_into_group(
name = "editor", name = "editor",
srcs = [ srcs = [
"editor.js", "browser_editor.js",
"reviewer_editor.js",
"note_creator.js",
], ],
package = "//ts/editor", package = "//ts/editor",
) )
@ -43,9 +45,9 @@ filegroup(
name = "js", name = "js",
srcs = [ srcs = [
"aqt", "aqt",
"editor",
"mathjax.js", "mathjax.js",
"reviewer", "reviewer",
"editor",
"//qt/aqt/data/web/js/vendor", "//qt/aqt/data/web/js/vendor",
], ],
visibility = ["//qt:__subpackages__"], visibility = ["//qt:__subpackages__"],

View file

@ -24,7 +24,12 @@ class EditCurrent(QDialog):
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close).setShortcut( self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close).setShortcut(
QKeySequence("Ctrl+Return") QKeySequence("Ctrl+Return")
) )
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) self.editor = aqt.editor.Editor(
self.mw,
self.form.fieldsArea,
self,
editorMode=aqt.editor.EditorMode.EDIT_CURRENT,
)
self.editor.card = self.mw.reviewer.card self.editor.card = self.mw.reviewer.card
self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0) self.editor.set_note(self.mw.reviewer.card.note(), focusTo=0)
restoreGeom(self, "editcurrent") restoreGeom(self, "editcurrent")

View file

@ -13,6 +13,7 @@ import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
import warnings import warnings
from enum import Enum
from random import randrange from random import randrange
from typing import Any, Callable, Match, cast from typing import Any, Callable, Match, cast
@ -80,6 +81,12 @@ audio = (
) )
class EditorMode(Enum):
ADD_CARDS = 0
EDIT_CURRENT = 1
BROWSER = 2
class Editor: class Editor:
"""The screen that embeds an editing widget should listen for changes via """The screen that embeds an editing widget should listen for changes via
the `operation_did_execute` hook, and call set_note() when the editor needs the `operation_did_execute` hook, and call set_note() when the editor needs
@ -91,13 +98,23 @@ class Editor:
""" """
def __init__( def __init__(
self, mw: AnkiQt, widget: QWidget, parentWindow: QWidget, addMode: bool = False self,
mw: AnkiQt,
widget: QWidget,
parentWindow: QWidget,
addMode: bool | None = None,
*,
editorMode: EditorMode = EditorMode.EDIT_CURRENT,
) -> None: ) -> None:
self.mw = mw self.mw = mw
self.widget = widget self.widget = widget
self.parentWindow = parentWindow self.parentWindow = parentWindow
self.note: Note | None = None self.note: Note | None = None
self.addMode = addMode # legacy argument provided?
if addMode is not None:
editorMode = EditorMode.ADD_CARDS if addMode else EditorMode.EDIT_CURRENT
self.addMode = editorMode is EditorMode.ADD_CARDS
self.editorMode = editorMode
self.currentField: int | None = None self.currentField: int | None = None
# Similar to currentField, but not set to None on a blur. May be # Similar to currentField, but not set to None on a blur. May be
# outside the bounds of the current notetype. # outside the bounds of the current notetype.
@ -124,11 +141,18 @@ class Editor:
self.web.set_bridge_command(self.onBridgeCmd, self) self.web.set_bridge_command(self.onBridgeCmd, self)
self.outerLayout.addWidget(self.web, 1) self.outerLayout.addWidget(self.web, 1)
if self.editorMode == EditorMode.ADD_CARDS:
file = "note_creator"
elif self.editorMode == EditorMode.BROWSER:
file = "browser_editor"
else:
file = "reviewer_editor"
# then load page # then load page
self.web.stdHtml( self.web.stdHtml(
"", "",
css=["css/editor.css"], css=[f"css/{file}.css"],
js=["js/editor.js"], js=[f"js/{file}.js"],
context=self, context=self,
default_css=False, default_css=False,
) )
@ -137,7 +161,7 @@ class Editor:
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self) gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
lefttopbtns_defs = [ lefttopbtns_defs = [
f"noteEditorPromise.then((noteEditor) => noteEditor.toolbar.notetypeButtons.appendButton({{ component: editorToolbar.Raw, props: {{ html: {json.dumps(button)} }} }}, -1));" f"uiPromise.then((noteEditor) => noteEditor.toolbar.notetypeButtons.appendButton({{ component: editorToolbar.Raw, props: {{ html: {json.dumps(button)} }} }}, -1));"
for button in lefttopbtns for button in lefttopbtns
] ]
lefttopbtns_js = "\n".join(lefttopbtns_defs) lefttopbtns_js = "\n".join(lefttopbtns_defs)
@ -150,7 +174,7 @@ class Editor:
righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns]) righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns])
righttopbtns_js = ( righttopbtns_js = (
f""" f"""
noteEditorPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{ uiPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
component: editorToolbar.AddonButtons, component: editorToolbar.AddonButtons,
id: "addons", id: "addons",
props: {{ buttons: [ {righttopbtns_defs} ] }}, props: {{ buttons: [ {righttopbtns_defs} ] }},
@ -501,9 +525,7 @@ noteEditorPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
js += " setSticky(%s);" % json.dumps(sticky) js += " setSticky(%s);" % json.dumps(sticky)
js = gui_hooks.editor_will_load_note(js, self.note, self) js = gui_hooks.editor_will_load_note(js, self.note, self)
self.web.evalWithCallback( self.web.evalWithCallback(f"uiPromise.then(() => {{ {js} }})", oncallback)
f"noteEditorPromise.then(() => {{ {js} }})", oncallback
)
def _save_current_note(self) -> None: def _save_current_note(self) -> None:
"Call after note is updated with data from webview." "Call after note is updated with data from webview."
@ -557,8 +579,8 @@ noteEditorPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
elif result == NoteFieldsCheckResult.FIELD_NOT_CLOZE: elif result == NoteFieldsCheckResult.FIELD_NOT_CLOZE:
cloze_hint = tr.adding_cloze_outside_cloze_field() cloze_hint = tr.adding_cloze_outside_cloze_field()
self.web.eval(f"setBackgrounds({json.dumps(cols)});") self.web.eval(f"uiPromise.then(() => setBackgrounds({json.dumps(cols)}));")
self.web.eval(f"setClozeHint({json.dumps(cloze_hint)});") self.web.eval(f"uiPromise.then(() => setClozeHint({json.dumps(cloze_hint)}));")
def showDupes(self) -> None: def showDupes(self) -> None:
aqt.dialogs.open( aqt.dialogs.open(
@ -1333,11 +1355,11 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
def set_cloze_button(editor: Editor) -> None: def set_cloze_button(editor: Editor) -> None:
if editor.note.note_type()["type"] == MODEL_CLOZE: if editor.note.note_type()["type"] == MODEL_CLOZE:
editor.web.eval( editor.web.eval(
'noteEditorPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.showButton("cloze")); ' 'uiPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.showButton("cloze")); '
) )
else: else:
editor.web.eval( editor.web.eval(
'noteEditorPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.hideButton("cloze")); ' 'uiPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.hideButton("cloze")); '
) )

View file

@ -35,28 +35,52 @@ _ts_deps = [
compile_svelte(deps = _ts_deps) compile_svelte(deps = _ts_deps)
typescript( typescript(
name = "editor_ts", name = "editor",
deps = _ts_deps + [ deps = _ts_deps + [
":svelte", ":svelte",
], ],
) )
_esbuild_deps = [
":editor",
":editor_css",
"//sass:button_mixins_lib",
"@npm//@mdi",
"@npm//bootstrap-icons",
"@npm//protobufjs",
]
esbuild( esbuild(
name = "editor", name = "browser_editor",
args = { args = {
"loader": {".svg": "text"}, "loader": {".svg": "text"},
}, },
entry_point = "index_wrapper.ts", entry_point = "index_browser.ts",
output_css = "editor.css", output_css = "browser_editor.css",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = _esbuild_deps,
":editor_css", )
":editor_ts",
"//sass:button_mixins_lib", esbuild(
"@npm//@mdi", name = "reviewer_editor",
"@npm//bootstrap-icons", args = {
"@npm//protobufjs", "loader": {".svg": "text"},
], },
entry_point = "index_reviewer.ts",
output_css = "reviewer_editor.css",
visibility = ["//visibility:public"],
deps = _esbuild_deps,
)
esbuild(
name = "note_creator",
args = {
"loader": {".svg": "text"},
},
entry_point = "index_creator.ts",
output_css = "note_creator.css",
visibility = ["//visibility:public"],
deps = _esbuild_deps,
) )
# Tests # Tests

View file

@ -0,0 +1,19 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import OldEditorAdapter from "../editor/OldEditorAdapter.svelte";
import type { NoteEditorAPI } from "../editor/OldEditorAdapter.svelte";
const api: Partial<NoteEditorAPI> = {};
let noteEditor: OldEditorAdapter;
export let uiResolve: (api: NoteEditorAPI) => void;
$: if (noteEditor) {
uiResolve(api as NoteEditorAPI);
}
</script>
<OldEditorAdapter bind:this={noteEditor} {api} />

View file

@ -0,0 +1,19 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import OldEditorAdapter from "../editor/OldEditorAdapter.svelte";
import type { NoteEditorAPI } from "../editor/OldEditorAdapter.svelte";
const api: Partial<NoteEditorAPI> = {};
let noteEditor: OldEditorAdapter;
export let uiResolve: (api: NoteEditorAPI) => void;
$: if (noteEditor) {
uiResolve(api as NoteEditorAPI);
}
</script>
<OldEditorAdapter bind:this={noteEditor} {api} />

View file

@ -248,7 +248,38 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}), }),
); );
import { wrapInternal } from "../lib/wrap";
onMount(() => { onMount(() => {
function wrap(before: string, after: string): void {
if (!get(focusInRichText)) {
return;
}
const input = get(activeInput!) as RichTextInputAPI;
input.element.then((element) => {
wrapInternal(element, before, after, false);
});
}
Object.assign(globalThis, {
setFields,
setDescriptions,
setFonts,
focusField,
setColorButtons,
setTags,
setSticky,
setBackgrounds,
setClozeHint,
saveNow: saveFieldNow,
activateStickyShortcuts,
focusIfField,
setNoteId,
wrap,
});
document.addEventListener("visibilitychange", saveOnPageHide); document.addEventListener("visibilitychange", saveOnPageHide);
return () => document.removeEventListener("visibilitychange", saveOnPageHide); return () => document.removeEventListener("visibilitychange", saveOnPageHide);
}); });

View file

@ -4,13 +4,14 @@ 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 { EditingInputAPI } from "./EditingArea.svelte"; import type { EditingInputAPI } from "./EditingArea.svelte";
import type { CodeMirror as CodeMirrorType } from "./code-mirror";
import CodeMirror from "./CodeMirror.svelte"; import CodeMirror from "./CodeMirror.svelte";
export interface PlainTextInputAPI extends EditingInputAPI { export interface PlainTextInputAPI extends EditingInputAPI {
name: "plain-text"; name: "plain-text";
moveCaretToEnd(): void; moveCaretToEnd(): void;
toggle(): boolean; toggle(): boolean;
getEditor(): CodeMirror.Editor; getEditor(): CodeMirrorType.Editor;
} }
export const parsingInstructions: string[] = []; export const parsingInstructions: string[] = [];
@ -95,7 +96,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
return hidden; return hidden;
} }
function getEditor(): CodeMirror.Editor { function getEditor(): CodeMirrorType.Editor {
return codeMirror?.editor; return codeMirror?.editor;
} }

View file

@ -0,0 +1,19 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import OldEditorAdapter from "../editor/OldEditorAdapter.svelte";
import type { NoteEditorAPI } from "../editor/OldEditorAdapter.svelte";
const api: Partial<NoteEditorAPI> = {};
let noteEditor: OldEditorAdapter;
export let uiResolve: (api: NoteEditorAPI) => void;
$: if (noteEditor) {
uiResolve(api as NoteEditorAPI);
}
</script>
<OldEditorAdapter bind:this={noteEditor} {api} />

View file

@ -60,53 +60,4 @@ export const i18n = setupI18n({
], ],
}); });
import OldEditorAdapter from "./OldEditorAdapter.svelte";
import type { NoteEditorAPI } from "./OldEditorAdapter.svelte";
async function setupNoteEditor(): Promise<NoteEditorAPI> {
await i18n;
const api: Partial<NoteEditorAPI> = {};
const noteEditor = new OldEditorAdapter({
target: document.body,
props: { api: api as NoteEditorAPI },
});
Object.assign(globalThis, {
setFields: noteEditor.setFields,
setDescriptions: noteEditor.setDescriptions,
setFonts: noteEditor.setFonts,
focusField: noteEditor.focusField,
setColorButtons: noteEditor.setColorButtons,
setTags: noteEditor.setTags,
setSticky: noteEditor.setSticky,
setBackgrounds: noteEditor.setBackgrounds,
setClozeHint: noteEditor.setClozeHint,
saveNow: noteEditor.saveFieldNow,
activateStickyShortcuts: noteEditor.activateStickyShortcuts,
focusIfField: noteEditor.focusIfField,
setNoteId: noteEditor.setNoteId,
});
return api as NoteEditorAPI;
}
import { get } from "svelte/store";
import { wrapInternal } from "../lib/wrap";
import type { RichTextInputAPI } from "./RichTextInput.svelte";
export async function wrap(before: string, after: string): Promise<void> {
const noteEditor = await noteEditorPromise;
if (!get(noteEditor.focusInRichText)) {
return;
}
const activeInput = get(noteEditor.activeInput) as RichTextInputAPI;
const element = await activeInput.element;
wrapInternal(element, before, after, false);
}
export const noteEditorPromise = setupNoteEditor();
export { editorToolbar } from "./EditorToolbar.svelte"; export { editorToolbar } from "./EditorToolbar.svelte";

View file

@ -0,0 +1,27 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { i18n } from ".";
import BrowserEditor from "./BrowserEditor.svelte";
import { promiseWithResolver } from "../lib/promise";
import { globalExport } from "../lib/globals";
const [uiPromise, uiResolve] = promiseWithResolver();
async function setupBrowserEditor(): Promise<void> {
await i18n;
new BrowserEditor({
target: document.body,
props: { uiResolve },
});
}
setupBrowserEditor();
import * as editor from ".";
globalExport({
...editor,
uiPromise,
noteEditorPromise: uiPromise,
});

View file

@ -0,0 +1,27 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { i18n } from ".";
import NoteCreator from "./NoteCreator.svelte";
import { promiseWithResolver } from "../lib/promise";
import { globalExport } from "../lib/globals";
const [uiPromise, uiResolve] = promiseWithResolver();
async function setupNoteCreator(): Promise<void> {
await i18n;
new NoteCreator({
target: document.body,
props: { uiResolve },
});
}
setupNoteCreator();
import * as editor from ".";
globalExport({
...editor,
uiPromise,
noteEditorPromise: uiPromise,
});

View file

@ -0,0 +1,27 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { i18n } from "../editor";
import ReviewerEditor from "./ReviewerEditor.svelte";
import { promiseWithResolver } from "../lib/promise";
import { globalExport } from "../lib/globals";
const [uiPromise, uiResolve] = promiseWithResolver();
async function setupReviewerEditor(): Promise<void> {
await i18n;
new ReviewerEditor({
target: document.body,
props: { uiResolve },
});
}
setupReviewerEditor();
import * as editor from "../editor";
globalExport({
...editor,
uiPromise,
noteEditorPromise: uiPromise,
});

View file

@ -1,11 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
// extend the global namespace with our exports - not sure if there's a better way with esbuild
import * as globals from "./index";
for (const key in globals) {
window[key] = globals[key];
}
// but also export as window.anki
window["anki"] = globals;

11
ts/lib/globals.ts Normal file
View file

@ -0,0 +1,11 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export function globalExport(globals: Record<string, unknown>): void {
for (const key in globals) {
window[key] = globals[key];
}
// but also export as window.anki
window["anki"] = globals;
}