diff --git a/qt/aqt/data/web/css/editor.scss b/qt/aqt/data/web/css/editor.scss
index c24c15275..af714c161 100644
--- a/qt/aqt/data/web/css/editor.scss
+++ b/qt/aqt/data/web/css/editor.scss
@@ -1,6 +1,24 @@
/* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
+#fields {
+ display: flex;
+ flex-direction: column;
+ margin: 5px;
+
+ & > * {
+ margin: 1px 0;
+
+ &:first-child {
+ margin-top: 0;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
+
.field {
border: 1px solid var(--border);
background: var(--frame-bg);
@@ -14,12 +32,6 @@
}
}
-.clearfix::after {
- content: "";
- display: table;
- clear: both;
-}
-
.fname {
vertical-align: middle;
padding: 0;
diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts
index a22b699ed..0791fd51b 100644
--- a/qt/aqt/data/web/js/editor.ts
+++ b/qt/aqt/data/web/js/editor.ts
@@ -109,13 +109,73 @@ function nodeIsElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE;
}
+const INLINE_TAGS = [
+ "A",
+ "ABBR",
+ "ACRONYM",
+ "AUDIO",
+ "B",
+ "BDI",
+ "BDO",
+ "BIG",
+ "BR",
+ "BUTTON",
+ "CANVAS",
+ "CITE",
+ "CODE",
+ "DATA",
+ "DATALIST",
+ "DEL",
+ "DFN",
+ "EM",
+ "EMBED",
+ "I",
+ "IFRAME",
+ "IMG",
+ "INPUT",
+ "INS",
+ "KBD",
+ "LABEL",
+ "MAP",
+ "MARK",
+ "METER",
+ "NOSCRIPT",
+ "OBJECT",
+ "OUTPUT",
+ "PICTURE",
+ "PROGRESS",
+ "Q",
+ "RUBY",
+ "S",
+ "SAMP",
+ "SCRIPT",
+ "SELECT",
+ "SLOT",
+ "SMALL",
+ "SPAN",
+ "STRONG",
+ "SUB",
+ "SUP",
+ "SVG",
+ "TEMPLATE",
+ "TEXTAREA",
+ "TIME",
+ "U",
+ "TT",
+ "VAR",
+ "VIDEO",
+ "WBR",
+];
+
+function nodeIsInline(node: Node): boolean {
+ return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
+}
+
function inListItem(): boolean {
const anchor = window.getSelection().anchorNode;
- let n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
-
let inList = false;
-
+ let n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
while (n) {
inList = inList || window.getComputedStyle(n).display == "list-item";
n = n.parentElement;
@@ -193,7 +253,8 @@ function clearChangeTimer(): void {
}
}
-function onFocus(elem: HTMLElement): void {
+function onFocus(evt: FocusEvent): void {
+ const elem = evt.currentTarget as HTMLElement;
if (currentField === elem) {
// anki window refocused; current element unchanged
return;
@@ -273,16 +334,36 @@ function onBlur(): void {
}
}
+function fieldContainsInlineContent(field: HTMLDivElement): boolean {
+ if (field.childNodes.length === 0) {
+ // for now, for all practical purposes, empty fields are in block mode
+ return false;
+ }
+
+ for (const child of field.children) {
+ if (!nodeIsInline(child)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
function saveField(type: "blur" | "key"): void {
clearChangeTimer();
if (!currentField) {
// no field has been focused yet
return;
}
- // type is either 'blur' or 'key'
- pycmd(
- `${type}:${currentFieldOrdinal()}:${currentNoteId}:${currentField.innerHTML}`
- );
+
+ const fieldText =
+ fieldContainsInlineContent(currentField) &&
+ currentField.innerHTML.endsWith("
")
+ ? // trim trailing
+ currentField.innerHTML.slice(0, -4)
+ : currentField.innerHTML;
+
+ pycmd(`${type}:${currentFieldOrdinal()}:${currentNoteId}:${fieldText}`);
}
function currentFieldOrdinal(): string {
@@ -356,44 +437,63 @@ function onCutOrCopy(): boolean {
return true;
}
+function createField(
+ index: number,
+ label: string,
+ color: string,
+ content: string
+): [HTMLDivElement, HTMLDivElement] {
+ const name = document.createElement("div");
+ name.id = `name${index}`;
+ name.className = "fname";
+
+ const fieldname = document.createElement("span");
+ fieldname.className = "fieldname";
+ fieldname.innerText = label;
+ name.appendChild(fieldname);
+
+ const field = document.createElement("div");
+ field.id = `f${index}`;
+ field.className = "field";
+ field.setAttribute("contenteditable", "true");
+ field.style.color = color;
+ field.addEventListener("keydown", onKey);
+ field.addEventListener("keyup", onKeyUp);
+ field.addEventListener("input", onInput);
+ field.addEventListener("focus", onFocus);
+ field.addEventListener("blur", onBlur);
+ field.addEventListener("paste", onPaste);
+ field.addEventListener("copy", onCutOrCopy);
+ field.addEventListener("oncut", onCutOrCopy);
+ field.innerHTML = content;
+
+ if (fieldContainsInlineContent(field)) {
+ field.appendChild(document.createElement("br"));
+ }
+
+ return [name, field];
+}
+
function setFields(fields: [string, string][]): void {
- let txt = "";
// webengine will include the variable after enter+backspace
// if we don't convert it to a literal colour
const color = window
.getComputedStyle(document.documentElement)
.getPropertyValue("--text-fg");
- for (let i = 0; i < fields.length; i++) {
- const n = fields[i][0];
- let f = fields[i][1];
- txt += `
-