diff --git a/client-data/board.css b/client-data/board.css index cdab39cb..3c099c14 100644 --- a/client-data/board.css +++ b/client-data/board.css @@ -256,7 +256,6 @@ line { } path { - fill: none; stroke-linecap: round; stroke-linejoin: round; } diff --git a/client-data/board.html b/client-data/board.html index 00e01632..b142f4b1 100644 --- a/client-data/board.html +++ b/client-data/board.html @@ -21,6 +21,7 @@ {{/languages}} + @@ -96,6 +97,7 @@ + diff --git a/client-data/tools/ellipse/ellipse.css b/client-data/tools/ellipse/ellipse.css index 599ff07d..82ee5fb6 100644 --- a/client-data/tools/ellipse/ellipse.css +++ b/client-data/tools/ellipse/ellipse.css @@ -1,3 +1,3 @@ -#canvas ellipse { +#canvas ellipse.nofill { fill: none; } diff --git a/client-data/tools/ellipse/ellipse.js b/client-data/tools/ellipse/ellipse.js index 2b0a7621..6c07302e 100644 --- a/client-data/tools/ellipse/ellipse.js +++ b/client-data/tools/ellipse/ellipse.js @@ -133,6 +133,7 @@ shape.setAttribute("stroke", data.color || "black"); shape.setAttribute("stroke-width", data.size || 10); shape.setAttribute("opacity", Math.max(0.1, Math.min(1, data.opacity)) || 1); + shape.setAttribute("class", "nofill"); Tools.drawingArea.appendChild(shape); return shape; } diff --git a/client-data/tools/eraser/eraser.js b/client-data/tools/eraser/eraser.js index e8ec74b6..11b2ad6d 100644 --- a/client-data/tools/eraser/eraser.js +++ b/client-data/tools/eraser/eraser.js @@ -54,6 +54,17 @@ target = document.elementFromPoint(touch.clientX, touch.clientY); } if (erasing && target !== Tools.svg && target !== Tools.drawingArea && inDrawingArea(target)) { + // search for a parent that is a MathElement. If one is found then act on that instead. + var a = target; + var els = []; + while (a) { + els.unshift(a); + a = a.parentElement; + } + var parentMathematics = els.find(el => el.getAttribute("class") === "MathElement"); + if ((parentMathematics) && parentMathematics.tagName === "svg") { + target = parentMathematics; + } msg.id = target.id; Tools.drawAndSend(msg); } diff --git a/client-data/tools/hand/hand.js b/client-data/tools/hand/hand.js index ffba5ee7..479af945 100644 --- a/client-data/tools/hand/hand.js +++ b/client-data/tools/hand/hand.js @@ -33,8 +33,20 @@ //Prevent the press from being interpreted by the browser evt.preventDefault(); if (!evt.target || !Tools.drawingArea.contains(evt.target)) return; - var tmatrix = get_translate_matrix(evt.target); - selected = { x: x - tmatrix.e, y: y - tmatrix.f, elem: evt.target }; + // search for a parent that is a MathElement. If one is found then act on that instead. + var target = evt.target; + var a = target; + var els = []; + while (a) { + els.unshift(a); + a = a.parentElement; + } + var parentMathematics = els.find(el => el.getAttribute("class") === "MathElement"); + if ((parentMathematics) && parentMathematics.tagName === "svg") { + target = parentMathematics; + } + var tmatrix = get_translate_matrix(target); + selected = { x: x - tmatrix.e, y: y - tmatrix.f, elem: target }; } function moveElement(x, y) { diff --git a/client-data/tools/line/line.css b/client-data/tools/line/line.css index 3ccde940..da6eaf6a 100755 --- a/client-data/tools/line/line.css +++ b/client-data/tools/line/line.css @@ -1,5 +1,8 @@ line { - fill: none; stroke-linecap: round; stroke-linejoin: round; } + +line.nofill { + fill: none; +} diff --git a/client-data/tools/line/line.js b/client-data/tools/line/line.js index a51edb90..675bce23 100644 --- a/client-data/tools/line/line.js +++ b/client-data/tools/line/line.js @@ -119,6 +119,7 @@ line.setAttribute("stroke", lineData.color || "black"); line.setAttribute("stroke-width", lineData.size || 10); line.setAttribute("opacity", Math.max(0.1, Math.min(1, lineData.opacity)) || 1); + line.setAttribute("class", "nofill"); Tools.drawingArea.appendChild(line); return line; } diff --git a/client-data/tools/mathematics/icon.svg b/client-data/tools/mathematics/icon.svg new file mode 100644 index 00000000..4b8a0d83 --- /dev/null +++ b/client-data/tools/mathematics/icon.svg @@ -0,0 +1 @@ + diff --git a/client-data/tools/mathematics/mathematics.css b/client-data/tools/mathematics/mathematics.css new file mode 100644 index 00000000..7fa937ca --- /dev/null +++ b/client-data/tools/mathematics/mathematics.css @@ -0,0 +1,16 @@ +#textToolInput { + position:fixed; + top:-1000px; /*Hidden*/ + left: 80px; + width:500px; +} +#textToolInput:focus { + top: 5px; +} + +text { + font-family:"Arial", "Helvetica", sans-serif; + user-select:none; + -moz-user-select:none; +} + diff --git a/client-data/tools/mathematics/mathematics.js b/client-data/tools/mathematics/mathematics.js new file mode 100644 index 00000000..45012e9e --- /dev/null +++ b/client-data/tools/mathematics/mathematics.js @@ -0,0 +1,259 @@ +/** + * WHITEBOPHIR + ********************************************************* + * @licstart The following is the entire license notice for the + * JavaScript code in this page. + * + * Copyright (C) 2013 Ophir LOJKINE + * + * + * The JavaScript code in this page is free software: you can + * redistribute it and/or modify it under the terms of the GNU + * General Public License (GNU GPL) as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) + * any later version. The code is distributed WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute non-source (e.g., minimized or compacted) forms of + * that code without the copy of the GNU GPL normally required by + * section 4, provided you include this license notice and a URL + * through which recipients can access the Corresponding Source. + * + * @licend + */ + +(function () { //Code isolation + var board = Tools.board; + + var input = document.createElement("textarea"); + input.id = "textToolInput"; + input.setAttribute("autocomplete", "off"); + + var curText = { + "x": 0, + "y": 0, + "size": 36, + "rawSize": 16, + "oldSize": 0, + "opacity": 1, + "color": "#000", + "id": 0, + "sentText": "", + "lastSending": 0 + }; + + var active = false; + + + function onStart() { + curText.oldSize = Tools.getSize(); + Tools.setSize(curText.rawSize); + } + + function onQuit() { + stopEdit(); + Tools.setSize(curText.oldSize); + } + + function clickHandler(x, y, evt, isTouchEvent) { + //if(document.querySelector("#menu").offsetWidth>Tools.menu_width+3) return; + // Assemble a list of parents of the evt.target and search it to see if any has class "MathElement" + if (evt.target === input) return; + var a = evt.target; + var els = []; + while (a) { + els.unshift(a); + a = a.parentElement; + } + var parentMathematics = els.find(el => el.getAttribute("class") === "MathElement"); + if ((parentMathematics) && parentMathematics.tagName === "svg") { + editOldMathematics(parentMathematics); + evt.preventDefault(); + return; + } + curText.rawSize = Tools.getSize(); + curText.size = parseInt(curText.rawSize * 1.5 + 12); + curText.opacity = Tools.getOpacity(); + curText.color = Tools.getColor(); + curText.x = x; + curText.y = y + curText.size / 2; + + stopEdit(); + startEdit(); + evt.preventDefault(); + } + + function editOldMathematics(elem) { + curText.id = elem.id; + var r = elem.getBoundingClientRect(); + var x = (r.left + document.documentElement.scrollLeft) / Tools.scale; + var y = (r.top + r.height + document.documentElement.scrollTop) / Tools.scale; + + curText.x = x; + curText.y = y; + curText.sentText = elem.getAttribute("aria-label"); + curText.size = parseInt(elem.getAttribute("font-size")); + curText.opacity = parseFloat(elem.getAttribute("opacity")); + curText.color = elem.getAttribute("fill"); + startEdit(); + input.value = elem.getAttribute("aria-label"); + } + + function startEdit() { + active = true; + if (!input.parentNode) board.appendChild(input); + input.value = ""; + var clientW = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); + var x = curText.x * Tools.scale - document.documentElement.scrollLeft; + if (x < 360) { + x = Math.max(60, clientW - 320); + } else { + x = 60; + } + + input.style.opacity = '0.5'; + input.style.left = x + 'px'; + input.style.top = curText.y * Tools.scale - document.documentElement.scrollTop - 20 + 'px'; + input.style.height = '150px'; + input.style.width = '300px'; + input.focus(); + input.addEventListener("keyup", textChangeHandler); + input.addEventListener("blur", textChangeHandler); + input.addEventListener("blur", blur); + } + + function stopEdit() { + try { input.blur(); } catch (e) { /* Internet Explorer */ } + active = false; + blur(); + curText.id = 0; + curText.sentText = ""; + input.value = ""; + input.removeEventListener("keyup", textChangeHandler); + } + + function blur() { + if (active) return; + input.style.top = '-1000px'; + } + + function textChangeHandler(evt) { + input.value = removeDoubleQuotes(input.value); // remove all double quotes; they are unnecessary in (La)TeX and difficult to escape + if (evt.which === 27) { // escape + stopEdit(); + } + if (performance.now() - curText.lastSending > 1000) { + if (curText.sentText !== input.value) { + //If the user clicked where there was no text, then create a new text field + if (curText.id === 0) { + curText.id = Tools.generateUID("m"); //"m" for math + Tools.drawAndSend({ + 'type': 'new', + 'id': curText.id, + 'color': curText.color, + 'size': curText.size, + 'opacity': curText.opacity, + 'x': curText.x, + 'y': curText.y + }) + } + let mathematicsSVG = getSVGFromMathJax(input.value); + Tools.drawAndSend({ + 'type': "update", + 'id': curText.id, + 'txt': input.value, + 'mWidth': mathematicsSVG.getAttribute('width'), + 'mHeight': mathematicsSVG.getAttribute('height'), + 'mViewBox': mathematicsSVG.getAttribute('viewBox'), + 'mInnerHTML': mathematicsSVG.innerHTML, + }); + curText.sentText = input.value; + curText.lastSending = performance.now(); + } + } else { + clearTimeout(curText.timeout); + curText.timeout = setTimeout(textChangeHandler, 500, evt); + } + } + function removeDoubleQuotes(inStr) { + return inStr.split('"').join(''); + } + + function getSVGFromMathJax(rawTeX) { + let userColor = Tools.getColor(); + let svgFromMathJax = MathJax.tex2svg("\\color{" + userColor + "}\\begin{align}" + rawTeX + '\\end{align}', {display: true}); + let svgOnly = svgFromMathJax.children[0]; + // Split the viewBox into separate strings + var strArrViewBox = svgOnly.getAttribute("viewBox").split(" "); + var clickableRect = Tools.createSVGElement("rect"); + clickableRect.setAttribute("class", "ClickHelper"); + clickableRect.setAttribute("x", strArrViewBox[0]); + clickableRect.setAttribute("y", strArrViewBox[1]); + clickableRect.setAttribute("width", strArrViewBox[2]); + clickableRect.setAttribute("height", strArrViewBox[3]); + clickableRect.setAttribute("opacity", 0); + clickableRect.setAttribute("stroke-opacity", 0); + clickableRect.setAttribute("stroke", userColor); + svgOnly.appendChild(clickableRect); + return svgOnly; + } + + function draw(data, isLocal) { + Tools.drawingEvent = true; + switch (data.type) { + case "new": + createMathematicsField(data); + break; + case "update": + var mathematicsField = document.getElementById(data.id); + if (mathematicsField === null) { + console.error("Mathematics: Hmmm... I received text that belongs to an unknown text field"); + return false; + } + updateMathematics(mathematicsField, data.txt, data.mWidth, data.mHeight, data.mViewBox, data.mInnerHTML); + break; + default: + console.error("Mathematics: Draw instruction with unknown type. ", data); + break; + } + } + + function updateMathematics(mathematicsField, rawTeX, mWidth, mHeight, mViewBox, mInnerHTML) { + mathematicsField.setAttribute('aria-label', rawTeX); + mathematicsField.setAttribute('width', mWidth); + mathematicsField.setAttribute('height', mHeight); + mathematicsField.setAttribute('viewBox', mViewBox); + mathematicsField.innerHTML = mInnerHTML; + } + + function createMathematicsField(fieldData) { + var elem = Tools.createSVGElement("svg"); + elem.id = fieldData.id; + elem.setAttribute("class", "MathElement"); + elem.setAttribute("x", fieldData.x); + elem.setAttribute("y", fieldData.y); + if (fieldData.txt) elem.setAttribute("aria-label", fieldData.txt); + if ((fieldData.mWidth && fieldData.mHeight) && (fieldData.mViewBox && fieldData.mInnerHTML)) { + updateMathematics(elem, fieldData.txt, fieldData.mWidth, fieldData.mHeight, fieldData.mViewBox, fieldData.mInnerHTML); + } + Tools.drawingArea.appendChild(elem); + return elem; + } + + Tools.add({ //The new tool + "name": "Mathematics", + "shortcut": "m", + "listeners": { + "press": clickHandler, + }, + "onstart": onStart, + "onquit": onQuit, + "draw": draw, + "stylesheet": "tools/mathematics/mathematics.css", + "icon": "tools/mathematics/icon.svg", + "mouseCursor": "mathematics" + }); + +})(); //End of code isolation diff --git a/client-data/tools/pencil/pencil.css b/client-data/tools/pencil/pencil.css index 66d3f9ef..8f474deb 100644 --- a/client-data/tools/pencil/pencil.css +++ b/client-data/tools/pencil/pencil.css @@ -1,5 +1,8 @@ path { - fill: none; stroke-linecap: round; stroke-linejoin: round; } + +path.nofill { + fill: none; +} diff --git a/client-data/tools/pencil/pencil.js b/client-data/tools/pencil/pencil.js index cef4d911..822d392f 100644 --- a/client-data/tools/pencil/pencil.js +++ b/client-data/tools/pencil/pencil.js @@ -135,6 +135,7 @@ line.setAttribute("stroke", lineData.color || "black"); line.setAttribute("stroke-width", lineData.size || 10); line.setAttribute("opacity", Math.max(0.1, Math.min(1, lineData.opacity)) || 1); + line.setAttribute("class", "nofill"); Tools.drawingArea.appendChild(line); return line; } diff --git a/client-data/tools/rect/rect.css b/client-data/tools/rect/rect.css index 88b704fe..e7da8cf3 100755 --- a/client-data/tools/rect/rect.css +++ b/client-data/tools/rect/rect.css @@ -1,3 +1,3 @@ -#drawingArea rect { +#drawingArea rect.nofill { fill: none; } diff --git a/client-data/tools/rect/rect.js b/client-data/tools/rect/rect.js index 89fe151f..151fb094 100644 --- a/client-data/tools/rect/rect.js +++ b/client-data/tools/rect/rect.js @@ -126,6 +126,7 @@ shape.setAttribute("stroke", data.color || "black"); shape.setAttribute("stroke-width", data.size || 10); shape.setAttribute("opacity", Math.max(0.1, Math.min(1, data.opacity)) || 1); + shape.setAttribute("class", "nofill"); Tools.drawingArea.appendChild(shape); return shape; } diff --git a/server/createSVG.js b/server/createSVG.js index fabbef57..162f80ca 100644 --- a/server/createSVG.js +++ b/server/createSVG.js @@ -17,7 +17,7 @@ function htmlspecialchars(str) { } function renderPath(el, pathstring) { - return '' + htmlspecialchars(el.txt || "") + ''; }, + /** + * @return {string} + */ + "Mathematics": function (el) { + return '' + (el.mInnerHTML || '') + ''; + }, /** * @return {string} */ @@ -61,7 +77,7 @@ const Tools = { * @return {string} */ "Rectangle": function (el) { - return '' + '' ); await Promise.all(elems.map(async function (elem) {