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)
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());")
def setup_choosers(self) -> None:

View file

@ -418,7 +418,12 @@ class Browser(QMainWindow):
)
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)
@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(
name = "editable",
srcs = [
@ -31,6 +23,16 @@ copy_files_into_group(
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(
name = "reviewer",
srcs = [

View file

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

View file

@ -24,7 +24,12 @@ class EditCurrent(QDialog):
self.form.buttonBox.button(QDialogButtonBox.StandardButton.Close).setShortcut(
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.set_note(self.mw.reviewer.card.note(), focusTo=0)
restoreGeom(self, "editcurrent")

View file

@ -13,6 +13,7 @@ import urllib.error
import urllib.parse
import urllib.request
import warnings
from enum import Enum
from random import randrange
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:
"""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
@ -91,13 +98,23 @@ class Editor:
"""
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:
self.mw = mw
self.widget = widget
self.parentWindow = parentWindow
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
# Similar to currentField, but not set to None on a blur. May be
# outside the bounds of the current notetype.
@ -124,11 +141,18 @@ class Editor:
self.web.set_bridge_command(self.onBridgeCmd, self)
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
self.web.stdHtml(
"",
css=["css/editor.css"],
js=["js/editor.js"],
css=[f"css/{file}.css"],
js=[f"js/{file}.js"],
context=self,
default_css=False,
)
@ -137,7 +161,7 @@ class Editor:
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
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
]
lefttopbtns_js = "\n".join(lefttopbtns_defs)
@ -150,7 +174,7 @@ class Editor:
righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns])
righttopbtns_js = (
f"""
noteEditorPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
uiPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
component: editorToolbar.AddonButtons,
id: "addons",
props: {{ buttons: [ {righttopbtns_defs} ] }},
@ -501,9 +525,7 @@ noteEditorPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
js += " setSticky(%s);" % json.dumps(sticky)
js = gui_hooks.editor_will_load_note(js, self.note, self)
self.web.evalWithCallback(
f"noteEditorPromise.then(() => {{ {js} }})", oncallback
)
self.web.evalWithCallback(f"uiPromise.then(() => {{ {js} }})", oncallback)
def _save_current_note(self) -> None:
"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:
cloze_hint = tr.adding_cloze_outside_cloze_field()
self.web.eval(f"setBackgrounds({json.dumps(cols)});")
self.web.eval(f"setClozeHint({json.dumps(cloze_hint)});")
self.web.eval(f"uiPromise.then(() => setBackgrounds({json.dumps(cols)}));")
self.web.eval(f"uiPromise.then(() => setClozeHint({json.dumps(cloze_hint)}));")
def showDupes(self) -> None:
aqt.dialogs.open(
@ -1333,11 +1355,11 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
def set_cloze_button(editor: Editor) -> None:
if editor.note.note_type()["type"] == MODEL_CLOZE:
editor.web.eval(
'noteEditorPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.showButton("cloze")); '
'uiPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.showButton("cloze")); '
)
else:
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)
typescript(
name = "editor_ts",
name = "editor",
deps = _ts_deps + [
":svelte",
],
)
_esbuild_deps = [
":editor",
":editor_css",
"//sass:button_mixins_lib",
"@npm//@mdi",
"@npm//bootstrap-icons",
"@npm//protobufjs",
]
esbuild(
name = "editor",
name = "browser_editor",
args = {
"loader": {".svg": "text"},
},
entry_point = "index_wrapper.ts",
output_css = "editor.css",
entry_point = "index_browser.ts",
output_css = "browser_editor.css",
visibility = ["//visibility:public"],
deps = [
":editor_css",
":editor_ts",
"//sass:button_mixins_lib",
"@npm//@mdi",
"@npm//bootstrap-icons",
"@npm//protobufjs",
],
deps = _esbuild_deps,
)
esbuild(
name = "reviewer_editor",
args = {
"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

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

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;
}