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 = $('
Dummy
'); + 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', '
' + IMAGES.whiteboard + '
', "Share a whiteboard", ".button-group-right"); + const cursorButton = addToolbarItem('ofmeet-cursor', '
' + IMAGES.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', '
' + IMAGES.confetti + '
', "Share some confetti", ".button-group-right"); + const whiteboardButton = addToolbarItem('ofmeet-whiteboard', '
' + IMAGES.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', '
' + IMAGES.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 = $('
Dummy
'); + 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', '
' + IMAGES.whiteboard + '
', "Share a whiteboard", ".button-group-right"); + const cursorButton = addToolbarItem('ofmeet-cursor', '
' + IMAGES.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', '
' + IMAGES.confetti + '
', "Share some confetti", ".button-group-right"); + const whiteboardButton = addToolbarItem('ofmeet-whiteboard', '
' + IMAGES.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', '
' + IMAGES.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