mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
split js code out into separate files, mathjax improvements
- js code that was previously bundled in .py files is now in the web folder - add helpers to create links to bundled files, and update stdHtml() to accept a list of javascript files to include instead of text - render MathJax in card layout and preview screens - these should be updated in the future to update the document dynamically like the reviewer does - start media server earlier so it can be used to serve content for the toolbar, etc - work around a bug in WebEngine on Windows that could cause the media server to hang
This commit is contained in:
parent
5ef1692c78
commit
7ad6966943
20 changed files with 669 additions and 611 deletions
|
@ -6,7 +6,6 @@ import time
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import anki.js
|
|
||||||
from anki.utils import fmtTimeSpan, ids2str
|
from anki.utils import fmtTimeSpan, ids2str
|
||||||
from anki.lang import _, ngettext
|
from anki.lang import _, ngettext
|
||||||
|
|
||||||
|
@ -108,6 +107,7 @@ class CollectionStats:
|
||||||
self.height = 200
|
self.height = 200
|
||||||
self.wholeCollection = False
|
self.wholeCollection = False
|
||||||
|
|
||||||
|
# assumes jquery & plot are available in document
|
||||||
def report(self, type=0):
|
def report(self, type=0):
|
||||||
# 0=days, 1=weeks, 2=months
|
# 0=days, 1=weeks, 2=months
|
||||||
self.type = type
|
self.type = type
|
||||||
|
@ -122,8 +122,7 @@ class CollectionStats:
|
||||||
txt += self._section(self.easeGraph())
|
txt += self._section(self.easeGraph())
|
||||||
txt += self._section(self.cardGraph())
|
txt += self._section(self.cardGraph())
|
||||||
txt += self._section(self.footer())
|
txt += self._section(self.footer())
|
||||||
return "<script>%s\n</script><center>%s</center>" % (
|
return "<center>%s</center>" % txt
|
||||||
anki.js.jquery+anki.js.plot, txt)
|
|
||||||
|
|
||||||
def _section(self, txt):
|
def _section(self, txt):
|
||||||
return "<div class=section>%s</div>" % txt
|
return "<div class=section>%s</div>" % txt
|
||||||
|
|
|
@ -1120,10 +1120,13 @@ where id in %s""" % ids2str(sf))
|
||||||
txt = re.sub("\[\[type:[^]]+\]\]", "", txt)
|
txt = re.sub("\[\[type:[^]]+\]\]", "", txt)
|
||||||
ti = lambda x: x
|
ti = lambda x: x
|
||||||
base = self.mw.baseHTML()
|
base = self.mw.baseHTML()
|
||||||
|
jsinc = ["jquery.js","browsersel.js",
|
||||||
|
"mathjax/conf.js", "mathjax/MathJax.js",
|
||||||
|
"mathjax/queue-typeset.js"]
|
||||||
self._previewWeb.stdHtml(
|
self._previewWeb.stdHtml(
|
||||||
ti(mungeQA(self.col, txt)), self.mw.reviewer._styles(),
|
ti(mungeQA(self.col, txt)), self.mw.reviewer._styles(),
|
||||||
bodyClass="card card%d" % (c.ord+1), head=base,
|
bodyClass="card card%d" % (c.ord+1), head=base,
|
||||||
js=anki.js.browserSel)
|
js=jsinc)
|
||||||
clearAudioQueue()
|
clearAudioQueue()
|
||||||
if self.mw.reviewer.autoplay(c):
|
if self.mw.reviewer.autoplay(c):
|
||||||
playFromText(txt)
|
playFromText(txt)
|
||||||
|
|
|
@ -13,8 +13,6 @@ from aqt.utils import saveGeom, restoreGeom, mungeQA,\
|
||||||
showWarning, openHelp, downArrow
|
showWarning, openHelp, downArrow
|
||||||
from anki.utils import isMac, isWin, joinFields
|
from anki.utils import isMac, isWin, joinFields
|
||||||
from aqt.webview import AnkiWebView
|
from aqt.webview import AnkiWebView
|
||||||
import anki.js
|
|
||||||
|
|
||||||
|
|
||||||
class CardLayout(QDialog):
|
class CardLayout(QDialog):
|
||||||
|
|
||||||
|
@ -226,14 +224,17 @@ Please create a new card type first."""))
|
||||||
c = self.card
|
c = self.card
|
||||||
ti = self.maybeTextInput
|
ti = self.maybeTextInput
|
||||||
base = self.mw.baseHTML()
|
base = self.mw.baseHTML()
|
||||||
|
jsinc = ["jquery.js","browsersel.js",
|
||||||
|
"mathjax/conf.js", "mathjax/MathJax.js",
|
||||||
|
"mathjax/queue-typeset.js"]
|
||||||
self.tab['pform'].frontWeb.setEnabled(False)
|
self.tab['pform'].frontWeb.setEnabled(False)
|
||||||
self.tab['pform'].backWeb.setEnabled(False)
|
self.tab['pform'].backWeb.setEnabled(False)
|
||||||
self.tab['pform'].frontWeb.stdHtml(
|
self.tab['pform'].frontWeb.stdHtml(
|
||||||
ti(mungeQA(self.mw.col, c.q(reload=True))), self.mw.reviewer._styles(),
|
ti(mungeQA(self.mw.col, c.q(reload=True))), self.mw.reviewer._styles(),
|
||||||
bodyClass="card card%d" % (c.ord+1), head=base),
|
bodyClass="card card%d" % (c.ord+1), head=base, js=jsinc),
|
||||||
self.tab['pform'].backWeb.stdHtml(
|
self.tab['pform'].backWeb.stdHtml(
|
||||||
ti(mungeQA(self.mw.col, c.a()), type='a'), self.mw.reviewer._styles(),
|
ti(mungeQA(self.mw.col, c.a()), type='a'), self.mw.reviewer._styles(),
|
||||||
bodyClass="card card%d" % (c.ord+1), head=base),
|
bodyClass="card card%d" % (c.ord+1), head=base, js=jsinc),
|
||||||
self.tab['pform'].frontWeb.setEnabled(True)
|
self.tab['pform'].frontWeb.setEnabled(True)
|
||||||
self.tab['pform'].backWeb.setEnabled(True)
|
self.tab['pform'].backWeb.setEnabled(True)
|
||||||
clearAudioQueue()
|
clearAudioQueue()
|
||||||
|
|
|
@ -6,7 +6,6 @@ from aqt.qt import *
|
||||||
from aqt.utils import askUser, getOnlyText, openLink, showWarning, shortcut, \
|
from aqt.utils import askUser, getOnlyText, openLink, showWarning, shortcut, \
|
||||||
openHelp, downArrow
|
openHelp, downArrow
|
||||||
from anki.utils import isMac, ids2str, fmtTimeSpan
|
from anki.utils import isMac, ids2str, fmtTimeSpan
|
||||||
import anki.js
|
|
||||||
from anki.errors import DeckRenameError
|
from anki.errors import DeckRenameError
|
||||||
import aqt
|
import aqt
|
||||||
from anki.sound import clearAudioQueue
|
from anki.sound import clearAudioQueue
|
||||||
|
@ -141,7 +140,7 @@ body { margin: 1em; -webkit-user-select: none; }
|
||||||
stats = self._renderStats()
|
stats = self._renderStats()
|
||||||
self.web.stdHtml(self._body%dict(
|
self.web.stdHtml(self._body%dict(
|
||||||
tree=tree, stats=stats, countwarn=self._countWarn()), css=css,
|
tree=tree, stats=stats, countwarn=self._countWarn()), css=css,
|
||||||
js=anki.js.jquery+anki.js.ui)
|
js=["jquery.js", "jquery-ui.js"])
|
||||||
self.web.key = "deckBrowser"
|
self.web.key = "deckBrowser"
|
||||||
self._drawButtons()
|
self._drawButtons()
|
||||||
|
|
||||||
|
|
465
aqt/editor.py
465
aqt/editor.py
|
@ -23,470 +23,13 @@ from aqt.webview import AnkiWebView
|
||||||
from aqt.utils import shortcut, showInfo, showWarning, getFile, \
|
from aqt.utils import shortcut, showInfo, showWarning, getFile, \
|
||||||
openHelp, tooltip, downArrow
|
openHelp, tooltip, downArrow
|
||||||
import aqt
|
import aqt
|
||||||
import anki.js
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp")
|
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp")
|
||||||
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a", "3gp", "spx", "oga")
|
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a", "3gp", "spx", "oga")
|
||||||
|
|
||||||
_html = """
|
_html = """
|
||||||
<style>
|
<style>html { background: %s; }</style>
|
||||||
.field {
|
|
||||||
border: 1px solid #aaa; background:#fff; color:#000; padding: 5px;
|
|
||||||
}
|
|
||||||
/* prevent floated images from being displayed outside field */
|
|
||||||
.field:after {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
height: 0;
|
|
||||||
clear: both;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
.fname { vertical-align: middle; padding: 0; }
|
|
||||||
img { max-width: 90%%; }
|
|
||||||
html { background: %s; }
|
|
||||||
body { margin: 5px; }
|
|
||||||
#topbuts { position: fixed; height: 24px; top: 0; padding: 2px; left:0;right:0}
|
|
||||||
.topbut { width: 16px; height: 16px; }
|
|
||||||
.rainbow {
|
|
||||||
background-image: -webkit-gradient(linear, left top, left bottom,
|
|
||||||
color-stop(0.00, #f77),
|
|
||||||
color-stop(50%%, #7f7),
|
|
||||||
color-stop(100%%, #77f));
|
|
||||||
}
|
|
||||||
.linkb { -webkit-appearance: none; border: 0; padding: 0px 2px; background: transparent; }
|
|
||||||
.linkb:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
||||||
|
|
||||||
.highlighted {
|
|
||||||
border-bottom: 3px solid #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prewrap { white-space: pre-wrap; }
|
|
||||||
|
|
||||||
#fields { margin-top: 35px; }
|
|
||||||
|
|
||||||
|
|
||||||
</style><script>
|
|
||||||
|
|
||||||
var currentField = null;
|
|
||||||
var changeTimer = null;
|
|
||||||
var dropTarget = null;
|
|
||||||
var prewrapMode = false;
|
|
||||||
|
|
||||||
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() {
|
|
||||||
clearChangeTimer();
|
|
||||||
if (currentField) {
|
|
||||||
currentField.blur();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function onKey() {
|
|
||||||
// esc clears focus, allowing dialog to close
|
|
||||||
if (window.event.which == 27) {
|
|
||||||
currentField.blur();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// catch enter key in prewrap mode
|
|
||||||
if (window.event.which == 13 && prewrapMode) {
|
|
||||||
window.event.preventDefault();
|
|
||||||
insertNewline();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
clearChangeTimer();
|
|
||||||
changeTimer = setTimeout(function () {
|
|
||||||
updateButtonState();
|
|
||||||
saveField("key");
|
|
||||||
}, 600);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 checkForEmptyField() {
|
|
||||||
if (currentField.innerHTML == "") {
|
|
||||||
currentField.innerHTML = "<br>";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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) {
|
|
||||||
currentField = elem;
|
|
||||||
pycmd("focus:" + currentField.id.substring(1));
|
|
||||||
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) {
|
|
||||||
$("#f"+n).focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDragOver(elem) {
|
|
||||||
// if we focus the target element immediately, the drag&drop turns into a
|
|
||||||
// copy, so note it down for later instead
|
|
||||||
dropTarget = elem;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
saveField("blur");
|
|
||||||
}
|
|
||||||
clearChangeTimer();
|
|
||||||
disableButtons();
|
|
||||||
};
|
|
||||||
|
|
||||||
function saveField(type) {
|
|
||||||
if (!currentField) {
|
|
||||||
// no field has been focused yet
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// type is either 'blur' or 'key'
|
|
||||||
pycmd(type + ":" + currentField.innerHTML);
|
|
||||||
clearChangeTimer();
|
|
||||||
};
|
|
||||||
|
|
||||||
function wrappedExceptForWhitespace(text, front, back) {
|
|
||||||
var match = text.match(/^(\s*)([^]*?)(\s*)$/);
|
|
||||||
return match[1] + front + match[2] + back + match[3];
|
|
||||||
};
|
|
||||||
|
|
||||||
function disableButtons() {
|
|
||||||
$("button.linkb").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) {
|
|
||||||
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 setFields(fields, focusTo, prewrap) {
|
|
||||||
var txt = "";
|
|
||||||
for (var i=0; i<fields.length; i++) {
|
|
||||||
var n = fields[i][0];
|
|
||||||
var f = fields[i][1];
|
|
||||||
if (!f) {
|
|
||||||
f = "<br>";
|
|
||||||
}
|
|
||||||
txt += "<tr><td class=fname>{0}</td></tr><tr><td width=100%%>".format(n);
|
|
||||||
txt += "<div id=f{0} onkeydown='onKey();' oninput='checkForEmptyField()' onmouseup='onKey();'".format(i);
|
|
||||||
txt += " onfocus='onFocus(this);' onblur='onBlur();' class=field ";
|
|
||||||
txt += "ondragover='onDragOver(this);' onpaste='onPaste(this);' ";
|
|
||||||
txt += "contentEditable=true class=field>{0}</div>".format(f);
|
|
||||||
txt += "</td></tr>";
|
|
||||||
}
|
|
||||||
$("#fields").html("<table cellpadding=0 width=100%%>"+txt+"</table>");
|
|
||||||
if (!focusTo) {
|
|
||||||
focusTo = 0;
|
|
||||||
}
|
|
||||||
if (focusTo >= 0) {
|
|
||||||
$("#f"+focusTo).focus();
|
|
||||||
}
|
|
||||||
maybeDisableButtons();
|
|
||||||
prewrapMode = prewrap;
|
|
||||||
if (prewrap) {
|
|
||||||
$(".field").addClass("prewrap");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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++) {
|
|
||||||
$("#f"+i).css("font-family", fonts[i][0]);
|
|
||||||
$("#f"+i).css("font-size", fonts[i][1]);
|
|
||||||
$("#f"+i)[0].dir = fonts[i][2] ? "rtl" : "ltr";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showDupes() {
|
|
||||||
$("#dupes").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideDupes() {
|
|
||||||
$("#dupes").hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
var pasteHTML = function(html, internal) {
|
|
||||||
if (!internal) {
|
|
||||||
html = filterHTML(html);
|
|
||||||
}
|
|
||||||
setFormat("inserthtml", html);
|
|
||||||
};
|
|
||||||
|
|
||||||
var filterHTML = function(html) {
|
|
||||||
// wrap it in <top> as we aren't allowed to change top level elements
|
|
||||||
var top = $.parseHTML("<ankitop>" + html + "</ankitop>")[0];
|
|
||||||
filterNode(top);
|
|
||||||
var outHtml = top.innerHTML;
|
|
||||||
//console.log(`input html: ${html}`);
|
|
||||||
//console.log(`outpt html: ${outHtml}`);
|
|
||||||
return outHtml;
|
|
||||||
};
|
|
||||||
|
|
||||||
var allowedTags = {};
|
|
||||||
|
|
||||||
var TAGS_WITHOUT_ATTRS = ["H1", "H2", "H3", "P", "DIV", "BR", "LI", "UL",
|
|
||||||
"OL", "B", "I", "U", "BLOCKQUOTE", "CODE", "EM",
|
|
||||||
"STRONG", "PRE", "SUB", "SUP", "TABLE", "DD", "DT", "DL"];
|
|
||||||
for (var i = 0; i < TAGS_WITHOUT_ATTRS.length; i++) {
|
|
||||||
allowedTags[TAGS_WITHOUT_ATTRS[i]] = {"attrs": []};
|
|
||||||
}
|
|
||||||
|
|
||||||
allowedTags["A"] = {"attrs": ["HREF"]};
|
|
||||||
allowedTags["TR"] = {"attrs": ["ROWSPAN"]};
|
|
||||||
allowedTags["TD"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
|
|
||||||
allowedTags["TH"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
|
|
||||||
allowedTags["IMG"] = {"attrs": ["SRC"]};
|
|
||||||
|
|
||||||
var blockRegex = /^(address|blockquote|br|center|div|dl|h[1-6]|hr|ol|p|pre|table|ul|dd|dt|li|tbody|td|tfoot|th|thead|tr)$/i;
|
|
||||||
function isBlockLevel(n) {
|
|
||||||
return blockRegex.test(n.nodeName);
|
|
||||||
};
|
|
||||||
|
|
||||||
function isInlineElement(n) {
|
|
||||||
return n && !isBlockLevel(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertDivToNewline(node, isParagraph) {
|
|
||||||
var html = node.innerHTML;
|
|
||||||
if (isInlineElement(node.previousSibling) && html) {
|
|
||||||
html = "\\n" + html;
|
|
||||||
}
|
|
||||||
if (isInlineElement(node.nextSibling)) {
|
|
||||||
html += "\\n";
|
|
||||||
}
|
|
||||||
if (isParagraph) {
|
|
||||||
html += "\\n";
|
|
||||||
}
|
|
||||||
node.outerHTML = html;
|
|
||||||
};
|
|
||||||
|
|
||||||
var filterNode = function(node) {
|
|
||||||
// text node?
|
|
||||||
if (node.nodeType == 3) {
|
|
||||||
if (prewrapMode) {
|
|
||||||
// collapse standard whitespace
|
|
||||||
var val = node.nodeValue.replace(/^[ \\r\\n\\t]+$/g, " ");
|
|
||||||
|
|
||||||
// non-breaking spaces can be represented as normal spaces
|
|
||||||
val = val.replace(/ |\u00a0/g, " ");
|
|
||||||
|
|
||||||
node.nodeValue = val;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// descend first, and take a copy of the child nodes as the loop will skip
|
|
||||||
// elements due to node modifications otherwise
|
|
||||||
|
|
||||||
var nodes = [];
|
|
||||||
for (var i = 0; i < node.childNodes.length; i++) {
|
|
||||||
nodes.push(node.childNodes[i]);
|
|
||||||
}
|
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
|
||||||
filterNode(nodes[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.tagName == "ANKITOP") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tag = allowedTags[node.tagName];
|
|
||||||
if (!tag) {
|
|
||||||
if (!node.innerHTML) {
|
|
||||||
node.parentNode.removeChild(node);
|
|
||||||
} else {
|
|
||||||
node.outerHTML = node.innerHTML;
|
|
||||||
}
|
|
||||||
} else if (prewrapMode && node.tagName == "BR") {
|
|
||||||
node.outerHTML = "\\n";
|
|
||||||
} else if (prewrapMode && node.tagName == "DIV") {
|
|
||||||
convertBlockToNewline(node, false);
|
|
||||||
} else if (prewrapMode && node.tagName == "P") {
|
|
||||||
convertBlockToNewline(node, true);
|
|
||||||
} else {
|
|
||||||
// allowed, filter out attributes
|
|
||||||
var toRemove = [];
|
|
||||||
for (var 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 (var i = 0; i < toRemove.length; i++) {
|
|
||||||
node.removeAttributeNode(toRemove[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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(); });
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
<div id="topbuts">%s</div>
|
<div id="topbuts">%s</div>
|
||||||
<div id="fields"></div>
|
<div id="fields"></div>
|
||||||
<div id="dupes" style="display:none;"><a href="#" onclick="pycmd('dupes');return false;">%s</a></div>
|
<div id="dupes" style="display:none;"><a href="#" onclick="pycmd('dupes');return false;">%s</a></div>
|
||||||
|
@ -560,10 +103,12 @@ class Editor:
|
||||||
""" % dict(flds=_("Fields"), cards=_("Cards"), rightbts="".join(righttopbtns))
|
""" % dict(flds=_("Fields"), cards=_("Cards"), rightbts="".join(righttopbtns))
|
||||||
bgcol = self.mw.app.palette().window().color().name()
|
bgcol = self.mw.app.palette().window().color().name()
|
||||||
# then load page
|
# then load page
|
||||||
self.web.stdHtml(_html % (
|
html = self.web.bundledCSS("editor.css") + _html
|
||||||
|
self.web.stdHtml(html % (
|
||||||
bgcol,
|
bgcol,
|
||||||
topbuts,
|
topbuts,
|
||||||
_("Show Duplicates")), js=anki.js.jquery, head=self.mw.baseHTML())
|
_("Show Duplicates")), head=self.mw.baseHTML(),
|
||||||
|
js=["jquery.js", "editor.js"])
|
||||||
|
|
||||||
# Top buttons
|
# Top buttons
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
|
@ -66,6 +66,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.setupAppMsg()
|
self.setupAppMsg()
|
||||||
self.setupKeys()
|
self.setupKeys()
|
||||||
self.setupThreads()
|
self.setupThreads()
|
||||||
|
self.setupMediaServer()
|
||||||
self.setupMainWindow()
|
self.setupMainWindow()
|
||||||
self.setupSystemSpecific()
|
self.setupSystemSpecific()
|
||||||
self.setupStyle()
|
self.setupStyle()
|
||||||
|
@ -77,7 +78,6 @@ class AnkiQt(QMainWindow):
|
||||||
self.setupHooks()
|
self.setupHooks()
|
||||||
self.setupRefreshTimer()
|
self.setupRefreshTimer()
|
||||||
self.updateTitleBar()
|
self.updateTitleBar()
|
||||||
self.setupMediaServer()
|
|
||||||
# screens
|
# screens
|
||||||
self.setupDeckBrowser()
|
self.setupDeckBrowser()
|
||||||
self.setupOverview()
|
self.setupOverview()
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import http.server
|
import http.server
|
||||||
|
import socketserver
|
||||||
import errno
|
import errno
|
||||||
|
|
||||||
# locate web folder in source/binary distribution
|
# locate web folder in source/binary distribution
|
||||||
|
@ -22,6 +23,11 @@ def _getExportFolder():
|
||||||
|
|
||||||
_exportFolder = _getExportFolder()
|
_exportFolder = _getExportFolder()
|
||||||
|
|
||||||
|
# webengine on windows sometimes opens a connection and fails to send a request,
|
||||||
|
# which will hang the server if unthreaded
|
||||||
|
class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||||
|
pass
|
||||||
|
|
||||||
class MediaServer(QThread):
|
class MediaServer(QThread):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
@ -29,7 +35,7 @@ class MediaServer(QThread):
|
||||||
self.server = None
|
self.server = None
|
||||||
while not self.server:
|
while not self.server:
|
||||||
try:
|
try:
|
||||||
self.server = http.server.HTTPServer(
|
self.server = ThreadedHTTPServer(
|
||||||
("localhost", self.port), RequestHandler)
|
("localhost", self.port), RequestHandler)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if e.errno == errno.EADDRINUSE:
|
if e.errno == errno.EADDRINUSE:
|
||||||
|
|
102
aqt/reviewer.py
102
aqt/reviewer.py
|
@ -121,56 +121,6 @@ class Reviewer:
|
||||||
_revHtml = """
|
_revHtml = """
|
||||||
<img src="qrc:/icons/rating.png" id=star class=marked>
|
<img src="qrc:/icons/rating.png" id=star class=marked>
|
||||||
<div id=qa></div>
|
<div id=qa></div>
|
||||||
<script type="text/x-mathjax-config">
|
|
||||||
MathJax.Hub.Config({
|
|
||||||
jax: ["input/TeX","output/CommonHTML"],
|
|
||||||
extensions: ["tex2jax.js"],
|
|
||||||
TeX: {
|
|
||||||
extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
|
|
||||||
},
|
|
||||||
messageStyle: "none",
|
|
||||||
skipStartupTypeset: true,
|
|
||||||
showMathMenu: false
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script type="text/javascript" src="_anki/mathjax/MathJax.js"></script>
|
|
||||||
<script>
|
|
||||||
var ankiPlatform = "desktop";
|
|
||||||
var typeans;
|
|
||||||
function _updateQA (q, answerMode, klass) {
|
|
||||||
$("#qa").html(q);
|
|
||||||
typeans = document.getElementById("typeans");
|
|
||||||
if (typeans) {
|
|
||||||
typeans.focus();
|
|
||||||
}
|
|
||||||
if (answerMode) {
|
|
||||||
var e = $("#answer");
|
|
||||||
if (e[0]) { e[0].scrollIntoView(); }
|
|
||||||
} else {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
if (klass) {
|
|
||||||
document.body.className = klass;
|
|
||||||
}
|
|
||||||
// don't allow drags of images, which cause them to be deleted
|
|
||||||
$("img").attr("draggable", false);
|
|
||||||
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
|
|
||||||
};
|
|
||||||
|
|
||||||
function _toggleStar (show) {
|
|
||||||
if (show) {
|
|
||||||
$(".marked").show();
|
|
||||||
} else {
|
|
||||||
$(".marked").hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _typeAnsPress() {
|
|
||||||
if (window.event.keyCode === 13) {
|
|
||||||
pycmd("ans");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _initWeb(self):
|
def _initWeb(self):
|
||||||
|
@ -179,13 +129,20 @@ function _typeAnsPress() {
|
||||||
base = self.mw.baseHTML()
|
base = self.mw.baseHTML()
|
||||||
# main window
|
# main window
|
||||||
self.web.onLoadFinished = self._showQuestion
|
self.web.onLoadFinished = self._showQuestion
|
||||||
self.web.stdHtml(self._revHtml, self._styles(), head=base)
|
self.web.stdHtml(self._revHtml, self._styles(), head=base,
|
||||||
|
js=["jquery.js",
|
||||||
|
"browsersel.js",
|
||||||
|
"mathjax/conf.js",
|
||||||
|
"mathjax/MathJax.js",
|
||||||
|
"reviewer.js"])
|
||||||
# show answer / ease buttons
|
# show answer / ease buttons
|
||||||
self.bottom.web.show()
|
self.bottom.web.show()
|
||||||
self.bottom.web.onLoadFinished = self._onBottomLoadFinished
|
self.bottom.web.onLoadFinished = self._onBottomLoadFinished
|
||||||
self.bottom.web.stdHtml(
|
self.bottom.web.stdHtml(
|
||||||
self._bottomHTML(),
|
self._bottomHTML(),
|
||||||
self.bottom._css + self._bottomCSS)
|
self.bottom._css + self._bottomCSS,
|
||||||
|
js=["jquery.js", "reviewer-bottom.js"]
|
||||||
|
)
|
||||||
|
|
||||||
# Showing the question
|
# Showing the question
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -573,46 +530,7 @@ padding: 3px;
|
||||||
</table>
|
</table>
|
||||||
</center>
|
</center>
|
||||||
<script>
|
<script>
|
||||||
var time = %(time)d;
|
time = %(time)d;
|
||||||
var maxTime = 0;
|
|
||||||
$(function () {
|
|
||||||
$("#ansbut").focus();
|
|
||||||
updateTime();
|
|
||||||
setInterval(function () { time += 1; updateTime() }, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
var updateTime = function () {
|
|
||||||
if (!maxTime) {
|
|
||||||
$("#time").text("");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
time = Math.min(maxTime, time);
|
|
||||||
var m = Math.floor(time / 60);
|
|
||||||
var s = time %% 60;
|
|
||||||
if (s < 10) {
|
|
||||||
s = "0" + s;
|
|
||||||
}
|
|
||||||
var e = $("#time");
|
|
||||||
if (maxTime == time) {
|
|
||||||
e.html("<font color=red>" + m + ":" + s + "</font>");
|
|
||||||
} else {
|
|
||||||
e.text(m + ":" + s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function showQuestion(txt, maxTime_) {
|
|
||||||
// much faster than jquery's .html()
|
|
||||||
$("#middle")[0].innerHTML = txt;
|
|
||||||
$("#ansbut").focus();
|
|
||||||
time = 0;
|
|
||||||
maxTime = maxTime_;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showAnswer(txt) {
|
|
||||||
$("#middle")[0].innerHTML = txt;
|
|
||||||
$("#defease").focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
""" % dict(rem=self._remaining(), edit=_("Edit"),
|
""" % dict(rem=self._remaining(), edit=_("Edit"),
|
||||||
editkey=_("Shortcut key: %s") % "E",
|
editkey=_("Shortcut key: %s") % "E",
|
||||||
|
|
|
@ -79,5 +79,6 @@ class DeckStats(QDialog):
|
||||||
stats = self.mw.col.stats()
|
stats = self.mw.col.stats()
|
||||||
stats.wholeCollection = self.wholeCollection
|
stats.wholeCollection = self.wholeCollection
|
||||||
self.report = stats.report(type=self.period)
|
self.report = stats.report(type=self.period)
|
||||||
self.form.web.stdHtml("<html><body>"+self.report+"</body></html>")
|
self.form.web.stdHtml("<html><body>"+self.report+"</body></html>",
|
||||||
|
js=["jquery.js", "plot.js"])
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
|
|
|
@ -7,7 +7,6 @@ from anki.hooks import runHook
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import openLink
|
from aqt.utils import openLink
|
||||||
from anki.utils import isMac, isWin
|
from anki.utils import isMac, isWin
|
||||||
import anki.js
|
|
||||||
|
|
||||||
# Page for debug messages
|
# Page for debug messages
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -151,7 +150,7 @@ class AnkiWebView(QWebEngineView):
|
||||||
dpi = screen.logicalDpiX()
|
dpi = screen.logicalDpiX()
|
||||||
return max(1, dpi / 96.0)
|
return max(1, dpi / 96.0)
|
||||||
|
|
||||||
def stdHtml(self, body, css="", bodyClass="", js=None, head=""):
|
def stdHtml(self, body, css="", bodyClass="", js=["jquery.js"], head=""):
|
||||||
if isWin:
|
if isWin:
|
||||||
buttonspec = "button { font-size: 12px; font-family:'Segoe UI'; }"
|
buttonspec = "button { font-size: 12px; font-family:'Segoe UI'; }"
|
||||||
fontspec = 'font-size:12px;font-family:"Segoe UI";'
|
fontspec = 'font-size:12px;font-family:"Segoe UI";'
|
||||||
|
@ -167,6 +166,7 @@ border-radius:5px; font-family: Helvetica }"""
|
||||||
family = self.font().family()
|
family = self.font().family()
|
||||||
fontspec = 'font-size:14px;font-family:%s;'%\
|
fontspec = 'font-size:14px;font-family:%s;'%\
|
||||||
family
|
family
|
||||||
|
jstxt = "\n".join(self.bundledScript(fname) for fname in js)
|
||||||
|
|
||||||
html="""
|
html="""
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
@ -174,9 +174,8 @@ border-radius:5px; font-family: Helvetica }"""
|
||||||
body { zoom: %f; %s }
|
body { zoom: %f; %s }
|
||||||
%s
|
%s
|
||||||
%s</style>
|
%s</style>
|
||||||
<script>
|
|
||||||
%s
|
%s
|
||||||
|
<script>
|
||||||
// prevent backspace key from going back a page
|
// prevent backspace key from going back a page
|
||||||
document.addEventListener("keydown", function(evt) {
|
document.addEventListener("keydown", function(evt) {
|
||||||
if (evt.keyCode != 8) {
|
if (evt.keyCode != 8) {
|
||||||
|
@ -202,11 +201,21 @@ document.addEventListener("keydown", function(evt) {
|
||||||
self.zoomFactor(),
|
self.zoomFactor(),
|
||||||
fontspec,
|
fontspec,
|
||||||
buttonspec,
|
buttonspec,
|
||||||
css, js or anki.js.jquery+anki.js.browserSel,
|
css, jstxt,
|
||||||
head, bodyClass, body)
|
head, bodyClass, body)
|
||||||
#print(html)
|
#print(html)
|
||||||
self.setHtml(html)
|
self.setHtml(html)
|
||||||
|
|
||||||
|
def webBundlePath(self, path):
|
||||||
|
from aqt import mw
|
||||||
|
return "http://localhost:%d/_anki/%s" % (mw.mediaServer.port, path)
|
||||||
|
|
||||||
|
def bundledScript(self, fname):
|
||||||
|
return '<script src="%s"></script>' % self.webBundlePath(fname)
|
||||||
|
|
||||||
|
def bundledCSS(self, fname):
|
||||||
|
return '<link rel="stylesheet" type="text/css" href="%s">' % self.webBundlePath(fname)
|
||||||
|
|
||||||
def eval(self, js):
|
def eval(self, js):
|
||||||
self.page().runJavaScript(js)
|
self.page().runJavaScript(js)
|
||||||
|
|
||||||
|
|
1
web/browsersel.js
Normal file
1
web/browsersel.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/* CSS Browser Selector v0.4.0 (Nov 02, 2010) Rafael Lima (http://rafael.adm.br) */function css_browser_selector(u){var ua=u.toLowerCase(),is=function(t){return ua.indexOf(t)>-1},g='gecko',w='webkit',s='safari',o='opera',m='mobile',h=document.documentElement,b=[(!(/opera|webtv/i.test(ua))&&/msie\s(\d)/.test(ua))?('ie ie'+RegExp.$1):is('firefox/2')?g+' ff2':is('firefox/3.5')?g+' ff3 ff3_5':is('firefox/3.6')?g+' ff3 ff3_6':is('firefox/3')?g+' ff3':is('gecko/')?g:is('opera')?o+(/version\/(\d+)/.test(ua)?' '+o+RegExp.$1:(/opera(\s|\/)(\d+)/.test(ua)?' '+o+RegExp.$2:'')):is('konqueror')?'konqueror':is('blackberry')?m+' blackberry':is('android')?m+' android':is('chrome')?w+' chrome':is('iron')?w+' iron':is('applewebkit/')?w+' '+s+(/version\/(\d+)/.test(ua)?' '+s+RegExp.$1:''):is('mozilla/')?g:'',is('j2me')?m+' j2me':is('iphone')?m+' iphone':is('ipod')?m+' ipod':is('ipad')?m+' ipad':is('mac')?'mac':is('darwin')?'mac':is('webtv')?'webtv':is('win')?'win'+(is('windows nt 6.0')?' vista':''):is('freebsd')?'freebsd':(is('x11')||is('linux'))?'linux':'','js']; c = b.join(' '); h.className += ' '+c; return c;}; css_browser_selector(navigator.userAgent);
|
73
web/editor.css
Normal file
73
web/editor.css
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
.field {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* prevent floated images from being displayed outside field */
|
||||||
|
.field:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
clear: both;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fname {
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbuts {
|
||||||
|
position: fixed;
|
||||||
|
height: 24px;
|
||||||
|
top: 0;
|
||||||
|
padding: 2px;
|
||||||
|
left: 0;
|
||||||
|
right: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbut {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rainbow {
|
||||||
|
background-image: -webkit-gradient(linear, left top, left bottom,
|
||||||
|
color-stop(0.00, #f77),
|
||||||
|
color-stop(50%, #7f7),
|
||||||
|
color-stop(100%, #77f));
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0px 2px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkb:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlighted {
|
||||||
|
border-bottom: 3px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prewrap {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fields {
|
||||||
|
margin-top: 35px;
|
||||||
|
}
|
423
web/editor.js
Normal file
423
web/editor.js
Normal file
|
@ -0,0 +1,423 @@
|
||||||
|
var currentField = null;
|
||||||
|
var changeTimer = null;
|
||||||
|
var dropTarget = null;
|
||||||
|
var prewrapMode = false;
|
||||||
|
|
||||||
|
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() {
|
||||||
|
clearChangeTimer();
|
||||||
|
if (currentField) {
|
||||||
|
currentField.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function onKey() {
|
||||||
|
// esc clears focus, allowing dialog to close
|
||||||
|
if (window.event.which == 27) {
|
||||||
|
currentField.blur();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// catch enter key in prewrap mode
|
||||||
|
if (window.event.which == 13 && prewrapMode) {
|
||||||
|
window.event.preventDefault();
|
||||||
|
insertNewline();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearChangeTimer();
|
||||||
|
changeTimer = setTimeout(function () {
|
||||||
|
updateButtonState();
|
||||||
|
saveField("key");
|
||||||
|
}, 600);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 checkForEmptyField() {
|
||||||
|
if (currentField.innerHTML == "") {
|
||||||
|
currentField.innerHTML = "<br>";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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) {
|
||||||
|
currentField = elem;
|
||||||
|
pycmd("focus:" + currentField.id.substring(1));
|
||||||
|
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) {
|
||||||
|
$("#f" + n).focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragOver(elem) {
|
||||||
|
// if we focus the target element immediately, the drag&drop turns into a
|
||||||
|
// copy, so note it down for later instead
|
||||||
|
dropTarget = elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
saveField("blur");
|
||||||
|
}
|
||||||
|
clearChangeTimer();
|
||||||
|
disableButtons();
|
||||||
|
};
|
||||||
|
|
||||||
|
function saveField(type) {
|
||||||
|
if (!currentField) {
|
||||||
|
// no field has been focused yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// type is either 'blur' or 'key'
|
||||||
|
pycmd(type + ":" + currentField.innerHTML);
|
||||||
|
clearChangeTimer();
|
||||||
|
};
|
||||||
|
|
||||||
|
function wrappedExceptForWhitespace(text, front, back) {
|
||||||
|
var match = text.match(/^(\s*)([^]*?)(\s*)$/);
|
||||||
|
return match[1] + front + match[2] + back + match[3];
|
||||||
|
};
|
||||||
|
|
||||||
|
function disableButtons() {
|
||||||
|
$("button.linkb").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) {
|
||||||
|
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 setFields(fields, focusTo, prewrap) {
|
||||||
|
var txt = "";
|
||||||
|
for (var i = 0; i < fields.length; i++) {
|
||||||
|
var n = fields[i][0];
|
||||||
|
var f = fields[i][1];
|
||||||
|
if (!f) {
|
||||||
|
f = "<br>";
|
||||||
|
}
|
||||||
|
txt += "<tr><td class=fname>{0}</td></tr><tr><td width=100%>".format(n);
|
||||||
|
txt += "<div id=f{0} onkeydown='onKey();' oninput='checkForEmptyField()' onmouseup='onKey();'".format(i);
|
||||||
|
txt += " onfocus='onFocus(this);' onblur='onBlur();' class=field ";
|
||||||
|
txt += "ondragover='onDragOver(this);' onpaste='onPaste(this);' ";
|
||||||
|
txt += "contentEditable=true class=field>{0}</div>".format(f);
|
||||||
|
txt += "</td></tr>";
|
||||||
|
}
|
||||||
|
$("#fields").html("<table cellpadding=0 width=100%>" + txt + "</table>");
|
||||||
|
if (!focusTo) {
|
||||||
|
focusTo = 0;
|
||||||
|
}
|
||||||
|
if (focusTo >= 0) {
|
||||||
|
$("#f" + focusTo).focus();
|
||||||
|
}
|
||||||
|
maybeDisableButtons();
|
||||||
|
prewrapMode = prewrap;
|
||||||
|
if (prewrap) {
|
||||||
|
$(".field").addClass("prewrap");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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++) {
|
||||||
|
$("#f" + i).css("font-family", fonts[i][0]);
|
||||||
|
$("#f" + i).css("font-size", fonts[i][1]);
|
||||||
|
$("#f" + i)[0].dir = fonts[i][2] ? "rtl" : "ltr";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDupes() {
|
||||||
|
$("#dupes").show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideDupes() {
|
||||||
|
$("#dupes").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pasteHTML = function (html, internal) {
|
||||||
|
if (!internal) {
|
||||||
|
html = filterHTML(html);
|
||||||
|
}
|
||||||
|
setFormat("inserthtml", html);
|
||||||
|
};
|
||||||
|
|
||||||
|
var filterHTML = function (html) {
|
||||||
|
// wrap it in <top> as we aren't allowed to change top level elements
|
||||||
|
var top = $.parseHTML("<ankitop>" + html + "</ankitop>")[0];
|
||||||
|
filterNode(top);
|
||||||
|
var outHtml = top.innerHTML;
|
||||||
|
//console.log(`input html: ${html}`);
|
||||||
|
//console.log(`outpt html: ${outHtml}`);
|
||||||
|
return outHtml;
|
||||||
|
};
|
||||||
|
|
||||||
|
var allowedTags = {};
|
||||||
|
|
||||||
|
var TAGS_WITHOUT_ATTRS = ["H1", "H2", "H3", "P", "DIV", "BR", "LI", "UL",
|
||||||
|
"OL", "B", "I", "U", "BLOCKQUOTE", "CODE", "EM",
|
||||||
|
"STRONG", "PRE", "SUB", "SUP", "TABLE", "DD", "DT", "DL"];
|
||||||
|
for (var i = 0; i < TAGS_WITHOUT_ATTRS.length; i++) {
|
||||||
|
allowedTags[TAGS_WITHOUT_ATTRS[i]] = {"attrs": []};
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedTags["A"] = {"attrs": ["HREF"]};
|
||||||
|
allowedTags["TR"] = {"attrs": ["ROWSPAN"]};
|
||||||
|
allowedTags["TD"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
|
||||||
|
allowedTags["TH"] = {"attrs": ["COLSPAN", "ROWSPAN"]};
|
||||||
|
allowedTags["IMG"] = {"attrs": ["SRC"]};
|
||||||
|
|
||||||
|
var blockRegex = /^(address|blockquote|br|center|div|dl|h[1-6]|hr|ol|p|pre|table|ul|dd|dt|li|tbody|td|tfoot|th|thead|tr)$/i;
|
||||||
|
function isBlockLevel(n) {
|
||||||
|
return blockRegex.test(n.nodeName);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isInlineElement(n) {
|
||||||
|
return n && !isBlockLevel(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertDivToNewline(node, isParagraph) {
|
||||||
|
var html = node.innerHTML;
|
||||||
|
if (isInlineElement(node.previousSibling) && html) {
|
||||||
|
html = "\\n" + html;
|
||||||
|
}
|
||||||
|
if (isInlineElement(node.nextSibling)) {
|
||||||
|
html += "\\n";
|
||||||
|
}
|
||||||
|
if (isParagraph) {
|
||||||
|
html += "\\n";
|
||||||
|
}
|
||||||
|
node.outerHTML = html;
|
||||||
|
};
|
||||||
|
|
||||||
|
var filterNode = function (node) {
|
||||||
|
// text node?
|
||||||
|
if (node.nodeType == 3) {
|
||||||
|
if (prewrapMode) {
|
||||||
|
// collapse standard whitespace
|
||||||
|
var val = node.nodeValue.replace(/^[ \\r\\n\\t]+$/g, " ");
|
||||||
|
|
||||||
|
// non-breaking spaces can be represented as normal spaces
|
||||||
|
val = val.replace(/ |\u00a0/g, " ");
|
||||||
|
|
||||||
|
node.nodeValue = val;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// descend first, and take a copy of the child nodes as the loop will skip
|
||||||
|
// elements due to node modifications otherwise
|
||||||
|
|
||||||
|
var nodes = [];
|
||||||
|
for (var i = 0; i < node.childNodes.length; i++) {
|
||||||
|
nodes.push(node.childNodes[i]);
|
||||||
|
}
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
filterNode(nodes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.tagName == "ANKITOP") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tag = allowedTags[node.tagName];
|
||||||
|
if (!tag) {
|
||||||
|
if (!node.innerHTML) {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
} else {
|
||||||
|
node.outerHTML = node.innerHTML;
|
||||||
|
}
|
||||||
|
} else if (prewrapMode && node.tagName == "BR") {
|
||||||
|
node.outerHTML = "\\n";
|
||||||
|
} else if (prewrapMode && node.tagName == "DIV") {
|
||||||
|
convertBlockToNewline(node, false);
|
||||||
|
} else if (prewrapMode && node.tagName == "P") {
|
||||||
|
convertBlockToNewline(node, true);
|
||||||
|
} else {
|
||||||
|
// allowed, filter out attributes
|
||||||
|
var toRemove = [];
|
||||||
|
for (var 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 (var i = 0; i < toRemove.length; i++) {
|
||||||
|
node.removeAttributeNode(toRemove[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
41
anki/js.py → web/jquery-ui.js
vendored
41
anki/js.py → web/jquery-ui.js
vendored
File diff suppressed because one or more lines are too long
5
web/jquery.js
vendored
Normal file
5
web/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10
web/mathjax/conf.js
Normal file
10
web/mathjax/conf.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
window.MathJax = {
|
||||||
|
jax: ["input/TeX","output/CommonHTML"],
|
||||||
|
extensions: ["tex2jax.js"],
|
||||||
|
TeX: {
|
||||||
|
extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"]
|
||||||
|
},
|
||||||
|
messageStyle: "none",
|
||||||
|
skipStartupTypeset: true,
|
||||||
|
showMathMenu: false
|
||||||
|
};
|
2
web/mathjax/queue-typeset.js
Normal file
2
web/mathjax/queue-typeset.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
|
||||||
|
|
24
web/plot.js
Normal file
24
web/plot.js
Normal file
File diff suppressed because one or more lines are too long
43
web/reviewer-bottom.js
Normal file
43
web/reviewer-bottom.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
var time; // set in python code
|
||||||
|
|
||||||
|
var maxTime = 0;
|
||||||
|
$(function () {
|
||||||
|
$("#ansbut").focus();
|
||||||
|
updateTime();
|
||||||
|
setInterval(function () {
|
||||||
|
time += 1;
|
||||||
|
updateTime()
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
var updateTime = function () {
|
||||||
|
if (!maxTime) {
|
||||||
|
$("#time").text("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
time = Math.min(maxTime, time);
|
||||||
|
var m = Math.floor(time / 60);
|
||||||
|
var s = time % 60;
|
||||||
|
if (s < 10) {
|
||||||
|
s = "0" + s;
|
||||||
|
}
|
||||||
|
var e = $("#time");
|
||||||
|
if (maxTime == time) {
|
||||||
|
e.html("<font color=red>" + m + ":" + s + "</font>");
|
||||||
|
} else {
|
||||||
|
e.text(m + ":" + s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showQuestion(txt, maxTime_) {
|
||||||
|
// much faster than jquery's .html()
|
||||||
|
$("#middle")[0].innerHTML = txt;
|
||||||
|
$("#ansbut").focus();
|
||||||
|
time = 0;
|
||||||
|
maxTime = maxTime_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAnswer(txt) {
|
||||||
|
$("#middle")[0].innerHTML = txt;
|
||||||
|
$("#defease").focus();
|
||||||
|
}
|
37
web/reviewer.js
Normal file
37
web/reviewer.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
var ankiPlatform = "desktop";
|
||||||
|
var typeans;
|
||||||
|
function _updateQA(q, answerMode, klass) {
|
||||||
|
$("#qa").html(q);
|
||||||
|
typeans = document.getElementById("typeans");
|
||||||
|
if (typeans) {
|
||||||
|
typeans.focus();
|
||||||
|
}
|
||||||
|
if (answerMode) {
|
||||||
|
var e = $("#answer");
|
||||||
|
if (e[0]) {
|
||||||
|
e[0].scrollIntoView();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
if (klass) {
|
||||||
|
document.body.className = klass;
|
||||||
|
}
|
||||||
|
// don't allow drags of images, which cause them to be deleted
|
||||||
|
$("img").attr("draggable", false);
|
||||||
|
MathJax.Hub.Queue(["Typeset", MathJax.Hub]);
|
||||||
|
};
|
||||||
|
|
||||||
|
function _toggleStar(show) {
|
||||||
|
if (show) {
|
||||||
|
$(".marked").show();
|
||||||
|
} else {
|
||||||
|
$(".marked").hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _typeAnsPress() {
|
||||||
|
if (window.event.keyCode === 13) {
|
||||||
|
pycmd("ans");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue