anki/web/editor.js
Damien Elmes dd01561537 fire saveField("key") when webview loses focus
fixes the following reported issue, caused by currentField
being sent to None:

1. Start from the main view.
2. Click Add
3. In the Add window, change deck
4. In the Choose Deck window, add a new deck, give a random name
5. Back in the Add window, call "Edit HTML"
6. Anki displays an error

This change will mean the editFocusLost hook will not fire when
switching between windows, which is probably an improvement.
2019-02-05 10:46:47 +10:00

488 lines
13 KiB
JavaScript

var currentField = null;
var changeTimer = null;
var dropTarget = null;
var currentNoteId = null;
String.prototype.format = function () {
var args = arguments;
return this.replace(/\{\d+\}/g, function (m) {
return args[m.match(/\d+/)];
});
};
function setFGButton(col) {
$("#forecolor")[0].style.backgroundColor = col;
}
function saveNow(keepFocus) {
if (!currentField) {
return;
}
clearChangeTimer();
if (keepFocus) {
saveField("key");
} else {
// triggers onBlur, which saves
currentField.blur();
}
}
function triggerKeyTimer() {
clearChangeTimer();
changeTimer = setTimeout(function () {
updateButtonState();
saveField("key");
}, 600);
}
function onKey() {
// esc clears focus, allowing dialog to close
if (window.event.which === 27) {
currentField.blur();
return;
}
// shift+tab goes to previous field
if (navigator.platform === "MacIntel" &&
window.event.which === 9 && window.event.shiftKey) {
window.event.preventDefault();
focusPrevious();
return;
}
triggerKeyTimer();
}
function insertNewline() {
if (!inPreEnvironment()) {
setFormat("insertText", "\n");
return;
}
// in some cases inserting a newline will not show any changes,
// as a trailing newline at the end of a block does not render
// differently. so in such cases we note the height has not
// changed and insert an extra newline.
var r = window.getSelection().getRangeAt(0);
if (!r.collapsed) {
// delete any currently selected text first, making
// sure the delete is undoable
setFormat("delete");
}
var oldHeight = currentField.clientHeight;
setFormat("inserthtml", "\n");
if (currentField.clientHeight === oldHeight) {
setFormat("inserthtml", "\n");
}
}
// is the cursor in an environment that respects whitespace?
function inPreEnvironment() {
var n = window.getSelection().anchorNode;
if (n.nodeType === 3) {
n = n.parentNode;
}
return window.getComputedStyle(n).whiteSpace.startsWith("pre");
}
function onInput() {
// make sure IME changes get saved
triggerKeyTimer();
}
function updateButtonState() {
var buts = ["bold", "italic", "underline", "superscript", "subscript"];
for (var i = 0; i < buts.length; i++) {
var name = buts[i];
if (document.queryCommandState(name)) {
$("#" + name).addClass("highlighted");
} else {
$("#" + name).removeClass("highlighted");
}
}
// fixme: forecolor
// 'col': document.queryCommandValue("forecolor")
}
function toggleEditorButton(buttonid) {
if ($(buttonid).hasClass("highlighted")) {
$(buttonid).removeClass("highlighted");
} else {
$(buttonid).addClass("highlighted");
}
}
function setFormat(cmd, arg, nosave) {
document.execCommand(cmd, false, arg);
if (!nosave) {
saveField('key');
updateButtonState();
}
}
function clearChangeTimer() {
if (changeTimer) {
clearTimeout(changeTimer);
changeTimer = null;
}
}
function onFocus(elem) {
if (currentField === elem) {
// anki window refocused; current element unchanged
return;
}
currentField = elem;
pycmd("focus:" + currentFieldOrdinal());
enableButtons();
// don't adjust cursor on mouse clicks
if (mouseDown) {
return;
}
// do this twice so that there's no flicker on newer versions
caretToEnd();
// scroll if bottom of element off the screen
function pos(obj) {
var cur = 0;
do {
cur += obj.offsetTop;
} while (obj = obj.offsetParent);
return cur;
}
var y = pos(elem);
if ((window.pageYOffset + window.innerHeight) < (y + elem.offsetHeight) ||
window.pageYOffset > y) {
window.scroll(0, y + elem.offsetHeight - window.innerHeight);
}
}
function focusField(n) {
if (n === null) {
return;
}
$("#f" + n).focus();
}
function focusPrevious() {
if (!currentField) {
return;
}
var previous = currentFieldOrdinal() - 1;
if (previous >= 0) {
focusField(previous);
}
}
function onDragOver(elem) {
var e = window.event;
e.dataTransfer.dropEffect = "copy";
e.preventDefault();
// if we focus the target element immediately, the drag&drop turns into a
// copy, so note it down for later instead
dropTarget = elem;
}
function makeDropTargetCurrent() {
dropTarget.focus();
// the focus event may not fire if the window is not active, so make sure
// the current field is set
currentField = dropTarget;
}
function onPaste(elem) {
pycmd("paste");
window.event.preventDefault();
}
function caretToEnd() {
var r = document.createRange();
r.selectNodeContents(currentField);
r.collapse(false);
var s = document.getSelection();
s.removeAllRanges();
s.addRange(r);
}
function onBlur() {
if (!currentField) {
return;
}
if (document.activeElement === currentField) {
// other widget or window focused; current field unchanged
saveField("key");
} else {
saveField("blur");
currentField = null;
disableButtons();
}
}
function saveField(type) {
clearChangeTimer();
if (!currentField) {
// no field has been focused yet
return;
}
// type is either 'blur' or 'key'
pycmd(type + ":" + currentFieldOrdinal() + ":" + currentNoteId + ":" + currentField.innerHTML);
}
function currentFieldOrdinal() {
return currentField.id.substring(1);
}
function wrappedExceptForWhitespace(text, front, back) {
var match = text.match(/^(\s*)([^]*?)(\s*)$/);
return match[1] + front + match[2] + back + match[3];
}
function disableButtons() {
$("button.linkb:not(.perm)").prop("disabled", true);
}
function enableButtons() {
$("button.linkb").prop("disabled", false);
}
// disable the buttons if a field is not currently focused
function maybeDisableButtons() {
if (!document.activeElement || document.activeElement.className !== "field") {
disableButtons();
} else {
enableButtons();
}
}
function wrap(front, back) {
if (currentField.dir === "rtl") {
front = "&#8235;" + front + "&#8236;";
back = "&#8235;" + back + "&#8236;";
}
var s = window.getSelection();
var r = s.getRangeAt(0);
var content = r.cloneContents();
var span = document.createElement("span");
span.appendChild(content);
var new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
setFormat("inserthtml", new_);
if (!span.innerHTML) {
// run with an empty selection; move cursor back past postfix
r = s.getRangeAt(0);
r.setStart(r.startContainer, r.startOffset - back.length);
r.collapse(true);
s.removeAllRanges();
s.addRange(r);
}
}
function onCutOrCopy() {
pycmd("cutOrCopy");
return true;
}
function setFields(fields) {
var txt = "";
for (var i = 0; i < fields.length; i++) {
var n = fields[i][0];
var f = fields[i][1];
txt += "<tr><td class=fname>{0}</td></tr><tr><td width=100%>".format(n);
txt += "<div id=f{0} onkeydown='onKey();' oninput='onInput()' onmouseup='onKey();'".format(i);
txt += " onfocus='onFocus(this);' onblur='onBlur();' class='field clearfix' ";
txt += "ondragover='onDragOver(this);' onpaste='onPaste(this);' ";
txt += "oncopy='onCutOrCopy(this);' oncut='onCutOrCopy(this);' ";
txt += "contentEditable=true class=field>{0}</div>".format(f);
txt += "</td></tr>";
}
$("#fields").html("<table cellpadding=0 width=100% style='table-layout: fixed;'>" + txt + "</table>");
maybeDisableButtons();
}
function setBackgrounds(cols) {
for (var i = 0; i < cols.length; i++) {
$("#f" + i).css("background", cols[i]);
}
}
function setFonts(fonts) {
for (var i = 0; i < fonts.length; i++) {
var n = $("#f" + i);
n.css("font-family", fonts[i][0])
.css("font-size", fonts[i][1]);
n[0].dir = fonts[i][2] ? "rtl" : "ltr";
}
}
function setNoteId(id) {
currentNoteId = id;
}
function showDupes() {
$("#dupes").show();
}
function hideDupes() {
$("#dupes").hide();
}
var pasteHTML = function (html, internal, extendedMode) {
html = filterHTML(html, internal, extendedMode);
setFormat("inserthtml", html);
};
var filterHTML = function (html, internal, extendedMode) {
// wrap it in <top> as we aren't allowed to change top level elements
var top = $.parseHTML("<ankitop>" + html + "</ankitop>")[0];
if (internal) {
filterInternalNode(top);
} else {
filterNode(top, extendedMode);
}
var outHtml = top.innerHTML;
if (!extendedMode) {
// collapse whitespace
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
}
outHtml = outHtml.trim();
//console.log(`input html: ${html}`);
//console.log(`outpt html: ${outHtml}`);
return outHtml;
};
var allowedTagsBasic = {};
var allowedTagsExtended = {};
var TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR",
"B", "I", "U", "EM", "STRONG", "SUB", "SUP"];
var i;
for (i = 0; i < TAGS_WITHOUT_ATTRS.length; i++) {
allowedTagsBasic[TAGS_WITHOUT_ATTRS[i]] = {"attrs": []};
}
TAGS_WITHOUT_ATTRS = ["H1", "H2", "H3", "LI", "UL", "OL", "BLOCKQUOTE", "CODE",
"PRE", "TABLE", "DD", "DT", "DL"];
for (i = 0; i < TAGS_WITHOUT_ATTRS.length; i++) {
allowedTagsExtended[TAGS_WITHOUT_ATTRS[i]] = {"attrs": []};
}
allowedTagsBasic["IMG"] = {"attrs": ["SRC"]};
allowedTagsExtended["A"] = {"attrs": ["HREF"]};
allowedTagsExtended["TR"] = {"attrs": ["ROWSPAN"]};
allowedTagsExtended["TD"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
allowedTagsExtended["TH"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
// add basic tags to extended
Object.assign(allowedTagsExtended, allowedTagsBasic);
// filtering from another field
var filterInternalNode = function (node) {
if (node.style) {
node.style.removeProperty("background-color");
node.style.removeProperty("font-size");
node.style.removeProperty("font-family");
}
// recurse
for (var i = 0; i < node.childNodes.length; i++) {
filterInternalNode(node.childNodes[i]);
}
};
// filtering from external sources
var filterNode = function (node, extendedMode) {
// text node?
if (node.nodeType === 3) {
return;
}
// descend first, and take a copy of the child nodes as the loop will skip
// elements due to node modifications otherwise
var nodes = [];
var i;
for (i = 0; i < node.childNodes.length; i++) {
nodes.push(node.childNodes[i]);
}
for (i = 0; i < nodes.length; i++) {
filterNode(nodes[i], extendedMode);
}
if (node.tagName === "ANKITOP") {
return;
}
var tag;
if (extendedMode) {
tag = allowedTagsExtended[node.tagName];
} else {
tag = allowedTagsBasic[node.tagName];
}
if (!tag) {
if (!node.innerHTML || node.tagName === 'TITLE') {
node.parentNode.removeChild(node);
} else {
node.outerHTML = node.innerHTML;
}
} else {
// allowed, filter out attributes
var toRemove = [];
for (i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
var attrName = attr.name.toUpperCase();
if (tag.attrs.indexOf(attrName) === -1) {
toRemove.push(attr);
}
}
for (i = 0; i < toRemove.length; i++) {
node.removeAttributeNode(toRemove[i]);
}
}
};
var adjustFieldsTopMargin = function() {
var topHeight = $("#topbuts").height();
var margin = topHeight + 8;
document.getElementById("fields").style.marginTop = margin + "px";
};
var mouseDown = 0;
$(function () {
document.body.onmousedown = function () {
mouseDown++;
};
document.body.onmouseup = function () {
mouseDown--;
};
document.onclick = function (evt) {
var src = window.event.srcElement;
if (src.tagName === "IMG") {
// image clicked; find contenteditable parent
var p = src;
while (p = p.parentNode) {
if (p.className === "field") {
$("#" + p.id).focus();
break;
}
}
}
};
// prevent editor buttons from taking focus
$("button.linkb").on("mousedown", function (e) {
e.preventDefault();
});
window.onresize = function() {
adjustFieldsTopMargin();
};
adjustFieldsTopMargin();
});