diff --git a/ofmeet/changelog.html b/ofmeet/changelog.html
index 881d39a7..0b30b4c6 100644
--- a/ofmeet/changelog.html
+++ b/ofmeet/changelog.html
@@ -51,20 +51,21 @@
1.0.2 -- October 31, 2020
1.0.1 -- October 6, 2020
1.0.00 -- August 31, 2020
@@ -72,7 +73,7 @@
diff --git a/ofmeet/classes/jitsi-meet/collab.js b/ofmeet/classes/jitsi-meet/collab.js
new file mode 100644
index 00000000..e34d4c75
--- /dev/null
+++ b/ofmeet/classes/jitsi-meet/collab.js
@@ -0,0 +1,863 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var collab = (function(coop)
+{
+ var FOREGROUND_COLORS = ["#111", "#eee"];
+ var CURSOR_HEIGHT = 50;
+ var CURSOR_ANGLE = (35 / 180) * Math.PI;
+ var CURSOR_WIDTH = Math.ceil(Math.sin(CURSOR_ANGLE) * CURSOR_HEIGHT);
+ var CLICK_TRANSITION_TIME = 3000;
+ var enableCursor = true;
+ var displayMouseClick = true;
+
+ var session = {
+
+ send: function(msg)
+ {
+ //console.log("session send", msg);
+ window.top.postMessage({ event: 'ofmeet.event.url.message', msg: msg}, '*');
+ }
+ };
+
+ var eventMaker = {};
+
+ eventMaker.performClick = function (target) {
+ // FIXME: should accept other parameters, like Ctrl/Alt/etc
+ var event = document.createEvent("MouseEvents");
+ event.initMouseEvent(
+ "click", // type
+ true, // canBubble
+ true, // cancelable
+ window, // view
+ 0, // detail
+ 0, // screenX
+ 0, // screenY
+ 0, // clientX
+ 0, // clientY
+ false, // ctrlKey
+ false, // altKey
+ false, // shiftKey
+ false, // metaKey
+ 0, // button
+ null // relatedTarget
+ );
+ // FIXME: I'm not sure this custom attribute always propagates?
+ // seems okay in Firefox/Chrome, but I've had problems with
+ // setting attributes on keyboard events in the past.
+ event.togetherjsInternal = true;
+ target = $(target)[0];
+ var cancelled = target.dispatchEvent(event);
+ if (cancelled) {
+ return;
+ }
+ if (target.tagName == "A") {
+ var href = target.href;
+ if (href) {
+ location.href = href;
+ return;
+ }
+ }
+ // FIXME: should do button clicks (like a form submit)
+ // FIXME: should run .onclick() as well
+ };
+
+ eventMaker.fireChange = function (target) {
+ target = $(target)[0];
+ var event = document.createEvent("HTMLEvents");
+ event.initEvent("change", true, true);
+ target.dispatchEvent(event);
+ };
+
+
+ var elementFinder = {};
+
+ elementFinder.ignoreElement = function ignoreElement(el)
+ {
+ if (el instanceof $) {
+ el = el[0];
+ }
+ while (el) {
+ if ($(el).hasClass("togetherjs")) {
+ return true;
+ }
+ el = el.parentNode;
+ }
+ return false;
+ };
+
+ elementFinder.elementLocation = function elementLocation(el) {
+ if (el instanceof $) {
+ // a jQuery element
+ el = el[0];
+ }
+ if (el[0] && el.attr && el[0].nodeType == 1) {
+ // Or a jQuery element not made by us
+ el = el[0];
+ }
+ if (el.id) {
+ return "#" + el.id;
+ }
+ if (el.tagName == "BODY") {
+ return "body";
+ }
+ if (el.tagName == "HEAD") {
+ return "head";
+ }
+ if (el === document) {
+ return "document";
+ }
+ if (el === document.body) {
+ return "body";
+ }
+ var parent = el.parentNode;
+ if ((! parent) || parent == el) {
+ console.warn("elementLocation(", el, ") has null parent");
+ throw new Error("No locatable parent found");
+ }
+ var parentLocation = elementLocation(parent);
+ var children = parent.childNodes;
+ var _len = children.length;
+ var index = 0;
+ for (var i=0; i<_len; i++) {
+ if (children[i] == el) {
+ break;
+ }
+ if (children[i].nodeType == document.ELEMENT_NODE) {
+ if ( children[i].className && typeof children[i].className.indexOf === 'function' && children[i].className.indexOf("togetherjs") != -1) {
+ // Don't count our UI
+ continue;
+ }
+ // Don't count text or comments
+ index++;
+ }
+ }
+ return parentLocation + ":nth-child(" + (index+1) + ")";
+ };
+
+ elementFinder.CannotFind = {
+ constructor: function CannotFind(location, reason, context) {
+ this.prefix = "";
+ this.location = location;
+ this.reason = reason;
+ this.context = context;
+ },
+ toString: function () {
+ var loc;
+ try {
+ loc = elementFinder.elementLocation(this.context);
+ } catch (e) {
+ loc = this.context;
+ }
+ return (
+ "[CannotFind " + this.prefix +
+ "(" + this.location + "): " +
+ this.reason + " in " +
+ loc + "]");
+ }
+ };
+
+ elementFinder.findElement = function findElement(loc, container) {
+ // FIXME: should this all just be done with document.querySelector()?
+ // But no! We can't ignore togetherjs elements with querySelector.
+ // But maybe! We *could* make togetherjs elements less obtrusive?
+ container = container || document;
+ var el, rest;
+ if (loc === "body") {
+ return document.body;
+ } else if (loc === "head") {
+ return document.head;
+ } else if (loc === "document") {
+ return document;
+ } else if (loc.indexOf("body") === 0) {
+ el = document.body;
+ try {
+ return findElement(loc.substr(("body").length), el);
+ } catch (e) {
+ //if (e instanceof elementFinder.CannotFind) {
+ // e.prefix = "body" + e.prefix;
+ //}
+ //throw e;
+ }
+ } else if (loc.indexOf("head") === 0) {
+ el = document.head;
+ try {
+ return findElement(loc.substr(("head").length), el);
+ } catch (e) {
+ if (e instanceof elementFinder.CannotFind) {
+ e.prefix = "head" + e.prefix;
+ }
+ throw e;
+ }
+ } else if (loc.indexOf("#") === 0) {
+ var id;
+ loc = loc.substr(1);
+ if (loc.indexOf(":") === -1) {
+ id = loc;
+ rest = "";
+ } else {
+ id = loc.substr(0, loc.indexOf(":"));
+ rest = loc.substr(loc.indexOf(":"));
+ }
+ el = document.getElementById(id);
+ if (! el) {
+ // BAO
+ //throw elementFinder.CannotFind("#" + id, "No element by that id", container);
+ }
+ if (rest) {
+ try {
+ return findElement(rest, el);
+ } catch (e) {
+/*
+ if (e && e instanceof elementFinder.CannotFind) {
+ e.prefix = "#" + id + e.prefix;
+ }
+ throw e;
+*/
+ console.error("elementFinder.findElement", e);
+ }
+ } else {
+ return el;
+ }
+ } else if (loc.indexOf(":nth-child(") === 0) {
+ loc = loc.substr((":nth-child(").length);
+ if (loc.indexOf(")") == -1) {
+ throw "Invalid location, missing ): " + loc;
+ }
+ var num = loc.substr(0, loc.indexOf(")"));
+ num = parseInt(num, 10);
+ var count = num;
+ loc = loc.substr(loc.indexOf(")") + 1);
+ var children = container.childNodes;
+ el = null;
+ for (var i=0; i height) {
+ return false;
+ }
+ last = el;
+ });
+ if ((! children.length) || (! last)) {
+ // There are no children, or only inapplicable children
+ return {
+ location: elementFinder.elementLocation(start[0]),
+ offset: height - start.offset().top,
+ absoluteTop: height,
+ documentHeight: $(document).height()
+ };
+ }
+ return search(last, height);
+ }
+ return search($(document.body), height);
+ };
+
+ elementFinder.pixelForPosition = function (position) {
+ /* Inverse of elementFinder.elementByPixel */
+ if (position.location == "body") {
+ return position.offset;
+ }
+ var el;
+ try {
+ el = elementFinder.findElement(position.location);
+ } catch (e) {
+ if (e instanceof elementFinder.CannotFind && position.absoluteTop) {
+ // We don't trust absoluteTop to be quite right locally, so we adjust
+ // for the total document height differences:
+ var percent = position.absoluteTop / position.documentHeight;
+ return $(document).height() * percent;
+ }
+ throw e;
+ }
+ var top = $(el).offset().top;
+ // FIXME: maybe here we should test for sanity, like if an element is
+ // hidden. We can use position.absoluteTop to get a sense of where the
+ // element roughly should be. If the sanity check failed we'd use
+ // absoluteTop
+ return top + position.offset;
+ };
+
+ // Number of milliseconds after page load in which a scroll-update
+ // related hello-back message will be processed:
+
+ var SCROLL_UPDATE_CUTOFF = 2000;
+
+ // FIXME: should check for a peer leaving and remove the cursor object
+ var Cursor = util.Class({
+ constructor: function (clientId) {
+ this.clientId = clientId;
+ this.element = $('');
+ this.elementClass = "togetherjs-scrolled-normal";
+ this.element.addClass(this.elementClass);
+ //this.updatePeer(peers.getPeer(clientId));
+ this.lastTop = this.lastLeft = null;
+ $(document.body).append(this.element);
+ //this.element.animateCursorEntry();
+ this.keydownTimeout = null;
+ this.clearKeydown = this.clearKeydown.bind(this);
+ this.atOtherUrl = false;
+ this.color = Math.floor(Math.random() * 0xffffff).toString(16);
+ while (this.color.length < 6) {
+ this.color = "0" + this.color;
+ }
+ this.color = "#" + this.color;
+ },
+
+ // How long after receiving a setKeydown call that we should show the
+ // user typing. This should be more than MIN_KEYDOWN_TIME:
+ KEYDOWN_WAIT_TIME: 2000,
+
+ updatePeer: function (peer) {
+ // FIXME: can I use peer.setElement()?
+ this.element.css({color: this.color});
+ var img = this.element.find("img.togetherjs-cursor-img");
+ img.attr("src", makeCursor(this.color));
+ var name = this.element.find(".togetherjs-cursor-name");
+ var nameContainer = this.element.find(".togetherjs-cursor-container");
+ name.text(peer.name);
+ nameContainer.css({
+ backgroundColor: this.color,
+ color: tinycolor.mostReadable(this.color, FOREGROUND_COLORS)
+ });
+
+ var path = this.element.find("svg path");
+
+ path.attr("fill", this.color);
+
+ // FIXME: should I just remove the element?
+ if (peer.status != "live") {
+ this.element.hide();
+ this.element.find("svg").animate({
+ opacity: 0
+ }, 350);
+ this.element.find(".togetherjs-cursor-container").animate({
+ width: 34,
+ height: 20,
+ padding: 12,
+ margin: 0
+ }, 200).animate({
+ width: 0,
+ height: 0,
+ padding: 0,
+ opacity: 0
+ }, 200);
+ } else {
+ this.element.show();
+ this.element.animate({
+ opacity:0.3
+ }).animate({
+ opacity:1
+ });
+ }
+ },
+
+ setClass: function (name) {
+ if (name != this.elementClass) {
+ this.element.removeClass(this.elementClass).addClass(name);
+ this.elementClass = name;
+ }
+ },
+
+ updatePosition: function (pos) {
+ var top, left;
+ if (this.atOtherUrl) {
+ this.element.show();
+ this.atOtherUrl = false;
+ }
+ if (pos.element) {
+ var target = $(elementFinder.findElement(pos.element));
+ var offset = target.offset();
+ if (offset) {
+ top = offset.top + pos.offsetY;
+ left = offset.left + pos.offsetX;
+ }
+ } else {
+ // No anchor, just an absolute position
+ top = pos.top;
+ left = pos.left;
+ }
+ // These are saved for use by .refresh():
+ this.lastTop = top;
+ this.lastLeft = left;
+ this.setPosition(top, left);
+ },
+
+ hideOtherUrl: function () {
+ if (this.atOtherUrl) {
+ return;
+ }
+ this.atOtherUrl = true;
+ // FIXME: should show away status better:
+ this.element.hide();
+ },
+
+ // place Cursor rotate function down here FIXME: this doesnt do anything anymore. This is in the CSS as an animation
+ rotateCursorDown: function(){
+ var e = $(this.element).find('svg');
+ e.animate({borderSpacing: -150, opacity: 1}, {
+ step: function(now, fx) {
+ if (fx.prop == "borderSpacing") {
+ e.css('-webkit-transform', 'rotate('+now+'deg)')
+ .css('-moz-transform', 'rotate('+now+'deg)')
+ .css('-ms-transform', 'rotate('+now+'deg)')
+ .css('-o-transform', 'rotate('+now+'deg)')
+ .css('transform', 'rotate('+now+'deg)');
+ } else {
+ e.css(fx.prop, now);
+ }
+ },
+ duration: 500
+ }, 'linear').promise().then(function () {
+ e.css('-webkit-transform', '')
+ .css('-moz-transform', '')
+ .css('-ms-transform', '')
+ .css('-o-transform', '')
+ .css('transform', '')
+ .css("opacity", "");
+ });
+ },
+
+ setPosition: function (top, left) {
+ var wTop = $(window).scrollTop();
+ var height = $(window).height();
+
+ if (top < wTop) {
+ // FIXME: this is a totally arbitrary number, but is meant to be big enough
+ // to keep the cursor name from being off the top of the screen.
+ top = 25;
+ this.setClass("togetherjs-scrolled-above");
+ } else if (top > wTop + height - CURSOR_HEIGHT) {
+ top = height - CURSOR_HEIGHT - 5;
+ this.setClass("togetherjs-scrolled-below");
+ } else {
+ this.setClass("togetherjs-scrolled-normal");
+ }
+ this.element.css({
+ top: top,
+ left: left
+ });
+ },
+
+ refresh: function () {
+ if (this.lastTop !== null) {
+ this.setPosition(this.lastTop, this.lastLeft);
+ }
+ },
+
+ setKeydown: function () {
+ if (this.keydownTimeout) {
+ clearTimeout(this.keydownTimeout);
+ } else {
+ this.element.find(".togetherjs-cursor-typing").show().animateKeyboard();
+ }
+ this.keydownTimeout = setTimeout(this.clearKeydown, this.KEYDOWN_WAIT_TIME);
+ },
+
+ clearKeydown: function () {
+ this.keydownTimeout = null;
+ this.element.find(".togetherjs-cursor-typing").hide().stopKeyboardAnimation();
+ },
+
+ _destroy: function () {
+ this.element.remove();
+ this.element = null;
+ }
+ });
+
+ Cursor._cursors = {};
+
+ Cursor.getClient = function (clientId) {
+ var c = Cursor._cursors[clientId];
+ if (! c) {
+ c = Cursor._cursors[clientId] = Cursor(clientId);
+ }
+ return c;
+ };
+
+ Cursor.forEach = function (callback, context) {
+ context = context || null;
+ for (var a in Cursor._cursors) {
+ if (Cursor._cursors.hasOwnProperty(a)) {
+ callback.call(context, Cursor._cursors[a], a);
+ }
+ }
+ };
+
+ Cursor.destroy = function (clientId) {
+ if (Cursor._cursors[clientId])
+ {
+ Cursor._cursors[clientId]._destroy();
+ delete Cursor._cursors[clientId];
+ }
+ };
+
+/*
+ peers.on("new-peer identity-updated status-updated", function (peer) {
+ var c = Cursor.getClient(peer.id);
+ c.updatePeer(peer);
+ });
+*/
+
+ var lastTime = 0;
+ var MIN_TIME = 100;
+ var lastPosX = -1;
+ var lastPosY = -1;
+ var lastMessage = null;
+
+ function mouseMove(event)
+ {
+ if (!enableCursor) return;
+
+ var now = Date.now();
+ if (now - lastTime < MIN_TIME) {
+ return;
+ }
+ lastTime = now;
+ var pageX = event.pageX;
+ var pageY = event.pageY;
+ if (Math.abs(lastPosX - pageX) < 3 && Math.abs(lastPosY - pageY) < 3) {
+ // Not a substantial enough change
+ return;
+ }
+ lastPosX = pageX;
+ lastPosY = pageY;
+ var target = event.target;
+
+ if (elementFinder.ignoreElement(target)) {
+ target = null;
+ }
+ if ((! target) || target == document.documentElement || target == document.body) {
+ lastMessage = {
+ type: "cursor-update",
+ top: pageY,
+ left: pageX
+ };
+ session.send(lastMessage);
+ return;
+ }
+ target = $(target);
+ var offset = target.offset();
+ if (! offset) {
+ // FIXME: this really is walkabout.js's problem to fire events on the
+ // document instead of a specific element
+ console.warn("Could not get offset of element:", target[0]);
+ return;
+ }
+ var offsetX = pageX - offset.left;
+ var offsetY = pageY - offset.top;
+ lastMessage = {
+ type: "cursor-update",
+ element: elementFinder.elementLocation(target),
+ offsetX: Math.floor(offsetX),
+ offsetY: Math.floor(offsetY)
+ };
+ session.send(lastMessage);
+ }
+
+
+
+ function makeCursor(color)
+ {
+ var canvas = $("");
+ canvas.attr("height", CURSOR_HEIGHT);
+ canvas.attr("width", CURSOR_WIDTH);
+ var context = canvas[0].getContext('2d');
+ context.fillStyle = color;
+ context.moveTo(0, 0);
+ context.beginPath();
+ context.lineTo(0, CURSOR_HEIGHT/1.2);
+ context.lineTo(Math.sin(CURSOR_ANGLE/2) * CURSOR_HEIGHT / 1.5,
+ Math.cos(CURSOR_ANGLE/2) * CURSOR_HEIGHT / 1.5);
+ context.lineTo(Math.sin(CURSOR_ANGLE) * CURSOR_HEIGHT / 1.2,
+ Math.cos(CURSOR_ANGLE) * CURSOR_HEIGHT / 1.2);
+ context.lineTo(0, 0);
+ context.shadowColor = 'rgba(0,0,0,0.3)';
+ context.shadowBlur = 2;
+ context.shadowOffsetX = 1;
+ context.shadowOffsetY = 2;
+ context.strokeStyle = "#ffffff";
+ context.stroke();
+ context.fill();
+ return canvas[0].toDataURL("image/png");
+ }
+
+ var scrollTimeout = null;
+ var scrollTimeoutSet = 0;
+ var SCROLL_DELAY_TIMEOUT = 75;
+ var SCROLL_DELAY_LIMIT = 300;
+
+ function scroll() {
+ var now = Date.now();
+ if (scrollTimeout) {
+ if (now - scrollTimeoutSet < SCROLL_DELAY_LIMIT) {
+ clearTimeout(scrollTimeout);
+ } else {
+ // Just let it progress anyway
+ return;
+ }
+ }
+ scrollTimeout = setTimeout(_scrollRefresh, SCROLL_DELAY_TIMEOUT);
+ if (! scrollTimeoutSet) {
+ scrollTimeoutSet = now;
+ }
+ }
+
+ var lastScrollMessage = null;
+
+ function _scrollRefresh() {
+ scrollTimeout = null;
+ scrollTimeoutSet = 0;
+ Cursor.forEach(function (c) {
+ c.refresh();
+ });
+ lastScrollMessage = {
+ type: "scroll-update",
+ position: elementFinder.elementByPixel($(window).scrollTop())
+ };
+ session.send(lastScrollMessage);
+ }
+
+
+ function documentClick(event) {
+
+ if (event.togetherjsInternal) {
+ // This is an artificial internal event
+ return;
+ }
+
+ if (!enableCursor) return;
+
+ // FIXME: this might just be my imagination, but somehow I just
+ // really don't want to do anything at this stage of the event
+ // handling (since I'm catching every click), and I'll just do
+ // something real soon:
+ setTimeout(function ()
+ {
+ var element = event.target;
+ if (element == document.documentElement) {
+ // For some reason clicking on gives the element here
+ element = document.body;
+ }
+ if (elementFinder.ignoreElement(element)) {
+ return;
+ }
+ //Prevent click events on video objects to avoid conflicts with
+ //togetherjs's own video events
+ //if (element.nodeName.toLowerCase() === 'video'){
+ // return;
+ //}
+
+ var location = elementFinder.elementLocation(element);
+ var offset = $(element).offset();
+ var offsetX = event.pageX - offset.left;
+ var offsetY = event.pageY - offset.top;
+
+ session.send({
+ type: "cursor-click",
+ element: location,
+ offsetX: offsetX,
+ offsetY: offsetY,
+ top: offset.top,
+ left: offset.left
+ });
+
+ if (displayMouseClick) displayClick({top: event.pageY, left: event.pageX}, 'red');
+ });
+ }
+
+
+ function displayClick(pos, color) {
+ if (!enableCursor) return;
+ //console.log("displayClick", pos, color);
+ // FIXME: should we hide the local click if no one else is going to see it?
+ // That means tracking who might be able to see our screen.
+ var element = $('');
+ $(document.body).append(element);
+
+ element.css({
+ top: pos.top,
+ left: pos.left,
+ borderColor: color
+ });
+ setTimeout(function () {
+ element.addClass("togetherjs-clicking");
+ }, 100);
+ setTimeout(function () {
+ element.remove();
+ }, CLICK_TRANSITION_TIME);
+ }
+
+ var lastKeydown = 0;
+ var MIN_KEYDOWN_TIME = 500;
+
+ function documentKeydown(event) {
+ if (!enableCursor) return;
+
+ setTimeout(function () {
+ var now = Date.now();
+ if (now - lastKeydown < MIN_KEYDOWN_TIME) {
+ return;
+ }
+ lastKeydown = now;
+ // FIXME: is event.target interesting here? That is, *what* the
+ // user is typing into, not just that the user is typing? Also
+ // I'm assuming we don't care if the user it typing into a
+ // togetherjs-related field, since chat activity is as interesting
+ // as any other activity.
+ session.send({type: "keydown"});
+ });
+ }
+
+
+ var handleCursorClick = function (pos, peer)
+ {
+ if (!pos || !peer) return;
+ if (!enableCursor) return;
+
+ // When the click is calculated isn't always the same as how the
+ // last cursor update was calculated, so we force the cursor to
+ // the last location during a click:
+
+ peer.updatePosition(pos);
+
+ if (!pos) return;
+
+ var topPos = pos.top + pos.offsetY;
+ var leftPos = pos.left + pos.offsetX;
+
+ if (pos.element) {
+ var target = $(elementFinder.findElement(pos.element));
+ var offset = target.offset();
+
+ if (offset)
+ {
+ topPos = offset.top + pos.offsetY;
+ leftPos = offset.left + pos.offsetX;
+
+ // TODO support remote click actions later
+ //if (target) eventMaker.performClick(target);
+ }
+ }
+ displayClick({top: topPos, left: leftPos}, 'red');
+ };
+
+ handleAppMessage = function(obj, from)
+ {
+ //try {
+ //console.log("remote handleAppMessage", obj, from);
+
+ p = Cursor.getClient(from);
+
+ if (p)
+ {
+ p.updatePeer({id: from, name: from, status: "live"});
+
+ if (obj.type == "cursor-update") p.updatePosition(obj);
+ if (obj.type == "cursor-click") handleCursorClick(obj, p);
+ }
+
+ //} catch (e) {console.error(e)}
+ }
+
+
+ window.addEventListener("unload", function ()
+ {
+ Cursor.forEach(function (c, clientId) {
+ Cursor.destroy(clientId);
+ });
+
+ document.removeEventListener("mousemove", mouseMove, true);
+ document.removeEventListener("click", documentClick, true);
+ document.removeEventListener("keydown", documentKeydown, true);
+
+ $(window).unbind("scroll", scroll);
+ });
+
+ window.addEventListener('message', function (event)
+ {
+ //console.log("remote action", event.data);
+
+ if (!event.data) return;
+
+ if (event.data.action == 'ofmeet.action.share')
+ {
+ if (event.data.json.msg)
+ {
+ handleAppMessage(event.data.json.msg, event.data.json.from);
+ }
+ else
+
+ if (event.data.json.action == 'setup')
+ {
+ document.addEventListener("mousemove", mouseMove, true);
+ document.addEventListener("click", documentClick, true);
+ // TODO handle collaboration later
+ //document.addEventListener("keydown", documentKeydown, true);
+ }
+ else
+
+ if (event.data.json.action == 'destroy')
+ {
+ Cursor.destroy(event.data.json.from);
+ document.removeEventListener("mousemove", mouseMove, true);
+ document.removeEventListener("click", documentClick, true);
+ // TODO handle collaboration later
+ //document.removeEventListener("keydown", documentKeydown, true);
+ }
+ }
+ });
+
+ $(window).scroll(scroll);
+ scroll();
+
+ return coop;
+
+}(collab || {}));
\ No newline at end of file
diff --git a/ofmeet/classes/jitsi-meet/cursor.css b/ofmeet/classes/jitsi-meet/cursor.css
new file mode 100644
index 00000000..bfce2b2f
--- /dev/null
+++ b/ofmeet/classes/jitsi-meet/cursor.css
@@ -0,0 +1,121 @@
+
+.togetherjs * {
+ -webkit-box-sizing: content-box !important;
+ -moz-box-sizing: content-box !important;
+ box-sizing: content-box !important;
+}
+
+.togetherjs-cursor svg {
+ -webkit-filter: drop-shadow(1px 3px 2px rgba(0, 0, 0, 0.3));
+ -webkit-transform: rotate(-10deg);
+}
+.togetherjs-cursor-img {
+ position: relative;
+ top: 0;
+}
+.togetherjs-cursor img {
+ width: 20px;
+ -webkit-filter: drop-shadow(0px 2px 1px rgba(0, 0, 0, 0.2));
+ /*FIX ME, moz filter not working...*/
+
+ -moz-filter: drop-shadow(0px 2px 1px rgba(0, 0, 0, 0.2));
+ filter: drop-shadow(0px 2px 1px rgba(0, 0, 0, 0.2));
+}
+.togetherjs-cursor {
+ position: absolute;
+ z-index: 999999;
+ font-size: 28px;
+ font-weight: bolder;
+ /* This magic CSS rule makes this element basically invisible to clicks/etc:
+ (good on all but IE: http://caniuse.com/pointer-events */
+
+ pointer-events: none;
+ /*FIXME: maybe these should use position: fixed so the cursor
+ stays stuck to the top of the screen until the appropriate time
+ (when .togetherjs-scrolled-above/below is removed)?*/
+
+}
+.togetherjs-cursor:hover {
+ cursor: pointer;
+}
+.togetherjs-cursor.togetherjs-scrolled-above {
+ position: fixed;
+}
+.togetherjs-cursor.togetherjs-scrolled-above svg {
+ -webkit-transition-duration: 0.8s;
+ -webkit-transition-property: -webkit-transform;
+ -webkit-transform: rotate(20deg);
+ transition-duration: 0.8s;
+ transition-property: transform;
+ transform: rotate(20deg);
+}
+.togetherjs-cursor.togetherjs-scrolled-above .togetherjs-cursor-down {
+ display: none;
+}
+.togetherjs-cursor.togetherjs-scrolled-below {
+ position: fixed;
+}
+.togetherjs-cursor.togetherjs-scrolled-below svg {
+ -webkit-transition-duration: 0.8s;
+ -webkit-transition-property: -webkit-transform;
+ -webkit-transform: rotate(-150deg);
+ transition-duration: 0.8s;
+ transition-property: transform;
+ transform: rotate(-150deg);
+}
+.togetherjs-cursor.togetherjs-scrolled-below .togetherjs-cursor-up {
+ display: none;
+}
+.togetherjs-cursor.togetherjs-scrolled-normal svg {
+ -webkit-transition-duration: 0.8s;
+ -webkit-transition-property: -webkit-transform;
+ -webkit-transform: rotate(-10deg);
+ transition-duration: 0.8s;
+ transition-property: transform;
+ transform: rotate(-10deg);
+}
+.togetherjs-cursor.togetherjs-scrolled-normal .togetherjs-cursor-up,
+.togetherjs-cursor.togetherjs-scrolled-normal .togetherjs-cursor-down {
+ display: none;
+}
+.togetherjs-cursor .togetherjs-cursor-container {
+ opacity: 0.9;
+ white-space: nowrap;
+ font-family: openSansLight, Helvetica, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 40%;
+ position: relative;
+ top: 5px;
+ left: 15px;
+ padding: 8px;
+ border-radius: 4px;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
+ -webkit-box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
+ box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
+}
+
+.togetherjs-click {
+ position: absolute;
+ z-index: 999998;
+ pointer-events: none;
+ width: 10px;
+ height: 10px;
+ margin: -5px 0 0 -5px;
+ border-radius: 5px;
+ border: 3px solid #ff3a29;
+ /* Note, you must call this like:
+ .transition(~"value, value, value")*/
+
+ transition: width 2s, height 2s, margin 2s, border 2s;
+ -moz-transition: width 2s, height 2s, margin 2s, border 2s;
+ -webkit-transition: width 2s, height 2s, margin 2s, border 2s;
+ -o-transition: width 2s, height 2s, margin 2s, border 2s;
+}
+.togetherjs-click.togetherjs-clicking {
+ width: 40px;
+ height: 40px;
+ margin: -20px 0 0 -20px;
+ border-radius: 20px;
+ border: 3px solid rgba(0, 0, 0, 0);
+}
diff --git a/ofmeet/classes/jitsi-meet/custom_ofmeet.js b/ofmeet/classes/jitsi-meet/custom_ofmeet.js
index c72a7e90..276d77f2 100644
--- a/ofmeet/classes/jitsi-meet/custom_ofmeet.js
+++ b/ofmeet/classes/jitsi-meet/custom_ofmeet.js
@@ -14,13 +14,14 @@ var ofmeet = (function(of)
IMAGES.poll = '';
IMAGES.kanban = '';
IMAGES.whiteboard = '';
- IMAGES.person = ''
- IMAGES.contact = '';
- IMAGES.mic = '';
- IMAGES.mail = '';
- IMAGES.desktop = '';
+ IMAGES.person = ''
+ IMAGES.contact = '';
+ IMAGES.mic = '';
+ IMAGES.mail = '';
+ IMAGES.desktop = '';
IMAGES.confetti = '';
- IMAGES.picture = '';
+ IMAGES.picture = '';
+ IMAGES.cursor = '';
const SMILIES = [":)", ":(", ":D", ":+1:", ":P", ":wave:", ":blush:", ":slightly_smiling_face:", ":scream:", ":*", ":-1:", ":mag:", ":heart:", ":innocent:", ":angry:", ":angel:", ";(", ":clap:", ";)", ":beer:"];
const nickColors = {}, padsList = [], captions = {msgsDisabled: true, msgs: []}, breakout = {rooms: [], duration: 60, roomCount: 10, wait: 10}, pdf_body = [];
@@ -30,7 +31,7 @@ var ofmeet = (function(of)
let padsModalOpened = false, contactsModalOpened = false, swRegistration = null, participants = {}, recordingAudioTrack = {}, recordingVideoTrack = {}, videoRecorder = {}, recorderStreams = {}, customStore = {}, filenames = {}, dbnames = [];
let clockTrack = {start: 0, stop: 0, joins: 0, leaves: 0}, handsRaised = 0;
let tags = {location: "", date: (new Date()).toISOString().split('T')[0], subject: "", host: "", activity: ""};
- let audioTemporaryUnmuted = false;
+ let audioTemporaryUnmuted = false, cursorShared = false;
//-------------------------------------------------------
//
// window events
@@ -1577,7 +1578,7 @@ var ofmeet = (function(of)
function handlePresence(presence)
{
- console.debug("handlePresence", presence);
+ //console.debug("handlePresence", presence);
const Strophe = APP.connection.xmpp.connection.Strophe;
const id = Strophe.getResourceFromJid(presence.getAttribute("from"));
const raisedHand = presence.querySelector("jitsi_participant_raisedHand");
@@ -2148,42 +2149,79 @@ var ofmeet = (function(of)
console.debug("postJoinSetup");
- if (interfaceConfig.OFMEET_ENABLE_CRYPTPAD)
+ if (interfaceConfig.OFMEET_ENABLE_CRYPTPAD || interfaceConfig.OFMEET_ENABLE_WHITEBOARD || interfaceConfig.OFMEET_ENABLE_CONFETTI)
{
- createPadsButton();
- }
-
- if (interfaceConfig.OFMEET_ENABLE_WHITEBOARD && interfaceConfig.OFMEET_WHITEBOARD_URL && interfaceConfig.OFMEET_WHITEBOARD_URL != "")
- {
- APP.conference.commands.addCommandListener("WHITEBOARD", function()
+ APP.conference.commands.addCommandListener("CURSOR", function(event)
{
- const url = interfaceConfig.OFMEET_WHITEBOARD_URL.endsWith("/") ? interfaceConfig.OFMEET_WHITEBOARD_URL + APP.conference.roomName : interfaceConfig.OFMEET_WHITEBOARD_URL + "/" + APP.conference.roomName;
- window.open(url, 'ofmeet-whiteboard');
+ //console.log("CURSOR remote event", event);
+ window.top.postMessage({ action: 'ofmeet.action.share', json: JSON.parse(event.value)}, '*');
});
- const whiteboardButton = addToolbarItem('ofmeet-whiteboard', '', "Share a whiteboard", ".button-group-right");
+ const cursorButton = addToolbarItem('ofmeet-cursor', '', "Share cursor/mouse pointer", ".button-group-right");
- if (whiteboardButton) whiteboardButton.addEventListener("click", function(evt)
+ if (cursorButton) cursorButton.addEventListener("click", function(evt)
{
evt.stopPropagation();
- APP.conference.commands.sendCommandOnce("WHITEBOARD", {value: !0})
+
+ if (!cursorShared) {
+ handleCursorEvent({data: {event: 'ofmeet.event.url.message', action: 'setup', from: APP.conference.getLocalDisplayName()}});
+ window.addEventListener('message', handleCursorEvent);
+ } else {
+ handleCursorEvent({data: {event: 'ofmeet.event.url.message', action: 'destroy', from: APP.conference.getLocalDisplayName()}});
+ window.removeEventListener('message', handleCursorEvent);
+ }
+ cursorShared = !cursorShared;
});
- }
- if (interfaceConfig.OFMEET_ENABLE_CONFETTI)
- {
- APP.conference.commands.addCommandListener("CONFETTI", function()
+ if (interfaceConfig.OFMEET_ENABLE_CRYPTPAD)
{
- window.confetti({particleCount: 100, spread: 70, origin: {y: .6}})
- });
+ createPadsButton();
+ }
+
+ if (interfaceConfig.OFMEET_ENABLE_WHITEBOARD && interfaceConfig.OFMEET_WHITEBOARD_URL && interfaceConfig.OFMEET_WHITEBOARD_URL != "")
+ {
+ APP.conference.commands.addCommandListener("WHITEBOARD", function()
+ {
+ const url = interfaceConfig.OFMEET_WHITEBOARD_URL.endsWith("/") ? interfaceConfig.OFMEET_WHITEBOARD_URL + APP.conference.roomName : interfaceConfig.OFMEET_WHITEBOARD_URL + "/" + APP.conference.roomName;
+ window.open(url, 'ofmeet-whiteboard');
+ });
- const confettiButton = addToolbarItem('ofmeet-confetti', '', "Share some confetti", ".button-group-right");
+ const whiteboardButton = addToolbarItem('ofmeet-whiteboard', '', "Share a whiteboard", ".button-group-right");
- if (confettiButton) confettiButton.addEventListener("click", function(evt)
+ if (whiteboardButton) whiteboardButton.addEventListener("click", function(evt)
+ {
+ evt.stopPropagation();
+ APP.conference.commands.sendCommandOnce("WHITEBOARD", {value: !0})
+ });
+ }
+
+ if (interfaceConfig.OFMEET_ENABLE_CONFETTI)
{
- evt.stopPropagation();
- APP.conference.commands.sendCommandOnce("CONFETTI", {value: !0})
- });
+ APP.conference.commands.addCommandListener("CONFETTI", function()
+ {
+ window.confetti({particleCount: 100, spread: 70, origin: {y: .6}})
+ });
+
+ const confettiButton = addToolbarItem('ofmeet-confetti', '', "Share some confetti", ".button-group-right");
+
+ if (confettiButton) confettiButton.addEventListener("click", function(evt)
+ {
+ evt.stopPropagation();
+ APP.conference.commands.sendCommandOnce("CONFETTI", {value: !0})
+ });
+ }
+ }
+ }
+
+ function handleCursorEvent(event)
+ {
+ //console.log("handleCursorEvent", event.data);
+
+ if (event.data.event == 'ofmeet.event.url.message')
+ {
+ event.data.from = APP.conference.getLocalDisplayName();
+ const value = JSON.stringify(event.data);
+ APP.conference.commands.sendCommandOnce("CURSOR", {value: value});
}
}
diff --git a/ofmeet/classes/jitsi-meet/index.html b/ofmeet/classes/jitsi-meet/index.html
index fdf1f075..a5df570a 100644
--- a/ofmeet/classes/jitsi-meet/index.html
+++ b/ofmeet/classes/jitsi-meet/index.html
@@ -9,6 +9,7 @@
+
+
+
+
diff --git a/ofmeet/classes/jitsi-meet/libs/lib-jitsi-meet.min.js b/ofmeet/classes/jitsi-meet/libs/lib-jitsi-meet.min.js
index e9bd61ba..11fc46d1 100644
--- a/ofmeet/classes/jitsi-meet/libs/lib-jitsi-meet.min.js
+++ b/ofmeet/classes/jitsi-meet/libs/lib-jitsi-meet.min.js
@@ -13218,7 +13218,8 @@
this.focusMucJid = e, f.info(`Ignore focus: ${e}, real JID: ${t}`), this.xmpp.caps.getFeatures(t, 15e3).then(e => {
this.focusFeatures = e, f.info("Jicofo supports restart by terminate: " + this.supportsRestartByTerminate())
}, e => {
- f.error("Failed to discover Jicofo features", e && e.message)
+ // BAO
+ // f.error("Failed to discover Jicofo features", e && e.message)
})
}
setParticipantPropertyListener(e) {
@@ -17331,6 +17332,7 @@
this.pingInterval && window.clearInterval(this.pingInterval), this.analyticsInterval && window.clearInterval(this.analyticsInterval)
}
sendRequest() {
+/* BAO
const e = this.lastRequestId++,
t = {
type: "e2e-ping-request",
@@ -17340,6 +17342,7 @@
id: e,
timeSent: window.performance.now()
}
+*/
}
handleResponse(e) {
const t = this.requests[e.id];
diff --git a/ofmeet/classes/jitsi-meet/tinycolor.js b/ofmeet/classes/jitsi-meet/tinycolor.js
new file mode 100644
index 00000000..c1f5991b
--- /dev/null
+++ b/ofmeet/classes/jitsi-meet/tinycolor.js
@@ -0,0 +1,869 @@
+// TinyColor v0.9.13
+// https://github.com/bgrins/TinyColor
+// 2012-11-28, Brian Grinstead, MIT License
+
+(function(root) {
+
+var trimLeft = /^[\s,#]+/,
+ trimRight = /\s+$/,
+ tinyCounter = 0,
+ math = Math,
+ mathRound = math.round,
+ mathMin = math.min,
+ mathMax = math.max,
+ mathRandom = math.random;
+
+function tinycolor (color, opts) {
+
+ color = (color) ? color : '';
+
+ // If input is already a tinycolor, return itself
+ if (typeof color == "object" && color.hasOwnProperty("_tc_id")) {
+ return color;
+ }
+
+ var rgb = inputToRGB(color);
+ var r = rgb.r,
+ g = rgb.g,
+ b = rgb.b,
+ a = rgb.a,
+ roundA = mathRound(100*a) / 100,
+ format = rgb.format;
+
+ // Don't let the range of [0,255] come back in [0,1].
+ // Potentially lose a little bit of precision here, but will fix issues where
+ // .5 gets interpreted as half of the total, instead of half of 1
+ // If it was supposed to be 128, this was already taken care of by `inputToRgb`
+ if (r < 1) { r = mathRound(r); }
+ if (g < 1) { g = mathRound(g); }
+ if (b < 1) { b = mathRound(b); }
+
+ return {
+ ok: rgb.ok,
+ format: format,
+ _tc_id: tinyCounter++,
+ alpha: a,
+ toHsv: function() {
+ var hsv = rgbToHsv(r, g, b);
+ return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: a };
+ },
+ toHsvString: function() {
+ var hsv = rgbToHsv(r, g, b);
+ var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
+ return (a == 1) ?
+ "hsv(" + h + ", " + s + "%, " + v + "%)" :
+ "hsva(" + h + ", " + s + "%, " + v + "%, "+ roundA + ")";
+ },
+ toHsl: function() {
+ var hsl = rgbToHsl(r, g, b);
+ return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: a };
+ },
+ toHslString: function() {
+ var hsl = rgbToHsl(r, g, b);
+ var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
+ return (a == 1) ?
+ "hsl(" + h + ", " + s + "%, " + l + "%)" :
+ "hsla(" + h + ", " + s + "%, " + l + "%, "+ roundA + ")";
+ },
+ toHex: function() {
+ return rgbToHex(r, g, b);
+ },
+ toHexString: function() {
+ return '#' + rgbToHex(r, g, b);
+ },
+ toRgb: function() {
+ return { r: mathRound(r), g: mathRound(g), b: mathRound(b), a: a };
+ },
+ toRgbString: function() {
+ return (a == 1) ?
+ "rgb(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ")" :
+ "rgba(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ", " + roundA + ")";
+ },
+ toPercentageRgb: function() {
+ return { r: mathRound(bound01(r, 255) * 100) + "%", g: mathRound(bound01(g, 255) * 100) + "%", b: mathRound(bound01(b, 255) * 100) + "%", a: a };
+ },
+ toPercentageRgbString: function() {
+ return (a == 1) ?
+ "rgb(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%)" :
+ "rgba(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%, " + roundA + ")";
+ },
+ toName: function() {
+ return hexNames[rgbToHex(r, g, b)] || false;
+ },
+ toFilter: function() {
+ var hex = rgbToHex(r, g, b);
+ var secondHex = hex;
+ var alphaHex = Math.round(parseFloat(a) * 255).toString(16);
+ var secondAlphaHex = alphaHex;
+ var gradientType = opts && opts.gradientType ? "GradientType = 1, " : "";
+
+ if (secondColor) {
+ var s = tinycolor(secondColor);
+ secondHex = s.toHex();
+ secondAlphaHex = Math.round(parseFloat(s.alpha) * 255).toString(16);
+ }
+
+ return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr=#" + pad2(alphaHex) + hex + ",endColorstr=#" + pad2(secondAlphaHex) + secondHex + ")";
+ },
+ toString: function(format) {
+ format = format || this.format;
+ var formattedString = false;
+ if (format === "rgb") {
+ formattedString = this.toRgbString();
+ }
+ if (format === "prgb") {
+ formattedString = this.toPercentageRgbString();
+ }
+ if (format === "hex") {
+ formattedString = this.toHexString();
+ }
+ if (format === "name") {
+ formattedString = this.toName();
+ }
+ if (format === "hsl") {
+ formattedString = this.toHslString();
+ }
+ if (format === "hsv") {
+ formattedString = this.toHsvString();
+ }
+
+ return formattedString || this.toHexString();
+ }
+ };
+}
+
+// If input is an object, force 1 into "1.0" to handle ratios properly
+// String input requires "1.0" as input, so 1 will be treated as 1
+tinycolor.fromRatio = function(color) {
+ if (typeof color == "object") {
+ var newColor = {};
+ for (var i in color) {
+ newColor[i] = convertToPercentage(color[i]);
+ }
+ color = newColor;
+ }
+
+ return tinycolor(color);
+};
+
+// Given a string or object, convert that input to RGB
+// Possible string inputs:
+//
+// "red"
+// "#f00" or "f00"
+// "#ff0000" or "ff0000"
+// "rgb 255 0 0" or "rgb (255, 0, 0)"
+// "rgb 1.0 0 0" or "rgb (1, 0, 0)"
+// "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
+// "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
+// "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
+// "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
+// "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
+//
+function inputToRGB(color) {
+
+ var rgb = { r: 255, g: 255, b: 255 };
+ var a = 1;
+ var ok = false;
+ var format = false;
+
+ if (typeof color == "string") {
+ color = stringInputToObject(color);
+ }
+
+ if (typeof color == "object") {
+ if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
+ rgb = rgbToRgb(color.r, color.g, color.b);
+ ok = true;
+ format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
+ }
+ else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
+ color.s = convertToPercentage(color.s);
+ color.v = convertToPercentage(color.v);
+ rgb = hsvToRgb(color.h, color.s, color.v);
+ ok = true;
+ format = "hsv";
+ }
+ else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
+ color.s = convertToPercentage(color.s);
+ color.l = convertToPercentage(color.l);
+ rgb = hslToRgb(color.h, color.s, color.l);
+ ok = true;
+ format = "hsl";
+ }
+
+ if (color.hasOwnProperty("a")) {
+ a = color.a;
+ }
+ }
+
+ a = parseFloat(a);
+
+ // Handle invalid alpha characters by setting to 1
+ if (isNaN(a) || a < 0 || a > 1) {
+ a = 1;
+ }
+
+ return {
+ ok: ok,
+ format: color.format || format,
+ r: mathMin(255, mathMax(rgb.r, 0)),
+ g: mathMin(255, mathMax(rgb.g, 0)),
+ b: mathMin(255, mathMax(rgb.b, 0)),
+ a: a
+ };
+}
+
+
+
+// Conversion Functions
+// --------------------
+
+// `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
+//
+
+// `rgbToRgb`
+// Handle bounds / percentage checking to conform to CSS color spec
+//
+// *Assumes:* r, g, b in [0, 255] or [0, 1]
+// *Returns:* { r, g, b } in [0, 255]
+function rgbToRgb(r, g, b){
+ return {
+ r: bound01(r, 255) * 255,
+ g: bound01(g, 255) * 255,
+ b: bound01(b, 255) * 255
+ };
+}
+
+// `rgbToHsl`
+// Converts an RGB color value to HSL.
+// *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
+// *Returns:* { h, s, l } in [0,1]
+function rgbToHsl(r, g, b) {
+
+ r = bound01(r, 255);
+ g = bound01(g, 255);
+ b = bound01(b, 255);
+
+ var max = mathMax(r, g, b), min = mathMin(r, g, b);
+ var h, s, l = (max + min) / 2;
+
+ if(max == min) {
+ h = s = 0; // achromatic
+ }
+ else {
+ var d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch(max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+
+ h /= 6;
+ }
+
+ return { h: h, s: s, l: l };
+}
+
+// `hslToRgb`
+// Converts an HSL color value to RGB.
+// *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
+// *Returns:* { r, g, b } in the set [0, 255]
+function hslToRgb(h, s, l) {
+ var r, g, b;
+
+ h = bound01(h, 360);
+ s = bound01(s, 100);
+ l = bound01(l, 100);
+
+ function hue2rgb(p, q, t) {
+ if(t < 0) t += 1;
+ if(t > 1) t -= 1;
+ if(t < 1/6) return p + (q - p) * 6 * t;
+ if(t < 1/2) return q;
+ if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+ return p;
+ }
+
+ if(s === 0) {
+ r = g = b = l; // achromatic
+ }
+ else {
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1/3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1/3);
+ }
+
+ return { r: r * 255, g: g * 255, b: b * 255 };
+}
+
+// `rgbToHsv`
+// Converts an RGB color value to HSV
+// *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
+// *Returns:* { h, s, v } in [0,1]
+function rgbToHsv(r, g, b) {
+
+ r = bound01(r, 255);
+ g = bound01(g, 255);
+ b = bound01(b, 255);
+
+ var max = mathMax(r, g, b), min = mathMin(r, g, b);
+ var h, s, v = max;
+
+ var d = max - min;
+ s = max === 0 ? 0 : d / max;
+
+ if(max == min) {
+ h = 0; // achromatic
+ }
+ else {
+ switch(max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+ return { h: h, s: s, v: v };
+}
+
+// `hsvToRgb`
+// Converts an HSV color value to RGB.
+// *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
+// *Returns:* { r, g, b } in the set [0, 255]
+ function hsvToRgb(h, s, v) {
+
+ h = bound01(h, 360) * 6;
+ s = bound01(s, 100);
+ v = bound01(v, 100);
+
+ var i = math.floor(h),
+ f = h - i,
+ p = v * (1 - s),
+ q = v * (1 - f * s),
+ t = v * (1 - (1 - f) * s),
+ mod = i % 6,
+ r = [v, q, p, p, t, v][mod],
+ g = [t, v, v, q, p, p][mod],
+ b = [p, p, t, v, v, q][mod];
+
+ return { r: r * 255, g: g * 255, b: b * 255 };
+}
+
+// `rgbToHex`
+// Converts an RGB color to hex
+// Assumes r, g, and b are contained in the set [0, 255]
+// Returns a 3 or 6 character hex
+function rgbToHex(r, g, b) {
+ var hex = [
+ pad2(mathRound(r).toString(16)),
+ pad2(mathRound(g).toString(16)),
+ pad2(mathRound(b).toString(16))
+ ];
+
+ // Return a 3 character hex if possible
+ if (hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
+ }
+
+ return hex.join("");
+}
+
+// `equals`
+// Can be called with any tinycolor input
+tinycolor.equals = function (color1, color2) {
+ if (!color1 || !color2) { return false; }
+ return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
+};
+tinycolor.random = function() {
+ return tinycolor.fromRatio({
+ r: mathRandom(),
+ g: mathRandom(),
+ b: mathRandom()
+ });
+};
+
+
+// Modification Functions
+// ----------------------
+// Thanks to less.js for some of the basics here
+//
+
+
+tinycolor.desaturate = function (color, amount) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.s -= ((amount || 10) / 100);
+ hsl.s = clamp01(hsl.s);
+ return tinycolor(hsl);
+};
+tinycolor.saturate = function (color, amount) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.s += ((amount || 10) / 100);
+ hsl.s = clamp01(hsl.s);
+ return tinycolor(hsl);
+};
+tinycolor.greyscale = function(color) {
+ return tinycolor.desaturate(color, 100);
+};
+tinycolor.lighten = function(color, amount) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.l += ((amount || 10) / 100);
+ hsl.l = clamp01(hsl.l);
+ return tinycolor(hsl);
+};
+tinycolor.darken = function (color, amount) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.l -= ((amount || 10) / 100);
+ hsl.l = clamp01(hsl.l);
+ return tinycolor(hsl);
+};
+tinycolor.complement = function(color) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.h = (hsl.h + 180) % 360;
+ return tinycolor(hsl);
+};
+
+
+// Combination Functions
+// ---------------------
+// Thanks to jQuery xColor for some of the ideas behind these
+//
+
+tinycolor.triad = function(color) {
+ var hsl = tinycolor(color).toHsl();
+ var h = hsl.h;
+ return [
+ tinycolor(color),
+ tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
+ tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
+ ];
+};
+tinycolor.tetrad = function(color) {
+ var hsl = tinycolor(color).toHsl();
+ var h = hsl.h;
+ return [
+ tinycolor(color),
+ tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
+ tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
+ tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
+ ];
+};
+tinycolor.splitcomplement = function(color) {
+ var hsl = tinycolor(color).toHsl();
+ var h = hsl.h;
+ return [
+ tinycolor(color),
+ tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
+ tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
+ ];
+};
+tinycolor.analogous = function(color, results, slices) {
+ results = results || 6;
+ slices = slices || 30;
+
+ var hsl = tinycolor(color).toHsl();
+ var part = 360 / slices;
+ var ret = [tinycolor(color)];
+
+ for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
+ hsl.h = (hsl.h + part) % 360;
+ ret.push(tinycolor(hsl));
+ }
+ return ret;
+};
+tinycolor.monochromatic = function(color, results) {
+ results = results || 6;
+ var hsv = tinycolor(color).toHsv();
+ var h = hsv.h, s = hsv.s, v = hsv.v;
+ var ret = [];
+ var modification = 1 / results;
+
+ while (results--) {
+ ret.push(tinycolor({ h: h, s: s, v: v}));
+ v = (v + modification) % 1;
+ }
+
+ return ret;
+};
+// Readability based on W3C recommendations: http://www.w3.org/TR/AERT#color-contrast
+// Returns object with two properties:
+// .brightness: the difference in brightness between the two colors
+// .color: the difference in color/hue between the two colors
+// An "acceptable" color is considered to have a brightness difference of 125 and a
+// color difference of 500
+tinycolor.readability = function(color1, color2) {
+ var a = tinycolor(color1).toRgb(), b = tinycolor(color2).toRgb();
+ var brightnessA = (a.r * 299 + a.g * 587 + a.b * 114) / 1000;
+ var brightnessB = (b.r * 299 + b.g * 587 + b.b * 114) / 1000;
+ var colorDiff = (
+ Math.max(a.r, b.r) - Math.min(a.r, b.r) +
+ Math.max(a.g, b.g) - Math.min(a.g, b.g) +
+ Math.max(a.b, b.b) - Math.min(a.b, b.b));
+ return {
+ brightness: Math.abs(brightnessA - brightnessB),
+ color: colorDiff
+ };
+};
+// True if using color1 over color2 (or vice versa) is "readable"
+// Based on: http://www.w3.org/TR/AERT#color-contrast
+// Example:
+// tinycolor.readable("#000", "#111") => false
+tinycolor.readable = function(color1, color2) {
+ var readability = tinycolor.readability(color1, color2);
+ return readability.brightness > 125 && readability.color > 500;
+};
+// Given a base color and a list of possible foreground or background
+// colors for that base, returns the most readable color.
+// Example:
+// tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
+tinycolor.mostReadable = function(baseColor, colorList) {
+ var bestColor;
+ var bestScore = 0;
+ var bestIsReadable = false;
+ for (var i=0; i < colorList.length; i++) {
+ var readability = tinycolor.readability(baseColor, colorList[i]);
+ var readable = readability.brightness > 125 && readability.color > 500;
+ // We normalize both around the "acceptable" breaking point,
+ // but rank brightness constrast higher than hue. Why? I'm
+ // not sure, seems reasonable.
+ var score = 3 * (readability.brightness / 125) + (readability.color / 500);
+ if ((readable && ! bestIsReadable) ||
+ (readable && bestIsReadable && score > bestScore) ||
+ ((! readable) && (! bestIsReadable) && score > bestScore)) {
+ bestIsReadable = readable;
+ bestScore = score;
+ bestColor = colorList[i];
+ }
+ }
+ return bestColor;
+};
+
+
+// Big List of Colors
+// ---------
+//
+var names = tinycolor.names = {
+ aliceblue: "f0f8ff",
+ antiquewhite: "faebd7",
+ aqua: "0ff",
+ aquamarine: "7fffd4",
+ azure: "f0ffff",
+ beige: "f5f5dc",
+ bisque: "ffe4c4",
+ black: "000",
+ blanchedalmond: "ffebcd",
+ blue: "00f",
+ blueviolet: "8a2be2",
+ brown: "a52a2a",
+ burlywood: "deb887",
+ burntsienna: "ea7e5d",
+ cadetblue: "5f9ea0",
+ chartreuse: "7fff00",
+ chocolate: "d2691e",
+ coral: "ff7f50",
+ cornflowerblue: "6495ed",
+ cornsilk: "fff8dc",
+ crimson: "dc143c",
+ cyan: "0ff",
+ darkblue: "00008b",
+ darkcyan: "008b8b",
+ darkgoldenrod: "b8860b",
+ darkgray: "a9a9a9",
+ darkgreen: "006400",
+ darkgrey: "a9a9a9",
+ darkkhaki: "bdb76b",
+ darkmagenta: "8b008b",
+ darkolivegreen: "556b2f",
+ darkorange: "ff8c00",
+ darkorchid: "9932cc",
+ darkred: "8b0000",
+ darksalmon: "e9967a",
+ darkseagreen: "8fbc8f",
+ darkslateblue: "483d8b",
+ darkslategray: "2f4f4f",
+ darkslategrey: "2f4f4f",
+ darkturquoise: "00ced1",
+ darkviolet: "9400d3",
+ deeppink: "ff1493",
+ deepskyblue: "00bfff",
+ dimgray: "696969",
+ dimgrey: "696969",
+ dodgerblue: "1e90ff",
+ firebrick: "b22222",
+ floralwhite: "fffaf0",
+ forestgreen: "228b22",
+ fuchsia: "f0f",
+ gainsboro: "dcdcdc",
+ ghostwhite: "f8f8ff",
+ gold: "ffd700",
+ goldenrod: "daa520",
+ gray: "808080",
+ green: "008000",
+ greenyellow: "adff2f",
+ grey: "808080",
+ honeydew: "f0fff0",
+ hotpink: "ff69b4",
+ indianred: "cd5c5c",
+ indigo: "4b0082",
+ ivory: "fffff0",
+ khaki: "f0e68c",
+ lavender: "e6e6fa",
+ lavenderblush: "fff0f5",
+ lawngreen: "7cfc00",
+ lemonchiffon: "fffacd",
+ lightblue: "add8e6",
+ lightcoral: "f08080",
+ lightcyan: "e0ffff",
+ lightgoldenrodyellow: "fafad2",
+ lightgray: "d3d3d3",
+ lightgreen: "90ee90",
+ lightgrey: "d3d3d3",
+ lightpink: "ffb6c1",
+ lightsalmon: "ffa07a",
+ lightseagreen: "20b2aa",
+ lightskyblue: "87cefa",
+ lightslategray: "789",
+ lightslategrey: "789",
+ lightsteelblue: "b0c4de",
+ lightyellow: "ffffe0",
+ lime: "0f0",
+ limegreen: "32cd32",
+ linen: "faf0e6",
+ magenta: "f0f",
+ maroon: "800000",
+ mediumaquamarine: "66cdaa",
+ mediumblue: "0000cd",
+ mediumorchid: "ba55d3",
+ mediumpurple: "9370db",
+ mediumseagreen: "3cb371",
+ mediumslateblue: "7b68ee",
+ mediumspringgreen: "00fa9a",
+ mediumturquoise: "48d1cc",
+ mediumvioletred: "c71585",
+ midnightblue: "191970",
+ mintcream: "f5fffa",
+ mistyrose: "ffe4e1",
+ moccasin: "ffe4b5",
+ navajowhite: "ffdead",
+ navy: "000080",
+ oldlace: "fdf5e6",
+ olive: "808000",
+ olivedrab: "6b8e23",
+ orange: "ffa500",
+ orangered: "ff4500",
+ orchid: "da70d6",
+ palegoldenrod: "eee8aa",
+ palegreen: "98fb98",
+ paleturquoise: "afeeee",
+ palevioletred: "db7093",
+ papayawhip: "ffefd5",
+ peachpuff: "ffdab9",
+ peru: "cd853f",
+ pink: "ffc0cb",
+ plum: "dda0dd",
+ powderblue: "b0e0e6",
+ purple: "800080",
+ red: "f00",
+ rosybrown: "bc8f8f",
+ royalblue: "4169e1",
+ saddlebrown: "8b4513",
+ salmon: "fa8072",
+ sandybrown: "f4a460",
+ seagreen: "2e8b57",
+ seashell: "fff5ee",
+ sienna: "a0522d",
+ silver: "c0c0c0",
+ skyblue: "87ceeb",
+ slateblue: "6a5acd",
+ slategray: "708090",
+ slategrey: "708090",
+ snow: "fffafa",
+ springgreen: "00ff7f",
+ steelblue: "4682b4",
+ tan: "d2b48c",
+ teal: "008080",
+ thistle: "d8bfd8",
+ tomato: "ff6347",
+ turquoise: "40e0d0",
+ violet: "ee82ee",
+ wheat: "f5deb3",
+ white: "fff",
+ whitesmoke: "f5f5f5",
+ yellow: "ff0",
+ yellowgreen: "9acd32"
+};
+
+// Make it easy to access colors via `hexNames[hex]`
+var hexNames = tinycolor.hexNames = flip(names);
+
+
+// Utilities
+// ---------
+
+// `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
+function flip(o) {
+ var flipped = { };
+ for (var i in o) {
+ if (o.hasOwnProperty(i)) {
+ flipped[o[i]] = i;
+ }
+ }
+ return flipped;
+}
+
+// Take input from [0, n] and return it as [0, 1]
+function bound01(n, max) {
+ if (isOnePointZero(n)) { n = "100%"; }
+
+ var processPercent = isPercentage(n);
+ n = mathMin(max, mathMax(0, parseFloat(n)));
+
+ // Automatically convert percentage into number
+ if (processPercent) {
+ n = parseInt(n * max, 10) / 100;
+ }
+
+ // Handle floating point rounding errors
+ if ((math.abs(n - max) < 0.000001)) {
+ return 1;
+ }
+
+ // Convert into [0, 1] range if it isn't already
+ return (n % max) / parseFloat(max);
+}
+
+// Force a number between 0 and 1
+function clamp01(val) {
+ return mathMin(1, mathMax(0, val));
+}
+
+// Parse an integer into hex
+function parseHex(val) {
+ return parseInt(val, 16);
+}
+
+// Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
+//
+function isOnePointZero(n) {
+ return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
+}
+
+// Check to see if string passed in is a percentage
+function isPercentage(n) {
+ return typeof n === "string" && n.indexOf('%') != -1;
+}
+
+// Force a hex value to have 2 characters
+function pad2(c) {
+ return c.length == 1 ? '0' + c : '' + c;
+}
+
+// Replace a decimal with it's percentage value
+function convertToPercentage(n) {
+ if (n <= 1) {
+ n = (n * 100) + "%";
+ }
+
+ return n;
+}
+
+var matchers = (function() {
+
+ //
+ var CSS_INTEGER = "[-\\+]?\\d+%?";
+
+ //
+ var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
+
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
+ var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
+
+ // Actual matching.
+ // Parentheses and commas are optional, but not required.
+ // Whitespace can take the place of commas or opening paren
+ var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+ var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+
+ return {
+ rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
+ rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
+ hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
+ hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
+ hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
+ hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
+ hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
+ };
+})();
+
+// `stringInputToObject`
+// Permissive string parsing. Take in a number of formats, and output an object
+// based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
+function stringInputToObject(color) {
+
+ color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
+ var named = false;
+ if (names[color]) {
+ color = names[color];
+ named = true;
+ }
+ else if (color == 'transparent') {
+ return { r: 0, g: 0, b: 0, a: 0 };
+ }
+
+ // Try to match string input using regular expressions.
+ // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
+ // Just return an object and let the conversion functions handle that.
+ // This way the result will be the same whether the tinycolor is initialized with string or object.
+ var match;
+ if ((match = matchers.rgb.exec(color))) {
+ return { r: match[1], g: match[2], b: match[3] };
+ }
+ if ((match = matchers.rgba.exec(color))) {
+ return { r: match[1], g: match[2], b: match[3], a: match[4] };
+ }
+ if ((match = matchers.hsl.exec(color))) {
+ return { h: match[1], s: match[2], l: match[3] };
+ }
+ if ((match = matchers.hsla.exec(color))) {
+ return { h: match[1], s: match[2], l: match[3], a: match[4] };
+ }
+ if ((match = matchers.hsv.exec(color))) {
+ return { h: match[1], s: match[2], v: match[3] };
+ }
+ if ((match = matchers.hex6.exec(color))) {
+ return {
+ r: parseHex(match[1]),
+ g: parseHex(match[2]),
+ b: parseHex(match[3]),
+ format: named ? "name" : "hex"
+ };
+ }
+ if ((match = matchers.hex3.exec(color))) {
+ return {
+ r: parseHex(match[1] + '' + match[1]),
+ g: parseHex(match[2] + '' + match[2]),
+ b: parseHex(match[3] + '' + match[3]),
+ format: named ? "name" : "hex"
+ };
+ }
+
+ return false;
+}
+
+// Node: Export function
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = tinycolor;
+}
+// AMD/requirejs: Define the module
+else if (typeof define !== "undefined") {
+ define(function () {return tinycolor;});
+}
+// Browser: Expose to window
+else {
+ root.tinycolor = tinycolor;
+}
+
+})(this);
diff --git a/ofmeet/classes/jitsi-meet/util.js b/ofmeet/classes/jitsi-meet/util.js
new file mode 100644
index 00000000..e1b0dca1
--- /dev/null
+++ b/ofmeet/classes/jitsi-meet/util.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+ var util = {};
+
+ util.urlParam = function(name)
+ {
+ var results = new RegExp('[\\?&]' + name + '=([^]*)').exec(window.location.href);
+ if (!results) { return undefined; }
+ return unescape(results[1] || undefined);
+ };
+
+ util.assert = function (cond) {
+ if (! cond) {
+ var args = ["Assertion error:"].concat(Array.prototype.slice.call(arguments, 1));
+ console.error.apply(console, args);
+ if (console.trace) {
+ console.trace();
+ }
+ throw new Error(args.join(" "));
+ }
+ };
+
+ util.extend = function (base, extensions) {
+ if (! extensions) {
+ extensions = base;
+ base = {};
+ }
+ for (var a in extensions) {
+ if (extensions.hasOwnProperty(a)) {
+ base[a] = extensions[a];
+ }
+ }
+ return base;
+ };
+
+ util.Class = function (superClass, prototype) {
+ var a;
+ if (prototype === undefined) {
+ prototype = superClass;
+ } else {
+ if (superClass.prototype) {
+ superClass = superClass.prototype;
+ }
+ var newPrototype = Object.create(superClass);
+ for (a in prototype) {
+ if (prototype.hasOwnProperty(a)) {
+ newPrototype[a] = prototype[a];
+ }
+ }
+ prototype = newPrototype;
+ }
+ var ClassObject = function () {
+ var obj = Object.create(prototype);
+ obj.constructor.apply(obj, arguments);
+ obj.constructor = ClassObject;
+ return obj;
+ };
+ ClassObject.prototype = prototype;
+ if (prototype.constructor.name) {
+ ClassObject.className = prototype.constructor.name;
+ ClassObject.toString = function () {
+ return '[Class ' + this.className + ']';
+ };
+ }
+ if (prototype.classMethods) {
+ for (a in prototype.classMethods) {
+ if (prototype.classMethods.hasOwnProperty(a)) {
+ ClassObject[a] = prototype.classMethods[a];
+ }
+ }
+ }
+ return ClassObject;
+ };
\ No newline at end of file
diff --git a/pade/classes/public/changelog.html b/pade/classes/public/changelog.html
index cd3e40e6..db1fd9a9 100644
--- a/pade/classes/public/changelog.html
+++ b/pade/classes/public/changelog.html
@@ -50,8 +50,9 @@ Changelog
1.6.11 -- October 31, 2020
1.6.10 -- September 30, 2020
diff --git a/pade/classes/public/js/background.js b/pade/classes/public/js/background.js
index 42cb180e..a040a57a 100644
--- a/pade/classes/public/js/background.js
+++ b/pade/classes/public/js/background.js
@@ -207,6 +207,27 @@ window.addEventListener("load", function()
if (pade.busy) return; // no presence broadcast while I am busy
+ var show = null, status = null;
+
+ if (idleState == "locked")
+ {
+ show = "xa";
+ status = getSetting("idleLockedMessage");
+ }
+ else
+
+ if (idleState == "idle")
+ {
+ show = "away";
+ status = getSetting("idleMessage");
+ }
+ else
+
+ if (idleState == "active")
+ {
+ status = getSetting("idleActiveMessage");
+ }
+
if (pade.chatWindow)
{
var converse = chrome.extension.getViews({windowId: pade.chatWindow.id})[0];
diff --git a/web/src/main/webapp/collab.js b/web/src/main/webapp/collab.js
new file mode 100644
index 00000000..e34d4c75
--- /dev/null
+++ b/web/src/main/webapp/collab.js
@@ -0,0 +1,863 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var collab = (function(coop)
+{
+ var FOREGROUND_COLORS = ["#111", "#eee"];
+ var CURSOR_HEIGHT = 50;
+ var CURSOR_ANGLE = (35 / 180) * Math.PI;
+ var CURSOR_WIDTH = Math.ceil(Math.sin(CURSOR_ANGLE) * CURSOR_HEIGHT);
+ var CLICK_TRANSITION_TIME = 3000;
+ var enableCursor = true;
+ var displayMouseClick = true;
+
+ var session = {
+
+ send: function(msg)
+ {
+ //console.log("session send", msg);
+ window.top.postMessage({ event: 'ofmeet.event.url.message', msg: msg}, '*');
+ }
+ };
+
+ var eventMaker = {};
+
+ eventMaker.performClick = function (target) {
+ // FIXME: should accept other parameters, like Ctrl/Alt/etc
+ var event = document.createEvent("MouseEvents");
+ event.initMouseEvent(
+ "click", // type
+ true, // canBubble
+ true, // cancelable
+ window, // view
+ 0, // detail
+ 0, // screenX
+ 0, // screenY
+ 0, // clientX
+ 0, // clientY
+ false, // ctrlKey
+ false, // altKey
+ false, // shiftKey
+ false, // metaKey
+ 0, // button
+ null // relatedTarget
+ );
+ // FIXME: I'm not sure this custom attribute always propagates?
+ // seems okay in Firefox/Chrome, but I've had problems with
+ // setting attributes on keyboard events in the past.
+ event.togetherjsInternal = true;
+ target = $(target)[0];
+ var cancelled = target.dispatchEvent(event);
+ if (cancelled) {
+ return;
+ }
+ if (target.tagName == "A") {
+ var href = target.href;
+ if (href) {
+ location.href = href;
+ return;
+ }
+ }
+ // FIXME: should do button clicks (like a form submit)
+ // FIXME: should run .onclick() as well
+ };
+
+ eventMaker.fireChange = function (target) {
+ target = $(target)[0];
+ var event = document.createEvent("HTMLEvents");
+ event.initEvent("change", true, true);
+ target.dispatchEvent(event);
+ };
+
+
+ var elementFinder = {};
+
+ elementFinder.ignoreElement = function ignoreElement(el)
+ {
+ if (el instanceof $) {
+ el = el[0];
+ }
+ while (el) {
+ if ($(el).hasClass("togetherjs")) {
+ return true;
+ }
+ el = el.parentNode;
+ }
+ return false;
+ };
+
+ elementFinder.elementLocation = function elementLocation(el) {
+ if (el instanceof $) {
+ // a jQuery element
+ el = el[0];
+ }
+ if (el[0] && el.attr && el[0].nodeType == 1) {
+ // Or a jQuery element not made by us
+ el = el[0];
+ }
+ if (el.id) {
+ return "#" + el.id;
+ }
+ if (el.tagName == "BODY") {
+ return "body";
+ }
+ if (el.tagName == "HEAD") {
+ return "head";
+ }
+ if (el === document) {
+ return "document";
+ }
+ if (el === document.body) {
+ return "body";
+ }
+ var parent = el.parentNode;
+ if ((! parent) || parent == el) {
+ console.warn("elementLocation(", el, ") has null parent");
+ throw new Error("No locatable parent found");
+ }
+ var parentLocation = elementLocation(parent);
+ var children = parent.childNodes;
+ var _len = children.length;
+ var index = 0;
+ for (var i=0; i<_len; i++) {
+ if (children[i] == el) {
+ break;
+ }
+ if (children[i].nodeType == document.ELEMENT_NODE) {
+ if ( children[i].className && typeof children[i].className.indexOf === 'function' && children[i].className.indexOf("togetherjs") != -1) {
+ // Don't count our UI
+ continue;
+ }
+ // Don't count text or comments
+ index++;
+ }
+ }
+ return parentLocation + ":nth-child(" + (index+1) + ")";
+ };
+
+ elementFinder.CannotFind = {
+ constructor: function CannotFind(location, reason, context) {
+ this.prefix = "";
+ this.location = location;
+ this.reason = reason;
+ this.context = context;
+ },
+ toString: function () {
+ var loc;
+ try {
+ loc = elementFinder.elementLocation(this.context);
+ } catch (e) {
+ loc = this.context;
+ }
+ return (
+ "[CannotFind " + this.prefix +
+ "(" + this.location + "): " +
+ this.reason + " in " +
+ loc + "]");
+ }
+ };
+
+ elementFinder.findElement = function findElement(loc, container) {
+ // FIXME: should this all just be done with document.querySelector()?
+ // But no! We can't ignore togetherjs elements with querySelector.
+ // But maybe! We *could* make togetherjs elements less obtrusive?
+ container = container || document;
+ var el, rest;
+ if (loc === "body") {
+ return document.body;
+ } else if (loc === "head") {
+ return document.head;
+ } else if (loc === "document") {
+ return document;
+ } else if (loc.indexOf("body") === 0) {
+ el = document.body;
+ try {
+ return findElement(loc.substr(("body").length), el);
+ } catch (e) {
+ //if (e instanceof elementFinder.CannotFind) {
+ // e.prefix = "body" + e.prefix;
+ //}
+ //throw e;
+ }
+ } else if (loc.indexOf("head") === 0) {
+ el = document.head;
+ try {
+ return findElement(loc.substr(("head").length), el);
+ } catch (e) {
+ if (e instanceof elementFinder.CannotFind) {
+ e.prefix = "head" + e.prefix;
+ }
+ throw e;
+ }
+ } else if (loc.indexOf("#") === 0) {
+ var id;
+ loc = loc.substr(1);
+ if (loc.indexOf(":") === -1) {
+ id = loc;
+ rest = "";
+ } else {
+ id = loc.substr(0, loc.indexOf(":"));
+ rest = loc.substr(loc.indexOf(":"));
+ }
+ el = document.getElementById(id);
+ if (! el) {
+ // BAO
+ //throw elementFinder.CannotFind("#" + id, "No element by that id", container);
+ }
+ if (rest) {
+ try {
+ return findElement(rest, el);
+ } catch (e) {
+/*
+ if (e && e instanceof elementFinder.CannotFind) {
+ e.prefix = "#" + id + e.prefix;
+ }
+ throw e;
+*/
+ console.error("elementFinder.findElement", e);
+ }
+ } else {
+ return el;
+ }
+ } else if (loc.indexOf(":nth-child(") === 0) {
+ loc = loc.substr((":nth-child(").length);
+ if (loc.indexOf(")") == -1) {
+ throw "Invalid location, missing ): " + loc;
+ }
+ var num = loc.substr(0, loc.indexOf(")"));
+ num = parseInt(num, 10);
+ var count = num;
+ loc = loc.substr(loc.indexOf(")") + 1);
+ var children = container.childNodes;
+ el = null;
+ for (var i=0; i height) {
+ return false;
+ }
+ last = el;
+ });
+ if ((! children.length) || (! last)) {
+ // There are no children, or only inapplicable children
+ return {
+ location: elementFinder.elementLocation(start[0]),
+ offset: height - start.offset().top,
+ absoluteTop: height,
+ documentHeight: $(document).height()
+ };
+ }
+ return search(last, height);
+ }
+ return search($(document.body), height);
+ };
+
+ elementFinder.pixelForPosition = function (position) {
+ /* Inverse of elementFinder.elementByPixel */
+ if (position.location == "body") {
+ return position.offset;
+ }
+ var el;
+ try {
+ el = elementFinder.findElement(position.location);
+ } catch (e) {
+ if (e instanceof elementFinder.CannotFind && position.absoluteTop) {
+ // We don't trust absoluteTop to be quite right locally, so we adjust
+ // for the total document height differences:
+ var percent = position.absoluteTop / position.documentHeight;
+ return $(document).height() * percent;
+ }
+ throw e;
+ }
+ var top = $(el).offset().top;
+ // FIXME: maybe here we should test for sanity, like if an element is
+ // hidden. We can use position.absoluteTop to get a sense of where the
+ // element roughly should be. If the sanity check failed we'd use
+ // absoluteTop
+ return top + position.offset;
+ };
+
+ // Number of milliseconds after page load in which a scroll-update
+ // related hello-back message will be processed:
+
+ var SCROLL_UPDATE_CUTOFF = 2000;
+
+ // FIXME: should check for a peer leaving and remove the cursor object
+ var Cursor = util.Class({
+ constructor: function (clientId) {
+ this.clientId = clientId;
+ this.element = $('');
+ this.elementClass = "togetherjs-scrolled-normal";
+ this.element.addClass(this.elementClass);
+ //this.updatePeer(peers.getPeer(clientId));
+ this.lastTop = this.lastLeft = null;
+ $(document.body).append(this.element);
+ //this.element.animateCursorEntry();
+ this.keydownTimeout = null;
+ this.clearKeydown = this.clearKeydown.bind(this);
+ this.atOtherUrl = false;
+ this.color = Math.floor(Math.random() * 0xffffff).toString(16);
+ while (this.color.length < 6) {
+ this.color = "0" + this.color;
+ }
+ this.color = "#" + this.color;
+ },
+
+ // How long after receiving a setKeydown call that we should show the
+ // user typing. This should be more than MIN_KEYDOWN_TIME:
+ KEYDOWN_WAIT_TIME: 2000,
+
+ updatePeer: function (peer) {
+ // FIXME: can I use peer.setElement()?
+ this.element.css({color: this.color});
+ var img = this.element.find("img.togetherjs-cursor-img");
+ img.attr("src", makeCursor(this.color));
+ var name = this.element.find(".togetherjs-cursor-name");
+ var nameContainer = this.element.find(".togetherjs-cursor-container");
+ name.text(peer.name);
+ nameContainer.css({
+ backgroundColor: this.color,
+ color: tinycolor.mostReadable(this.color, FOREGROUND_COLORS)
+ });
+
+ var path = this.element.find("svg path");
+
+ path.attr("fill", this.color);
+
+ // FIXME: should I just remove the element?
+ if (peer.status != "live") {
+ this.element.hide();
+ this.element.find("svg").animate({
+ opacity: 0
+ }, 350);
+ this.element.find(".togetherjs-cursor-container").animate({
+ width: 34,
+ height: 20,
+ padding: 12,
+ margin: 0
+ }, 200).animate({
+ width: 0,
+ height: 0,
+ padding: 0,
+ opacity: 0
+ }, 200);
+ } else {
+ this.element.show();
+ this.element.animate({
+ opacity:0.3
+ }).animate({
+ opacity:1
+ });
+ }
+ },
+
+ setClass: function (name) {
+ if (name != this.elementClass) {
+ this.element.removeClass(this.elementClass).addClass(name);
+ this.elementClass = name;
+ }
+ },
+
+ updatePosition: function (pos) {
+ var top, left;
+ if (this.atOtherUrl) {
+ this.element.show();
+ this.atOtherUrl = false;
+ }
+ if (pos.element) {
+ var target = $(elementFinder.findElement(pos.element));
+ var offset = target.offset();
+ if (offset) {
+ top = offset.top + pos.offsetY;
+ left = offset.left + pos.offsetX;
+ }
+ } else {
+ // No anchor, just an absolute position
+ top = pos.top;
+ left = pos.left;
+ }
+ // These are saved for use by .refresh():
+ this.lastTop = top;
+ this.lastLeft = left;
+ this.setPosition(top, left);
+ },
+
+ hideOtherUrl: function () {
+ if (this.atOtherUrl) {
+ return;
+ }
+ this.atOtherUrl = true;
+ // FIXME: should show away status better:
+ this.element.hide();
+ },
+
+ // place Cursor rotate function down here FIXME: this doesnt do anything anymore. This is in the CSS as an animation
+ rotateCursorDown: function(){
+ var e = $(this.element).find('svg');
+ e.animate({borderSpacing: -150, opacity: 1}, {
+ step: function(now, fx) {
+ if (fx.prop == "borderSpacing") {
+ e.css('-webkit-transform', 'rotate('+now+'deg)')
+ .css('-moz-transform', 'rotate('+now+'deg)')
+ .css('-ms-transform', 'rotate('+now+'deg)')
+ .css('-o-transform', 'rotate('+now+'deg)')
+ .css('transform', 'rotate('+now+'deg)');
+ } else {
+ e.css(fx.prop, now);
+ }
+ },
+ duration: 500
+ }, 'linear').promise().then(function () {
+ e.css('-webkit-transform', '')
+ .css('-moz-transform', '')
+ .css('-ms-transform', '')
+ .css('-o-transform', '')
+ .css('transform', '')
+ .css("opacity", "");
+ });
+ },
+
+ setPosition: function (top, left) {
+ var wTop = $(window).scrollTop();
+ var height = $(window).height();
+
+ if (top < wTop) {
+ // FIXME: this is a totally arbitrary number, but is meant to be big enough
+ // to keep the cursor name from being off the top of the screen.
+ top = 25;
+ this.setClass("togetherjs-scrolled-above");
+ } else if (top > wTop + height - CURSOR_HEIGHT) {
+ top = height - CURSOR_HEIGHT - 5;
+ this.setClass("togetherjs-scrolled-below");
+ } else {
+ this.setClass("togetherjs-scrolled-normal");
+ }
+ this.element.css({
+ top: top,
+ left: left
+ });
+ },
+
+ refresh: function () {
+ if (this.lastTop !== null) {
+ this.setPosition(this.lastTop, this.lastLeft);
+ }
+ },
+
+ setKeydown: function () {
+ if (this.keydownTimeout) {
+ clearTimeout(this.keydownTimeout);
+ } else {
+ this.element.find(".togetherjs-cursor-typing").show().animateKeyboard();
+ }
+ this.keydownTimeout = setTimeout(this.clearKeydown, this.KEYDOWN_WAIT_TIME);
+ },
+
+ clearKeydown: function () {
+ this.keydownTimeout = null;
+ this.element.find(".togetherjs-cursor-typing").hide().stopKeyboardAnimation();
+ },
+
+ _destroy: function () {
+ this.element.remove();
+ this.element = null;
+ }
+ });
+
+ Cursor._cursors = {};
+
+ Cursor.getClient = function (clientId) {
+ var c = Cursor._cursors[clientId];
+ if (! c) {
+ c = Cursor._cursors[clientId] = Cursor(clientId);
+ }
+ return c;
+ };
+
+ Cursor.forEach = function (callback, context) {
+ context = context || null;
+ for (var a in Cursor._cursors) {
+ if (Cursor._cursors.hasOwnProperty(a)) {
+ callback.call(context, Cursor._cursors[a], a);
+ }
+ }
+ };
+
+ Cursor.destroy = function (clientId) {
+ if (Cursor._cursors[clientId])
+ {
+ Cursor._cursors[clientId]._destroy();
+ delete Cursor._cursors[clientId];
+ }
+ };
+
+/*
+ peers.on("new-peer identity-updated status-updated", function (peer) {
+ var c = Cursor.getClient(peer.id);
+ c.updatePeer(peer);
+ });
+*/
+
+ var lastTime = 0;
+ var MIN_TIME = 100;
+ var lastPosX = -1;
+ var lastPosY = -1;
+ var lastMessage = null;
+
+ function mouseMove(event)
+ {
+ if (!enableCursor) return;
+
+ var now = Date.now();
+ if (now - lastTime < MIN_TIME) {
+ return;
+ }
+ lastTime = now;
+ var pageX = event.pageX;
+ var pageY = event.pageY;
+ if (Math.abs(lastPosX - pageX) < 3 && Math.abs(lastPosY - pageY) < 3) {
+ // Not a substantial enough change
+ return;
+ }
+ lastPosX = pageX;
+ lastPosY = pageY;
+ var target = event.target;
+
+ if (elementFinder.ignoreElement(target)) {
+ target = null;
+ }
+ if ((! target) || target == document.documentElement || target == document.body) {
+ lastMessage = {
+ type: "cursor-update",
+ top: pageY,
+ left: pageX
+ };
+ session.send(lastMessage);
+ return;
+ }
+ target = $(target);
+ var offset = target.offset();
+ if (! offset) {
+ // FIXME: this really is walkabout.js's problem to fire events on the
+ // document instead of a specific element
+ console.warn("Could not get offset of element:", target[0]);
+ return;
+ }
+ var offsetX = pageX - offset.left;
+ var offsetY = pageY - offset.top;
+ lastMessage = {
+ type: "cursor-update",
+ element: elementFinder.elementLocation(target),
+ offsetX: Math.floor(offsetX),
+ offsetY: Math.floor(offsetY)
+ };
+ session.send(lastMessage);
+ }
+
+
+
+ function makeCursor(color)
+ {
+ var canvas = $("");
+ canvas.attr("height", CURSOR_HEIGHT);
+ canvas.attr("width", CURSOR_WIDTH);
+ var context = canvas[0].getContext('2d');
+ context.fillStyle = color;
+ context.moveTo(0, 0);
+ context.beginPath();
+ context.lineTo(0, CURSOR_HEIGHT/1.2);
+ context.lineTo(Math.sin(CURSOR_ANGLE/2) * CURSOR_HEIGHT / 1.5,
+ Math.cos(CURSOR_ANGLE/2) * CURSOR_HEIGHT / 1.5);
+ context.lineTo(Math.sin(CURSOR_ANGLE) * CURSOR_HEIGHT / 1.2,
+ Math.cos(CURSOR_ANGLE) * CURSOR_HEIGHT / 1.2);
+ context.lineTo(0, 0);
+ context.shadowColor = 'rgba(0,0,0,0.3)';
+ context.shadowBlur = 2;
+ context.shadowOffsetX = 1;
+ context.shadowOffsetY = 2;
+ context.strokeStyle = "#ffffff";
+ context.stroke();
+ context.fill();
+ return canvas[0].toDataURL("image/png");
+ }
+
+ var scrollTimeout = null;
+ var scrollTimeoutSet = 0;
+ var SCROLL_DELAY_TIMEOUT = 75;
+ var SCROLL_DELAY_LIMIT = 300;
+
+ function scroll() {
+ var now = Date.now();
+ if (scrollTimeout) {
+ if (now - scrollTimeoutSet < SCROLL_DELAY_LIMIT) {
+ clearTimeout(scrollTimeout);
+ } else {
+ // Just let it progress anyway
+ return;
+ }
+ }
+ scrollTimeout = setTimeout(_scrollRefresh, SCROLL_DELAY_TIMEOUT);
+ if (! scrollTimeoutSet) {
+ scrollTimeoutSet = now;
+ }
+ }
+
+ var lastScrollMessage = null;
+
+ function _scrollRefresh() {
+ scrollTimeout = null;
+ scrollTimeoutSet = 0;
+ Cursor.forEach(function (c) {
+ c.refresh();
+ });
+ lastScrollMessage = {
+ type: "scroll-update",
+ position: elementFinder.elementByPixel($(window).scrollTop())
+ };
+ session.send(lastScrollMessage);
+ }
+
+
+ function documentClick(event) {
+
+ if (event.togetherjsInternal) {
+ // This is an artificial internal event
+ return;
+ }
+
+ if (!enableCursor) return;
+
+ // FIXME: this might just be my imagination, but somehow I just
+ // really don't want to do anything at this stage of the event
+ // handling (since I'm catching every click), and I'll just do
+ // something real soon:
+ setTimeout(function ()
+ {
+ var element = event.target;
+ if (element == document.documentElement) {
+ // For some reason clicking on gives the element here
+ element = document.body;
+ }
+ if (elementFinder.ignoreElement(element)) {
+ return;
+ }
+ //Prevent click events on video objects to avoid conflicts with
+ //togetherjs's own video events
+ //if (element.nodeName.toLowerCase() === 'video'){
+ // return;
+ //}
+
+ var location = elementFinder.elementLocation(element);
+ var offset = $(element).offset();
+ var offsetX = event.pageX - offset.left;
+ var offsetY = event.pageY - offset.top;
+
+ session.send({
+ type: "cursor-click",
+ element: location,
+ offsetX: offsetX,
+ offsetY: offsetY,
+ top: offset.top,
+ left: offset.left
+ });
+
+ if (displayMouseClick) displayClick({top: event.pageY, left: event.pageX}, 'red');
+ });
+ }
+
+
+ function displayClick(pos, color) {
+ if (!enableCursor) return;
+ //console.log("displayClick", pos, color);
+ // FIXME: should we hide the local click if no one else is going to see it?
+ // That means tracking who might be able to see our screen.
+ var element = $('');
+ $(document.body).append(element);
+
+ element.css({
+ top: pos.top,
+ left: pos.left,
+ borderColor: color
+ });
+ setTimeout(function () {
+ element.addClass("togetherjs-clicking");
+ }, 100);
+ setTimeout(function () {
+ element.remove();
+ }, CLICK_TRANSITION_TIME);
+ }
+
+ var lastKeydown = 0;
+ var MIN_KEYDOWN_TIME = 500;
+
+ function documentKeydown(event) {
+ if (!enableCursor) return;
+
+ setTimeout(function () {
+ var now = Date.now();
+ if (now - lastKeydown < MIN_KEYDOWN_TIME) {
+ return;
+ }
+ lastKeydown = now;
+ // FIXME: is event.target interesting here? That is, *what* the
+ // user is typing into, not just that the user is typing? Also
+ // I'm assuming we don't care if the user it typing into a
+ // togetherjs-related field, since chat activity is as interesting
+ // as any other activity.
+ session.send({type: "keydown"});
+ });
+ }
+
+
+ var handleCursorClick = function (pos, peer)
+ {
+ if (!pos || !peer) return;
+ if (!enableCursor) return;
+
+ // When the click is calculated isn't always the same as how the
+ // last cursor update was calculated, so we force the cursor to
+ // the last location during a click:
+
+ peer.updatePosition(pos);
+
+ if (!pos) return;
+
+ var topPos = pos.top + pos.offsetY;
+ var leftPos = pos.left + pos.offsetX;
+
+ if (pos.element) {
+ var target = $(elementFinder.findElement(pos.element));
+ var offset = target.offset();
+
+ if (offset)
+ {
+ topPos = offset.top + pos.offsetY;
+ leftPos = offset.left + pos.offsetX;
+
+ // TODO support remote click actions later
+ //if (target) eventMaker.performClick(target);
+ }
+ }
+ displayClick({top: topPos, left: leftPos}, 'red');
+ };
+
+ handleAppMessage = function(obj, from)
+ {
+ //try {
+ //console.log("remote handleAppMessage", obj, from);
+
+ p = Cursor.getClient(from);
+
+ if (p)
+ {
+ p.updatePeer({id: from, name: from, status: "live"});
+
+ if (obj.type == "cursor-update") p.updatePosition(obj);
+ if (obj.type == "cursor-click") handleCursorClick(obj, p);
+ }
+
+ //} catch (e) {console.error(e)}
+ }
+
+
+ window.addEventListener("unload", function ()
+ {
+ Cursor.forEach(function (c, clientId) {
+ Cursor.destroy(clientId);
+ });
+
+ document.removeEventListener("mousemove", mouseMove, true);
+ document.removeEventListener("click", documentClick, true);
+ document.removeEventListener("keydown", documentKeydown, true);
+
+ $(window).unbind("scroll", scroll);
+ });
+
+ window.addEventListener('message', function (event)
+ {
+ //console.log("remote action", event.data);
+
+ if (!event.data) return;
+
+ if (event.data.action == 'ofmeet.action.share')
+ {
+ if (event.data.json.msg)
+ {
+ handleAppMessage(event.data.json.msg, event.data.json.from);
+ }
+ else
+
+ if (event.data.json.action == 'setup')
+ {
+ document.addEventListener("mousemove", mouseMove, true);
+ document.addEventListener("click", documentClick, true);
+ // TODO handle collaboration later
+ //document.addEventListener("keydown", documentKeydown, true);
+ }
+ else
+
+ if (event.data.json.action == 'destroy')
+ {
+ Cursor.destroy(event.data.json.from);
+ document.removeEventListener("mousemove", mouseMove, true);
+ document.removeEventListener("click", documentClick, true);
+ // TODO handle collaboration later
+ //document.removeEventListener("keydown", documentKeydown, true);
+ }
+ }
+ });
+
+ $(window).scroll(scroll);
+ scroll();
+
+ return coop;
+
+}(collab || {}));
\ No newline at end of file
diff --git a/web/src/main/webapp/cursor.css b/web/src/main/webapp/cursor.css
new file mode 100644
index 00000000..bfce2b2f
--- /dev/null
+++ b/web/src/main/webapp/cursor.css
@@ -0,0 +1,121 @@
+
+.togetherjs * {
+ -webkit-box-sizing: content-box !important;
+ -moz-box-sizing: content-box !important;
+ box-sizing: content-box !important;
+}
+
+.togetherjs-cursor svg {
+ -webkit-filter: drop-shadow(1px 3px 2px rgba(0, 0, 0, 0.3));
+ -webkit-transform: rotate(-10deg);
+}
+.togetherjs-cursor-img {
+ position: relative;
+ top: 0;
+}
+.togetherjs-cursor img {
+ width: 20px;
+ -webkit-filter: drop-shadow(0px 2px 1px rgba(0, 0, 0, 0.2));
+ /*FIX ME, moz filter not working...*/
+
+ -moz-filter: drop-shadow(0px 2px 1px rgba(0, 0, 0, 0.2));
+ filter: drop-shadow(0px 2px 1px rgba(0, 0, 0, 0.2));
+}
+.togetherjs-cursor {
+ position: absolute;
+ z-index: 999999;
+ font-size: 28px;
+ font-weight: bolder;
+ /* This magic CSS rule makes this element basically invisible to clicks/etc:
+ (good on all but IE: http://caniuse.com/pointer-events */
+
+ pointer-events: none;
+ /*FIXME: maybe these should use position: fixed so the cursor
+ stays stuck to the top of the screen until the appropriate time
+ (when .togetherjs-scrolled-above/below is removed)?*/
+
+}
+.togetherjs-cursor:hover {
+ cursor: pointer;
+}
+.togetherjs-cursor.togetherjs-scrolled-above {
+ position: fixed;
+}
+.togetherjs-cursor.togetherjs-scrolled-above svg {
+ -webkit-transition-duration: 0.8s;
+ -webkit-transition-property: -webkit-transform;
+ -webkit-transform: rotate(20deg);
+ transition-duration: 0.8s;
+ transition-property: transform;
+ transform: rotate(20deg);
+}
+.togetherjs-cursor.togetherjs-scrolled-above .togetherjs-cursor-down {
+ display: none;
+}
+.togetherjs-cursor.togetherjs-scrolled-below {
+ position: fixed;
+}
+.togetherjs-cursor.togetherjs-scrolled-below svg {
+ -webkit-transition-duration: 0.8s;
+ -webkit-transition-property: -webkit-transform;
+ -webkit-transform: rotate(-150deg);
+ transition-duration: 0.8s;
+ transition-property: transform;
+ transform: rotate(-150deg);
+}
+.togetherjs-cursor.togetherjs-scrolled-below .togetherjs-cursor-up {
+ display: none;
+}
+.togetherjs-cursor.togetherjs-scrolled-normal svg {
+ -webkit-transition-duration: 0.8s;
+ -webkit-transition-property: -webkit-transform;
+ -webkit-transform: rotate(-10deg);
+ transition-duration: 0.8s;
+ transition-property: transform;
+ transform: rotate(-10deg);
+}
+.togetherjs-cursor.togetherjs-scrolled-normal .togetherjs-cursor-up,
+.togetherjs-cursor.togetherjs-scrolled-normal .togetherjs-cursor-down {
+ display: none;
+}
+.togetherjs-cursor .togetherjs-cursor-container {
+ opacity: 0.9;
+ white-space: nowrap;
+ font-family: openSansLight, Helvetica, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 40%;
+ position: relative;
+ top: 5px;
+ left: 15px;
+ padding: 8px;
+ border-radius: 4px;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ -moz-box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
+ -webkit-box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
+ box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.5);
+}
+
+.togetherjs-click {
+ position: absolute;
+ z-index: 999998;
+ pointer-events: none;
+ width: 10px;
+ height: 10px;
+ margin: -5px 0 0 -5px;
+ border-radius: 5px;
+ border: 3px solid #ff3a29;
+ /* Note, you must call this like:
+ .transition(~"value, value, value")*/
+
+ transition: width 2s, height 2s, margin 2s, border 2s;
+ -moz-transition: width 2s, height 2s, margin 2s, border 2s;
+ -webkit-transition: width 2s, height 2s, margin 2s, border 2s;
+ -o-transition: width 2s, height 2s, margin 2s, border 2s;
+}
+.togetherjs-click.togetherjs-clicking {
+ width: 40px;
+ height: 40px;
+ margin: -20px 0 0 -20px;
+ border-radius: 20px;
+ border: 3px solid rgba(0, 0, 0, 0);
+}
diff --git a/web/src/main/webapp/custom_ofmeet.js b/web/src/main/webapp/custom_ofmeet.js
index c72a7e90..276d77f2 100644
--- a/web/src/main/webapp/custom_ofmeet.js
+++ b/web/src/main/webapp/custom_ofmeet.js
@@ -14,13 +14,14 @@ var ofmeet = (function(of)
IMAGES.poll = '';
IMAGES.kanban = '';
IMAGES.whiteboard = '';
- IMAGES.person = ''
- IMAGES.contact = '';
- IMAGES.mic = '';
- IMAGES.mail = '';
- IMAGES.desktop = '';
+ IMAGES.person = ''
+ IMAGES.contact = '';
+ IMAGES.mic = '';
+ IMAGES.mail = '';
+ IMAGES.desktop = '';
IMAGES.confetti = '';
- IMAGES.picture = '';
+ IMAGES.picture = '';
+ IMAGES.cursor = '';
const SMILIES = [":)", ":(", ":D", ":+1:", ":P", ":wave:", ":blush:", ":slightly_smiling_face:", ":scream:", ":*", ":-1:", ":mag:", ":heart:", ":innocent:", ":angry:", ":angel:", ";(", ":clap:", ";)", ":beer:"];
const nickColors = {}, padsList = [], captions = {msgsDisabled: true, msgs: []}, breakout = {rooms: [], duration: 60, roomCount: 10, wait: 10}, pdf_body = [];
@@ -30,7 +31,7 @@ var ofmeet = (function(of)
let padsModalOpened = false, contactsModalOpened = false, swRegistration = null, participants = {}, recordingAudioTrack = {}, recordingVideoTrack = {}, videoRecorder = {}, recorderStreams = {}, customStore = {}, filenames = {}, dbnames = [];
let clockTrack = {start: 0, stop: 0, joins: 0, leaves: 0}, handsRaised = 0;
let tags = {location: "", date: (new Date()).toISOString().split('T')[0], subject: "", host: "", activity: ""};
- let audioTemporaryUnmuted = false;
+ let audioTemporaryUnmuted = false, cursorShared = false;
//-------------------------------------------------------
//
// window events
@@ -1577,7 +1578,7 @@ var ofmeet = (function(of)
function handlePresence(presence)
{
- console.debug("handlePresence", presence);
+ //console.debug("handlePresence", presence);
const Strophe = APP.connection.xmpp.connection.Strophe;
const id = Strophe.getResourceFromJid(presence.getAttribute("from"));
const raisedHand = presence.querySelector("jitsi_participant_raisedHand");
@@ -2148,42 +2149,79 @@ var ofmeet = (function(of)
console.debug("postJoinSetup");
- if (interfaceConfig.OFMEET_ENABLE_CRYPTPAD)
+ if (interfaceConfig.OFMEET_ENABLE_CRYPTPAD || interfaceConfig.OFMEET_ENABLE_WHITEBOARD || interfaceConfig.OFMEET_ENABLE_CONFETTI)
{
- createPadsButton();
- }
-
- if (interfaceConfig.OFMEET_ENABLE_WHITEBOARD && interfaceConfig.OFMEET_WHITEBOARD_URL && interfaceConfig.OFMEET_WHITEBOARD_URL != "")
- {
- APP.conference.commands.addCommandListener("WHITEBOARD", function()
+ APP.conference.commands.addCommandListener("CURSOR", function(event)
{
- const url = interfaceConfig.OFMEET_WHITEBOARD_URL.endsWith("/") ? interfaceConfig.OFMEET_WHITEBOARD_URL + APP.conference.roomName : interfaceConfig.OFMEET_WHITEBOARD_URL + "/" + APP.conference.roomName;
- window.open(url, 'ofmeet-whiteboard');
+ //console.log("CURSOR remote event", event);
+ window.top.postMessage({ action: 'ofmeet.action.share', json: JSON.parse(event.value)}, '*');
});
- const whiteboardButton = addToolbarItem('ofmeet-whiteboard', '', "Share a whiteboard", ".button-group-right");
+ const cursorButton = addToolbarItem('ofmeet-cursor', '', "Share cursor/mouse pointer", ".button-group-right");
- if (whiteboardButton) whiteboardButton.addEventListener("click", function(evt)
+ if (cursorButton) cursorButton.addEventListener("click", function(evt)
{
evt.stopPropagation();
- APP.conference.commands.sendCommandOnce("WHITEBOARD", {value: !0})
+
+ if (!cursorShared) {
+ handleCursorEvent({data: {event: 'ofmeet.event.url.message', action: 'setup', from: APP.conference.getLocalDisplayName()}});
+ window.addEventListener('message', handleCursorEvent);
+ } else {
+ handleCursorEvent({data: {event: 'ofmeet.event.url.message', action: 'destroy', from: APP.conference.getLocalDisplayName()}});
+ window.removeEventListener('message', handleCursorEvent);
+ }
+ cursorShared = !cursorShared;
});
- }
- if (interfaceConfig.OFMEET_ENABLE_CONFETTI)
- {
- APP.conference.commands.addCommandListener("CONFETTI", function()
+ if (interfaceConfig.OFMEET_ENABLE_CRYPTPAD)
{
- window.confetti({particleCount: 100, spread: 70, origin: {y: .6}})
- });
+ createPadsButton();
+ }
+
+ if (interfaceConfig.OFMEET_ENABLE_WHITEBOARD && interfaceConfig.OFMEET_WHITEBOARD_URL && interfaceConfig.OFMEET_WHITEBOARD_URL != "")
+ {
+ APP.conference.commands.addCommandListener("WHITEBOARD", function()
+ {
+ const url = interfaceConfig.OFMEET_WHITEBOARD_URL.endsWith("/") ? interfaceConfig.OFMEET_WHITEBOARD_URL + APP.conference.roomName : interfaceConfig.OFMEET_WHITEBOARD_URL + "/" + APP.conference.roomName;
+ window.open(url, 'ofmeet-whiteboard');
+ });
- const confettiButton = addToolbarItem('ofmeet-confetti', '', "Share some confetti", ".button-group-right");
+ const whiteboardButton = addToolbarItem('ofmeet-whiteboard', '', "Share a whiteboard", ".button-group-right");
- if (confettiButton) confettiButton.addEventListener("click", function(evt)
+ if (whiteboardButton) whiteboardButton.addEventListener("click", function(evt)
+ {
+ evt.stopPropagation();
+ APP.conference.commands.sendCommandOnce("WHITEBOARD", {value: !0})
+ });
+ }
+
+ if (interfaceConfig.OFMEET_ENABLE_CONFETTI)
{
- evt.stopPropagation();
- APP.conference.commands.sendCommandOnce("CONFETTI", {value: !0})
- });
+ APP.conference.commands.addCommandListener("CONFETTI", function()
+ {
+ window.confetti({particleCount: 100, spread: 70, origin: {y: .6}})
+ });
+
+ const confettiButton = addToolbarItem('ofmeet-confetti', '', "Share some confetti", ".button-group-right");
+
+ if (confettiButton) confettiButton.addEventListener("click", function(evt)
+ {
+ evt.stopPropagation();
+ APP.conference.commands.sendCommandOnce("CONFETTI", {value: !0})
+ });
+ }
+ }
+ }
+
+ function handleCursorEvent(event)
+ {
+ //console.log("handleCursorEvent", event.data);
+
+ if (event.data.event == 'ofmeet.event.url.message')
+ {
+ event.data.from = APP.conference.getLocalDisplayName();
+ const value = JSON.stringify(event.data);
+ APP.conference.commands.sendCommandOnce("CURSOR", {value: value});
}
}
diff --git a/web/src/main/webapp/index.html b/web/src/main/webapp/index.html
index fdf1f075..a5df570a 100644
--- a/web/src/main/webapp/index.html
+++ b/web/src/main/webapp/index.html
@@ -9,6 +9,7 @@
+
+
+
+
diff --git a/web/src/main/webapp/libs/lib-jitsi-meet.min.js b/web/src/main/webapp/libs/lib-jitsi-meet.min.js
index e9bd61ba..11fc46d1 100644
--- a/web/src/main/webapp/libs/lib-jitsi-meet.min.js
+++ b/web/src/main/webapp/libs/lib-jitsi-meet.min.js
@@ -13218,7 +13218,8 @@
this.focusMucJid = e, f.info(`Ignore focus: ${e}, real JID: ${t}`), this.xmpp.caps.getFeatures(t, 15e3).then(e => {
this.focusFeatures = e, f.info("Jicofo supports restart by terminate: " + this.supportsRestartByTerminate())
}, e => {
- f.error("Failed to discover Jicofo features", e && e.message)
+ // BAO
+ // f.error("Failed to discover Jicofo features", e && e.message)
})
}
setParticipantPropertyListener(e) {
@@ -17331,6 +17332,7 @@
this.pingInterval && window.clearInterval(this.pingInterval), this.analyticsInterval && window.clearInterval(this.analyticsInterval)
}
sendRequest() {
+/* BAO
const e = this.lastRequestId++,
t = {
type: "e2e-ping-request",
@@ -17340,6 +17342,7 @@
id: e,
timeSent: window.performance.now()
}
+*/
}
handleResponse(e) {
const t = this.requests[e.id];
diff --git a/web/src/main/webapp/tinycolor.js b/web/src/main/webapp/tinycolor.js
new file mode 100644
index 00000000..c1f5991b
--- /dev/null
+++ b/web/src/main/webapp/tinycolor.js
@@ -0,0 +1,869 @@
+// TinyColor v0.9.13
+// https://github.com/bgrins/TinyColor
+// 2012-11-28, Brian Grinstead, MIT License
+
+(function(root) {
+
+var trimLeft = /^[\s,#]+/,
+ trimRight = /\s+$/,
+ tinyCounter = 0,
+ math = Math,
+ mathRound = math.round,
+ mathMin = math.min,
+ mathMax = math.max,
+ mathRandom = math.random;
+
+function tinycolor (color, opts) {
+
+ color = (color) ? color : '';
+
+ // If input is already a tinycolor, return itself
+ if (typeof color == "object" && color.hasOwnProperty("_tc_id")) {
+ return color;
+ }
+
+ var rgb = inputToRGB(color);
+ var r = rgb.r,
+ g = rgb.g,
+ b = rgb.b,
+ a = rgb.a,
+ roundA = mathRound(100*a) / 100,
+ format = rgb.format;
+
+ // Don't let the range of [0,255] come back in [0,1].
+ // Potentially lose a little bit of precision here, but will fix issues where
+ // .5 gets interpreted as half of the total, instead of half of 1
+ // If it was supposed to be 128, this was already taken care of by `inputToRgb`
+ if (r < 1) { r = mathRound(r); }
+ if (g < 1) { g = mathRound(g); }
+ if (b < 1) { b = mathRound(b); }
+
+ return {
+ ok: rgb.ok,
+ format: format,
+ _tc_id: tinyCounter++,
+ alpha: a,
+ toHsv: function() {
+ var hsv = rgbToHsv(r, g, b);
+ return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: a };
+ },
+ toHsvString: function() {
+ var hsv = rgbToHsv(r, g, b);
+ var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
+ return (a == 1) ?
+ "hsv(" + h + ", " + s + "%, " + v + "%)" :
+ "hsva(" + h + ", " + s + "%, " + v + "%, "+ roundA + ")";
+ },
+ toHsl: function() {
+ var hsl = rgbToHsl(r, g, b);
+ return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: a };
+ },
+ toHslString: function() {
+ var hsl = rgbToHsl(r, g, b);
+ var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
+ return (a == 1) ?
+ "hsl(" + h + ", " + s + "%, " + l + "%)" :
+ "hsla(" + h + ", " + s + "%, " + l + "%, "+ roundA + ")";
+ },
+ toHex: function() {
+ return rgbToHex(r, g, b);
+ },
+ toHexString: function() {
+ return '#' + rgbToHex(r, g, b);
+ },
+ toRgb: function() {
+ return { r: mathRound(r), g: mathRound(g), b: mathRound(b), a: a };
+ },
+ toRgbString: function() {
+ return (a == 1) ?
+ "rgb(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ")" :
+ "rgba(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ", " + roundA + ")";
+ },
+ toPercentageRgb: function() {
+ return { r: mathRound(bound01(r, 255) * 100) + "%", g: mathRound(bound01(g, 255) * 100) + "%", b: mathRound(bound01(b, 255) * 100) + "%", a: a };
+ },
+ toPercentageRgbString: function() {
+ return (a == 1) ?
+ "rgb(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%)" :
+ "rgba(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%, " + roundA + ")";
+ },
+ toName: function() {
+ return hexNames[rgbToHex(r, g, b)] || false;
+ },
+ toFilter: function() {
+ var hex = rgbToHex(r, g, b);
+ var secondHex = hex;
+ var alphaHex = Math.round(parseFloat(a) * 255).toString(16);
+ var secondAlphaHex = alphaHex;
+ var gradientType = opts && opts.gradientType ? "GradientType = 1, " : "";
+
+ if (secondColor) {
+ var s = tinycolor(secondColor);
+ secondHex = s.toHex();
+ secondAlphaHex = Math.round(parseFloat(s.alpha) * 255).toString(16);
+ }
+
+ return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr=#" + pad2(alphaHex) + hex + ",endColorstr=#" + pad2(secondAlphaHex) + secondHex + ")";
+ },
+ toString: function(format) {
+ format = format || this.format;
+ var formattedString = false;
+ if (format === "rgb") {
+ formattedString = this.toRgbString();
+ }
+ if (format === "prgb") {
+ formattedString = this.toPercentageRgbString();
+ }
+ if (format === "hex") {
+ formattedString = this.toHexString();
+ }
+ if (format === "name") {
+ formattedString = this.toName();
+ }
+ if (format === "hsl") {
+ formattedString = this.toHslString();
+ }
+ if (format === "hsv") {
+ formattedString = this.toHsvString();
+ }
+
+ return formattedString || this.toHexString();
+ }
+ };
+}
+
+// If input is an object, force 1 into "1.0" to handle ratios properly
+// String input requires "1.0" as input, so 1 will be treated as 1
+tinycolor.fromRatio = function(color) {
+ if (typeof color == "object") {
+ var newColor = {};
+ for (var i in color) {
+ newColor[i] = convertToPercentage(color[i]);
+ }
+ color = newColor;
+ }
+
+ return tinycolor(color);
+};
+
+// Given a string or object, convert that input to RGB
+// Possible string inputs:
+//
+// "red"
+// "#f00" or "f00"
+// "#ff0000" or "ff0000"
+// "rgb 255 0 0" or "rgb (255, 0, 0)"
+// "rgb 1.0 0 0" or "rgb (1, 0, 0)"
+// "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
+// "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
+// "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
+// "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
+// "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
+//
+function inputToRGB(color) {
+
+ var rgb = { r: 255, g: 255, b: 255 };
+ var a = 1;
+ var ok = false;
+ var format = false;
+
+ if (typeof color == "string") {
+ color = stringInputToObject(color);
+ }
+
+ if (typeof color == "object") {
+ if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
+ rgb = rgbToRgb(color.r, color.g, color.b);
+ ok = true;
+ format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
+ }
+ else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
+ color.s = convertToPercentage(color.s);
+ color.v = convertToPercentage(color.v);
+ rgb = hsvToRgb(color.h, color.s, color.v);
+ ok = true;
+ format = "hsv";
+ }
+ else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
+ color.s = convertToPercentage(color.s);
+ color.l = convertToPercentage(color.l);
+ rgb = hslToRgb(color.h, color.s, color.l);
+ ok = true;
+ format = "hsl";
+ }
+
+ if (color.hasOwnProperty("a")) {
+ a = color.a;
+ }
+ }
+
+ a = parseFloat(a);
+
+ // Handle invalid alpha characters by setting to 1
+ if (isNaN(a) || a < 0 || a > 1) {
+ a = 1;
+ }
+
+ return {
+ ok: ok,
+ format: color.format || format,
+ r: mathMin(255, mathMax(rgb.r, 0)),
+ g: mathMin(255, mathMax(rgb.g, 0)),
+ b: mathMin(255, mathMax(rgb.b, 0)),
+ a: a
+ };
+}
+
+
+
+// Conversion Functions
+// --------------------
+
+// `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
+//
+
+// `rgbToRgb`
+// Handle bounds / percentage checking to conform to CSS color spec
+//
+// *Assumes:* r, g, b in [0, 255] or [0, 1]
+// *Returns:* { r, g, b } in [0, 255]
+function rgbToRgb(r, g, b){
+ return {
+ r: bound01(r, 255) * 255,
+ g: bound01(g, 255) * 255,
+ b: bound01(b, 255) * 255
+ };
+}
+
+// `rgbToHsl`
+// Converts an RGB color value to HSL.
+// *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
+// *Returns:* { h, s, l } in [0,1]
+function rgbToHsl(r, g, b) {
+
+ r = bound01(r, 255);
+ g = bound01(g, 255);
+ b = bound01(b, 255);
+
+ var max = mathMax(r, g, b), min = mathMin(r, g, b);
+ var h, s, l = (max + min) / 2;
+
+ if(max == min) {
+ h = s = 0; // achromatic
+ }
+ else {
+ var d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ switch(max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+
+ h /= 6;
+ }
+
+ return { h: h, s: s, l: l };
+}
+
+// `hslToRgb`
+// Converts an HSL color value to RGB.
+// *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
+// *Returns:* { r, g, b } in the set [0, 255]
+function hslToRgb(h, s, l) {
+ var r, g, b;
+
+ h = bound01(h, 360);
+ s = bound01(s, 100);
+ l = bound01(l, 100);
+
+ function hue2rgb(p, q, t) {
+ if(t < 0) t += 1;
+ if(t > 1) t -= 1;
+ if(t < 1/6) return p + (q - p) * 6 * t;
+ if(t < 1/2) return q;
+ if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
+ return p;
+ }
+
+ if(s === 0) {
+ r = g = b = l; // achromatic
+ }
+ else {
+ var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ var p = 2 * l - q;
+ r = hue2rgb(p, q, h + 1/3);
+ g = hue2rgb(p, q, h);
+ b = hue2rgb(p, q, h - 1/3);
+ }
+
+ return { r: r * 255, g: g * 255, b: b * 255 };
+}
+
+// `rgbToHsv`
+// Converts an RGB color value to HSV
+// *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
+// *Returns:* { h, s, v } in [0,1]
+function rgbToHsv(r, g, b) {
+
+ r = bound01(r, 255);
+ g = bound01(g, 255);
+ b = bound01(b, 255);
+
+ var max = mathMax(r, g, b), min = mathMin(r, g, b);
+ var h, s, v = max;
+
+ var d = max - min;
+ s = max === 0 ? 0 : d / max;
+
+ if(max == min) {
+ h = 0; // achromatic
+ }
+ else {
+ switch(max) {
+ case r: h = (g - b) / d + (g < b ? 6 : 0); break;
+ case g: h = (b - r) / d + 2; break;
+ case b: h = (r - g) / d + 4; break;
+ }
+ h /= 6;
+ }
+ return { h: h, s: s, v: v };
+}
+
+// `hsvToRgb`
+// Converts an HSV color value to RGB.
+// *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
+// *Returns:* { r, g, b } in the set [0, 255]
+ function hsvToRgb(h, s, v) {
+
+ h = bound01(h, 360) * 6;
+ s = bound01(s, 100);
+ v = bound01(v, 100);
+
+ var i = math.floor(h),
+ f = h - i,
+ p = v * (1 - s),
+ q = v * (1 - f * s),
+ t = v * (1 - (1 - f) * s),
+ mod = i % 6,
+ r = [v, q, p, p, t, v][mod],
+ g = [t, v, v, q, p, p][mod],
+ b = [p, p, t, v, v, q][mod];
+
+ return { r: r * 255, g: g * 255, b: b * 255 };
+}
+
+// `rgbToHex`
+// Converts an RGB color to hex
+// Assumes r, g, and b are contained in the set [0, 255]
+// Returns a 3 or 6 character hex
+function rgbToHex(r, g, b) {
+ var hex = [
+ pad2(mathRound(r).toString(16)),
+ pad2(mathRound(g).toString(16)),
+ pad2(mathRound(b).toString(16))
+ ];
+
+ // Return a 3 character hex if possible
+ if (hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
+ return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
+ }
+
+ return hex.join("");
+}
+
+// `equals`
+// Can be called with any tinycolor input
+tinycolor.equals = function (color1, color2) {
+ if (!color1 || !color2) { return false; }
+ return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
+};
+tinycolor.random = function() {
+ return tinycolor.fromRatio({
+ r: mathRandom(),
+ g: mathRandom(),
+ b: mathRandom()
+ });
+};
+
+
+// Modification Functions
+// ----------------------
+// Thanks to less.js for some of the basics here
+//
+
+
+tinycolor.desaturate = function (color, amount) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.s -= ((amount || 10) / 100);
+ hsl.s = clamp01(hsl.s);
+ return tinycolor(hsl);
+};
+tinycolor.saturate = function (color, amount) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.s += ((amount || 10) / 100);
+ hsl.s = clamp01(hsl.s);
+ return tinycolor(hsl);
+};
+tinycolor.greyscale = function(color) {
+ return tinycolor.desaturate(color, 100);
+};
+tinycolor.lighten = function(color, amount) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.l += ((amount || 10) / 100);
+ hsl.l = clamp01(hsl.l);
+ return tinycolor(hsl);
+};
+tinycolor.darken = function (color, amount) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.l -= ((amount || 10) / 100);
+ hsl.l = clamp01(hsl.l);
+ return tinycolor(hsl);
+};
+tinycolor.complement = function(color) {
+ var hsl = tinycolor(color).toHsl();
+ hsl.h = (hsl.h + 180) % 360;
+ return tinycolor(hsl);
+};
+
+
+// Combination Functions
+// ---------------------
+// Thanks to jQuery xColor for some of the ideas behind these
+//
+
+tinycolor.triad = function(color) {
+ var hsl = tinycolor(color).toHsl();
+ var h = hsl.h;
+ return [
+ tinycolor(color),
+ tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
+ tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
+ ];
+};
+tinycolor.tetrad = function(color) {
+ var hsl = tinycolor(color).toHsl();
+ var h = hsl.h;
+ return [
+ tinycolor(color),
+ tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
+ tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
+ tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
+ ];
+};
+tinycolor.splitcomplement = function(color) {
+ var hsl = tinycolor(color).toHsl();
+ var h = hsl.h;
+ return [
+ tinycolor(color),
+ tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
+ tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
+ ];
+};
+tinycolor.analogous = function(color, results, slices) {
+ results = results || 6;
+ slices = slices || 30;
+
+ var hsl = tinycolor(color).toHsl();
+ var part = 360 / slices;
+ var ret = [tinycolor(color)];
+
+ for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
+ hsl.h = (hsl.h + part) % 360;
+ ret.push(tinycolor(hsl));
+ }
+ return ret;
+};
+tinycolor.monochromatic = function(color, results) {
+ results = results || 6;
+ var hsv = tinycolor(color).toHsv();
+ var h = hsv.h, s = hsv.s, v = hsv.v;
+ var ret = [];
+ var modification = 1 / results;
+
+ while (results--) {
+ ret.push(tinycolor({ h: h, s: s, v: v}));
+ v = (v + modification) % 1;
+ }
+
+ return ret;
+};
+// Readability based on W3C recommendations: http://www.w3.org/TR/AERT#color-contrast
+// Returns object with two properties:
+// .brightness: the difference in brightness between the two colors
+// .color: the difference in color/hue between the two colors
+// An "acceptable" color is considered to have a brightness difference of 125 and a
+// color difference of 500
+tinycolor.readability = function(color1, color2) {
+ var a = tinycolor(color1).toRgb(), b = tinycolor(color2).toRgb();
+ var brightnessA = (a.r * 299 + a.g * 587 + a.b * 114) / 1000;
+ var brightnessB = (b.r * 299 + b.g * 587 + b.b * 114) / 1000;
+ var colorDiff = (
+ Math.max(a.r, b.r) - Math.min(a.r, b.r) +
+ Math.max(a.g, b.g) - Math.min(a.g, b.g) +
+ Math.max(a.b, b.b) - Math.min(a.b, b.b));
+ return {
+ brightness: Math.abs(brightnessA - brightnessB),
+ color: colorDiff
+ };
+};
+// True if using color1 over color2 (or vice versa) is "readable"
+// Based on: http://www.w3.org/TR/AERT#color-contrast
+// Example:
+// tinycolor.readable("#000", "#111") => false
+tinycolor.readable = function(color1, color2) {
+ var readability = tinycolor.readability(color1, color2);
+ return readability.brightness > 125 && readability.color > 500;
+};
+// Given a base color and a list of possible foreground or background
+// colors for that base, returns the most readable color.
+// Example:
+// tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
+tinycolor.mostReadable = function(baseColor, colorList) {
+ var bestColor;
+ var bestScore = 0;
+ var bestIsReadable = false;
+ for (var i=0; i < colorList.length; i++) {
+ var readability = tinycolor.readability(baseColor, colorList[i]);
+ var readable = readability.brightness > 125 && readability.color > 500;
+ // We normalize both around the "acceptable" breaking point,
+ // but rank brightness constrast higher than hue. Why? I'm
+ // not sure, seems reasonable.
+ var score = 3 * (readability.brightness / 125) + (readability.color / 500);
+ if ((readable && ! bestIsReadable) ||
+ (readable && bestIsReadable && score > bestScore) ||
+ ((! readable) && (! bestIsReadable) && score > bestScore)) {
+ bestIsReadable = readable;
+ bestScore = score;
+ bestColor = colorList[i];
+ }
+ }
+ return bestColor;
+};
+
+
+// Big List of Colors
+// ---------
+//
+var names = tinycolor.names = {
+ aliceblue: "f0f8ff",
+ antiquewhite: "faebd7",
+ aqua: "0ff",
+ aquamarine: "7fffd4",
+ azure: "f0ffff",
+ beige: "f5f5dc",
+ bisque: "ffe4c4",
+ black: "000",
+ blanchedalmond: "ffebcd",
+ blue: "00f",
+ blueviolet: "8a2be2",
+ brown: "a52a2a",
+ burlywood: "deb887",
+ burntsienna: "ea7e5d",
+ cadetblue: "5f9ea0",
+ chartreuse: "7fff00",
+ chocolate: "d2691e",
+ coral: "ff7f50",
+ cornflowerblue: "6495ed",
+ cornsilk: "fff8dc",
+ crimson: "dc143c",
+ cyan: "0ff",
+ darkblue: "00008b",
+ darkcyan: "008b8b",
+ darkgoldenrod: "b8860b",
+ darkgray: "a9a9a9",
+ darkgreen: "006400",
+ darkgrey: "a9a9a9",
+ darkkhaki: "bdb76b",
+ darkmagenta: "8b008b",
+ darkolivegreen: "556b2f",
+ darkorange: "ff8c00",
+ darkorchid: "9932cc",
+ darkred: "8b0000",
+ darksalmon: "e9967a",
+ darkseagreen: "8fbc8f",
+ darkslateblue: "483d8b",
+ darkslategray: "2f4f4f",
+ darkslategrey: "2f4f4f",
+ darkturquoise: "00ced1",
+ darkviolet: "9400d3",
+ deeppink: "ff1493",
+ deepskyblue: "00bfff",
+ dimgray: "696969",
+ dimgrey: "696969",
+ dodgerblue: "1e90ff",
+ firebrick: "b22222",
+ floralwhite: "fffaf0",
+ forestgreen: "228b22",
+ fuchsia: "f0f",
+ gainsboro: "dcdcdc",
+ ghostwhite: "f8f8ff",
+ gold: "ffd700",
+ goldenrod: "daa520",
+ gray: "808080",
+ green: "008000",
+ greenyellow: "adff2f",
+ grey: "808080",
+ honeydew: "f0fff0",
+ hotpink: "ff69b4",
+ indianred: "cd5c5c",
+ indigo: "4b0082",
+ ivory: "fffff0",
+ khaki: "f0e68c",
+ lavender: "e6e6fa",
+ lavenderblush: "fff0f5",
+ lawngreen: "7cfc00",
+ lemonchiffon: "fffacd",
+ lightblue: "add8e6",
+ lightcoral: "f08080",
+ lightcyan: "e0ffff",
+ lightgoldenrodyellow: "fafad2",
+ lightgray: "d3d3d3",
+ lightgreen: "90ee90",
+ lightgrey: "d3d3d3",
+ lightpink: "ffb6c1",
+ lightsalmon: "ffa07a",
+ lightseagreen: "20b2aa",
+ lightskyblue: "87cefa",
+ lightslategray: "789",
+ lightslategrey: "789",
+ lightsteelblue: "b0c4de",
+ lightyellow: "ffffe0",
+ lime: "0f0",
+ limegreen: "32cd32",
+ linen: "faf0e6",
+ magenta: "f0f",
+ maroon: "800000",
+ mediumaquamarine: "66cdaa",
+ mediumblue: "0000cd",
+ mediumorchid: "ba55d3",
+ mediumpurple: "9370db",
+ mediumseagreen: "3cb371",
+ mediumslateblue: "7b68ee",
+ mediumspringgreen: "00fa9a",
+ mediumturquoise: "48d1cc",
+ mediumvioletred: "c71585",
+ midnightblue: "191970",
+ mintcream: "f5fffa",
+ mistyrose: "ffe4e1",
+ moccasin: "ffe4b5",
+ navajowhite: "ffdead",
+ navy: "000080",
+ oldlace: "fdf5e6",
+ olive: "808000",
+ olivedrab: "6b8e23",
+ orange: "ffa500",
+ orangered: "ff4500",
+ orchid: "da70d6",
+ palegoldenrod: "eee8aa",
+ palegreen: "98fb98",
+ paleturquoise: "afeeee",
+ palevioletred: "db7093",
+ papayawhip: "ffefd5",
+ peachpuff: "ffdab9",
+ peru: "cd853f",
+ pink: "ffc0cb",
+ plum: "dda0dd",
+ powderblue: "b0e0e6",
+ purple: "800080",
+ red: "f00",
+ rosybrown: "bc8f8f",
+ royalblue: "4169e1",
+ saddlebrown: "8b4513",
+ salmon: "fa8072",
+ sandybrown: "f4a460",
+ seagreen: "2e8b57",
+ seashell: "fff5ee",
+ sienna: "a0522d",
+ silver: "c0c0c0",
+ skyblue: "87ceeb",
+ slateblue: "6a5acd",
+ slategray: "708090",
+ slategrey: "708090",
+ snow: "fffafa",
+ springgreen: "00ff7f",
+ steelblue: "4682b4",
+ tan: "d2b48c",
+ teal: "008080",
+ thistle: "d8bfd8",
+ tomato: "ff6347",
+ turquoise: "40e0d0",
+ violet: "ee82ee",
+ wheat: "f5deb3",
+ white: "fff",
+ whitesmoke: "f5f5f5",
+ yellow: "ff0",
+ yellowgreen: "9acd32"
+};
+
+// Make it easy to access colors via `hexNames[hex]`
+var hexNames = tinycolor.hexNames = flip(names);
+
+
+// Utilities
+// ---------
+
+// `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
+function flip(o) {
+ var flipped = { };
+ for (var i in o) {
+ if (o.hasOwnProperty(i)) {
+ flipped[o[i]] = i;
+ }
+ }
+ return flipped;
+}
+
+// Take input from [0, n] and return it as [0, 1]
+function bound01(n, max) {
+ if (isOnePointZero(n)) { n = "100%"; }
+
+ var processPercent = isPercentage(n);
+ n = mathMin(max, mathMax(0, parseFloat(n)));
+
+ // Automatically convert percentage into number
+ if (processPercent) {
+ n = parseInt(n * max, 10) / 100;
+ }
+
+ // Handle floating point rounding errors
+ if ((math.abs(n - max) < 0.000001)) {
+ return 1;
+ }
+
+ // Convert into [0, 1] range if it isn't already
+ return (n % max) / parseFloat(max);
+}
+
+// Force a number between 0 and 1
+function clamp01(val) {
+ return mathMin(1, mathMax(0, val));
+}
+
+// Parse an integer into hex
+function parseHex(val) {
+ return parseInt(val, 16);
+}
+
+// Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
+//
+function isOnePointZero(n) {
+ return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
+}
+
+// Check to see if string passed in is a percentage
+function isPercentage(n) {
+ return typeof n === "string" && n.indexOf('%') != -1;
+}
+
+// Force a hex value to have 2 characters
+function pad2(c) {
+ return c.length == 1 ? '0' + c : '' + c;
+}
+
+// Replace a decimal with it's percentage value
+function convertToPercentage(n) {
+ if (n <= 1) {
+ n = (n * 100) + "%";
+ }
+
+ return n;
+}
+
+var matchers = (function() {
+
+ //
+ var CSS_INTEGER = "[-\\+]?\\d+%?";
+
+ //
+ var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
+
+ // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
+ var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
+
+ // Actual matching.
+ // Parentheses and commas are optional, but not required.
+ // Whitespace can take the place of commas or opening paren
+ var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+ var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
+
+ return {
+ rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
+ rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
+ hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
+ hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
+ hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
+ hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
+ hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
+ };
+})();
+
+// `stringInputToObject`
+// Permissive string parsing. Take in a number of formats, and output an object
+// based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
+function stringInputToObject(color) {
+
+ color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
+ var named = false;
+ if (names[color]) {
+ color = names[color];
+ named = true;
+ }
+ else if (color == 'transparent') {
+ return { r: 0, g: 0, b: 0, a: 0 };
+ }
+
+ // Try to match string input using regular expressions.
+ // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
+ // Just return an object and let the conversion functions handle that.
+ // This way the result will be the same whether the tinycolor is initialized with string or object.
+ var match;
+ if ((match = matchers.rgb.exec(color))) {
+ return { r: match[1], g: match[2], b: match[3] };
+ }
+ if ((match = matchers.rgba.exec(color))) {
+ return { r: match[1], g: match[2], b: match[3], a: match[4] };
+ }
+ if ((match = matchers.hsl.exec(color))) {
+ return { h: match[1], s: match[2], l: match[3] };
+ }
+ if ((match = matchers.hsla.exec(color))) {
+ return { h: match[1], s: match[2], l: match[3], a: match[4] };
+ }
+ if ((match = matchers.hsv.exec(color))) {
+ return { h: match[1], s: match[2], v: match[3] };
+ }
+ if ((match = matchers.hex6.exec(color))) {
+ return {
+ r: parseHex(match[1]),
+ g: parseHex(match[2]),
+ b: parseHex(match[3]),
+ format: named ? "name" : "hex"
+ };
+ }
+ if ((match = matchers.hex3.exec(color))) {
+ return {
+ r: parseHex(match[1] + '' + match[1]),
+ g: parseHex(match[2] + '' + match[2]),
+ b: parseHex(match[3] + '' + match[3]),
+ format: named ? "name" : "hex"
+ };
+ }
+
+ return false;
+}
+
+// Node: Export function
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = tinycolor;
+}
+// AMD/requirejs: Define the module
+else if (typeof define !== "undefined") {
+ define(function () {return tinycolor;});
+}
+// Browser: Expose to window
+else {
+ root.tinycolor = tinycolor;
+}
+
+})(this);
diff --git a/web/src/main/webapp/util.js b/web/src/main/webapp/util.js
new file mode 100644
index 00000000..e1b0dca1
--- /dev/null
+++ b/web/src/main/webapp/util.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+ var util = {};
+
+ util.urlParam = function(name)
+ {
+ var results = new RegExp('[\\?&]' + name + '=([^]*)').exec(window.location.href);
+ if (!results) { return undefined; }
+ return unescape(results[1] || undefined);
+ };
+
+ util.assert = function (cond) {
+ if (! cond) {
+ var args = ["Assertion error:"].concat(Array.prototype.slice.call(arguments, 1));
+ console.error.apply(console, args);
+ if (console.trace) {
+ console.trace();
+ }
+ throw new Error(args.join(" "));
+ }
+ };
+
+ util.extend = function (base, extensions) {
+ if (! extensions) {
+ extensions = base;
+ base = {};
+ }
+ for (var a in extensions) {
+ if (extensions.hasOwnProperty(a)) {
+ base[a] = extensions[a];
+ }
+ }
+ return base;
+ };
+
+ util.Class = function (superClass, prototype) {
+ var a;
+ if (prototype === undefined) {
+ prototype = superClass;
+ } else {
+ if (superClass.prototype) {
+ superClass = superClass.prototype;
+ }
+ var newPrototype = Object.create(superClass);
+ for (a in prototype) {
+ if (prototype.hasOwnProperty(a)) {
+ newPrototype[a] = prototype[a];
+ }
+ }
+ prototype = newPrototype;
+ }
+ var ClassObject = function () {
+ var obj = Object.create(prototype);
+ obj.constructor.apply(obj, arguments);
+ obj.constructor = ClassObject;
+ return obj;
+ };
+ ClassObject.prototype = prototype;
+ if (prototype.constructor.name) {
+ ClassObject.className = prototype.constructor.name;
+ ClassObject.toString = function () {
+ return '[Class ' + this.className + ']';
+ };
+ }
+ if (prototype.classMethods) {
+ for (a in prototype.classMethods) {
+ if (prototype.classMethods.hasOwnProperty(a)) {
+ ClassObject[a] = prototype.classMethods[a];
+ }
+ }
+ }
+ return ClassObject;
+ };
\ No newline at end of file