Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css">
<link rel="stylesheet" href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css">
<link rel="icon" href="./favicon.ico"/>

<!-- WhiteboardJS -->
Expand Down Expand Up @@ -88,7 +89,7 @@ <h1 class="jumbotron-heading">WhiteBoardJS</h1>
<label class="btn btn-dark">
<input type="radio" name="toolRadio" id="circleTool" autocomplete="off"><i class="las la-circle"></i>
</label>
<label class="btn btn-dark disabled">
<label class="btn btn-dark">
<input type="radio" name="toolRadio" id="mathsTool" autocomplete="off"><i class="las la-square-root-alt"></i>
</label>
<label class="btn btn-dark disabled">
Expand Down Expand Up @@ -119,6 +120,33 @@ <h1 class="jumbotron-heading">WhiteBoardJS</h1>
</div>
</div>

<!-- Math Formula Modal -->
<div class="modal fade" id="mathModal" tabindex="-1" role="dialog" aria-labelledby="mathModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mathModalTitle"><i class="las la-square-root-alt"></i> Insert Math Formula</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="mathLatexInput">LaTeX Formula</label>
<input type="text" class="form-control" id="mathLatexInput" placeholder="e.g. \frac{a}{b}, x^2 + y^2 = z^2, \sqrt{2}">
</div>
<div id="mathPreview" class="border rounded p-3 text-center" style="min-height:50px;">
<span class="text-muted">Preview will appear here</span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="mathInsertBtn">Insert</button>
</div>
</div>
</div>
</div>

<!-- Welcome Window -->
<div class="modal fade" id="welcomeModal" tabindex="-1" role="dialog" aria-labelledby="welcomeModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
Expand Down Expand Up @@ -148,10 +176,11 @@ <h5 class="modal-title"> Welcome to WhiteboardJS ! </h5>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.touchpunch/0.2.3/jquery.ui.touch-punch.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/jquery.tinymce.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js"></script>

<!-- Whiteboard JS -->
<script>
function getEventCoords(e){if(e.originalEvent&&e.originalEvent.touches&&e.originalEvent.touches.length)return{pageX:e.originalEvent.touches[0].pageX,pageY:e.originalEvent.touches[0].pageY};return{pageX:e.pageX,pageY:e.pageY}}function parseCssPx(v){var n=parseInt(v,10);return isNaN(n)?0:n}var elID=tool=0,appSelector="#app",contentSelector=appSelector+" .wbjs-content";$(function(){$(appSelector).addClass("vw-100").addClass("vh-100").addClass("overflow-hidden"),$("input[name='toolRadio']").change(function(){switch($(this).attr("id")){case"mouseTool":tool=0,$(contentSelector).css({cursor:"grab"});break;case"textTool":tool=1,$(contentSelector).css("cursor","text");break;case"rectTool":tool=2,$(contentSelector).css("cursor","crosshair");break;case"circleTool":tool=3,$(contentSelector).css("cursor","crosshair")}$(appSelector).css({cursor:"text"})}),$("#welcomeModal.modal").modal().show(),$("#toolbar").draggable({handle:"#move-toolbar",containment:appSelector,scroll:!1}),$(contentSelector).draggable({start:function(){$(this).css("cursor","grabbing")},stop:function(){var t=parseCssPx($(this).css("top")),s=parseCssPx($(this).css("left"));(t>0||s>0)&&($(this).children().css({top:"+="+t,left:"+="+s}),$(this).css({width:"+="+s,height:"+="+t,top:"0",left:"0"}));var e=$(appSelector).height()-t-$(this).height();t<0&&e>0&&$(this).css({height:"+="+e});var l=$(appSelector).width()-s-$(this).width();s<0&&l>0&&$(this).css({width:"+="+l}),$(this).css("cursor","grab")},cancel:contentSelector+" *",scroll:!1});document.querySelector(contentSelector).addEventListener("touchmove",function(e){if(tool!==0)e.preventDefault()},{passive:!1});var ignoreNextClick=!1;function addElement(t){var c=getEventCoords(t),cT=Math.abs(parseCssPx($(contentSelector).css("top"))),cL=Math.abs(parseCssPx($(contentSelector).css("left")));switch(tool){case 1:var s=$(contentSelector).append('<div class="wbjs-el"><div class="btn btn-link position-absolute drg-btn"><i class="las la-arrows-alt"></i></div><p class="p-2 m-0" id="wbjs-el-'+elID+'"></p></div>').children(":last-child");s.children("p").tinymce({menubar:!1,inline:!0,plugins:["link","textcolor","contextmenu","autolink"],toolbar:["bold italic underline | fontsizeselect | forecolor backcolor"],auto_focus:"wbjs-el-"+elID}),s.css({position:"absolute",top:cT+c.pageY,left:cL+c.pageX,"min-width":"62px","min-height":"24px"}),$("#mouseTool").trigger("click"),s.draggable({handle:"div.btn",scroll:!1}),elID++;break;case 2:var s=$(contentSelector).append('<div class="wbjs-el" id="wbjs-el-'+elID+'"><div class="btn btn-link position-absolute drg-btn"><i class="las la-arrows-alt"></i></div></div>').children(":last-child");s.css({position:"absolute",top:cT+c.pageY,left:cL+c.pageX,border:"1px solid black",width:"64px",height:"64px"}),$("#mouseTool").trigger("click"),s.draggable({scroll:!1}),s.resizable(),elID++;break;case 3:var s=$(contentSelector).append('<div class="wbjs-el" id="wbjs-el-'+elID+'"><div class="btn btn-link position-absolute drg-btn"><i class="las la-arrows-alt"></i></div></div>').children(":last-child");s.css({position:"absolute",top:cT+c.pageY,left:cL+c.pageX,border:"1px solid black","border-radius":"100%",width:"64px",height:"64px"}),$("#mouseTool").trigger("click"),s.draggable({scroll:!1}),s.resizable(),elID++}}$(contentSelector).on("click",function(e){if(ignoreNextClick){ignoreNextClick=!1;return}addElement(e)});var touchMoved=!1;$(contentSelector).on("touchstart",function(){touchMoved=!1}).on("touchmove",function(){touchMoved=!0}).on("touchend",function(e){if(!touchMoved&&tool!==0){var ct=e.originalEvent&&e.originalEvent.changedTouches;if(ct&&ct.length>0){addElement({pageX:ct[0].pageX,pageY:ct[0].pageY});ignoreNextClick=!0}}});
function getEventCoords(e){if(e.originalEvent&&e.originalEvent.touches&&e.originalEvent.touches.length)return{pageX:e.originalEvent.touches[0].pageX,pageY:e.originalEvent.touches[0].pageY};return{pageX:e.pageX,pageY:e.pageY}}function parseCssPx(v){var n=parseInt(v,10);return isNaN(n)?0:n}var elID=tool=0,appSelector="#app",contentSelector=appSelector+" .wbjs-content",mathInsertCoords=null;$(function(){$(appSelector).addClass("vw-100").addClass("vh-100").addClass("overflow-hidden"),$("input[name='toolRadio']").change(function(){switch($(this).attr("id")){case"mouseTool":tool=0,$(contentSelector).css({cursor:"grab"});break;case"textTool":tool=1,$(contentSelector).css("cursor","text");break;case"rectTool":tool=2,$(contentSelector).css("cursor","crosshair");break;case"circleTool":tool=3,$(contentSelector).css("cursor","crosshair");break;case"mathsTool":tool=4,$(contentSelector).css("cursor","crosshair");break}$(appSelector).css({cursor:"text"})}),$("#welcomeModal.modal").modal().show(),$("#toolbar").draggable({handle:"#move-toolbar",containment:appSelector,scroll:!1}),$(contentSelector).draggable({start:function(){$(this).css("cursor","grabbing")},stop:function(){var t=parseCssPx($(this).css("top")),s=parseCssPx($(this).css("left"));(t>0||s>0)&&($(this).children().css({top:"+="+t,left:"+="+s}),$(this).css({width:"+="+s,height:"+="+t,top:"0",left:"0"}));var e=$(appSelector).height()-t-$(this).height();t<0&&e>0&&$(this).css({height:"+="+e});var l=$(appSelector).width()-s-$(this).width();s<0&&l>0&&$(this).css({width:"+="+l}),$(this).css("cursor","grab")},cancel:contentSelector+" *",scroll:!1});document.querySelector(contentSelector).addEventListener("touchmove",function(e){if(tool!==0)e.preventDefault()},{passive:!1});var ignoreNextClick=!1;function addElement(t){var c=getEventCoords(t),cT=Math.abs(parseCssPx($(contentSelector).css("top"))),cL=Math.abs(parseCssPx($(contentSelector).css("left")));switch(tool){case 1:var s=$(contentSelector).append('<div class="wbjs-el"><div class="btn btn-link position-absolute drg-btn"><i class="las la-arrows-alt"></i></div><p class="p-2 m-0" id="wbjs-el-'+elID+'"></p></div>').children(":last-child");s.children("p").tinymce({menubar:!1,inline:!0,plugins:["link","textcolor","contextmenu","autolink"],toolbar:["bold italic underline | fontsizeselect | forecolor backcolor"],auto_focus:"wbjs-el-"+elID}),s.css({position:"absolute",top:cT+c.pageY,left:cL+c.pageX,"min-width":"62px","min-height":"24px"}),$("#mouseTool").trigger("click"),s.draggable({handle:"div.btn",scroll:!1}),elID++;break;case 2:var s=$(contentSelector).append('<div class="wbjs-el" id="wbjs-el-'+elID+'"><div class="btn btn-link position-absolute drg-btn"><i class="las la-arrows-alt"></i></div></div>').children(":last-child");s.css({position:"absolute",top:cT+c.pageY,left:cL+c.pageX,border:"1px solid black",width:"64px",height:"64px"}),$("#mouseTool").trigger("click"),s.draggable({scroll:!1}),s.resizable(),elID++;break;case 3:var s=$(contentSelector).append('<div class="wbjs-el" id="wbjs-el-'+elID+'"><div class="btn btn-link position-absolute drg-btn"><i class="las la-arrows-alt"></i></div></div>').children(":last-child");s.css({position:"absolute",top:cT+c.pageY,left:cL+c.pageX,border:"1px solid black","border-radius":"100%",width:"64px",height:"64px"}),$("#mouseTool").trigger("click"),s.draggable({scroll:!1}),s.resizable(),elID++;break;case 4:mathInsertCoords={pageX:c.pageX,pageY:c.pageY,ctTop:cT,ctLeft:cL},$("#mathLatexInput").val(""),$("#mathPreview").html('<span class="text-muted">Preview will appear here</span>'),$("#mathModal").modal("show");break}}$(contentSelector).on("click",function(e){if(ignoreNextClick){ignoreNextClick=!1;return}addElement(e)});var touchMoved=!1;$(contentSelector).on("touchstart",function(){touchMoved=!1}).on("touchmove",function(){touchMoved=!0}).on("touchend",function(e){if(!touchMoved&&tool!==0){var ct=e.originalEvent&&e.originalEvent.changedTouches;if(ct&&ct.length>0){addElement({pageX:ct[0].pageX,pageY:ct[0].pageY});ignoreNextClick=!0}}});$("#mathLatexInput").on("input",function(){var l=$(this).val().trim();if(l){try{katex.render(l,document.getElementById("mathPreview"),{throwOnError:!1,displayMode:!0})}catch(e){$("#mathPreview").html('<span class="text-danger">Invalid formula</span>')}}else{$("#mathPreview").html('<span class="text-muted">Preview will appear here</span>')}});$("#mathInsertBtn").on("click",function(){var l=$("#mathLatexInput").val().trim();if(!l||!mathInsertCoords)return;var r=$("<span></span>")[0];try{katex.render(l,r,{throwOnError:!1,displayMode:!0})}catch(e){return}var s=$(contentSelector).append('<div class="wbjs-el" id="wbjs-el-'+elID+'"><div class="btn btn-link position-absolute drg-btn"><i class="las la-arrows-alt"></i></div><div class="wbjs-math p-2"></div></div>').children(":last-child");s.find(".wbjs-math").append(r),s.css({position:"absolute",top:mathInsertCoords.ctTop+mathInsertCoords.pageY,left:mathInsertCoords.ctLeft+mathInsertCoords.pageX}),$("#mathModal").modal("hide"),$("#mouseTool").trigger("click"),s.draggable({handle:"div.btn",scroll:!1}),elID++,mathInsertCoords=null});$("#mathLatexInput").on("keydown",function(e){if(e.key==="Enter"){e.preventDefault(),$("#mathInsertBtn").trigger("click")}});$("#mathModal").on("hidden.bs.modal",function(){mathInsertCoords=null;if(tool===4){$("#mouseTool").trigger("click")}});
$("#app > *:not(main)").css('z-index', '-500');
$("#gotoDemo").click(function(){
$("#app > *").css('z-index', '500');
Expand Down
34 changes: 32 additions & 2 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css">
<link rel="stylesheet" href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css">
<link rel="icon" href="./favicon.ico"/>

<!-- WhiteboardJS -->
Expand Down Expand Up @@ -44,7 +45,7 @@
<label class="btn btn-dark">
<input type="radio" name="toolRadio" id="circleTool" autocomplete="off"><i class="las la-circle"></i>
</label>
<label class="btn btn-dark disabled">
<label class="btn btn-dark">
<input type="radio" name="toolRadio" id="mathsTool" autocomplete="off"><i class="las la-square-root-alt"></i>
</label>
Comment on lines +48 to 50
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repo has two entrypoints: src/index.html (served by src/app.js) and the root index.html (used for the GitHub Pages demo). This PR enables mathsTool only in src/index.html; the root index.html still has the maths button disabled and doesn’t load KaTeX, so the hosted demo won’t show this feature. Consider updating the root index.html as well (or consolidating to avoid duplicated UI).

Copilot uses AI. Check for mistakes.
<label class="btn btn-dark disabled">
Expand Down Expand Up @@ -75,6 +76,33 @@
</div>
</div>

<!-- Math Formula Modal -->
<div class="modal fade" id="mathModal" tabindex="-1" role="dialog" aria-labelledby="mathModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mathModalTitle"><i class="las la-square-root-alt"></i> Insert Math Formula</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="mathLatexInput">LaTeX Formula</label>
<input type="text" class="form-control" id="mathLatexInput" placeholder="e.g. \frac{a}{b}, x^2 + y^2 = z^2, \sqrt{2}">
</div>
<div id="mathPreview" class="border rounded p-3 text-center" style="min-height:50px;">
<span class="text-muted">Preview will appear here</span>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="mathInsertBtn">Insert</button>
</div>
</div>
</div>
</div>

<!-- Welcome Window -->
<div class="modal fade" id="welcomeModal" tabindex="-1" role="dialog" aria-labelledby="welcomeModalTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
Expand All @@ -87,7 +115,7 @@ <h5 class="modal-title" id="welcomeModalTitle"> Welcome to WhiteboardJS! </h5>
</div>
<div class="modal-body">
WhiteboardJS is a tiny powerful whiteboard that works on any device!<br />
Use the toolbar to add text, rectangles, and circles.<br />
Use the toolbar to add text, rectangles, circles, and math formulas.<br />
<strong>Tip:</strong> Save your board anytime with your browser's <em>File &gt; Save As</em> — it saves as a standalone HTML file you can reopen later.
</div>
<div class="modal-footer">
Expand All @@ -106,6 +134,8 @@ <h5 class="modal-title" id="welcomeModalTitle"> Welcome to WhiteboardJS! </h5>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/tinymce.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/4.9.11/jquery.tinymce.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js"></script>

<!-- Whiteboard JS -->
<script src="jquery.whiteboard.js"></script>

Expand Down
77 changes: 77 additions & 0 deletions src/jquery.whiteboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
* - 1 textTool
* - 2 rectTool
* - 3 circleTool
* - 4 mathsTool
* ...
*/
var elID = tool = 0;
var appSelector = "#app";
var contentSelector = appSelector + " .wbjs-content";
var mathInsertCoords = null;

// Helper: get page coordinates from mouse or touch event
function getEventCoords(event) {
Expand Down Expand Up @@ -48,6 +50,10 @@ $(function(){
tool = 3;
$(contentSelector).css("cursor", "crosshair");
break;
case "mathsTool":
tool = 4;
$(contentSelector).css("cursor", "crosshair");
break;
//TODO: To be continued...
}
$(appSelector).css({"cursor": "text"});
Expand Down Expand Up @@ -182,6 +188,12 @@ $(function(){
newEl.resizable();
elID++;
break;
case 4:
mathInsertCoords = { pageX: coords.pageX, pageY: coords.pageY, ctTop: ctTop, ctLeft: ctLeft };
$("#mathLatexInput").val("");
$("#mathPreview").html('<span class="text-muted">Preview will appear here</span>');
$("#mathModal").modal("show");
break;
}
}

Expand All @@ -208,4 +220,69 @@ $(function(){
}
}
});

//-- Math formula modal: live preview
$("#mathLatexInput").on("input", function() {
var latex = $(this).val().trim();
if (latex) {
try {
katex.render(latex, document.getElementById("mathPreview"), { throwOnError: false, displayMode: true });
} catch(e) {
$("#mathPreview").html('<span class="text-danger">Invalid formula</span>');
}
} else {
$("#mathPreview").html('<span class="text-muted">Preview will appear here</span>');
}
});

//-- Math formula modal: insert button
$("#mathInsertBtn").on("click", function() {
var latex = $("#mathLatexInput").val().trim();
if (!latex || !mathInsertCoords) return;

var rendered = $('<span></span>')[0];
try {
katex.render(latex, rendered, { throwOnError: false, displayMode: true });
} catch(e) {
return;
}

var newEl = $(contentSelector).append(
'<div class="wbjs-el" id="wbjs-el-'+elID+'">' +
'<div class="btn btn-link position-absolute drg-btn"><i class="las la-arrows-alt"></i></div>' +
'<div class="wbjs-math p-2"></div>' +
'</div>'
).children(":last-child");

newEl.find(".wbjs-math").append(rendered);

newEl.css({
"position": "absolute",
"top": mathInsertCoords.ctTop + mathInsertCoords.pageY,
"left": mathInsertCoords.ctLeft + mathInsertCoords.pageX
});

$("#mathModal").modal("hide");
$("#mouseTool").trigger("click");

newEl.draggable({handle: "div.btn", scroll: false});
elID++;
mathInsertCoords = null;
});

//-- Math formula modal: allow Enter key to insert
$("#mathLatexInput").on("keydown", function(e) {
if (e.key === "Enter") {
e.preventDefault();
$("#mathInsertBtn").trigger("click");
}
});

//-- Math formula modal: reset tool on cancel
$("#mathModal").on("hidden.bs.modal", function() {
mathInsertCoords = null;
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says “reset tool on cancel”, but the handler only clears mathInsertCoords and leaves the current tool as mathsTool. Either update the comment to match the intended behavior, or actually reset the active tool (e.g., trigger #mouseTool on modal close / cancel) so users don’t get stuck repeatedly reopening the modal on the next click.

Suggested change
mathInsertCoords = null;
mathInsertCoords = null;
if (tool === 4) {
$("#mouseTool").trigger("click");
}

Copilot uses AI. Check for mistakes.
if (tool === 4) {
$("#mouseTool").trigger("click");
}
});
});