mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
Merge pull request #952 from hgiesel/fieldsflex
Deal with inline content vs block content and <br> in editor.ts
This commit is contained in:
commit
f45d51c92d
3 changed files with 158 additions and 48 deletions
|
@ -1,6 +1,24 @@
|
||||||
/* Copyright: Ankitects Pty Ltd and contributors
|
/* Copyright: Ankitects Pty Ltd and contributors
|
||||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
* 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 {
|
.field {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--frame-bg);
|
background: var(--frame-bg);
|
||||||
|
@ -14,12 +32,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.clearfix::after {
|
|
||||||
content: "";
|
|
||||||
display: table;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fname {
|
.fname {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -109,13 +109,73 @@ function nodeIsElement(node: Node): node is Element {
|
||||||
return node.nodeType === Node.ELEMENT_NODE;
|
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 {
|
function inListItem(): boolean {
|
||||||
const anchor = window.getSelection().anchorNode;
|
const anchor = window.getSelection().anchorNode;
|
||||||
|
|
||||||
let n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
|
|
||||||
|
|
||||||
let inList = false;
|
let inList = false;
|
||||||
|
let n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
|
||||||
while (n) {
|
while (n) {
|
||||||
inList = inList || window.getComputedStyle(n).display == "list-item";
|
inList = inList || window.getComputedStyle(n).display == "list-item";
|
||||||
n = n.parentElement;
|
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) {
|
if (currentField === elem) {
|
||||||
// anki window refocused; current element unchanged
|
// anki window refocused; current element unchanged
|
||||||
return;
|
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 {
|
function saveField(type: "blur" | "key"): void {
|
||||||
clearChangeTimer();
|
clearChangeTimer();
|
||||||
if (!currentField) {
|
if (!currentField) {
|
||||||
// no field has been focused yet
|
// no field has been focused yet
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// type is either 'blur' or 'key'
|
|
||||||
pycmd(
|
const fieldText =
|
||||||
`${type}:${currentFieldOrdinal()}:${currentNoteId}:${currentField.innerHTML}`
|
fieldContainsInlineContent(currentField) &&
|
||||||
);
|
currentField.innerHTML.endsWith("<br>")
|
||||||
|
? // trim trailing <br>
|
||||||
|
currentField.innerHTML.slice(0, -4)
|
||||||
|
: currentField.innerHTML;
|
||||||
|
|
||||||
|
pycmd(`${type}:${currentFieldOrdinal()}:${currentNoteId}:${fieldText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentFieldOrdinal(): string {
|
function currentFieldOrdinal(): string {
|
||||||
|
@ -356,44 +437,63 @@ function onCutOrCopy(): boolean {
|
||||||
return true;
|
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 {
|
function setFields(fields: [string, string][]): void {
|
||||||
let txt = "";
|
|
||||||
// webengine will include the variable after enter+backspace
|
// webengine will include the variable after enter+backspace
|
||||||
// if we don't convert it to a literal colour
|
// if we don't convert it to a literal colour
|
||||||
const color = window
|
const color = window
|
||||||
.getComputedStyle(document.documentElement)
|
.getComputedStyle(document.documentElement)
|
||||||
.getPropertyValue("--text-fg");
|
.getPropertyValue("--text-fg");
|
||||||
for (let i = 0; i < fields.length; i++) {
|
|
||||||
const n = fields[i][0];
|
const elements = fields.flatMap(([name, fieldcontent], index: number) =>
|
||||||
let f = fields[i][1];
|
createField(index, name, color, fieldcontent)
|
||||||
txt += `
|
|
||||||
<tr>
|
|
||||||
<td class=fname id="name${i}">
|
|
||||||
<span class="fieldname">${n}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td width=100%>
|
|
||||||
<div id="f${i}"
|
|
||||||
onkeydown="onKey(window.event);"
|
|
||||||
onkeyup="onKeyUp(window.event);"
|
|
||||||
oninput="onInput();"
|
|
||||||
onmouseup="onKey(window.event);"
|
|
||||||
onfocus="onFocus(this);"
|
|
||||||
onblur="onBlur();"
|
|
||||||
class="field clearfix"
|
|
||||||
onpaste="onPaste(this);"
|
|
||||||
oncopy="onCutOrCopy(this);"
|
|
||||||
oncut="onCutOrCopy(this);"
|
|
||||||
contentEditable
|
|
||||||
style="color: ${color}"
|
|
||||||
>${f}</div>
|
|
||||||
</td>
|
|
||||||
</tr>`;
|
|
||||||
}
|
|
||||||
$("#fields").html(
|
|
||||||
`<table cellpadding=0 width=100% style='table-layout: fixed;'>${txt}</table>`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fieldsContainer = document.getElementById("fields");
|
||||||
|
// can be replaced with ParentNode.replaceChildren in Chrome 86+
|
||||||
|
while (fieldsContainer.firstChild) {
|
||||||
|
fieldsContainer.removeChild(fieldsContainer.firstChild);
|
||||||
|
}
|
||||||
|
for (const element of elements) {
|
||||||
|
fieldsContainer.appendChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
maybeDisableButtons();
|
maybeDisableButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -456,8 +556,6 @@ let filterHTML = function (
|
||||||
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
|
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
|
||||||
}
|
}
|
||||||
outHtml = outHtml.trim();
|
outHtml = outHtml.trim();
|
||||||
//console.log(`input html: ${html}`);
|
|
||||||
//console.log(`outpt html: ${outHtml}`);
|
|
||||||
return outHtml;
|
return outHtml;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"lib": ["es6", "dom", "dom.iterable"],
|
"lib": ["es2019", "dom", "dom.iterable"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
|
|
Loading…
Reference in a new issue