123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662 |
- /**
- * 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
- */
-
- var Tools = {};
-
- Tools.i18n = (function i18n() {
- var translations = JSON.parse(document.getElementById("translations").text);
- return {
- "t": function translate(s) {
- var key = s.toLowerCase().replace(/ /g, '_');
- return translations[key] || s;
- }
- };
- })();
-
- Tools.server_config = JSON.parse(document.getElementById("configuration").text);
-
- Tools.board = document.getElementById("board");
- Tools.svg = document.getElementById("canvas");
- Tools.drawingArea = Tools.svg.getElementById("drawingArea");
-
- //Initialization
- Tools.curTool = null;
- Tools.drawingEvent = true;
- Tools.showMarker = true;
- Tools.showOtherCursors = true;
- Tools.showMyCursor = true;
-
- Tools.isIE = /MSIE|Trident/.test(window.navigator.userAgent);
-
- Tools.socket = null;
- Tools.connect = function () {
- var self = this;
-
- // Destroy socket if one already exists
- if (self.socket) {
- self.socket.destroy();
- delete self.socket;
- self.socket = null;
- }
-
-
- this.socket = io.connect('', {
- "path": window.location.pathname.split("/boards/")[0] + "/socket.io",
- "reconnection": true,
- "reconnectionDelay": 100, //Make the xhr connections as fast as possible
- "timeout": 1000 * 60 * 20 // Timeout after 20 minutes
- });
-
- //Receive draw instructions from the server
- this.socket.on("broadcast", function (msg) {
- handleMessage(msg).finally(function afterload() {
- var loadingEl = document.getElementById("loadingMessage");
- loadingEl.classList.add("hidden");
- });
- });
-
- this.socket.on("reconnect", function onReconnection() {
- Tools.socket.emit('joinboard', Tools.boardName);
- });
- };
-
- Tools.connect();
-
- Tools.boardName = (function () {
- var path = window.location.pathname.split("/");
- return decodeURIComponent(path[path.length - 1]);
- })();
-
- //Get the board as soon as the page is loaded
- Tools.socket.emit("getboard", Tools.boardName);
-
- Tools.HTML = {
- template: new Minitpl("#tools > .tool"),
- addShortcut: function addShortcut(key, callback) {
- window.addEventListener("keydown", function (e) {
- if (e.key === key && !e.target.matches("input[type=text], textarea")) {
- callback();
- }
- });
- },
- addTool: function (toolName, toolIcon, toolIconHTML, toolShortcut, oneTouch) {
- var callback = function () {
- Tools.change(toolName);
- };
- this.addShortcut(toolShortcut, function () {
- Tools.change(toolName);
- document.activeElement.blur && document.activeElement.blur();
- });
- return this.template.add(function (elem) {
- elem.addEventListener("click", callback);
- elem.id = "toolID-" + toolName;
- elem.getElementsByClassName("tool-name")[0].textContent = Tools.i18n.t(toolName);
- var toolIconElem = elem.getElementsByClassName("tool-icon")[0];
- toolIconElem.src = toolIcon;
- toolIconElem.alt = toolIcon;
- if (oneTouch) elem.classList.add("oneTouch");
- elem.title =
- Tools.i18n.t(toolName) + " (" +
- Tools.i18n.t("keyboard shortcut") + ": " +
- toolShortcut + ")" +
- (Tools.list[toolName].secondary ? " [" + Tools.i18n.t("click_to_toggle") + "]" : "");
- if (Tools.list[toolName].secondary) {
- elem.classList.add('hasSecondary');
- var secondaryIcon = elem.getElementsByClassName('secondaryIcon')[0];
- secondaryIcon.src = Tools.list[toolName].secondary.icon;
- toolIconElem.classList.add("primaryIcon");
- }
- });
- },
- changeTool: function (oldToolName, newToolName) {
- var oldTool = document.getElementById("toolID-" + oldToolName);
- var newTool = document.getElementById("toolID-" + newToolName);
- if (oldTool) oldTool.classList.remove("curTool");
- if (newTool) newTool.classList.add("curTool");
- },
- toggle: function (toolName, name, icon) {
- var elem = document.getElementById("toolID-" + toolName);
-
- // Change secondary icon
- var primaryIcon = elem.getElementsByClassName("primaryIcon")[0];
- var secondaryIcon = elem.getElementsByClassName("secondaryIcon")[0];
- var primaryIconSrc = primaryIcon.src;
- var secondaryIconSrc = secondaryIcon.src;
- primaryIcon.src = secondaryIconSrc;
- secondaryIcon.src = primaryIconSrc;
-
- // Change primary icon
- elem.getElementsByClassName("tool-icon")[0].src = icon;
- elem.getElementsByClassName("tool-name")[0].textContent = Tools.i18n.t(name);
- },
- addStylesheet: function (href) {
- //Adds a css stylesheet to the html or svg document
- var link = document.createElement("link");
- link.href = href;
- link.rel = "stylesheet";
- link.type = "text/css";
- document.head.appendChild(link);
- },
- colorPresetTemplate: new Minitpl("#colorPresetSel .colorPresetButton"),
- addColorButton: function (button) {
- var setColor = Tools.setColor.bind(Tools, button.color);
- if (button.key) this.addShortcut(button.key, setColor);
- return this.colorPresetTemplate.add(function (elem) {
- elem.addEventListener("click", setColor);
- elem.id = "color_" + button.color.replace(/^#/, '');
- elem.style.backgroundColor = button.color;
- if (button.key) {
- elem.title = Tools.i18n.t("keyboard shortcut") + ": " + button.key;
- }
- });
- }
- };
-
- Tools.list = {}; // An array of all known tools. {"toolName" : {toolObject}}
-
- Tools.isBlocked = function toolIsBanned(tool) {
- if (tool.name.includes(",")) throw new Error("Tool Names must not contain a comma");
- return Tools.server_config.BLOCKED_TOOLS.includes(tool.name);
- };
-
- /**
- * Register a new tool, without touching the User Interface
- */
- Tools.register = function registerTool(newTool) {
- if (Tools.isBlocked(newTool)) return;
-
- if (newTool.name in Tools.list) {
- console.log("Tools.add: The tool '" + newTool.name + "' is already" +
- "in the list. Updating it...");
- }
-
- //Format the new tool correctly
- Tools.applyHooks(Tools.toolHooks, newTool);
-
- //Add the tool to the list
- Tools.list[newTool.name] = newTool;
-
- // Register the change handlers
- if (newTool.onSizeChange) Tools.sizeChangeHandlers.push(newTool.onSizeChange);
-
- //There may be pending messages for the tool
- var pending = Tools.pendingMessages[newTool.name];
- if (pending) {
- console.log("Drawing pending messages for '%s'.", newTool.name);
- var msg;
- while (msg = pending.shift()) {
- //Transmit the message to the tool (precising that it comes from the network)
- newTool.draw(msg, false);
- }
- }
- };
-
- /**
- * Add a new tool to the user interface
- */
- Tools.add = function (newTool) {
- if (Tools.isBlocked(newTool)) return;
-
- Tools.register(newTool);
-
- if (newTool.stylesheet) {
- Tools.HTML.addStylesheet(newTool.stylesheet);
- }
-
- //Add the tool to the GUI
- Tools.HTML.addTool(newTool.name, newTool.icon, newTool.iconHTML, newTool.shortcut, newTool.oneTouch);
- };
-
- Tools.change = function (toolName) {
- var newTool = Tools.list[toolName];
- var oldTool = Tools.curTool;
- if (!newTool) throw new Error("Trying to select a tool that has never been added!");
- if (newTool === oldTool) {
- if (newTool.secondary) {
- newTool.secondary.active = !newTool.secondary.active;
- var props = newTool.secondary.active ? newTool.secondary : newTool;
- Tools.HTML.toggle(newTool.name, props.name, props.icon);
- if (newTool.secondary.switch) newTool.secondary.switch();
- }
- return;
- }
- if (!newTool.oneTouch) {
- //Update the GUI
- var curToolName = (Tools.curTool) ? Tools.curTool.name : "";
- try {
- Tools.HTML.changeTool(curToolName, toolName);
- } catch (e) {
- console.error("Unable to update the GUI with the new tool. " + e);
- }
- Tools.svg.style.cursor = newTool.mouseCursor || "auto";
- Tools.board.title = Tools.i18n.t(newTool.helpText || "");
-
- //There is not necessarily already a curTool
- if (Tools.curTool !== null) {
- //It's useless to do anything if the new tool is already selected
- if (newTool === Tools.curTool) return;
-
- //Remove the old event listeners
- Tools.removeToolListeners(Tools.curTool);
-
- //Call the callbacks of the old tool
- Tools.curTool.onquit(newTool);
- }
-
- //Add the new event listeners
- Tools.addToolListeners(newTool);
- Tools.curTool = newTool;
- }
-
- //Call the start callback of the new tool
- newTool.onstart(oldTool);
- };
-
- Tools.addToolListeners = function addToolListeners(tool) {
- for (var event in tool.compiledListeners) {
- var listener = tool.compiledListeners[event];
- var target = listener.target || Tools.board;
- target.addEventListener(event, listener, { 'passive': false });
- }
- };
-
- Tools.removeToolListeners = function removeToolListeners(tool) {
- for (var event in tool.compiledListeners) {
- var listener = tool.compiledListeners[event];
- var target = listener.target || Tools.board;
- target.removeEventListener(event, listener);
- // also attempt to remove with capture = true in IE
- if (Tools.isIE) target.removeEventListener(event, listener, true);
- }
- };
-
- (function () {
- // Handle secondary tool switch with shift (key code 16)
- function handleShift(active, evt) {
- if (evt.keyCode === 16 && Tools.curTool.secondary && Tools.curTool.secondary.active !== active) {
- Tools.change(Tools.curTool.name);
- }
- }
- window.addEventListener("keydown", handleShift.bind(null, true));
- window.addEventListener("keyup", handleShift.bind(null, false));
- })();
-
- Tools.send = function (data, toolName) {
- toolName = toolName || Tools.curTool.name;
- var d = data;
- d.tool = toolName;
- Tools.applyHooks(Tools.messageHooks, d);
- var message = {
- "board": Tools.boardName,
- "data": d
- };
- Tools.socket.emit('broadcast', message);
- };
-
- Tools.drawAndSend = function (data, tool) {
- if (tool == null) tool = Tools.curTool;
- tool.draw(data, true);
- Tools.send(data, tool.name);
- };
-
- //Object containing the messages that have been received before the corresponding tool
- //is loaded. keys : the name of the tool, values : array of messages for this tool
- Tools.pendingMessages = {};
-
- // Send a message to the corresponding tool
- function messageForTool(message) {
- var name = message.tool,
- tool = Tools.list[name];
-
- if (tool) {
- Tools.applyHooks(Tools.messageHooks, message);
- tool.draw(message, false);
- } else {
- ///We received a message destinated to a tool that we don't have
- //So we add it to the pending messages
- if (!Tools.pendingMessages[name]) Tools.pendingMessages[name] = [message];
- else Tools.pendingMessages[name].push(message);
- }
-
- if (message.tool !== 'Hand' && message.deltax != null && message.deltay != null) {
- //this message has special info for the mover
- messageForTool({ tool: 'Hand', type: 'update', deltax: message.deltax || 0, deltay: message.deltay || 0, id: message.id });
- }
- }
-
- // Apply the function to all arguments by batches
- function batchCall(fn, args) {
- var BATCH_SIZE = 1024;
- if (args.length === 0) {
- return Promise.resolve();
- } else {
- var batch = args.slice(0, BATCH_SIZE);
- var rest = args.slice(BATCH_SIZE);
- return Promise.all(batch.map(fn))
- .then(function () {
- return new Promise(requestAnimationFrame);
- }).then(batchCall.bind(null, fn, rest));
- }
- }
-
- // Call messageForTool recursively on the message and its children
- function handleMessage(message) {
- //Check if the message is in the expected format
- if (!message.tool && !message._children) {
- console.error("Received a badly formatted message (no tool). ", message);
- }
- if (message.tool) messageForTool(message);
- if (message._children) return batchCall(handleMessage, message._children);
- else return Promise.resolve();
- }
-
- Tools.unreadMessagesCount = 0;
- Tools.newUnreadMessage = function () {
- Tools.unreadMessagesCount++;
- updateDocumentTitle();
- };
-
- window.addEventListener("focus", function () {
- Tools.unreadMessagesCount = 0;
- updateDocumentTitle();
- });
-
- function updateDocumentTitle() {
- document.title =
- (Tools.unreadMessagesCount ? '(' + Tools.unreadMessagesCount + ') ' : '') +
- Tools.boardName +
- " | WBO";
- }
-
- (function () {
- // Scroll and hash handling
- var scrollTimeout, lastStateUpdate = Date.now();
-
- window.addEventListener("scroll", function onScroll() {
- var scale = Tools.getScale();
- var x = document.documentElement.scrollLeft / scale,
- y = document.documentElement.scrollTop / scale;
-
- clearTimeout(scrollTimeout);
- scrollTimeout = setTimeout(function updateHistory() {
- var hash = '#' + (x | 0) + ',' + (y | 0) + ',' + Tools.getScale().toFixed(1);
- if (Date.now() - lastStateUpdate > 5000 && hash !== window.location.hash) {
- window.history.pushState({}, "", hash);
- lastStateUpdate = Date.now();
- } else {
- window.history.replaceState({}, "", hash);
- }
- }, 100);
- });
-
- function setScrollFromHash() {
- var coords = window.location.hash.slice(1).split(',');
- var x = coords[0] | 0;
- var y = coords[1] | 0;
- var scale = parseFloat(coords[2]);
- resizeCanvas({ x: x, y: y });
- Tools.setScale(scale);
- window.scrollTo(x * scale, y * scale);
- }
-
- window.addEventListener("hashchange", setScrollFromHash, false);
- window.addEventListener("popstate", setScrollFromHash, false);
- window.addEventListener("DOMContentLoaded", setScrollFromHash, false);
- })();
-
- function resizeCanvas(m) {
- //Enlarge the canvas whenever something is drawn near its border
- var x = m.x | 0, y = m.y | 0
- var MAX_BOARD_SIZE = Tools.server_config.MAX_BOARD_SIZE || 65536; // Maximum value for any x or y on the board
- if (x > Tools.svg.width.baseVal.value - 2000) {
- Tools.svg.width.baseVal.value = Math.min(x + 2000, MAX_BOARD_SIZE);
- }
- if (y > Tools.svg.height.baseVal.value - 2000) {
- Tools.svg.height.baseVal.value = Math.min(y + 2000, MAX_BOARD_SIZE);
- }
- }
-
- function updateUnreadCount(m) {
- if (document.hidden && ["child", "update"].indexOf(m.type) === -1) {
- Tools.newUnreadMessage();
- }
- }
-
- // List of hook functions that will be applied to messages before sending or drawing them
- Tools.messageHooks = [resizeCanvas, updateUnreadCount];
-
- Tools.scale = 1.0;
- var scaleTimeout = null;
- Tools.setScale = function setScale(scale) {
- var fullScale = Math.max(window.innerWidth, window.innerHeight) / Tools.server_config.MAX_BOARD_SIZE;
- var minScale = Math.max(0.1, fullScale);
- var maxScale = 10;
- if (isNaN(scale)) scale = 1;
- scale = Math.max(minScale, Math.min(maxScale, scale));
- Tools.svg.style.willChange = 'transform';
- Tools.svg.style.transform = 'scale(' + scale + ')';
- clearTimeout(scaleTimeout);
- scaleTimeout = setTimeout(function () {
- Tools.svg.style.willChange = 'auto';
- }, 1000);
- Tools.scale = scale;
- return scale;
- }
- Tools.getScale = function getScale() {
- return Tools.scale;
- }
-
- //List of hook functions that will be applied to tools before adding them
- Tools.toolHooks = [
- function checkToolAttributes(tool) {
- if (typeof (tool.name) !== "string") throw "A tool must have a name";
- if (typeof (tool.listeners) !== "object") {
- tool.listeners = {};
- }
- if (typeof (tool.onstart) !== "function") {
- tool.onstart = function () { };
- }
- if (typeof (tool.onquit) !== "function") {
- tool.onquit = function () { };
- }
- },
- function compileListeners(tool) {
- //compile listeners into compiledListeners
- var listeners = tool.listeners;
-
- //A tool may provide precompiled listeners
- var compiled = tool.compiledListeners || {};
- tool.compiledListeners = compiled;
-
- function compile(listener) { //closure
- return (function listen(evt) {
- var x = evt.pageX / Tools.getScale(),
- y = evt.pageY / Tools.getScale();
- return listener(x, y, evt, false);
- });
- }
-
- function compileTouch(listener) { //closure
- return (function touchListen(evt) {
- //Currently, we don't handle multitouch
- if (evt.changedTouches.length === 1) {
- //evt.preventDefault();
- var touch = evt.changedTouches[0];
- var x = touch.pageX / Tools.getScale(),
- y = touch.pageY / Tools.getScale();
- return listener(x, y, evt, true);
- }
- return true;
- });
- }
-
- function wrapUnsetHover(f, toolName) {
- return (function unsetHover(evt) {
- document.activeElement && document.activeElement.blur && document.activeElement.blur();
- return f(evt);
- });
- }
-
- if (listeners.press) {
- compiled["mousedown"] = wrapUnsetHover(compile(listeners.press), tool.name);
- compiled["touchstart"] = wrapUnsetHover(compileTouch(listeners.press), tool.name);
- }
- if (listeners.move) {
- compiled["mousemove"] = compile(listeners.move);
- compiled["touchmove"] = compileTouch(listeners.move);
- }
- if (listeners.release) {
- var release = compile(listeners.release),
- releaseTouch = compileTouch(listeners.release);
- compiled["mouseup"] = release;
- if (!Tools.isIE) compiled["mouseleave"] = release;
- compiled["touchleave"] = releaseTouch;
- compiled["touchend"] = releaseTouch;
- compiled["touchcancel"] = releaseTouch;
- }
- }
- ];
-
- Tools.applyHooks = function (hooks, object) {
- //Apply every hooks on the object
- hooks.forEach(function (hook) {
- hook(object);
- });
- };
-
-
- // Utility functions
-
- Tools.generateUID = function (prefix, suffix) {
- var uid = Date.now().toString(36); //Create the uids in chronological order
- uid += (Math.round(Math.random() * 36)).toString(36); //Add a random character at the end
- if (prefix) uid = prefix + uid;
- if (suffix) uid = uid + suffix;
- return uid;
- };
-
- Tools.createSVGElement = function createSVGElement(name, attrs) {
- var elem = document.createElementNS(Tools.svg.namespaceURI, name);
- if (typeof (attrs) !== "object") return elem;
- Object.keys(attrs).forEach(function (key, i) {
- elem.setAttributeNS(null, key, attrs[key]);
- });
- return elem;
- };
-
- Tools.positionElement = function (elem, x, y) {
- elem.style.top = y + "px";
- elem.style.left = x + "px";
- };
-
- Tools.colorPresets = [
- { color: "#001f3f", key: '1' },
- { color: "#FF4136", key: '2' },
- { color: "#0074D9", key: '3' },
- { color: "#FF851B", key: '4' },
- { color: "#FFDC00", key: '5' },
- { color: "#3D9970", key: '6' },
- { color: "#91E99B", key: '7' },
- { color: "#90468b", key: '8' },
- { color: "#7FDBFF", key: '9' },
- { color: "#AAAAAA", key: '0' },
- { color: "#E65194" }
- ];
-
- Tools.color_chooser = document.getElementById("chooseColor");
-
- Tools.setColor = function (color) {
- Tools.color_chooser.value = color;
- };
-
- Tools.getColor = (function color() {
- var color_index = (Math.random() * Tools.colorPresets.length) | 0;
- var initial_color = Tools.colorPresets[color_index].color;
- Tools.setColor(initial_color);
- return function () { return Tools.color_chooser.value; };
- })();
-
- Tools.colorPresets.forEach(Tools.HTML.addColorButton.bind(Tools.HTML));
-
- Tools.sizeChangeHandlers = [];
- Tools.setSize = (function size() {
- var chooser = document.getElementById("chooseSize");
-
- function update() {
- var size = Math.max(1, Math.min(50, chooser.value | 0));
- chooser.value = size;
- Tools.sizeChangeHandlers.forEach(function (handler) {
- handler(size);
- });
- }
- update();
-
- chooser.onchange = chooser.oninput = update;
- return function (value) {
- if (value !== null && value !== undefined) { chooser.value = value; update(); }
- return parseInt(chooser.value);
- };
- })();
-
- Tools.getSize = (function () { return Tools.setSize() });
-
- Tools.getOpacity = (function opacity() {
- var chooser = document.getElementById("chooseOpacity");
- var opacityIndicator = document.getElementById("opacityIndicator");
-
- function update() {
- opacityIndicator.setAttribute("opacity", chooser.value);
- }
- update();
-
- chooser.onchange = chooser.oninput = update;
- return function () {
- return Math.max(0.1, Math.min(1, chooser.value));
- };
- })();
-
-
- //Scale the canvas on load
- Tools.svg.width.baseVal.value = document.body.clientWidth;
- Tools.svg.height.baseVal.value = document.body.clientHeight;
-
- /**
- What does a "tool" object look like?
- newtool = {
- "name" : "SuperTool",
- "listeners" : {
- "press" : function(x,y,evt){...},
- "move" : function(x,y,evt){...},
- "release" : function(x,y,evt){...},
- },
- "draw" : function(data, isLocal){
- //Print the data on Tools.svg
- },
- "onstart" : function(oldTool){...},
- "onquit" : function(newTool){...},
- "stylesheet" : "style.css",
- }
- */
|