mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -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 json
|
||||
|
||||
import anki.js
|
||||
from anki.utils import fmtTimeSpan, ids2str
|
||||
from anki.lang import _, ngettext
|
||||
|
||||
|
@ -108,6 +107,7 @@ class CollectionStats:
|
|||
self.height = 200
|
||||
self.wholeCollection = False
|
||||
|
||||
# assumes jquery & plot are available in document
|
||||
def report(self, type=0):
|
||||
# 0=days, 1=weeks, 2=months
|
||||
self.type = type
|
||||
|
@ -122,8 +122,7 @@ class CollectionStats:
|
|||
txt += self._section(self.easeGraph())
|
||||
txt += self._section(self.cardGraph())
|
||||
txt += self._section(self.footer())
|
||||
return "<script>%s\n</script><center>%s</center>" % (
|
||||
anki.js.jquery+anki.js.plot, txt)
|
||||
return "<center>%s</center>" % txt
|
||||
|
||||
def _section(self, txt):
|
||||
return "<div class=section>%s</div>" % txt
|
||||
|
|
|
@ -1120,10 +1120,13 @@ where id in %s""" % ids2str(sf))
|
|||
txt = re.sub("\[\[type:[^]]+\]\]", "", txt)
|
||||
ti = lambda x: x
|
||||
base = self.mw.baseHTML()
|
||||
jsinc = ["jquery.js","browsersel.js",
|
||||
"mathjax/conf.js", "mathjax/MathJax.js",
|
||||
"mathjax/queue-typeset.js"]
|
||||
self._previewWeb.stdHtml(
|
||||
ti(mungeQA(self.col, txt)), self.mw.reviewer._styles(),
|
||||
bodyClass="card card%d" % (c.ord+1), head=base,
|
||||
js=anki.js.browserSel)
|
||||
js=jsinc)
|
||||
clearAudioQueue()
|
||||
if self.mw.reviewer.autoplay(c):
|
||||
playFromText(txt)
|
||||
|
|
|
@ -13,8 +13,6 @@ from aqt.utils import saveGeom, restoreGeom, mungeQA,\
|
|||
showWarning, openHelp, downArrow
|
||||
from anki.utils import isMac, isWin, joinFields
|
||||
from aqt.webview import AnkiWebView
|
||||
import anki.js
|
||||
|
||||
|
||||
class CardLayout(QDialog):
|
||||
|
||||
|
@ -226,14 +224,17 @@ Please create a new card type first."""))
|
|||
c = self.card
|
||||
ti = self.maybeTextInput
|
||||
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'].backWeb.setEnabled(False)
|
||||
self.tab['pform'].frontWeb.stdHtml(
|
||||
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(
|
||||
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'].backWeb.setEnabled(True)
|
||||
clearAudioQueue()
|
||||
|
|
|
@ -6,7 +6,6 @@ from aqt.qt import *
|
|||
from aqt.utils import askUser, getOnlyText, openLink, showWarning, shortcut, \
|
||||
openHelp, downArrow
|
||||
from anki.utils import isMac, ids2str, fmtTimeSpan
|
||||
import anki.js
|
||||
from anki.errors import DeckRenameError
|
||||
import aqt
|
||||
from anki.sound import clearAudioQueue
|
||||
|
@ -141,7 +140,7 @@ body { margin: 1em; -webkit-user-select: none; }
|
|||
stats = self._renderStats()
|
||||
self.web.stdHtml(self._body%dict(
|
||||
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._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, \
|
||||
openHelp, tooltip, downArrow
|
||||
import aqt
|
||||
import anki.js
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp")
|
||||
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a", "3gp", "spx", "oga")
|
||||
|
||||
_html = """
|
||||
<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>
|
||||
<style>html { background: %s; }</style>
|
||||
<div id="topbuts">%s</div>
|
||||
<div id="fields"></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))
|
||||
bgcol = self.mw.app.palette().window().color().name()
|
||||
# then load page
|
||||
self.web.stdHtml(_html % (
|
||||
html = self.web.bundledCSS("editor.css") + _html
|
||||
self.web.stdHtml(html % (
|
||||
bgcol,
|
||||
topbuts,
|
||||
_("Show Duplicates")), js=anki.js.jquery, head=self.mw.baseHTML())
|
||||
_("Show Duplicates")), head=self.mw.baseHTML(),
|
||||
js=["jquery.js", "editor.js"])
|
||||
|
||||
# Top buttons
|
||||
######################################################################
|
||||
|
|
|
@ -66,6 +66,7 @@ class AnkiQt(QMainWindow):
|
|||
self.setupAppMsg()
|
||||
self.setupKeys()
|
||||
self.setupThreads()
|
||||
self.setupMediaServer()
|
||||
self.setupMainWindow()
|
||||
self.setupSystemSpecific()
|
||||
self.setupStyle()
|
||||
|
@ -77,7 +78,6 @@ class AnkiQt(QMainWindow):
|
|||
self.setupHooks()
|
||||
self.setupRefreshTimer()
|
||||
self.updateTitleBar()
|
||||
self.setupMediaServer()
|
||||
# screens
|
||||
self.setupDeckBrowser()
|
||||
self.setupOverview()
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
from aqt.qt import *
|
||||
from http import HTTPStatus
|
||||
import http.server
|
||||
import socketserver
|
||||
import errno
|
||||
|
||||
# locate web folder in source/binary distribution
|
||||
|
@ -22,6 +23,11 @@ def _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):
|
||||
|
||||
def run(self):
|
||||
|
@ -29,7 +35,7 @@ class MediaServer(QThread):
|
|||
self.server = None
|
||||
while not self.server:
|
||||
try:
|
||||
self.server = http.server.HTTPServer(
|
||||
self.server = ThreadedHTTPServer(
|
||||
("localhost", self.port), RequestHandler)
|
||||
except OSError as e:
|
||||
if e.errno == errno.EADDRINUSE:
|
||||
|
|
102
aqt/reviewer.py
102
aqt/reviewer.py
|
@ -121,56 +121,6 @@ class Reviewer:
|
|||
_revHtml = """
|
||||
<img src="qrc:/icons/rating.png" id=star class=marked>
|
||||
<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):
|
||||
|
@ -179,13 +129,20 @@ function _typeAnsPress() {
|
|||
base = self.mw.baseHTML()
|
||||
# main window
|
||||
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
|
||||
self.bottom.web.show()
|
||||
self.bottom.web.onLoadFinished = self._onBottomLoadFinished
|
||||
self.bottom.web.stdHtml(
|
||||
self._bottomHTML(),
|
||||
self.bottom._css + self._bottomCSS)
|
||||
self.bottom._css + self._bottomCSS,
|
||||
js=["jquery.js", "reviewer-bottom.js"]
|
||||
)
|
||||
|
||||
# Showing the question
|
||||
##########################################################################
|
||||
|
@ -573,46 +530,7 @@ padding: 3px;
|
|||
</table>
|
||||
</center>
|
||||
<script>
|
||||
var 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();
|
||||
}
|
||||
|
||||
time = %(time)d;
|
||||
</script>
|
||||
""" % dict(rem=self._remaining(), edit=_("Edit"),
|
||||
editkey=_("Shortcut key: %s") % "E",
|
||||
|
|
|
@ -79,5 +79,6 @@ class DeckStats(QDialog):
|
|||
stats = self.mw.col.stats()
|
||||
stats.wholeCollection = self.wholeCollection
|
||||
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()
|
||||
|
|
|
@ -7,7 +7,6 @@ from anki.hooks import runHook
|
|||
from aqt.qt import *
|
||||
from aqt.utils import openLink
|
||||
from anki.utils import isMac, isWin
|
||||
import anki.js
|
||||
|
||||
# Page for debug messages
|
||||
##########################################################################
|
||||
|
@ -151,7 +150,7 @@ class AnkiWebView(QWebEngineView):
|
|||
dpi = screen.logicalDpiX()
|
||||
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:
|
||||
buttonspec = "button { 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()
|
||||
fontspec = 'font-size:14px;font-family:%s;'%\
|
||||
family
|
||||
jstxt = "\n".join(self.bundledScript(fname) for fname in js)
|
||||
|
||||
html="""
|
||||
<!doctype html>
|
||||
|
@ -174,9 +174,8 @@ border-radius:5px; font-family: Helvetica }"""
|
|||
body { zoom: %f; %s }
|
||||
%s
|
||||
%s</style>
|
||||
<script>
|
||||
%s
|
||||
|
||||
<script>
|
||||
// prevent backspace key from going back a page
|
||||
document.addEventListener("keydown", function(evt) {
|
||||
if (evt.keyCode != 8) {
|
||||
|
@ -202,11 +201,21 @@ document.addEventListener("keydown", function(evt) {
|
|||
self.zoomFactor(),
|
||||
fontspec,
|
||||
buttonspec,
|
||||
css, js or anki.js.jquery+anki.js.browserSel,
|
||||
css, jstxt,
|
||||
head, bodyClass, body)
|
||||
#print(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):
|
||||
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