From 8df1dc19f2ac90f899b49518faed1eaf271865f9 Mon Sep 17 00:00:00 2001 From: MTRNord Date: Tue, 31 Jul 2018 19:27:13 +0200 Subject: [PATCH 01/82] Add Color picker for board title and let it have less space between boards. (Default colors are still working) --- www/kanban/app-kanban.less | 22 +- www/kanban/inner.js | 35 +- www/kanban/jkanban.js | 64 +- www/kanban/jscolor.js | 1855 ++++++++++++++++++++++++++++++++++++ 4 files changed, 1939 insertions(+), 37 deletions(-) create mode 100644 www/kanban/jscolor.js diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less index ab49cfda5..2985f2fe7 100644 --- a/www/kanban/app-kanban.less +++ b/www/kanban/app-kanban.less @@ -3,8 +3,8 @@ @import (once) "../../customize/src/less2/include/tools.less"; .framework_main( @bg-color: @colortheme_kanban-bg, -@warn-color: @colortheme_kanban-warn, -@color: @colortheme_kanban-color); + @warn-color: @colortheme_kanban-warn, + @color: @colortheme_kanban-color); // body &.cp-app-kanban { @@ -115,39 +115,39 @@ } .kanban-header-yellow { - background: #FC3; + background: #FC3 !important; } .kanban-header-orange { - background: #F91; + background: #F91 !important; } .kanban-header-blue { - background: #0AC; + background: #0AC !important; } .kanban-header-red { - background: #E43; + background: #E43 !important; } .kanban-header-green { - background: #8C4; + background: #8C4 !important; } .kanban-header-purple { - background: #c851ff; + background: #c851ff !important; } .kanban-header-cyan { - background: #00ffff; + background: #00ffff !important; } .kanban-header-lightgreen { - background: #c3ff5b; + background: #c3ff5b !important; } .kanban-header-lightblue { - background: #adeeff; + background: #adeeff !important; } @media (max-width: @browser_media-medium-screen) { diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 647032548..ad1a3e64f 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -10,6 +10,7 @@ define([ '/common/modes.js', '/customize/messages.js', '/kanban/jkanban.js', + '/kanban/jscolor.js', 'css!/kanban/jkanban.css', ], function ( $, @@ -105,7 +106,7 @@ define([ var kanban = new window.jKanban({ element: '#cp-app-kanban-content', - gutter: '15px', + gutter: '5px', widthBoard: '300px', buttonContent: '❌', colors: COLORS, @@ -209,22 +210,26 @@ define([ verbose("in color click"); var board = $(el.parentNode).attr("data-id"); var boardJSON = kanban.getBoardJSON(board); + var onchange = function (colorL) { + var elL = el; + var boardL = $(elL.parentNode).attr("data-id"); + var boardJSONL = kanban.getBoardJSON(boardL); + var currentColor = boardJSONL.color; + verbose("Current color " + currentColor); + if (currentColor !== colorL.toString()) { + $(elL).removeClass("kanban-header-" + currentColor); + boardJSONL.color = colorL.toString(); + kanban.onChange(); + } + }; + var jscolorL; + el._jscLinkedInstance = undefined; + jscolorL = new jscolor(el,{onFineChange: onchange, valueElement:undefined}); + jscolorL.show(); var currentColor = boardJSON.color; - verbose("Current color " + currentColor); - var index = kanban.options.colors.findIndex(function (element) { - return (element === currentColor); - }) + 1; - verbose("Next index " + index); - if (index >= kanban.options.colors.length) { index = 0; } - var nextColor = kanban.options.colors[index]; - verbose("Next color " + nextColor); - boardJSON.color = nextColor; - $(el).removeClass("kanban-header-" + currentColor); - $(el).addClass("kanban-header-" + nextColor); - kanban.onChange(); + jscolorL.fromString(currentColor); }, - buttonClick: function (el, boardId, e) { - e.stopPropagation(); + buttonClick: function (el, boardId) { if (framework.isReadOnly() || framework.isLocked()) { return; } UI.confirm(Messages.kanban_deleteBoard, function (yes) { if (!yes) { return; } diff --git a/www/kanban/jkanban.js b/www/kanban/jkanban.js index 02e395635..e70a5d92e 100644 --- a/www/kanban/jkanban.js +++ b/www/kanban/jkanban.js @@ -52,6 +52,7 @@ widthBoard: '250px', responsive: '700', colors: ["yellow", "green", "blue", "red", "orange"], + responsivePercentage: false, boards: [], dragBoards: true, addItemButton: false, @@ -85,12 +86,12 @@ //Init Drag Board self.drakeBoard = self.dragula([self.container], { moves: function (el, source, handle, sibling) { - if (self.options.readOnly) { return false; } + if (self.options.readOnly) { return false; } if (!self.options.dragBoards) return false; return (handle.classList.contains('kanban-board-header') || handle.classList.contains('kanban-title-board')); }, accepts: function (el, target, source, sibling) { - if (self.options.readOnly) { return false; } + if (self.options.readOnly) { return false; } return target.classList.contains('kanban-container'); }, revertOnSpill: true, @@ -112,7 +113,7 @@ el.classList.remove('is-moving'); self.options.dropBoard(el, target, source, sibling); if (typeof (el.dropfn) === 'function') - el.dropfn(el, target, source, sibling); + el.dropfn(el, target, source, sibling); el.dropfn(el, target, source, sibling); // TODO: update board object board order console.log("Drop " + $(el).attr("data-id") + " just before " + (sibling ? $(sibling).attr("data-id") : " end ")); @@ -145,15 +146,18 @@ //Init Drag Item self.drake = self.dragula(self.boardContainer, { moves: function (el, source, handle, sibling) { - if (self.options.readOnly) { return false; } + if (self.options.readOnly) { return false; } return handle.classList.contains('kanban-item'); }, accepts: function (el, target, source, sibling) { - if (self.options.readOnly) { return false; } + if (self.options.readOnly) { return false; } return true; }, revertOnSpill: true }) + .on('cancel', function(el, container, source) { + self.enableAllBoards(); + }) .on('drag', function (el, source) { // we need to calculate the position before starting to drag self.dragItemPos = self.findElementPosition(el); @@ -184,7 +188,9 @@ var boardId = source.parentNode.dataset.id; self.options.dragcancelEl(el, boardId); }) - .on('drop', function (el, target, source, sibling) { + .on('drop', function(el, target, source, sibling) { + self.enableAllBoards(); + console.log("In drop"); // TODO: update board object board order @@ -229,7 +235,7 @@ // if (board1==board2 && pos2 0 && allB !== undefined) { + for (var i = 0; i < allB.length; i++) { + allB[i].classList.remove('disabled-board'); + } + } + }; + this.addElement = function (boardID, element) { - // add Element to JSON + // add Element to JSON var boardJSON = __findBoardJSON(boardID); boardJSON.item.push({ title: element.title @@ -272,8 +287,19 @@ return self; }; - this.addBoards = function (boards) { - var boardWidth = self.options.widthBoard; + + this.addBoards = function(boards) { + if (self.options.responsivePercentage) { + self.container.style.width = '100%'; + self.options.gutter = '1%'; + if (window.innerWidth > self.options.responsive) { + var boardWidth = (100 - boards.length * 2) / boards.length; + } else { + var boardWidth = 100 - (boards.length * 2); + } + } else { + var boardWidth = self.options.widthBoard; + } var addButton = self.options.addItemButton; var buttonContent = self.options.buttonContent; @@ -285,12 +311,24 @@ if (self.options.boards !== boards) self.options.boards.push(board); + /*if (!self.options.responsivePercentage) { + //add width to container + if (self.container.style.width === '') { + self.container.style.width = parseInt(boardWidth) + (parseInt(self.options.gutter) * 2) + 'px'; + } else { + self.container.style.width = parseInt(self.container.style.width) + parseInt(boardWidth) + (parseInt(self.options.gutter) * 2) + 'px'; + } + }*/ //create node var boardNode = document.createElement('div'); boardNode.dataset.id = board.id; boardNode.classList.add('kanban-board'); //set style - boardNode.style.width = boardWidth; + if (self.options.responsivePercentage) { + boardNode.style.width = boardWidth + '%'; + } else { + boardNode.style.width = boardWidth; + } boardNode.style.marginLeft = self.options.gutter; boardNode.style.marginRight = self.options.gutter; // header board @@ -303,6 +341,10 @@ headerBoard.classList.add(value); }); if (board.color !== '' && board.color !== undefined) { + headerBoard._jscLinkedInstance = undefined; + jscolorL = new jscolor(headerBoard,{valueElement:undefined}); + jscolorL.fromString(board.color); + headerBoard._jscLinkedInstance = undefined; headerBoard.classList.add("kanban-header-" + board.color); } titleBoard = document.createElement('div'); diff --git a/www/kanban/jscolor.js b/www/kanban/jscolor.js new file mode 100644 index 000000000..c9bfa795b --- /dev/null +++ b/www/kanban/jscolor.js @@ -0,0 +1,1855 @@ +/** + * jscolor - JavaScript Color Picker + * + * @link http://jscolor.com + * @license For open source use: GPLv3 + * For commercial use: JSColor Commercial License + * @author Jan Odvarko + * @version 2.0.5 + * + * See usage examples at http://jscolor.com/examples/ + */ + + +"use strict"; + + +if (!window.jscolor) { window.jscolor = (function () { + + +var jsc = { + + + register : function () { + jsc.attachDOMReadyEvent(jsc.init); + jsc.attachEvent(document, 'mousedown', jsc.onDocumentMouseDown); + jsc.attachEvent(document, 'touchstart', jsc.onDocumentTouchStart); + jsc.attachEvent(window, 'resize', jsc.onWindowResize); + }, + + + init : function () { + if (jsc.jscolor.lookupClass) { + jsc.jscolor.installByClassName(jsc.jscolor.lookupClass); + } + }, + + + tryInstallOnElements : function (elms, className) { + var matchClass = new RegExp('(^|\\s)(' + className + ')(\\s*(\\{[^}]*\\})|\\s|$)', 'i'); + + for (var i = 0; i < elms.length; i += 1) { + if (elms[i].type !== undefined && elms[i].type.toLowerCase() == 'color') { + if (jsc.isColorAttrSupported) { + // skip inputs of type 'color' if supported by the browser + continue; + } + } + var m; + if (!elms[i].jscolor && elms[i].className && (m = elms[i].className.match(matchClass))) { + var targetElm = elms[i]; + var optsStr = null; + + var dataOptions = jsc.getDataAttr(targetElm, 'jscolor'); + if (dataOptions !== null) { + optsStr = dataOptions; + } else if (m[4]) { + optsStr = m[4]; + } + + var opts = {}; + if (optsStr) { + try { + opts = (new Function ('return (' + optsStr + ')'))(); + } catch(eParseError) { + jsc.warn('Error parsing jscolor options: ' + eParseError + ':\n' + optsStr); + } + } + targetElm.jscolor = new jsc.jscolor(targetElm, opts); + } + } + }, + + + isColorAttrSupported : (function () { + var elm = document.createElement('input'); + if (elm.setAttribute) { + elm.setAttribute('type', 'color'); + if (elm.type.toLowerCase() == 'color') { + return true; + } + } + return false; + })(), + + + isCanvasSupported : (function () { + var elm = document.createElement('canvas'); + return !!(elm.getContext && elm.getContext('2d')); + })(), + + + fetchElement : function (mixed) { + return typeof mixed === 'string' ? document.getElementById(mixed) : mixed; + }, + + + isElementType : function (elm, type) { + return elm.nodeName.toLowerCase() === type.toLowerCase(); + }, + + + getDataAttr : function (el, name) { + var attrName = 'data-' + name; + var attrValue = el.getAttribute(attrName); + if (attrValue !== null) { + return attrValue; + } + return null; + }, + + + attachEvent : function (el, evnt, func) { + if (el.addEventListener) { + el.addEventListener(evnt, func, false); + } else if (el.attachEvent) { + el.attachEvent('on' + evnt, func); + } + }, + + + detachEvent : function (el, evnt, func) { + if (el.removeEventListener) { + el.removeEventListener(evnt, func, false); + } else if (el.detachEvent) { + el.detachEvent('on' + evnt, func); + } + }, + + + _attachedGroupEvents : {}, + + + attachGroupEvent : function (groupName, el, evnt, func) { + if (!jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + jsc._attachedGroupEvents[groupName] = []; + } + jsc._attachedGroupEvents[groupName].push([el, evnt, func]); + jsc.attachEvent(el, evnt, func); + }, + + + detachGroupEvents : function (groupName) { + if (jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + for (var i = 0; i < jsc._attachedGroupEvents[groupName].length; i += 1) { + var evt = jsc._attachedGroupEvents[groupName][i]; + jsc.detachEvent(evt[0], evt[1], evt[2]); + } + delete jsc._attachedGroupEvents[groupName]; + } + }, + + + attachDOMReadyEvent : function (func) { + var fired = false; + var fireOnce = function () { + if (!fired) { + fired = true; + func(); + } + }; + + if (document.readyState === 'complete') { + setTimeout(fireOnce, 1); // async + return; + } + + if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', fireOnce, false); + + // Fallback + window.addEventListener('load', fireOnce, false); + + } else if (document.attachEvent) { + // IE + document.attachEvent('onreadystatechange', function () { + if (document.readyState === 'complete') { + document.detachEvent('onreadystatechange', arguments.callee); + fireOnce(); + } + }) + + // Fallback + window.attachEvent('onload', fireOnce); + + // IE7/8 + if (document.documentElement.doScroll && window == window.top) { + var tryScroll = function () { + if (!document.body) { return; } + try { + document.documentElement.doScroll('left'); + fireOnce(); + } catch (e) { + setTimeout(tryScroll, 1); + } + }; + tryScroll(); + } + } + }, + + + warn : function (msg) { + if (window.console && window.console.warn) { + window.console.warn(msg); + } + }, + + + preventDefault : function (e) { + if (e.preventDefault) { e.preventDefault(); } + e.returnValue = false; + }, + + + captureTarget : function (target) { + // IE + if (target.setCapture) { + jsc._capturedTarget = target; + jsc._capturedTarget.setCapture(); + } + }, + + + releaseTarget : function () { + // IE + if (jsc._capturedTarget) { + jsc._capturedTarget.releaseCapture(); + jsc._capturedTarget = null; + } + }, + + + fireEvent : function (el, evnt) { + if (!el) { + return; + } + if (document.createEvent) { + var ev = document.createEvent('HTMLEvents'); + ev.initEvent(evnt, true, true); + el.dispatchEvent(ev); + } else if (document.createEventObject) { + var ev = document.createEventObject(); + el.fireEvent('on' + evnt, ev); + } else if (el['on' + evnt]) { // alternatively use the traditional event model + el['on' + evnt](); + } + }, + + + classNameToList : function (className) { + return className.replace(/^\s+|\s+$/g, '').split(/\s+/); + }, + + + // The className parameter (str) can only contain a single class name + hasClass : function (elm, className) { + if (!className) { + return false; + } + return -1 != (' ' + elm.className.replace(/\s+/g, ' ') + ' ').indexOf(' ' + className + ' '); + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + setClass : function (elm, className) { + var classList = jsc.classNameToList(className); + for (var i = 0; i < classList.length; i += 1) { + if (!jsc.hasClass(elm, classList[i])) { + elm.className += (elm.className ? ' ' : '') + classList[i]; + } + } + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + unsetClass : function (elm, className) { + var classList = jsc.classNameToList(className); + for (var i = 0; i < classList.length; i += 1) { + var repl = new RegExp( + '^\\s*' + classList[i] + '\\s*|' + + '\\s*' + classList[i] + '\\s*$|' + + '\\s+' + classList[i] + '(\\s+)', + 'g' + ); + elm.className = elm.className.replace(repl, '$1'); + } + }, + + + getStyle : function (elm) { + return window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle; + }, + + + setStyle : (function () { + var helper = document.createElement('div'); + var getSupportedProp = function (names) { + for (var i = 0; i < names.length; i += 1) { + if (names[i] in helper.style) { + return names[i]; + } + } + }; + var props = { + borderRadius: getSupportedProp(['borderRadius', 'MozBorderRadius', 'webkitBorderRadius']), + boxShadow: getSupportedProp(['boxShadow', 'MozBoxShadow', 'webkitBoxShadow']) + }; + return function (elm, prop, value) { + switch (prop.toLowerCase()) { + case 'opacity': + var alphaOpacity = Math.round(parseFloat(value) * 100); + elm.style.opacity = value; + elm.style.filter = 'alpha(opacity=' + alphaOpacity + ')'; + break; + default: + elm.style[props[prop]] = value; + break; + } + }; + })(), + + + setBorderRadius : function (elm, value) { + jsc.setStyle(elm, 'borderRadius', value || '0'); + }, + + + setBoxShadow : function (elm, value) { + jsc.setStyle(elm, 'boxShadow', value || 'none'); + }, + + + getElementPos : function (e, relativeToViewport) { + var x=0, y=0; + var rect = e.getBoundingClientRect(); + x = rect.left; + y = rect.top; + if (!relativeToViewport) { + var viewPos = jsc.getViewPos(); + x += viewPos[0]; + y += viewPos[1]; + } + return [x, y]; + }, + + + getElementSize : function (e) { + return [e.offsetWidth, e.offsetHeight]; + }, + + + // get pointer's X/Y coordinates relative to viewport + getAbsPointerPos : function (e) { + if (!e) { e = window.event; } + var x = 0, y = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + x = e.changedTouches[0].clientX; + y = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + x = e.clientX; + y = e.clientY; + } + return { x: x, y: y }; + }, + + + // get pointer's X/Y coordinates relative to target element + getRelPointerPos : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + var targetRect = target.getBoundingClientRect(); + + var x = 0, y = 0; + + var clientX = 0, clientY = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + clientX = e.changedTouches[0].clientX; + clientY = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + clientX = e.clientX; + clientY = e.clientY; + } + + x = clientX - targetRect.left; + y = clientY - targetRect.top; + return { x: x, y: y }; + }, + + + getViewPos : function () { + var doc = document.documentElement; + return [ + (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0), + (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) + ]; + }, + + + getViewSize : function () { + var doc = document.documentElement; + return [ + (window.innerWidth || doc.clientWidth), + (window.innerHeight || doc.clientHeight), + ]; + }, + + + redrawPosition : function () { + + if (jsc.picker && jsc.picker.owner) { + var thisObj = jsc.picker.owner; + + var tp, vp; + + if (thisObj.fixed) { + // Fixed elements are positioned relative to viewport, + // therefore we can ignore the scroll offset + tp = jsc.getElementPos(thisObj.targetElement, true); // target pos + vp = [0, 0]; // view pos + } else { + tp = jsc.getElementPos(thisObj.targetElement); // target pos + vp = jsc.getViewPos(); // view pos + } + + var ts = jsc.getElementSize(thisObj.targetElement); // target size + var vs = jsc.getViewSize(); // view size + var ps = jsc.getPickerOuterDims(thisObj); // picker size + var a, b, c; + switch (thisObj.position.toLowerCase()) { + case 'left': a=1; b=0; c=-1; break; + case 'right':a=1; b=0; c=1; break; + case 'top': a=0; b=1; c=-1; break; + default: a=0; b=1; c=1; break; + } + var l = (ts[b]+ps[b])/2; + + // compute picker position + if (!thisObj.smartPosition) { + var pp = [ + tp[a], + tp[b]+ts[b]-l+l*c + ]; + } else { + var pp = [ + -vp[a]+tp[a]+ps[a] > vs[a] ? + (-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) : + tp[a], + -vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ? + (-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) : + (tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c) + ]; + } + + var x = pp[a]; + var y = pp[b]; + var positionValue = thisObj.fixed ? 'fixed' : 'absolute'; + var contractShadow = + (pp[0] + ps[0] > tp[0] || pp[0] < tp[0] + ts[0]) && + (pp[1] + ps[1] < tp[1] + ts[1]); + + jsc._drawPosition(thisObj, x, y, positionValue, contractShadow); + } + }, + + + _drawPosition : function (thisObj, x, y, positionValue, contractShadow) { + var vShadow = contractShadow ? 0 : thisObj.shadowBlur; // px + + jsc.picker.wrap.style.position = positionValue; + jsc.picker.wrap.style.left = x + 'px'; + jsc.picker.wrap.style.top = y + 'px'; + + jsc.setBoxShadow( + jsc.picker.boxS, + thisObj.shadow ? + new jsc.BoxShadow(0, vShadow, thisObj.shadowBlur, 0, thisObj.shadowColor) : + null); + }, + + + getPickerDims : function (thisObj) { + var displaySlider = !!jsc.getSliderComponent(thisObj); + var dims = [ + 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.width + + (displaySlider ? 2 * thisObj.insetWidth + jsc.getPadToSliderPadding(thisObj) + thisObj.sliderSize : 0), + 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.height + + (thisObj.closable ? 2 * thisObj.insetWidth + thisObj.padding + thisObj.buttonHeight : 0) + ]; + return dims; + }, + + + getPickerOuterDims : function (thisObj) { + var dims = jsc.getPickerDims(thisObj); + return [ + dims[0] + 2 * thisObj.borderWidth, + dims[1] + 2 * thisObj.borderWidth + ]; + }, + + + getPadToSliderPadding : function (thisObj) { + return Math.max(thisObj.padding, 1.5 * (2 * thisObj.pointerBorderWidth + thisObj.pointerThickness)); + }, + + + getPadYComponent : function (thisObj) { + switch (thisObj.mode.charAt(1).toLowerCase()) { + case 'v': return 'v'; break; + } + return 's'; + }, + + + getSliderComponent : function (thisObj) { + if (thisObj.mode.length > 2) { + switch (thisObj.mode.charAt(2).toLowerCase()) { + case 's': return 's'; break; + case 'v': return 'v'; break; + } + } + return null; + }, + + + onDocumentMouseDown : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + + if (target._jscLinkedInstance) { + if (target._jscLinkedInstance.showOnClick) { + target._jscLinkedInstance.show(); + } + } else if (target._jscControlName) { + jsc.onControlPointerStart(e, target, target._jscControlName, 'mouse'); + } else { + // Mouse is outside the picker controls -> hide the color picker! + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + } + }, + + + onDocumentTouchStart : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + + if (target._jscLinkedInstance) { + if (target._jscLinkedInstance.showOnClick) { + target._jscLinkedInstance.show(); + } + } else if (target._jscControlName) { + jsc.onControlPointerStart(e, target, target._jscControlName, 'touch'); + } else { + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + } + }, + + + onWindowResize : function (e) { + jsc.redrawPosition(); + }, + + + onParentScroll : function (e) { + // hide the picker when one of the parent elements is scrolled + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + }, + + + _pointerMoveEvent : { + mouse: 'mousemove', + touch: 'touchmove' + }, + _pointerEndEvent : { + mouse: 'mouseup', + touch: 'touchend' + }, + + + _pointerOrigin : null, + _capturedTarget : null, + + + onControlPointerStart : function (e, target, controlName, pointerType) { + var thisObj = target._jscInstance; + + jsc.preventDefault(e); + jsc.captureTarget(target); + + var registerDragEvents = function (doc, offset) { + jsc.attachGroupEvent('drag', doc, jsc._pointerMoveEvent[pointerType], + jsc.onDocumentPointerMove(e, target, controlName, pointerType, offset)); + jsc.attachGroupEvent('drag', doc, jsc._pointerEndEvent[pointerType], + jsc.onDocumentPointerEnd(e, target, controlName, pointerType)); + }; + + registerDragEvents(document, [0, 0]); + + if (window.parent && window.frameElement) { + var rect = window.frameElement.getBoundingClientRect(); + var ofs = [-rect.left, -rect.top]; + registerDragEvents(window.parent.window.document, ofs); + } + + var abs = jsc.getAbsPointerPos(e); + var rel = jsc.getRelPointerPos(e); + jsc._pointerOrigin = { + x: abs.x - rel.x, + y: abs.y - rel.y + }; + + switch (controlName) { + case 'pad': + // if the slider is at the bottom, move it up + switch (jsc.getSliderComponent(thisObj)) { + case 's': if (thisObj.hsv[1] === 0) { thisObj.fromHSV(null, 100, null); }; break; + case 'v': if (thisObj.hsv[2] === 0) { thisObj.fromHSV(null, null, 100); }; break; + } + jsc.setPad(thisObj, e, 0, 0); + break; + + case 'sld': + jsc.setSld(thisObj, e, 0); + break; + } + + jsc.dispatchFineChange(thisObj); + }, + + + onDocumentPointerMove : function (e, target, controlName, pointerType, offset) { + return function (e) { + var thisObj = target._jscInstance; + switch (controlName) { + case 'pad': + if (!e) { e = window.event; } + jsc.setPad(thisObj, e, offset[0], offset[1]); + jsc.dispatchFineChange(thisObj); + break; + + case 'sld': + if (!e) { e = window.event; } + jsc.setSld(thisObj, e, offset[1]); + jsc.dispatchFineChange(thisObj); + break; + } + } + }, + + + onDocumentPointerEnd : function (e, target, controlName, pointerType) { + return function (e) { + var thisObj = target._jscInstance; + jsc.detachGroupEvents('drag'); + jsc.releaseTarget(); + // Always dispatch changes after detaching outstanding mouse handlers, + // in case some user interaction will occur in user's onchange callback + // that would intrude with current mouse events + jsc.dispatchChange(thisObj); + }; + }, + + + dispatchChange : function (thisObj) { + if (thisObj.valueElement) { + if (jsc.isElementType(thisObj.valueElement, 'input')) { + jsc.fireEvent(thisObj.valueElement, 'change'); + } + } + }, + + + dispatchFineChange : function (thisObj) { + if (thisObj.onFineChange) { + var callback; + if (typeof thisObj.onFineChange === 'string') { + callback = new Function(thisObj.onFineChange); + } else { + callback = thisObj.onFineChange; + } + callback(thisObj); + } + }, + + + setPad : function (thisObj, e, ofsX, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var x = ofsX + pointerAbs.x - jsc._pointerOrigin.x - thisObj.padding - thisObj.insetWidth; + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth; + + var xVal = x * (360 / (thisObj.width - 1)); + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getPadYComponent(thisObj)) { + case 's': thisObj.fromHSV(xVal, yVal, null, jsc.leaveSld); break; + case 'v': thisObj.fromHSV(xVal, null, yVal, jsc.leaveSld); break; + } + }, + + + setSld : function (thisObj, e, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth; + + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getSliderComponent(thisObj)) { + case 's': thisObj.fromHSV(null, yVal, null, jsc.leavePad); break; + case 'v': thisObj.fromHSV(null, null, yVal, jsc.leavePad); break; + } + }, + + + _vmlNS : 'jsc_vml_', + _vmlCSS : 'jsc_vml_css_', + _vmlReady : false, + + + initVML : function () { + if (!jsc._vmlReady) { + // init VML namespace + var doc = document; + if (!doc.namespaces[jsc._vmlNS]) { + doc.namespaces.add(jsc._vmlNS, 'urn:schemas-microsoft-com:vml'); + } + if (!doc.styleSheets[jsc._vmlCSS]) { + var tags = ['shape', 'shapetype', 'group', 'background', 'path', 'formulas', 'handles', 'fill', 'stroke', 'shadow', 'textbox', 'textpath', 'imagedata', 'line', 'polyline', 'curve', 'rect', 'roundrect', 'oval', 'arc', 'image']; + var ss = doc.createStyleSheet(); + ss.owningElement.id = jsc._vmlCSS; + for (var i = 0; i < tags.length; i += 1) { + ss.addRule(jsc._vmlNS + '\\:' + tags[i], 'behavior:url(#default#VML);'); + } + } + jsc._vmlReady = true; + } + }, + + + createPalette : function () { + + var paletteObj = { + elm: null, + draw: null + }; + + if (jsc.isCanvasSupported) { + // Canvas implementation for modern browsers + + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, type) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var hGrad = ctx.createLinearGradient(0, 0, canvas.width, 0); + hGrad.addColorStop(0 / 6, '#F00'); + hGrad.addColorStop(1 / 6, '#FF0'); + hGrad.addColorStop(2 / 6, '#0F0'); + hGrad.addColorStop(3 / 6, '#0FF'); + hGrad.addColorStop(4 / 6, '#00F'); + hGrad.addColorStop(5 / 6, '#F0F'); + hGrad.addColorStop(6 / 6, '#F00'); + + ctx.fillStyle = hGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + var vGrad = ctx.createLinearGradient(0, 0, 0, canvas.height); + switch (type.toLowerCase()) { + case 's': + vGrad.addColorStop(0, 'rgba(255,255,255,0)'); + vGrad.addColorStop(1, 'rgba(255,255,255,1)'); + break; + case 'v': + vGrad.addColorStop(0, 'rgba(0,0,0,0)'); + vGrad.addColorStop(1, 'rgba(0,0,0,1)'); + break; + } + ctx.fillStyle = vGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + paletteObj.elm = canvas; + paletteObj.draw = drawFunc; + + } else { + // VML fallback for IE 7 and 8 + + jsc.initVML(); + + var vmlContainer = document.createElement('div'); + vmlContainer.style.position = 'relative'; + vmlContainer.style.overflow = 'hidden'; + + var hGrad = document.createElement(jsc._vmlNS + ':fill'); + hGrad.type = 'gradient'; + hGrad.method = 'linear'; + hGrad.angle = '90'; + hGrad.colors = '16.67% #F0F, 33.33% #00F, 50% #0FF, 66.67% #0F0, 83.33% #FF0' + + var hRect = document.createElement(jsc._vmlNS + ':rect'); + hRect.style.position = 'absolute'; + hRect.style.left = -1 + 'px'; + hRect.style.top = -1 + 'px'; + hRect.stroked = false; + hRect.appendChild(hGrad); + vmlContainer.appendChild(hRect); + + var vGrad = document.createElement(jsc._vmlNS + ':fill'); + vGrad.type = 'gradient'; + vGrad.method = 'linear'; + vGrad.angle = '180'; + vGrad.opacity = '0'; + + var vRect = document.createElement(jsc._vmlNS + ':rect'); + vRect.style.position = 'absolute'; + vRect.style.left = -1 + 'px'; + vRect.style.top = -1 + 'px'; + vRect.stroked = false; + vRect.appendChild(vGrad); + vmlContainer.appendChild(vRect); + + var drawFunc = function (width, height, type) { + vmlContainer.style.width = width + 'px'; + vmlContainer.style.height = height + 'px'; + + hRect.style.width = + vRect.style.width = + (width + 1) + 'px'; + hRect.style.height = + vRect.style.height = + (height + 1) + 'px'; + + // Colors must be specified during every redraw, otherwise IE won't display + // a full gradient during a subsequential redraw + hGrad.color = '#F00'; + hGrad.color2 = '#F00'; + + switch (type.toLowerCase()) { + case 's': + vGrad.color = vGrad.color2 = '#FFF'; + break; + case 'v': + vGrad.color = vGrad.color2 = '#000'; + break; + } + }; + + paletteObj.elm = vmlContainer; + paletteObj.draw = drawFunc; + } + + return paletteObj; + }, + + + createSliderGradient : function () { + + var sliderObj = { + elm: null, + draw: null + }; + + if (jsc.isCanvasSupported) { + // Canvas implementation for modern browsers + + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, color1, color2) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var grad = ctx.createLinearGradient(0, 0, 0, canvas.height); + grad.addColorStop(0, color1); + grad.addColorStop(1, color2); + + ctx.fillStyle = grad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + sliderObj.elm = canvas; + sliderObj.draw = drawFunc; + + } else { + // VML fallback for IE 7 and 8 + + jsc.initVML(); + + var vmlContainer = document.createElement('div'); + vmlContainer.style.position = 'relative'; + vmlContainer.style.overflow = 'hidden'; + + var grad = document.createElement(jsc._vmlNS + ':fill'); + grad.type = 'gradient'; + grad.method = 'linear'; + grad.angle = '180'; + + var rect = document.createElement(jsc._vmlNS + ':rect'); + rect.style.position = 'absolute'; + rect.style.left = -1 + 'px'; + rect.style.top = -1 + 'px'; + rect.stroked = false; + rect.appendChild(grad); + vmlContainer.appendChild(rect); + + var drawFunc = function (width, height, color1, color2) { + vmlContainer.style.width = width + 'px'; + vmlContainer.style.height = height + 'px'; + + rect.style.width = (width + 1) + 'px'; + rect.style.height = (height + 1) + 'px'; + + grad.color = color1; + grad.color2 = color2; + }; + + sliderObj.elm = vmlContainer; + sliderObj.draw = drawFunc; + } + + return sliderObj; + }, + + + leaveValue : 1<<0, + leaveStyle : 1<<1, + leavePad : 1<<2, + leaveSld : 1<<3, + + + BoxShadow : (function () { + var BoxShadow = function (hShadow, vShadow, blur, spread, color, inset) { + this.hShadow = hShadow; + this.vShadow = vShadow; + this.blur = blur; + this.spread = spread; + this.color = color; + this.inset = !!inset; + }; + + BoxShadow.prototype.toString = function () { + var vals = [ + Math.round(this.hShadow) + 'px', + Math.round(this.vShadow) + 'px', + Math.round(this.blur) + 'px', + Math.round(this.spread) + 'px', + this.color + ]; + if (this.inset) { + vals.push('inset'); + } + return vals.join(' '); + }; + + return BoxShadow; + })(), + + + // + // Usage: + // var myColor = new jscolor( [, ]) + // + + jscolor : function (targetElement, options) { + + // General options + // + this.value = null; // initial HEX color. To change it later, use methods fromString(), fromHSV() and fromRGB() + this.valueElement = targetElement; // element that will be used to display and input the color code + this.styleElement = targetElement; // element that will preview the picked color using CSS backgroundColor + this.required = true; // whether the associated text can be left empty + this.refine = true; // whether to refine the entered color code (e.g. uppercase it and remove whitespace) + this.hash = false; // whether to prefix the HEX color code with # symbol + this.uppercase = true; // whether to show the color code in upper case + this.onFineChange = null; // called instantly every time the color changes (value can be either a function or a string with javascript code) + this.activeClass = 'jscolor-active'; // class to be set to the target element when a picker window is open on it + this.overwriteImportant = false; // whether to overwrite colors of styleElement using !important + this.minS = 0; // min allowed saturation (0 - 100) + this.maxS = 100; // max allowed saturation (0 - 100) + this.minV = 0; // min allowed value (brightness) (0 - 100) + this.maxV = 100; // max allowed value (brightness) (0 - 100) + + // Accessing the picked color + // + this.hsv = [0, 0, 100]; // read-only [0-360, 0-100, 0-100] + this.rgb = [255, 255, 255]; // read-only [0-255, 0-255, 0-255] + + // Color Picker options + // + this.width = 181; // width of color palette (in px) + this.height = 101; // height of color palette (in px) + this.showOnClick = true; // whether to display the color picker when user clicks on its target element + this.mode = 'HSV'; // HSV | HVS | HS | HV - layout of the color picker controls + this.position = 'bottom'; // left | right | top | bottom - position relative to the target element + this.smartPosition = true; // automatically change picker position when there is not enough space for it + this.sliderSize = 16; // px + this.crossSize = 8; // px + this.closable = false; // whether to display the Close button + this.closeText = 'Close'; + this.buttonColor = '#000000'; // CSS color + this.buttonHeight = 18; // px + this.padding = 12; // px + this.backgroundColor = '#FFFFFF'; // CSS color + this.borderWidth = 1; // px + this.borderColor = '#BBBBBB'; // CSS color + this.borderRadius = 8; // px + this.insetWidth = 1; // px + this.insetColor = '#BBBBBB'; // CSS color + this.shadow = true; // whether to display shadow + this.shadowBlur = 15; // px + this.shadowColor = 'rgba(0,0,0,0.2)'; // CSS color + this.pointerColor = '#4C4C4C'; // px + this.pointerBorderColor = '#FFFFFF'; // px + this.pointerBorderWidth = 1; // px + this.pointerThickness = 2; // px + this.zIndex = 1000; + this.container = null; // where to append the color picker (BODY element by default) + + + for (var opt in options) { + if (options.hasOwnProperty(opt)) { + this[opt] = options[opt]; + } + } + + + this.hide = function () { + if (isPickerOwner()) { + detachPicker(); + } + }; + + + this.show = function () { + drawPicker(); + }; + + + this.redraw = function () { + if (isPickerOwner()) { + drawPicker(); + } + }; + + + this.importColor = function () { + if (!this.valueElement) { + this.exportColor(); + } else { + if (jsc.isElementType(this.valueElement, 'input')) { + if (!this.refine) { + if (!this.fromString(this.valueElement.value, jsc.leaveValue)) { + if (this.styleElement) { + this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage; + this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor; + this.styleElement.style.color = this.styleElement._jscOrigStyle.color; + } + this.exportColor(jsc.leaveValue | jsc.leaveStyle); + } + } else if (!this.required && /^\s*$/.test(this.valueElement.value)) { + this.valueElement.value = ''; + if (this.styleElement) { + this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage; + this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor; + this.styleElement.style.color = this.styleElement._jscOrigStyle.color; + } + this.exportColor(jsc.leaveValue | jsc.leaveStyle); + + } else if (this.fromString(this.valueElement.value)) { + // managed to import color successfully from the value -> OK, don't do anything + } else { + this.exportColor(); + } + } else { + // not an input element -> doesn't have any value + this.exportColor(); + } + } + }; + + + this.exportColor = function (flags) { + if (!(flags & jsc.leaveValue) && this.valueElement) { + var value = this.toString(); + if (this.uppercase) { value = value.toUpperCase(); } + if (this.hash) { value = '#' + value; } + + if (jsc.isElementType(this.valueElement, 'input')) { + this.valueElement.value = value; + } else { + this.valueElement.innerHTML = value; + } + } + if (!(flags & jsc.leaveStyle)) { + if (this.styleElement) { + var bgColor = '#' + this.toString(); + var fgColor = this.isLight() ? '#000' : '#FFF'; + + this.styleElement.style.backgroundImage = 'none'; + this.styleElement.style.backgroundColor = bgColor; + this.styleElement.style.color = fgColor; + + if (this.overwriteImportant) { + this.styleElement.setAttribute('style', + 'background: ' + bgColor + ' !important; ' + + 'color: ' + fgColor + ' !important;' + ); + } + } + } + if (!(flags & jsc.leavePad) && isPickerOwner()) { + redrawPad(); + } + if (!(flags & jsc.leaveSld) && isPickerOwner()) { + redrawSld(); + } + }; + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // + this.fromHSV = function (h, s, v, flags) { // null = don't change + if (h !== null) { + if (isNaN(h)) { return false; } + h = Math.max(0, Math.min(360, h)); + } + if (s !== null) { + if (isNaN(s)) { return false; } + s = Math.max(0, Math.min(100, this.maxS, s), this.minS); + } + if (v !== null) { + if (isNaN(v)) { return false; } + v = Math.max(0, Math.min(100, this.maxV, v), this.minV); + } + + this.rgb = HSV_RGB( + h===null ? this.hsv[0] : (this.hsv[0]=h), + s===null ? this.hsv[1] : (this.hsv[1]=s), + v===null ? this.hsv[2] : (this.hsv[2]=v) + ); + + this.exportColor(flags); + }; + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // + this.fromRGB = function (r, g, b, flags) { // null = don't change + if (r !== null) { + if (isNaN(r)) { return false; } + r = Math.max(0, Math.min(255, r)); + } + if (g !== null) { + if (isNaN(g)) { return false; } + g = Math.max(0, Math.min(255, g)); + } + if (b !== null) { + if (isNaN(b)) { return false; } + b = Math.max(0, Math.min(255, b)); + } + + var hsv = RGB_HSV( + r===null ? this.rgb[0] : r, + g===null ? this.rgb[1] : g, + b===null ? this.rgb[2] : b + ); + if (hsv[0] !== null) { + this.hsv[0] = Math.max(0, Math.min(360, hsv[0])); + } + if (hsv[2] !== 0) { + this.hsv[1] = hsv[1]===null ? null : Math.max(0, this.minS, Math.min(100, this.maxS, hsv[1])); + } + this.hsv[2] = hsv[2]===null ? null : Math.max(0, this.minV, Math.min(100, this.maxV, hsv[2])); + + // update RGB according to final HSV, as some values might be trimmed + var rgb = HSV_RGB(this.hsv[0], this.hsv[1], this.hsv[2]); + this.rgb[0] = rgb[0]; + this.rgb[1] = rgb[1]; + this.rgb[2] = rgb[2]; + + this.exportColor(flags); + }; + + + this.fromString = function (str, flags) { + var m; + if (m = str.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i)) { + // HEX notation + // + + if (m[1].length === 6) { + // 6-char notation + this.fromRGB( + parseInt(m[1].substr(0,2),16), + parseInt(m[1].substr(2,2),16), + parseInt(m[1].substr(4,2),16), + flags + ); + } else { + // 3-char notation + this.fromRGB( + parseInt(m[1].charAt(0) + m[1].charAt(0),16), + parseInt(m[1].charAt(1) + m[1].charAt(1),16), + parseInt(m[1].charAt(2) + m[1].charAt(2),16), + flags + ); + } + return true; + + } else if (m = str.match(/^\W*rgba?\(([^)]*)\)\W*$/i)) { + var params = m[1].split(','); + var re = /^\s*(\d*)(\.\d+)?\s*$/; + var mR, mG, mB; + if ( + params.length >= 3 && + (mR = params[0].match(re)) && + (mG = params[1].match(re)) && + (mB = params[2].match(re)) + ) { + var r = parseFloat((mR[1] || '0') + (mR[2] || '')); + var g = parseFloat((mG[1] || '0') + (mG[2] || '')); + var b = parseFloat((mB[1] || '0') + (mB[2] || '')); + this.fromRGB(r, g, b, flags); + return true; + } + } + return false; + }; + + + this.toString = function () { + return ( + (0x100 | Math.round(this.rgb[0])).toString(16).substr(1) + + (0x100 | Math.round(this.rgb[1])).toString(16).substr(1) + + (0x100 | Math.round(this.rgb[2])).toString(16).substr(1) + ); + }; + + + this.toHEXString = function () { + return '#' + this.toString().toUpperCase(); + }; + + + this.toRGBString = function () { + return ('rgb(' + + Math.round(this.rgb[0]) + ',' + + Math.round(this.rgb[1]) + ',' + + Math.round(this.rgb[2]) + ')' + ); + }; + + + this.isLight = function () { + return ( + 0.213 * this.rgb[0] + + 0.715 * this.rgb[1] + + 0.072 * this.rgb[2] > + 255 / 2 + ); + }; + + + this._processParentElementsInDOM = function () { + if (this._linkedElementsProcessed) { return; } + this._linkedElementsProcessed = true; + + var elm = this.targetElement; + do { + // If the target element or one of its parent nodes has fixed position, + // then use fixed positioning instead + // + // Note: In Firefox, getComputedStyle returns null in a hidden iframe, + // that's why we need to check if the returned style object is non-empty + var currStyle = jsc.getStyle(elm); + if (currStyle && currStyle.position.toLowerCase() === 'fixed') { + this.fixed = true; + } + + if (elm !== this.targetElement) { + // Ensure to attach onParentScroll only once to each parent element + // (multiple targetElements can share the same parent nodes) + // + // Note: It's not just offsetParents that can be scrollable, + // that's why we loop through all parent nodes + if (!elm._jscEventsAttached) { + jsc.attachEvent(elm, 'scroll', jsc.onParentScroll); + elm._jscEventsAttached = true; + } + } + } while ((elm = elm.parentNode) && !jsc.isElementType(elm, 'body')); + }; + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // + // returns: [ 0-360, 0-100, 0-100 ] + // + function RGB_HSV (r, g, b) { + r /= 255; + g /= 255; + b /= 255; + var n = Math.min(Math.min(r,g),b); + var v = Math.max(Math.max(r,g),b); + var m = v - n; + if (m === 0) { return [ null, 0, 100 * v ]; } + var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m); + return [ + 60 * (h===6?0:h), + 100 * (m/v), + 100 * v + ]; + } + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // + // returns: [ 0-255, 0-255, 0-255 ] + // + function HSV_RGB (h, s, v) { + var u = 255 * (v / 100); + + if (h === null) { + return [ u, u, u ]; + } + + h /= 60; + s /= 100; + + var i = Math.floor(h); + var f = i%2 ? h-i : 1-(h-i); + var m = u * (1 - s); + var n = u * (1 - s * f); + switch (i) { + case 6: + case 0: return [u,n,m]; + case 1: return [n,u,m]; + case 2: return [m,u,n]; + case 3: return [m,n,u]; + case 4: return [n,m,u]; + case 5: return [u,m,n]; + } + } + + + function detachPicker () { + jsc.unsetClass(THIS.targetElement, THIS.activeClass); + jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap); + delete jsc.picker.owner; + } + + + function drawPicker () { + + // At this point, when drawing the picker, we know what the parent elements are + // and we can do all related DOM operations, such as registering events on them + // or checking their positioning + THIS._processParentElementsInDOM(); + + if (!jsc.picker) { + jsc.picker = { + owner: null, + wrap : document.createElement('div'), + box : document.createElement('div'), + boxS : document.createElement('div'), // shadow area + boxB : document.createElement('div'), // border + pad : document.createElement('div'), + padB : document.createElement('div'), // border + padM : document.createElement('div'), // mouse/touch area + padPal : jsc.createPalette(), + cross : document.createElement('div'), + crossBY : document.createElement('div'), // border Y + crossBX : document.createElement('div'), // border X + crossLY : document.createElement('div'), // line Y + crossLX : document.createElement('div'), // line X + sld : document.createElement('div'), + sldB : document.createElement('div'), // border + sldM : document.createElement('div'), // mouse/touch area + sldGrad : jsc.createSliderGradient(), + sldPtrS : document.createElement('div'), // slider pointer spacer + sldPtrIB : document.createElement('div'), // slider pointer inner border + sldPtrMB : document.createElement('div'), // slider pointer middle border + sldPtrOB : document.createElement('div'), // slider pointer outer border + btn : document.createElement('div'), + btnT : document.createElement('span') // text + }; + + jsc.picker.pad.appendChild(jsc.picker.padPal.elm); + jsc.picker.padB.appendChild(jsc.picker.pad); + jsc.picker.cross.appendChild(jsc.picker.crossBY); + jsc.picker.cross.appendChild(jsc.picker.crossBX); + jsc.picker.cross.appendChild(jsc.picker.crossLY); + jsc.picker.cross.appendChild(jsc.picker.crossLX); + jsc.picker.padB.appendChild(jsc.picker.cross); + jsc.picker.box.appendChild(jsc.picker.padB); + jsc.picker.box.appendChild(jsc.picker.padM); + + jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm); + jsc.picker.sldB.appendChild(jsc.picker.sld); + jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB); + jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB); + jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB); + jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS); + jsc.picker.box.appendChild(jsc.picker.sldB); + jsc.picker.box.appendChild(jsc.picker.sldM); + + jsc.picker.btn.appendChild(jsc.picker.btnT); + jsc.picker.box.appendChild(jsc.picker.btn); + + jsc.picker.boxB.appendChild(jsc.picker.box); + jsc.picker.wrap.appendChild(jsc.picker.boxS); + jsc.picker.wrap.appendChild(jsc.picker.boxB); + } + + var p = jsc.picker; + + var displaySlider = !!jsc.getSliderComponent(THIS); + var dims = jsc.getPickerDims(THIS); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var padToSliderPadding = jsc.getPadToSliderPadding(THIS); + var borderRadius = Math.min( + THIS.borderRadius, + Math.round(THIS.padding * Math.PI)); // px + var padCursor = 'crosshair'; + + // wrap + p.wrap.style.clear = 'both'; + p.wrap.style.width = (dims[0] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.height = (dims[1] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.zIndex = THIS.zIndex; + + // picker + p.box.style.width = dims[0] + 'px'; + p.box.style.height = dims[1] + 'px'; + + p.boxS.style.position = 'absolute'; + p.boxS.style.left = '0'; + p.boxS.style.top = '0'; + p.boxS.style.width = '100%'; + p.boxS.style.height = '100%'; + jsc.setBorderRadius(p.boxS, borderRadius + 'px'); + + // picker border + p.boxB.style.position = 'relative'; + p.boxB.style.border = THIS.borderWidth + 'px solid'; + p.boxB.style.borderColor = THIS.borderColor; + p.boxB.style.background = THIS.backgroundColor; + jsc.setBorderRadius(p.boxB, borderRadius + 'px'); + + // IE hack: + // If the element is transparent, IE will trigger the event on the elements under it, + // e.g. on Canvas or on elements with border + p.padM.style.background = + p.sldM.style.background = + '#FFF'; + jsc.setStyle(p.padM, 'opacity', '0'); + jsc.setStyle(p.sldM, 'opacity', '0'); + + // pad + p.pad.style.position = 'relative'; + p.pad.style.width = THIS.width + 'px'; + p.pad.style.height = THIS.height + 'px'; + + // pad palettes (HSV and HVS) + p.padPal.draw(THIS.width, THIS.height, jsc.getPadYComponent(THIS)); + + // pad border + p.padB.style.position = 'absolute'; + p.padB.style.left = THIS.padding + 'px'; + p.padB.style.top = THIS.padding + 'px'; + p.padB.style.border = THIS.insetWidth + 'px solid'; + p.padB.style.borderColor = THIS.insetColor; + + // pad mouse area + p.padM._jscInstance = THIS; + p.padM._jscControlName = 'pad'; + p.padM.style.position = 'absolute'; + p.padM.style.left = '0'; + p.padM.style.top = '0'; + p.padM.style.width = (THIS.padding + 2 * THIS.insetWidth + THIS.width + padToSliderPadding / 2) + 'px'; + p.padM.style.height = dims[1] + 'px'; + p.padM.style.cursor = padCursor; + + // pad cross + p.cross.style.position = 'absolute'; + p.cross.style.left = + p.cross.style.top = + '0'; + p.cross.style.width = + p.cross.style.height = + crossOuterSize + 'px'; + + // pad cross border Y and X + p.crossBY.style.position = + p.crossBX.style.position = + 'absolute'; + p.crossBY.style.background = + p.crossBX.style.background = + THIS.pointerBorderColor; + p.crossBY.style.width = + p.crossBX.style.height = + (2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.crossBY.style.height = + p.crossBX.style.width = + crossOuterSize + 'px'; + p.crossBY.style.left = + p.crossBX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2) - THIS.pointerBorderWidth) + 'px'; + p.crossBY.style.top = + p.crossBX.style.left = + '0'; + + // pad cross line Y and X + p.crossLY.style.position = + p.crossLX.style.position = + 'absolute'; + p.crossLY.style.background = + p.crossLX.style.background = + THIS.pointerColor; + p.crossLY.style.height = + p.crossLX.style.width = + (crossOuterSize - 2 * THIS.pointerBorderWidth) + 'px'; + p.crossLY.style.width = + p.crossLX.style.height = + THIS.pointerThickness + 'px'; + p.crossLY.style.left = + p.crossLX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2)) + 'px'; + p.crossLY.style.top = + p.crossLX.style.left = + THIS.pointerBorderWidth + 'px'; + + // slider + p.sld.style.overflow = 'hidden'; + p.sld.style.width = THIS.sliderSize + 'px'; + p.sld.style.height = THIS.height + 'px'; + + // slider gradient + p.sldGrad.draw(THIS.sliderSize, THIS.height, '#000', '#000'); + + // slider border + p.sldB.style.display = displaySlider ? 'block' : 'none'; + p.sldB.style.position = 'absolute'; + p.sldB.style.right = THIS.padding + 'px'; + p.sldB.style.top = THIS.padding + 'px'; + p.sldB.style.border = THIS.insetWidth + 'px solid'; + p.sldB.style.borderColor = THIS.insetColor; + + // slider mouse area + p.sldM._jscInstance = THIS; + p.sldM._jscControlName = 'sld'; + p.sldM.style.display = displaySlider ? 'block' : 'none'; + p.sldM.style.position = 'absolute'; + p.sldM.style.right = '0'; + p.sldM.style.top = '0'; + p.sldM.style.width = (THIS.sliderSize + padToSliderPadding / 2 + THIS.padding + 2 * THIS.insetWidth) + 'px'; + p.sldM.style.height = dims[1] + 'px'; + p.sldM.style.cursor = 'default'; + + // slider pointer inner and outer border + p.sldPtrIB.style.border = + p.sldPtrOB.style.border = + THIS.pointerBorderWidth + 'px solid ' + THIS.pointerBorderColor; + + // slider pointer outer border + p.sldPtrOB.style.position = 'absolute'; + p.sldPtrOB.style.left = -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.sldPtrOB.style.top = '0'; + + // slider pointer middle border + p.sldPtrMB.style.border = THIS.pointerThickness + 'px solid ' + THIS.pointerColor; + + // slider pointer spacer + p.sldPtrS.style.width = THIS.sliderSize + 'px'; + p.sldPtrS.style.height = sliderPtrSpace + 'px'; + + // the Close button + function setBtnBorder () { + var insetColors = THIS.insetColor.split(/\s+/); + var outsetColor = insetColors.length < 2 ? insetColors[0] : insetColors[1] + ' ' + insetColors[0] + ' ' + insetColors[0] + ' ' + insetColors[1]; + p.btn.style.borderColor = outsetColor; + } + p.btn.style.display = THIS.closable ? 'block' : 'none'; + p.btn.style.position = 'absolute'; + p.btn.style.left = THIS.padding + 'px'; + p.btn.style.bottom = THIS.padding + 'px'; + p.btn.style.padding = '0 15px'; + p.btn.style.height = THIS.buttonHeight + 'px'; + p.btn.style.border = THIS.insetWidth + 'px solid'; + setBtnBorder(); + p.btn.style.color = THIS.buttonColor; + p.btn.style.font = '12px sans-serif'; + p.btn.style.textAlign = 'center'; + try { + p.btn.style.cursor = 'pointer'; + } catch(eOldIE) { + p.btn.style.cursor = 'hand'; + } + p.btn.onmousedown = function () { + THIS.hide(); + }; + p.btnT.style.lineHeight = THIS.buttonHeight + 'px'; + p.btnT.innerHTML = ''; + p.btnT.appendChild(document.createTextNode(THIS.closeText)); + + // place pointers + redrawPad(); + redrawSld(); + + // If we are changing the owner without first closing the picker, + // make sure to first deal with the old owner + if (jsc.picker.owner && jsc.picker.owner !== THIS) { + jsc.unsetClass(jsc.picker.owner.targetElement, THIS.activeClass); + } + + // Set the new picker owner + jsc.picker.owner = THIS; + + // The redrawPosition() method needs picker.owner to be set, that's why we call it here, + // after setting the owner + if (jsc.isElementType(container, 'body')) { + jsc.redrawPosition(); + } else { + jsc._drawPosition(THIS, 0, 0, 'relative', false); + } + + if (p.wrap.parentNode != container) { + container.appendChild(p.wrap); + } + + jsc.setClass(THIS.targetElement, THIS.activeClass); + } + + + function redrawPad () { + // redraw the pad pointer + switch (jsc.getPadYComponent(THIS)) { + case 's': var yComponent = 1; break; + case 'v': var yComponent = 2; break; + } + var x = Math.round((THIS.hsv[0] / 360) * (THIS.width - 1)); + var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1)); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var ofs = -Math.floor(crossOuterSize / 2); + jsc.picker.cross.style.left = (x + ofs) + 'px'; + jsc.picker.cross.style.top = (y + ofs) + 'px'; + + // redraw the slider + switch (jsc.getSliderComponent(THIS)) { + case 's': + var rgb1 = HSV_RGB(THIS.hsv[0], 100, THIS.hsv[2]); + var rgb2 = HSV_RGB(THIS.hsv[0], 0, THIS.hsv[2]); + var color1 = 'rgb(' + + Math.round(rgb1[0]) + ',' + + Math.round(rgb1[1]) + ',' + + Math.round(rgb1[2]) + ')'; + var color2 = 'rgb(' + + Math.round(rgb2[0]) + ',' + + Math.round(rgb2[1]) + ',' + + Math.round(rgb2[2]) + ')'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + case 'v': + var rgb = HSV_RGB(THIS.hsv[0], THIS.hsv[1], 100); + var color1 = 'rgb(' + + Math.round(rgb[0]) + ',' + + Math.round(rgb[1]) + ',' + + Math.round(rgb[2]) + ')'; + var color2 = '#000'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + } + } + + + function redrawSld () { + var sldComponent = jsc.getSliderComponent(THIS); + if (sldComponent) { + // redraw the slider pointer + switch (sldComponent) { + case 's': var yComponent = 1; break; + case 'v': var yComponent = 2; break; + } + var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1)); + jsc.picker.sldPtrOB.style.top = (y - (2 * THIS.pointerBorderWidth + THIS.pointerThickness) - Math.floor(sliderPtrSpace / 2)) + 'px'; + } + } + + + function isPickerOwner () { + return jsc.picker && jsc.picker.owner === THIS; + } + + + function blurValue () { + THIS.importColor(); + } + + + // Find the target element + if (typeof targetElement === 'string') { + var id = targetElement; + var elm = document.getElementById(id); + if (elm) { + this.targetElement = elm; + } else { + jsc.warn('Could not find target element with ID \'' + id + '\''); + } + } else if (targetElement) { + this.targetElement = targetElement; + } else { + jsc.warn('Invalid target element: \'' + targetElement + '\''); + } + + if (this.targetElement._jscLinkedInstance) { + jsc.warn('Cannot link jscolor twice to the same element. Skipping.'); + return; + } + this.targetElement._jscLinkedInstance = this; + + // Find the value element + this.valueElement = jsc.fetchElement(this.valueElement); + // Find the style element + this.styleElement = jsc.fetchElement(this.styleElement); + + var THIS = this; + var container = + this.container ? + jsc.fetchElement(this.container) : + document.getElementsByTagName('body')[0]; + var sliderPtrSpace = 3; // px + + // For BUTTON elements it's important to stop them from sending the form when clicked + // (e.g. in Safari) + if (jsc.isElementType(this.targetElement, 'button')) { + if (this.targetElement.onclick) { + var origCallback = this.targetElement.onclick; + this.targetElement.onclick = function (evt) { + origCallback.call(this, evt); + return false; + }; + } else { + this.targetElement.onclick = function () { return false; }; + } + } + + /* + var elm = this.targetElement; + do { + // If the target element or one of its offsetParents has fixed position, + // then use fixed positioning instead + // + // Note: In Firefox, getComputedStyle returns null in a hidden iframe, + // that's why we need to check if the returned style object is non-empty + var currStyle = jsc.getStyle(elm); + if (currStyle && currStyle.position.toLowerCase() === 'fixed') { + this.fixed = true; + } + + if (elm !== this.targetElement) { + // attach onParentScroll so that we can recompute the picker position + // when one of the offsetParents is scrolled + if (!elm._jscEventsAttached) { + jsc.attachEvent(elm, 'scroll', jsc.onParentScroll); + elm._jscEventsAttached = true; + } + } + } while ((elm = elm.offsetParent) && !jsc.isElementType(elm, 'body')); + */ + + // valueElement + if (this.valueElement) { + if (jsc.isElementType(this.valueElement, 'input')) { + var updateField = function () { + THIS.fromString(THIS.valueElement.value, jsc.leaveValue); + jsc.dispatchFineChange(THIS); + }; + jsc.attachEvent(this.valueElement, 'keyup', updateField); + jsc.attachEvent(this.valueElement, 'input', updateField); + jsc.attachEvent(this.valueElement, 'blur', blurValue); + this.valueElement.setAttribute('autocomplete', 'off'); + } + } + + // styleElement + if (this.styleElement) { + this.styleElement._jscOrigStyle = { + backgroundImage : this.styleElement.style.backgroundImage, + backgroundColor : this.styleElement.style.backgroundColor, + color : this.styleElement.style.color + }; + } + + if (this.value) { + // Try to set the color from the .value option and if unsuccessful, + // export the current color + this.fromString(this.value) || this.exportColor(); + } else { + this.importColor(); + } + } + +}; + + +//================================ +// Public properties and methods +//================================ + + +// By default, search for all elements with class="jscolor" and install a color picker on them. +// +// You can change what class name will be looked for by setting the property jscolor.lookupClass +// anywhere in your HTML document. To completely disable the automatic lookup, set it to null. +// +jsc.jscolor.lookupClass = 'jscolor'; + + +jsc.jscolor.installByClassName = function (className) { + var inputElms = document.getElementsByTagName('input'); + var buttonElms = document.getElementsByTagName('button'); + + jsc.tryInstallOnElements(inputElms, className); + jsc.tryInstallOnElements(buttonElms, className); +}; + + +jsc.register(); + + +return jsc.jscolor; + + +})(); } From 1bf1253963da21a29d974068b05739a5d8557985 Mon Sep 17 00:00:00 2001 From: MTRNord Date: Tue, 31 Jul 2018 20:08:06 +0200 Subject: [PATCH 02/82] Add color picker to items --- www/kanban/inner.js | 2 +- www/kanban/jkanban.js | 148 ++++++++++++++++++++++++------------------ 2 files changed, 87 insertions(+), 63 deletions(-) diff --git a/www/kanban/inner.js b/www/kanban/inner.js index ad1a3e64f..685331f0b 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -137,7 +137,7 @@ define([ // Remove the input $(el).text(name); // Save the value for the correct board - var board = $(el.parentNode.parentNode).attr("data-id"); + var board = $(el.parentNode.parentNode.parentNode).attr("data-id"); var pos = kanban.findElementPosition(el); kanban.getBoardJSON(board).item[pos].title = name; kanban.onChange(); diff --git a/www/kanban/jkanban.js b/www/kanban/jkanban.js index e70a5d92e..9c2a1cc5b 100644 --- a/www/kanban/jkanban.js +++ b/www/kanban/jkanban.js @@ -31,7 +31,7 @@ * @author: Riccardo Tartaglia */ - //Require dragula + //Require dragula var dragula = require('dragula'); (function () { @@ -85,18 +85,18 @@ //Init Drag Board self.drakeBoard = self.dragula([self.container], { - moves: function (el, source, handle, sibling) { - if (self.options.readOnly) { return false; } - if (!self.options.dragBoards) return false; - return (handle.classList.contains('kanban-board-header') || handle.classList.contains('kanban-title-board')); - }, - accepts: function (el, target, source, sibling) { - if (self.options.readOnly) { return false; } - return target.classList.contains('kanban-container'); - }, - revertOnSpill: true, - direction: 'horizontal', - }) + moves: function (el, source, handle, sibling) { + if (self.options.readOnly) { return false; } + if (!self.options.dragBoards) return false; + return (handle.classList.contains('kanban-board-header') || handle.classList.contains('kanban-title-board')); + }, + accepts: function (el, target, source, sibling) { + if (self.options.readOnly) { return false; } + return target.classList.contains('kanban-container'); + }, + revertOnSpill: true, + direction: 'horizontal', + }) .on('drag', function (el, source) { el.classList.add('is-moving'); self.options.dragBoard(el, source); @@ -145,16 +145,16 @@ //Init Drag Item self.drake = self.dragula(self.boardContainer, { - moves: function (el, source, handle, sibling) { - if (self.options.readOnly) { return false; } - return handle.classList.contains('kanban-item'); - }, - accepts: function (el, target, source, sibling) { - if (self.options.readOnly) { return false; } - return true; - }, - revertOnSpill: true - }) + moves: function (el, source, handle, sibling) { + if (self.options.readOnly) { return false; } + return handle.classList.contains('kanban-item'); + }, + accepts: function (el, target, source, sibling) { + if (self.options.readOnly) { return false; } + return true; + }, + revertOnSpill: true + }) .on('cancel', function(el, container, source) { self.enableAllBoards(); }) @@ -374,14 +374,38 @@ var nodeItem = document.createElement('div'); nodeItem.classList.add('kanban-item'); nodeItem.dataset.eid = itemKanban.id; - nodeItem.innerHTML = itemKanban.title; + var nodeItemText = document.createElement('div'); + nodeItemText.classList.add('kanban-item-text'); + nodeItemText.dataset.eid = itemKanban.id; + nodeItemText.innerHTML = itemKanban.title; + nodeItem.appendChild(nodeItemText); //add function - nodeItem.clickfn = itemKanban.click; - nodeItem.dragfn = itemKanban.drag; - nodeItem.dragendfn = itemKanban.dragend; - nodeItem.dropfn = itemKanban.drop; + nodeItemText.clickfn = itemKanban.click; + nodeItemText.dragfn = itemKanban.drag; + nodeItemText.dragendfn = itemKanban.dragend; + nodeItemText.dropfn = itemKanban.drop; //add click handler of item - __onclickHandler(nodeItem); + __onclickHandler(nodeItemText); + + var onchange = function (colorL) { + var currentColor = itemKanban.color; + if (currentColor !== colorL.toString()) { + itemKanban.color = colorL.toString(); + self.onChange(); + } + }; + + var jscolorL; + nodeItem._jscLinkedInstance = undefined; + jscolorL = new jscolor(nodeItem,{onFineChange: onchange, valueElement:undefined}); + var currentColor = itemKanban.color; + // If not defined dont have it undefined + if (currentColor == undefined) { + currentColor = '' + } + console.log(currentColor); + jscolorL.fromString(currentColor); + contentBoard.appendChild(nodeItem); } //footer board @@ -575,7 +599,7 @@ }()); -}, { + }, { "dragula": 9 }], 2: [function (require, module, exports) { @@ -583,7 +607,7 @@ return Array.prototype.slice.call(a, n); } -}, {}], + }, {}], 3: [function (require, module, exports) { 'use strict'; @@ -598,7 +622,7 @@ }); }; -}, { + }, { "ticky": 10 }], 4: [function (require, module, exports) { @@ -669,7 +693,7 @@ return thing; }; -}, { + }, { "./debounce": 3, "atoa": 2 }], @@ -786,7 +810,7 @@ } }).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -}, { + }, { "./eventmap": 6, "custom-event": 7 }], @@ -807,7 +831,7 @@ module.exports = eventmap; }).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -}, {}], + }, {}], 7: [function (require, module, exports) { (function (global) { @@ -837,33 +861,33 @@ // IE >= 9 'function' === typeof document.createEvent ? function CustomEvent(type, params) { - var e = document.createEvent('CustomEvent'); - if (params) { - e.initCustomEvent(type, params.bubbles, params.cancelable, params.detail); - } else { - e.initCustomEvent(type, false, false, void 0); - } - return e; - } : - - // IE <= 8 - function CustomEvent(type, params) { - var e = document.createEventObject(); - e.type = type; - if (params) { - e.bubbles = Boolean(params.bubbles); - e.cancelable = Boolean(params.cancelable); - e.detail = params.detail; - } else { - e.bubbles = false; - e.cancelable = false; - e.detail = void 0; + var e = document.createEvent('CustomEvent'); + if (params) { + e.initCustomEvent(type, params.bubbles, params.cancelable, params.detail); + } else { + e.initCustomEvent(type, false, false, void 0); + } + return e; + } : + + // IE <= 8 + function CustomEvent(type, params) { + var e = document.createEventObject(); + e.type = type; + if (params) { + e.bubbles = Boolean(params.bubbles); + e.cancelable = Boolean(params.cancelable); + e.detail = params.detail; + } else { + e.bubbles = false; + e.cancelable = false; + e.detail = void 0; + } + return e; } - return e; - } }).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -}, {}], + }, {}], 8: [function (require, module, exports) { 'use strict'; @@ -899,7 +923,7 @@ rm: rmClass }; -}, {}], + }, {}], 9: [function (require, module, exports) { (function (global) { 'use strict'; @@ -1586,7 +1610,7 @@ module.exports = dragula; }).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) -}, { + }, { "./classes": 8, "contra/emitter": 4, "crossvent": 5 @@ -1605,5 +1629,5 @@ } module.exports = tick; -}, {}] + }, {}] }, {}, [1]); From 343e63f41b838419ce5a1f20b440a36d5ad553ce Mon Sep 17 00:00:00 2001 From: MTRNord Date: Tue, 31 Jul 2018 20:27:29 +0200 Subject: [PATCH 03/82] Fix drag bug --- www/kanban/jkanban.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/kanban/jkanban.js b/www/kanban/jkanban.js index 9c2a1cc5b..0ecaf2a22 100644 --- a/www/kanban/jkanban.js +++ b/www/kanban/jkanban.js @@ -113,7 +113,7 @@ el.classList.remove('is-moving'); self.options.dropBoard(el, target, source, sibling); if (typeof (el.dropfn) === 'function') - el.dropfn(el, target, source, sibling); el.dropfn(el, target, source, sibling); + el.dropfn(el, target, source, sibling); // TODO: update board object board order console.log("Drop " + $(el).attr("data-id") + " just before " + (sibling ? $(sibling).attr("data-id") : " end ")); From 525703e7d85c6458d7ba9fccb1d3b689c7d380b2 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 4 Sep 2018 10:36:19 +0200 Subject: [PATCH 04/82] Fix autostore popup displayed for dropped files --- www/common/common-ui-elements.js | 1 + www/common/outer/async-store.js | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 2d2d641cd..3f75d3369 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2329,6 +2329,7 @@ define([ UIElements.displayStorePadPopup = function (common, data) { if (storePopupState) { return; } storePopupState = true; + if (data && data.stored) { return; } // We won't display the popup for dropped files var text = Messages.autostore_notstored; var footer = Messages.autostore_settings; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index bb94b1e75..2be4ded4f 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -796,12 +796,20 @@ define([ password: data.password, path: data.path }, cb); + // Let inner know that dropped files shouldn't trigger the popup + postMessage(clientId, "AUTOSTORE_DISPLAY_POPUP", { + stored: true + }); return; } } else { sendDriveEvent('DRIVE_CHANGE', { path: ['drive', UserObject.FILES_DATA] }, clientId); + // Let inner know that dropped files shouldn't trigger the popup + postMessage(clientId, "AUTOSTORE_DISPLAY_POPUP", { + stored: true + }); } onSync(cb); }; From ab07554d0b4581501f5d260fcf03252486a74db6 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 4 Sep 2018 10:45:18 +0200 Subject: [PATCH 05/82] Fix initial size of the image preview in the mediatag dialog --- www/pad/app-pad.less | 17 +++++++++++++++++ www/pad/mediatag-plugin-dialog.js | 9 +-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/www/pad/app-pad.less b/www/pad/app-pad.less index 43109f3d3..65c38b053 100644 --- a/www/pad/app-pad.less +++ b/www/pad/app-pad.less @@ -47,6 +47,23 @@ body.cp-app-pad { display: block; overflow-x: auto; max-height: 100vh; + .cke_dialog_contents { + #ck-mediatag-preview { + margin: auto; + resize: both; + max-width: 300px; + max-height: 300px; + overflow: auto; + } + media-tag { + display: flex; + border-style: solid; + border-color: black; + &> * { + flex-shrink: 0; + } + } + } } .cke_wysiwyg_frame { diff --git a/www/pad/mediatag-plugin-dialog.js b/www/pad/mediatag-plugin-dialog.js index 11ee8daff..88fb43ef6 100644 --- a/www/pad/mediatag-plugin-dialog.js +++ b/www/pad/mediatag-plugin-dialog.js @@ -43,9 +43,7 @@ CKEDITOR.dialog.add('mediatag', function (editor) { type: 'html', id: 'preview', html: ''+ - '
' + '
' }, ] }, @@ -77,11 +75,6 @@ CKEDITOR.dialog.add('mediatag', function (editor) { var $preview = $(dialog).find('#ck-mediatag-preview'); var $clone = $(el.$).clone(); - $clone.css({ - display: 'flex', - 'border-style': 'solid', - 'border-color': 'black' - }); $preview.html('').append($clone); var center = function () { From 2f3576f90e9ece5a006fb1fd5dcc276df5e0ec6a Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 4 Sep 2018 10:50:34 +0200 Subject: [PATCH 06/82] Fix image preview not resized correctly in the mediatag dialog --- www/pad/app-pad.less | 3 --- www/pad/mediatag-plugin-dialog.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/www/pad/app-pad.less b/www/pad/app-pad.less index 65c38b053..213e21109 100644 --- a/www/pad/app-pad.less +++ b/www/pad/app-pad.less @@ -59,9 +59,6 @@ body.cp-app-pad { display: flex; border-style: solid; border-color: black; - &> * { - flex-shrink: 0; - } } } } diff --git a/www/pad/mediatag-plugin-dialog.js b/www/pad/mediatag-plugin-dialog.js index 88fb43ef6..472525edd 100644 --- a/www/pad/mediatag-plugin-dialog.js +++ b/www/pad/mediatag-plugin-dialog.js @@ -118,7 +118,7 @@ CKEDITOR.dialog.add('mediatag', function (editor) { update(); }); - setTimeout(center); + setTimeout(update); }, onOk: function() { var dialog = this; From 5b6aa4ba0a69bd30aba3b166c5aa1e01fa5fde12 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 4 Sep 2018 14:49:22 +0200 Subject: [PATCH 07/82] Reorder autostore options in Settings --- www/settings/inner.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/www/settings/inner.js b/www/settings/inner.js index ddb11f4d9..58fdb5524 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -236,9 +236,9 @@ define([ label: { class: 'noTitle' } }); var $div2 = $(h('div.cp-settings-autostore-radio', [ - opt1, + opt3, opt2, - opt3 + opt1 ])).appendTo($div); $div.find('input[type="radio"]').on('change', function () { From 2bd02ca69e4cf18f4ccdfd9ee1fdf8349db735d9 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 4 Sep 2018 08:59:38 -0400 Subject: [PATCH 08/82] bump version to 2.7.0 --- customize.dist/pages.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/customize.dist/pages.js b/customize.dist/pages.js index 22e33b01b..559d0e452 100644 --- a/customize.dist/pages.js +++ b/customize.dist/pages.js @@ -94,7 +94,7 @@ define([ ]) ]) ]), - h('div.cp-version-footer', "CryptPad v2.6.0 (Gibbon)") + h('div.cp-version-footer', "CryptPad v2.7.0 (Hedgehog)") ]); }; diff --git a/package.json b/package.json index 23e848602..2dca65ee5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "2.6.0", + "version": "2.7.0", "license": "AGPL-3.0+", "repository": { "type": "git", From 2b2995d2c0a0747f35625bf65353e837db3ad0af Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 4 Sep 2018 09:00:32 -0400 Subject: [PATCH 09/82] update a few english translations --- customize.dist/translations/messages.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 8510c06f3..5c2bc2b4c 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -669,7 +669,7 @@ define(function () { // pad out.pad_showToolbar = "Show toolbar"; out.pad_hideToolbar = "Hide toolbar"; - out.pad_base64 = "This pad contains images stored in an inefficient way. These images will increase significantly the size of the pad in your CryptDrive, and they will make it slower to load. Do you want to migrate these images to a better format (they will be stored separately in your drive)?"; // XXX + out.pad_base64 = "This pad contains images stored in an inefficient way. These images will significantly increase the size of the pad in your CryptDrive, and make it slower to load. You can migrate these files to a new format which will be stored separately in your CryptDrive. Do you want to migrate these images now?"; // markdown toolbar out.mdToolbar_button = "Show or hide the Markdown toolbar"; @@ -1244,13 +1244,13 @@ define(function () { out.chrome68 = "It seems that you're using the browser Chrome or Chromium version 68. It contains a bug resulting in the page turning completely white after a few seconds or the page being unresponsive to clicks. To fix this issue, you can switch to another tab and come back, or try to scroll in the page. This bug should be fixed in the next version of your browser."; // Manual pad storage popup - out.autostore_notstored = "This pad is not in your CryptDrive. Do you want to store it now?"; // XXX - out.autostore_settings = "You can enable automatic pad storage in your Settings page!"; // XXX + out.autostore_notstored = "This pad is not in your CryptDrive. Do you want to store it now?"; + out.autostore_settings = "You can enable automatic pad storage in your Settings page!"; out.autostore_store = "Store"; out.autostore_hide = "Don't store"; out.autostore_error = "Unexpected error: we were unable to store this pad, please try again."; out.autostore_saved = "The pad was successfully stored in your CryptDrive!"; - out.autostore_forceSave = "Store the file in CryptDrive"; // File upload modal + out.autostore_forceSave = "Store the file in your CryptDrive"; // File upload modal out.autostore_notAvailable = "You must store this pad in your CryptDrive before being able to use this feature."; // Properties/tags/move to trash return out; From a203a67a538f0b0fc2b53cd2631f4ac9b7921a9a Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 4 Sep 2018 09:06:32 -0400 Subject: [PATCH 10/82] update another translation --- customize.dist/translations/messages.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 5c2bc2b4c..bf4b2160d 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -1,4 +1,4 @@ -define(function () { +DEFINE(function () { var out = {}; out.main_title = "CryptPad: Zero Knowledge, Collaborative Real Time Editing"; @@ -252,7 +252,7 @@ define(function () { out.pad_mediatagRatio = "Keep ratio"; out.pad_mediatagBorder = "Border width (px)"; out.pad_mediatagPreview = "Preview"; - out.pad_mediatagImport = 'Save in CryptDrive'; + out.pad_mediatagImport = 'Save in your CryptDrive'; out.pad_mediatagOptions = 'Image properties'; // Kanban From 0e70961074a814a6f9b23949d524de43c8d8ff7c Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 4 Sep 2018 09:14:13 -0400 Subject: [PATCH 11/82] password-protected shared folders aren't urgent. dropping the XXX --- www/drive/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/drive/main.js b/www/drive/main.js index d81f07f39..7395b163a 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -41,7 +41,7 @@ define([ var secret = Utils.Hash.getSecrets('drive', hash); if (hash) { // Add a shared folder! - // XXX password? + // TODO password? Cryptpad.addSharedFolder(secret, function (id) { window.CryptPad_newSharedFolder = id; // Update the hash in the address bar From 4a2fa77bb73b4a5f8187bca62d84eff0a03a61f8 Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 4 Sep 2018 15:28:20 +0200 Subject: [PATCH 12/82] French translation --- customize.dist/translations/messages.fr.js | 27 ++++++++++++++++++++++ customize.dist/translations/messages.js | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 60744fc8e..f24cf04b0 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -220,6 +220,7 @@ define(function () { out.notifyRenamed = "{0} a changé son nom en {1}"; out.notifyLeft = "{0} a quitté la session collaborative"; + out.ok = 'OK'; out.okButton = 'OK (Entrée)'; out.cancel = "Annuler"; @@ -246,6 +247,11 @@ define(function () { out.pad_mediatagTitle = "Options du Media-Tag"; out.pad_mediatagWidth = "Largeur (px)"; out.pad_mediatagHeight = "Hauteur (px)"; + out.pad_mediatagRatio = "Préserver les proportions"; + out.pad_mediatagBorder = "Éaisseur de la bordure (px)"; + out.pad_mediatagPreview = "Aperçu"; + out.pad_mediatagImport = 'Sauver dans votre CryptDrive'; + out.pad_mediatagOptions = 'Propriétés de l\'image'; // Kanban out.kanban_newBoard = "Nouveau tableau"; @@ -561,6 +567,14 @@ define(function () { out.settings_importConfirm = "Êtes-vous sûr de vouloir importer les pads récents de ce navigateur dans le CryptDrive de votre compte utilisateur ?"; out.settings_importDone = "Importation terminée"; + out.settings_autostoreTitle = "Stockage des pads dans CryptDrive"; + out.settings_autostoreHint = "Le stockage Automatique des pads permet de sauver tous les pads que vous visitez dans votre CryptDrive, sans action de votre part.
" + + "Le stockage Manuel (toujours demander) permet de ne pas stocker automatiquement les pads, mais d'afficher un message vous demandant s'il faut le faire ou non.
" + + "Le stockage Manuel (ne pas demander) permet de ne pas stocker les pads ni d'afficher le message. Une option permettant de les stocker sera toujours disponible, mais cachée."; + out.settings_autostoreYes = "Automatique"; + out.settings_autostoreNo = "Manuel (ne pas demander)"; + out.settings_autostoreMaybe = "Manuel (toujours demander)"; + out.settings_userFeedbackTitle = "Retour d'expérience"; out.settings_userFeedbackHint1 = "CryptPad peut envoyer des retours d'expérience très limités vers le serveur, de manière à nous permettre d'améliorer l'expérience des utilisateurs. "; out.settings_userFeedbackHint2 = "Le contenu de vos pads et les clés de déchiffrement ne seront jamais partagés avec le serveur."; @@ -646,6 +660,7 @@ define(function () { // pad out.pad_showToolbar = "Afficher la barre d'outils"; out.pad_hideToolbar = "Cacher la barre d'outils"; + out.pad_base64 = "Ce pad contient des images stockées de manière inefficace. Ces images vont augmenter de manière significative la taille du pad dans votre CryptDrive, et le rendre plus lent à charger. Vous pouvez migrer ces fichiers afin de les stocker séparément dans votre CryptDrive. Voulez-vous commencer la migration maintenant?"; // markdown toolbar out.mdToolbar_button = "Afficher ou cacher la barre d'outils Markdown"; @@ -1177,5 +1192,17 @@ define(function () { out.sharedFolders_create_password = "Mot de passe du dossier"; out.sharedFolders_share = "Partager cette URL avec d'autres utilisateurs enregistrés leur donne accès au dossier partagé. Une fois l'URL ouverte, le dossier partagé sera ajouté au répertoire racine de leur CryptDrive."; + out.chrome68 = "Il semblerait que vous utilisiez le navigateur Chrome version 68. Ce navigateur contient un bug rendant certaines pages entièrement blanches après quelques secondes ou bloquant les clics. Pour corriger ce problème, vous pouvez vous déplacer vers un nouvel onglet et revenir ou vous pouvez essayer de faire défiler la page. Ce bug devrait être corrigé dans la prochaine version du navigateur."; + + // Manual pad storage popup + out.autostore_notstored = "Ce pad n'est pas dans votre CryptDrive. Souhaitez-vous le stocker ?"; + out.autostore_settings = "Vous pouvez activer le stockage automatique des pads dans vos Préférences !"; + out.autostore_store = "Stocker"; + out.autostore_hide = "Ne pas stocker"; + out.autostore_error = "Erreur : nous n'avons pas réussi à stocker ce pad, veuillez ré-essayer."; + out.autostore_saved = "Ce pad a été stocké avec succès dans votre CryptDrive !"; + out.autostore_forceSave = "Stocker le fichier dans votre CryptDrive"; // File upload modal + out.autostore_notAvailable = "Vous devez stocker ce pad dans votre CryptDrive avant de pouvoir utiliser cette fonctionnalité."; + return out; }); diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index bf4b2160d..f88f211e3 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -1,4 +1,4 @@ -DEFINE(function () { +define(function () { var out = {}; out.main_title = "CryptPad: Zero Knowledge, Collaborative Real Time Editing"; From 8dec54d550c3ed2900cd444024c6079bec9bbb20 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 4 Sep 2018 09:38:26 -0400 Subject: [PATCH 13/82] WIP Changelog --- CHANGELOG.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c93d79324..b569fa269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,60 @@ +# Hedgehog release (v2.7.0) + +## Update notes + +### Features + +* checkmark styles +* contextmenu to 'adopt' media-tags +* new corner popup UI +* migrate base64 images +* configurable pad storage behaviour +* add progress bar for loading screen +* improved media-tag properties dialog in rich text pad + +### Bug fixes + +* Chrome 68 warning +* increase requirejs timeout for sharedWorkers + + +# Gibbon release (v2.6.0) + +* cp-tools font + * template icon +* nicer spinner +* footer on homepage +* refactored less files + * documentation for what was done +* shared folders + * it still says shared drive... + * application_config.js allows us to enable them + * but they are disabled by default +* use fs-extra to support folders stored across different partitions +* fancy less loader +* hack to prevent alertify from injecting css + * see common-interface.js +* support roHref +* fix bugs in password change logic +* Migrate-6 feedback key +* ProxyManager + * `disableSharedFolders` + +## Goals + +## Update notes + +* shared workers + * add `config.disableSharedFolders = true;` + * per-user `localStorage.CryptPad_SF = "1";` + + + +### Features + +### Bug fixes + + # Fossa release (v2.5.0) ## Goals From e83b225295705b1398fa17dce5a71f08baebf547 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 4 Sep 2018 10:28:01 -0400 Subject: [PATCH 14/82] update changelogs --- CHANGELOG.md | 64 +++++++++++++++++++++++++--------------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b569fa269..924a970f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,59 +1,57 @@ # Hedgehog release (v2.7.0) +## Goals + +This release overlapped with the publication and presentation of a paper written about CryptPad's architecture. +As such, we didn't plan for any very ambitious new features, and instead focused on bug fixes and some new workflows. + ## Update notes +This is a fairly simple release. Just download the latest commits and update your cache-busting string. + ### Features -* checkmark styles -* contextmenu to 'adopt' media-tags -* new corner popup UI -* migrate base64 images -* configurable pad storage behaviour -* add progress bar for loading screen -* improved media-tag properties dialog in rich text pad +* In order to address some privacy concerns, we've changed CryptPad such that pads are not immediately stored in your CryptDrive as soon as you open them. Instead, users are presented with a prompt in the bottom-right corner which asks them whether they'd like to store it manually. Alternatively, you can use your settings page to revert to the old automatic behaviour, or choose not to store, and to never be asked. +* It was brought to our attention that it was possible to upload base64-encoded images in the rich text editor. These images had a negative performance impact on such pads. From now on, if these images are detected in a pad, users are prompted to run a migration to convert them to uploaded (and encrypted) files. +* We've added a progress bar which is displayed while you are loading a pad, as we found that it was not very clear whether large pads were loading, or if they had become unresponsive due to a bug. +* We've added an option to allow users to right-click uploaded files wherever they appear, and to store that file in their CryptDrive. +* We've improved the dialog which is used to modify the properties of encrypted media embedded within rich text pads. ### Bug fixes -* Chrome 68 warning -* increase requirejs timeout for sharedWorkers - +* Due to a particularly disastrous bug in Chrome 68 which was unfortunately beyond our power to fix, we've added a warning for anyone affected by that bug to let them know the cause. +* We've increased the module loading timeout value used by requirejs in our sharedWorker implementation to match the value used by the rest of CryptPad. # Gibbon release (v2.6.0) -* cp-tools font - * template icon -* nicer spinner -* footer on homepage -* refactored less files - * documentation for what was done -* shared folders - * it still says shared drive... - * application_config.js allows us to enable them - * but they are disabled by default -* use fs-extra to support folders stored across different partitions -* fancy less loader -* hack to prevent alertify from injecting css - * see common-interface.js -* support roHref -* fix bugs in password change logic -* Migrate-6 feedback key -* ProxyManager - * `disableSharedFolders` - ## Goals +For this release we focused on deploying two very large changes in CryptPad. +For one, we'd worked on a large refactoring of the system we use to compile CSS from LESS, so as to make it more efficient. +Secondly, we reworked the architecture we use for implementing the CryptDrive functionality, so as to integrate support for shared folders. + ## Update notes -* shared workers - * add `config.disableSharedFolders = true;` - * per-user `localStorage.CryptPad_SF = "1";` +To test the _shared folders_ functionality, users can run the following command in their browser console: +`localStorage.CryptPad_SF = "1";` +Alternatively, if the instance administrator would like to enable shared folders for all users, they can do so via their `/customize/application_config.js` file, by adding the following line: + +`config.disableSharedFolders = true;` ### Features +* As mentioned in the _goals_ for this release, we've merged in the work done to drastically improve performance when compiling styles. The system features documentation for anyone interested in understanding how it works. +* We've refactored the APIs used to interact with your CryptDrive, implementing a single interface with which applications can interact, which then manages any number of sub-objects each representing a shared folder. Shared folders are still disabled by default. See the _Update notes_ section for more information. +* The home page now features the same footer which has been displayed on all other information pages until now. +* We've added a slightly nicer spinner icon on loading pages. +* We've created a custom font _cp-tools_ for our custom-designed icons + ### Bug fixes +* We've accepted a pull request implementing serverside support for moving files across different drives, for system administrators hosting CryptPad on systems which segregate folders on different partitions. +* We've addressed a report of an edge case in CryptPad's user password change logic which could cause users to delete their accounts. # Fossa release (v2.5.0) From e6743887fcbedd13663e27c2aafc8e70bf60f31f Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 6 Sep 2018 18:41:22 +0200 Subject: [PATCH 15/82] Refactor contacts/messenger so that it is usable directly in the apps --- .../src/less2/include/framework.less | 2 + .../src/less2/include/messenger.less | 252 ++++++++++ customize.dist/src/less2/include/toolbar.less | 31 +- www/common/common-messaging.js | 8 +- www/common/common-messenger.js | 380 +++++++++++---- www/common/cryptpad-common.js | 8 + www/common/outer/async-store.js | 77 +-- www/common/outer/store-rpc.js | 2 + www/common/sframe-app-framework.js | 1 + www/common/sframe-app-outer.js | 3 +- www/common/sframe-common-outer.js | 8 + www/common/sframe-messenger-inner.js | 4 +- www/common/sframe-protocol.js | 4 + www/common/toolbar3.js | 72 ++- www/contacts/app-contacts.less | 228 +-------- www/contacts/inner.js | 19 +- www/contacts/messenger-ui.js | 455 +++++++++++------- 17 files changed, 1001 insertions(+), 553 deletions(-) create mode 100644 customize.dist/src/less2/include/messenger.less diff --git a/customize.dist/src/less2/include/framework.less b/customize.dist/src/less2/include/framework.less index 4853f663b..a273e8f77 100644 --- a/customize.dist/src/less2/include/framework.less +++ b/customize.dist/src/less2/include/framework.less @@ -12,6 +12,7 @@ @import (reference) './font.less'; @import (reference) "./app-print.less"; @import (reference) "./app-noscroll.less"; +@import (reference) "./messenger.less"; .framework_main(@bg-color, @warn-color, @color) { --LessLoader_require: LessLoader_currentFile(); @@ -36,6 +37,7 @@ .tippy_main(); .checkmark_main(20px); .password_main(); + .messenger_main(); .creation_main( @bg-color: @bg-color, @color: @color diff --git a/customize.dist/src/less2/include/messenger.less b/customize.dist/src/less2/include/messenger.less new file mode 100644 index 000000000..cdac34752 --- /dev/null +++ b/customize.dist/src/less2/include/messenger.less @@ -0,0 +1,252 @@ +@import (reference) './avatar.less'; +@import (reference) "./colortheme-all.less"; + +.messenger_main() { + --LessLoader_require: LessLoader_currentFile(); +}; +& { + @keyframes example { + 0% { + background: rgba(0,0,0,0.1); + } + 50% { + background: rgba(0,0,0,0.3); + } + 100% { + background: rgba(0,0,0,0.1); + } + } + + @button-border: 2px; + @bg-color: @colortheme_friends-bg; + @color: @colortheme_friends-color; + + #cp-app-contacts-container { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + min-height: 0; + &.ready { + background-size: cover; + background-position: center; + } + } + + .cp-app-contacts-spinner { + display: none; + } + + .cp-app-contacts-initializing { + .cp-app-contacts-spinner { + color: white; + display: block; + } + .cp-app-contacts-info { + display: none; + } + #cp-app-contacts-friendlist, + #cp-app-contacts-messaging { + display: flex; + justify-content: center; + align-items: center; + } + } + + #cp-app-contacts-friendlist { + width: 350px; + max-width: 30%; + height: 100%; + background-color: lighten(@bg-color, 10%); + overflow-y: auto; + .cp-app-contacts-friend { + background: rgba(0,0,0,0.1); + padding: 5px; + margin: 10px; + cursor: pointer; + position: relative; + .cp-app-contacts-right-col { + margin-left: 5px; + display: flex; + flex-flow: column; + } + &:hover { + background-color: rgba(0,0,0,0.3); + } + &.cp-app-contacts-notify { + animation: example 2s ease-in-out infinite; + } + } + .cp-app-contacts-remove { + cursor: pointer; + width: 20px; + &:hover { + color: darken(@color, 20%); + } + } + } + + #cp-app-contacts-friendlist .cp-app-contacts-friend, #cp-app-contacts-messaging .cp-avatar { + .avatar_main(30px); + &.cp-avatar { + display: flex; + } + cursor: pointer; + color: @color; + media-tag { + img { + color: #000; + } + } + media-tag, .cp-avatar-default { + margin-right: 5px; + } + .cp-app-contacts-status { + width: 5px; + display: inline-block; + position: absolute; + right: 0; + top: 0; + bottom: 0; + opacity: 0.7; + background-color: #777; + &.cp-app-contacts-online { + background-color: green; + } + &.cp-app-contacts-offline { + background-color: red; + } + } + } + + .placeholder (@color: #bbb) { + &::-webkit-input-placeholder { /* WebKit, Blink, Edge */ + color: @color; + } + &:-moz-placeholder { /* Mozilla Firefox 4 to 18 */ + color: @color; + opacity: 1; + } + &::-moz-placeholder { /* Mozilla Firefox 19+ */ + color: @color; + opacity: 1; + } + &:-ms-input-placeholder { /* Internet Explorer 10-11 */ + color: @color; + } + &::-ms-input-placeholder { /* Microsoft Edge */ + color: @color; + } + } + + #cp-app-contacts-messaging { + flex: 1; + height: 100%; + background-color: lighten(@bg-color, 20%); + min-width: 0; + + .cp-app-contacts-info { + padding: 20px; + } + .cp-app-contacts-header { + background-color: lighten(@bg-color, 15%); + padding: 0; + display: flex; + justify-content: space-between; + align-items: center; + height: 50px; + + .hover () { + height: 100%; + line-height: 30px; + padding: 10px; + &:hover { + background-color: rgba(50,50,50,0.3); + } + } + + .cp-avatar, + .cp-app-contacts-right-col { + flex:1 1 auto; + } + .cp-app-contacts-remove-history { + .hover; + } + .cp-avatar { + margin: 10px; + } + .cp-app-contacts-more-history { + //display: none; + .hover; + &.cp-app-contacts-faded { + color: darken(@bg-color, 5%); + } + } + } + .cp-app-contacts-chat { + height: 100%; + display: flex; + flex-flow: column; + .cp-app-contacts-messages { + padding: 0 20px; + margin: 10px 0; + flex: 1; + overflow-x: auto; + .cp-app-contacts-message { + & > div { + padding: 0 10px; + } + .cp-app-contacts-content { + overflow: hidden; + word-wrap: break-word; + &> * { + margin: 0; + } + } + .cp-app-contacts-date { + display: none; + font-style: italic; + } + .cp-app-contacts-sender { + margin-top: 10px; + font-weight: bold; + background-color: rgba(0,0,0,0.1); + } + } + } + } + .cp-app-contacts-input { + background-color: lighten(@bg-color, 15%); + height: auto; + min-height: 50px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 5%; + textarea { + margin: 5px 0; + padding: 5px 10px; + border: none; + height: 50px; + flex: 1; + background-color: darken(@bg-color, 10%); + color: @color; + resize: none; + overflow-y: auto; + .placeholder(#bbb); + &[disabled=true] { + .placeholder(#999); + } + } + button { + height: 50px; + border-radius: 0; + border: none; + background-color: darken(@bg-color, 15%); + &:hover { + background-color: darken(@bg-color, 20%); + } + } + } + } +} diff --git a/customize.dist/src/less2/include/toolbar.less b/customize.dist/src/less2/include/toolbar.less index 858c0a8d9..5a17cc291 100644 --- a/customize.dist/src/less2/include/toolbar.less +++ b/customize.dist/src/less2/include/toolbar.less @@ -134,9 +134,37 @@ } } - .cp-toolbar-userlist-drawer { + .cp-toolbar-chat-drawer { background-color: @toolbar-bg-color; background-color: var(--toolbar-bg-color); + font: @colortheme_app-font-size @colortheme_font; + width: 400px; + display: block; + overflow-y: auto; + overflow-x: hidden; + padding: 0; + box-sizing: border-box; + position: relative; + order: -2; + #cp-app-contacts-container { + height: 100%; + } + .cp-toolbar-chat-drawer-close { + color: @toolbar-color; + color: var(--toolbar-color); + position: absolute; + top: 0; + right: 1px; + font-size: 15px; + opacity: 0.5; + cursor: pointer; + text-shadow: unset; + &:hover { + opacity: 1; + } + } + } + .cp-toolbar-userlist-drawer { font: @colortheme_app-font-size @colortheme_font; min-width: 175px; width: 175px; @@ -145,6 +173,7 @@ overflow-x: hidden; padding: 10px; box-sizing: border-box; + order: -1; .cp-toolbar-userlist-drawer-close { position: absolute; margin-top: -10px; diff --git a/www/common/common-messaging.js b/www/common/common-messaging.js index 1c75927fc..b0d13ec81 100644 --- a/www/common/common-messaging.js +++ b/www/common/common-messaging.js @@ -150,7 +150,8 @@ define([ } cfg.friendComplete({ logText: Messages.contacts_added, - netfluxId: sender + netfluxId: sender, + friend: msgData }); var msg = ["FRIEND_REQ_ACK", chan]; var msgStr = Crypto.encrypt(JSON.stringify(msg), key); @@ -163,7 +164,7 @@ define([ if (i !== -1) { pendingRequests.splice(i, 1); } cfg.friendComplete({ logText: Messages.contacts_rejected, - netfluxId: sender + netfluxId: sender, }); cfg.updateMetadata(); return; @@ -180,7 +181,8 @@ define([ } cfg.friendComplete({ logText: Messages.contacts_added, - netfluxId: sender + netfluxId: sender, + friend: data }); }); return; diff --git a/www/common/common-messenger.js b/www/common/common-messenger.js index cd0a5626d..73275a2cc 100644 --- a/www/common/common-messenger.js +++ b/www/common/common-messenger.js @@ -5,7 +5,9 @@ define([ '/common/common-util.js', '/common/common-realtime.js', '/common/common-constants.js', -], function (Crypto, Curve, Hash, Util, Realtime, Constants) { + + '/bower_components/nthen/index.js', +], function (Crypto, Curve, Hash, Util, Realtime, Constants, nThen) { 'use strict'; var Msg = { inputs: [], @@ -65,6 +67,7 @@ define([ update: [], friend: [], unfriend: [], + ready: [] }, range_requests: {}, }; @@ -95,19 +98,26 @@ define([ Msg.hk = network.historyKeeper; var friends = getFriendList(proxy); - var getChannel = function (curvePublic) { - var friend = friends[curvePublic]; - if (!friend) { return; } - var chanId = friend.channel; - if (!chanId) { return; } + var getChannel = function (chanId) { return channels[chanId]; }; - var initRangeRequest = function (txid, curvePublic, sig, cb) { + var getFriendFromChannel = function (id) { + var friend; + for (var k in friends) { + if (friends[k].channel === id) { + friend = friends[k]; + break; + } + } + return friend; + }; + + var initRangeRequest = function (txid, chanId, sig, cb) { messenger.range_requests[txid] = { messages: [], cb: cb, - curvePublic: curvePublic, + chanId: chanId, sig: sig, }; }; @@ -120,24 +130,22 @@ define([ delete messenger.range_requests[txid]; }; - messenger.getMoreHistory = function (curvePublic, hash, count, cb) { + messenger.getMoreHistory = function (chanId, hash, count, cb) { if (typeof(cb) !== 'function') { return; } if (typeof(hash) !== 'string') { - // FIXME hash is not necessarily defined. - // What does this mean? - console.error("not sure what to do here"); - return; + // Channel is empty! + return void cb(void 0, []); } - var chan = getChannel(curvePublic); + var chan = getChannel(chanId); if (typeof(chan) === 'undefined') { console.error("chan is undefined. we're going to have a problem here"); return; } var txid = Util.uid(); - initRangeRequest(txid, curvePublic, hash, cb); + initRangeRequest(txid, chanId, hash, cb); var msg = [ 'GET_HISTORY_RANGE', chan.id, { from: hash, count: count, @@ -151,23 +159,58 @@ define([ }); }; - var getCurveForChannel = function (id) { + /*var getCurveForChannel = function (id) { var channel = channels[id]; if (!channel) { return; } return channel.curve; - }; + };*/ + + /*messenger.getChannelHead = function (id, cb) { + var channel = getChannel(id); + if (channel.isFriendChat) { + var friend; + for (var k in friends) { + if (friends[k].channel === id) { + friend = friends[k]; + break; + } + } + if (!friend) { return void cb('NO_SUCH_FRIEND'); } + cb(void 0, friend.lastKnownHash); + } else { + // TODO room + cb('NOT_IMPLEMENTED'); + } + };*/ - messenger.getChannelHead = function (curvePublic, cb) { - var friend = friends[curvePublic]; - if (!friend) { return void cb('NO_SUCH_FRIEND'); } - cb(void 0, friend.lastKnownHash); + messenger.setChannelHead = function (id, hash, cb) { + var channel = getChannel(id); + if (channel.isFriendChat) { + var friend = getFriendFromChannel(id); + if (!friend) { return void cb('NO_SUCH_FRIEND'); } + friend.lastKnownHash = hash; + } else { + // TODO room + return void cb('NOT_IMPLEMENTED'); + } + cb(); }; - messenger.setChannelHead = function (curvePublic, hash, cb) { - var friend = friends[curvePublic]; - if (!friend) { return void cb('NO_SUCH_FRIEND'); } - friend.lastKnownHash = hash; - cb(); + // Make sure the data we have about our friends are up-to-date when we see them online + var checkFriendData = function (curve, data) { + if (curve === proxy.curvePublic) { return; } + var friend = getFriend(proxy, curve); + var types = []; + Object.keys(data).forEach(function (k) { + if (friend[k] !== data[k]) { + types.push(k); + friend[k] = data[k]; + } + }); + + eachHandler('update', function (f) { + f(clone(data), types); + }); }; // Id message allows us to map a netfluxId with a public curve key @@ -206,20 +249,22 @@ define([ // the sender field. This is to prevent replay attacks. if (parsed[2] !== sender || !parsed[1]) { return; } channel.mapId[sender] = parsed[1]; + checkFriendData(parsed[1].curvePublic, parsed[1]); eachHandler('join', function (f) { f(parsed[1], channel.id); }); if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK // Answer with your own key - var rMsg = [Types.mapIdAck, proxy.curvePublic, channel.wc.myID]; + var myData = createData(proxy); + delete myData.channel; + var rMsg = [Types.mapIdAck, myData, channel.wc.myID]; var rMsgStr = JSON.stringify(rMsg); var cryptMsg = channel.encryptor.encrypt(rMsgStr); network.sendto(sender, cryptMsg); }; - var orderMessages = function (curvePublic, new_messages /*, sig */) { - var channel = getChannel(curvePublic); + var orderMessages = function (channel, new_messages /*, sig */) { var messages = channel.messages; // TODO improve performance, guarantee correct ordering @@ -250,8 +295,9 @@ define([ author: parsedMsg[1], time: parsedMsg[2], text: parsedMsg[3], + channel: channel.id // this makes debugging a whole lot easier - curve: getCurveForChannel(channel.id), + //curve: getCurveForChannel(channel.id), }; channel.messages.push(res); @@ -262,31 +308,21 @@ define([ return true; } if (parsedMsg[0] === Types.update) { - if (parsedMsg[1] === proxy.curvePublic) { return; } - curvePublic = parsedMsg[1]; - var newdata = parsedMsg[3]; - var data = getFriend(proxy, parsedMsg[1]); - var types = []; - Object.keys(newdata).forEach(function (k) { - if (data[k] !== newdata[k]) { - types.push(k); - data[k] = newdata[k]; - } - }); - - eachHandler('update', function (f) { - f(clone(newdata), curvePublic); - }); + checkFriendData(parsedMsg[1], parsedMsg[3]); return; } if (parsedMsg[0] === Types.unfriend) { curvePublic = parsedMsg[1]; - delete friends[curvePublic]; - removeFromFriendList(parsedMsg[1], function () { + // If this a removal from our part by in another tab, do nothing. + // The channel is already closed in the proxy.on('remove') part + if (curvePublic === proxy.curvePublic) { return; } + + removeFromFriendList(curvePublic, function () { channel.wc.leave(Types.unfriend); + delete channels[channel.id]; eachHandler('unfriend', function (f) { - f(curvePublic); + f(curvePublic, false); }); }); return; @@ -324,7 +360,7 @@ define([ }); }); eachHandler('update', function (f) { - f(myData, myData.curvePublic); + f(myData, ['displayName', 'profile', 'avatar']); }); friends.me = myData; } @@ -356,8 +392,7 @@ define([ req.messages.push(parsed[2]); } else if (type === 'HISTORY_RANGE_END') { // process all the messages (decrypt) - var curvePublic = req.curvePublic; - var channel = getChannel(curvePublic); + var channel = getChannel(req.chanId); var decrypted = req.messages.map(function (msg) { if (msg[2] !== 'MSG') { return; } @@ -379,11 +414,11 @@ define([ author: O.d[1], time: O.d[2], text: O.d[3], - curve: curvePublic, + channel: req.chanId }; }); - orderMessages(curvePublic, decrypted, req.sig); + orderMessages(channel, decrypted, req.sig); req.cb(void 0, decrypted); return deleteRangeRequest(txid); } else { @@ -395,6 +430,7 @@ define([ if ((parsed.validateKey || parsed.owners) && parsed.channel) { return; } + // End of initial history if (parsed.state && parsed.state === 1 && parsed.channel) { if (channels[parsed.channel]) { // parsed.channel is Ready @@ -409,6 +445,7 @@ define([ } return; } + // Initial history message var chan = parsed[3]; if (!chan || !channels[chan]) { return; } pushMsg(channels[chan], parsed[4]); @@ -440,7 +477,7 @@ define([ if (!data) { // friend is not valid console.error('friend is not valid'); - return; + return void cb('INVALID_FRIEND'); } var channel = channels[data.channel]; @@ -458,12 +495,13 @@ define([ var msgStr = JSON.stringify(msg); var cryptMsg = channel.encryptor.encrypt(msgStr); - // TODO emit remove_friend event? try { channel.wc.bcast(cryptMsg).then(function () { - delete friends[curvePublic]; - delete channels[curvePublic]; - Realtime.whenRealtimeSyncs(realtime, function () { + removeFromFriendList(curvePublic, function () { + delete channels[channel.id]; + eachHandler('unfriend', function (f) { + f(curvePublic, true); + }); cb(); }); }, function (err) { @@ -476,7 +514,7 @@ define([ }; var getChannelMessagesSince = function (chan, data, keys) { - console.log('Fetching [%s] messages since [%s]', data.curvePublic, data.lastKnownHash || ''); + console.log('Fetching [%s] messages since [%s]', chan.id, data.lastKnownHash || ''); var cfg = { validateKey: keys.validateKey, owners: [proxy.edPublic, data.edPublic], @@ -489,39 +527,19 @@ define([ }); }; - var openFriendChannel = function (data, f) { - var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate); + var openChannel = function (data) { + var keys = data.keys; var encryptor = Curve.createEncryptor(keys); network.join(data.channel).then(function (chan) { var channel = channels[data.channel] = { id: data.channel, + isFriendChat: data.isFriendChat, sending: false, - friendEd: f, - keys: keys, - curve: data.curvePublic, encryptor: encryptor, messages: [], wc: chan, userList: [], mapId: {}, - send: function (payload, cb) { - if (!network.webChannels.some(function (wc) { - if (wc.id === channel.wc.id) { return true; } - })) { - return void cb('NO_SUCH_CHANNEL'); - } - - var msg = [Types.message, proxy.curvePublic, +new Date(), payload]; - var msgStr = JSON.stringify(msg); - var cryptMsg = channel.encryptor.encrypt(msgStr); - - channel.wc.bcast(cryptMsg).then(function () { - pushMsg(channel, cryptMsg); - cb(); - }, function (err) { - cb(err); - }); - } }; chan.on('message', function (msg, sender) { onMessage(msg, sender, chan); @@ -530,9 +548,12 @@ define([ var onJoining = function (peer) { if (peer === Msg.hk) { return; } if (channel.userList.indexOf(peer) !== -1) { return; } - channel.userList.push(peer); - var msg = [Types.mapId, proxy.curvePublic, chan.myID]; + + // Join event will be sent once we are able to ID this peer + var myData = createData(proxy); + delete myData.channel; + var msg = [Types.mapId, myData, chan.myID]; var msgStr = JSON.stringify(msg); var cryptMsg = channel.encryptor.encrypt(msgStr); network.sendto(peer, cryptMsg); @@ -544,16 +565,24 @@ define([ }); chan.on('join', onJoining); chan.on('leave', function (peer) { - var curvePublic = channel.mapId[peer]; var i = channel.userList.indexOf(peer); while (i !== -1) { channel.userList.splice(i, 1); i = channel.userList.indexOf(peer); } // update status - if (!curvePublic) { return; } + var otherData = channel.mapId[peer]; + if (!otherData) { return; } + + // Make sure the leaving user is not connected with another netflux id + if (channel.userList.some(function (nId) { + return channel.mapId[nId] + && channel.mapId[nId].curvePublic === otherData.curvePublic; + })) { return; } + + // Send the notification eachHandler('leave', function (f) { - f(curvePublic, channel.id); + f(otherData, channel.id); }); }); @@ -573,7 +602,7 @@ define([ })); }; - messenger.openFriendChannel = function (curvePublic, cb) { + /*messenger.openFriendChannel = function (curvePublic, cb) { if (typeof(curvePublic) !== 'string') { return void cb('INVALID_ID'); } if (typeof(cb) !== 'function') { throw new Error('expected callback'); } @@ -585,10 +614,10 @@ define([ if (!channel) { return void cb('E_NO_CHANNEL'); } joining[channel] = cb; openFriendChannel(friend, curvePublic); - }; + };*/ - messenger.sendMessage = function (curvePublic, payload, cb) { - var channel = getChannel(curvePublic); + messenger.sendMessage = function (id, payload, cb) { + var channel = getChannel(id); if (!channel) { return void cb('NO_CHANNEL'); } if (!network.webChannels.some(function (wc) { if (wc.id === channel.wc.id) { return true; } @@ -597,6 +626,9 @@ define([ } var msg = [Types.message, proxy.curvePublic, +new Date(), payload]; + if (!channel.isFriendChat) { + msg.push(proxy[Constants.displayNameKey]); + } var msgStr = JSON.stringify(msg); var cryptMsg = channel.encryptor.encrypt(msgStr); @@ -608,18 +640,27 @@ define([ }); }; - messenger.getStatus = function (curvePublic, cb) { - var channel = getChannel(curvePublic); + messenger.getStatus = function (chanId, cb) { + // Display green status if one member is not me + var channel = getChannel(chanId); if (!channel) { return void cb('NO_SUCH_CHANNEL'); } var online = channel.userList.some(function (nId) { - return channel.mapId[nId] === curvePublic; + var data = channel.mapId[nId] || undefined; + if (!data) { return false; } + return data.curvePublic !== proxy.curvePublic; }); cb(void 0, online); }; - messenger.getFriendInfo = function (curvePublic, cb) { + messenger.getFriendInfo = function (channel, cb) { setTimeout(function () { - var friend = friends[curvePublic]; + var friend; + for (var k in friends) { + if (friends[k].channel === channel) { + friend = friends[k]; + break; + } + } if (!friend) { return void cb('NO_SUCH_FRIEND'); } // this clone will be redundant when ui uses postmessage cb(void 0, clone(friend)); @@ -633,28 +674,163 @@ define([ }); }; - // TODO listen for changes to your friend list - // emit 'update' events for clients + var loadFriend = function (friend, cb) { + var channel = friend.channel; + if (getChannel(channel)) { return void cb(); } - //var update = function (curvePublic + joining[channel] = cb; + var keys = Curve.deriveKeys(friend.curvePublic, proxy.curvePrivate); + var data = { + keys: keys, + channel: friend.channel, + lastKnownHash: friend.lastKnownHash, + owners: [proxy.edPublic, friend.edPublic], + isFriendChat: true + }; + openChannel(data); + }; + + // Detect friends changes made in another worker proxy.on('change', ['friends'], function (o, n, p) { var curvePublic; if (o === undefined) { // new friend added curvePublic = p.slice(-1)[0]; - eachHandler('friend', function (f) { - f(curvePublic, clone(n)); + + // Load channel + var friend = friends[curvePublic]; + if (typeof(friend) !== 'object') { return; } + var channel = friend.channel; + if (!channel) { return; } + loadFriend(friend, function () { + eachHandler('friend', function (f) { + f(curvePublic); + }); }); return; } + if (typeof(n) === 'undefined') { + // Handled by .on('remove') + return; + } console.error(o, n, p); }).on('remove', ['friends'], function (o, p) { + var curvePublic = p[1]; + if (!curvePublic) { return; } + if (p[2] !== 'channel') { return; } + var channel = channels[o]; + channel.wc.leave(Types.unfriend); + delete channels[channel.id]; eachHandler('unfriend', function (f) { - f(p[1]); // TODO + f(curvePublic, true); }); }); + // Friend added in our contacts in the current worker + messenger.onFriendAdded = function (friendData) { + var friend = friends[friendData.curvePublic]; + if (typeof(friend) !== 'object') { return; } + var channel = friend.channel; + if (!channel) { return; } + loadFriend(friend, function () { + eachHandler('friend', function (f) { + f(friend.curvePublic); + }); + }); + }; + + var ready = false; + var init = function () { + var friends = getFriendList(proxy); + + nThen(function (waitFor) { + Object.keys(friends).forEach(function (key) { + if (key === 'me') { return; } + var friend = clone(friends[key]); + if (typeof(friend) !== 'object') { return; } + var channel = friend.channel; + if (!channel) { return; } + loadFriend(friend, waitFor()); + }); + // TODO load rooms + }).nThen(function () { + // TODO send event chat ready + // Remove spinner in chatbox + ready = true; + eachHandler('ready', function (f) { + f(); + }); + }); + }; + init(); + + var getRooms = function (curvePublic, cb) { + if (curvePublic) { + // We need to get data about a new friend's room + var friend = getFriend(proxy, curvePublic); + if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); } + var channel = getChannel(friend.channel); + if (!channel) { return void cb({error: 'NO_SUCH_CHANNEL'}); } + return void cb([{ + id: channel.id, + isFriendChat: true, + name: friend.displayName, + lastKnownHash: friend.lastKnownHash, + curvePublic: friend.curvePublic + }]); + } + + var rooms = Object.keys(channels).map(function (id) { + var r = getChannel(id); + var name, lastKnownHash, curvePublic; + if (r.isFriendChat) { + var friend = getFriendFromChannel(id); + if (!friend) { return null; } + name = friend.displayName; + lastKnownHash = friend.lastKnownHash; + curvePublic = friend.curvePublic; + } else { + // TODO room get metadata (name) && lastKnownHash + } + return { + id: r.id, + isFriendChat: r.isFriendChat, + name: name, + lastKnownHash: lastKnownHash, + curvePublic: curvePublic + }; + }).filter(function (x) { return x; }); + cb(rooms); + }; + + var getUserList = function (data, cb) { + var room = getChannel(data.id); + if (!room) { return void cb({error: 'NO_SUCH_CHANNEL'}); } + if (room.isFriendChat) { + var friend = getFriendFromChannel(data.id); + if (!friend) { return void cb({error: 'NO_SUCH_FRIEND'}); } + cb([friend]); + } else { + // TODO room userlist in rooms... + // (this is the static userlist, not the netflux one) + } + }; + + messenger.execCommand = function (obj, cb) { + var cmd = obj.cmd; + var data = obj.data; + if (cmd === 'IS_READY') { + return void cb(ready); + } + if (cmd === 'GET_ROOMS') { + return void getRooms(data, cb); + } + if (cmd === 'GET_USERLIST') { + return void getUserList(data, cb); + } + }; + Object.freeze(messenger); return messenger; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 2b29ff8ec..75bc92129 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -622,6 +622,12 @@ define([ messenger.setChannelHead = function (data, cb) { postMessage("CONTACTS_SET_CHANNEL_HEAD", data, cb); }; + + messenger.execCommand = function (data, cb) { + postMessage("CHAT_COMMAND", data, cb); + }; + + messenger.onEvent = Util.mkEvent(); messenger.onMessageEvent = Util.mkEvent(); messenger.onJoinEvent = Util.mkEvent(); messenger.onLeaveEvent = Util.mkEvent(); @@ -1059,6 +1065,8 @@ define([ CONTACTS_UPDATE: common.messenger.onUpdateEvent.fire, CONTACTS_FRIEND: common.messenger.onFriendEvent.fire, CONTACTS_UNFRIEND: common.messenger.onUnfriendEvent.fire, + // Chat + CHAT_EVENT: common.messenger.onEvent.fire, // Pad PAD_READY: common.padRpc.onReadyEvent.fire, PAD_MESSAGE: common.padRpc.onMessageEvent.fire, diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 2be4ded4f..23594db6e 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -59,6 +59,9 @@ define([ obj[key] = data.value; } broadcast([clientId], "UPDATE_METADATA"); + if (Array.isArray(path) && path[0] === 'profile' && store.messenger) { + store.messenger.updateMyData(); + } onSync(cb); }; @@ -597,6 +600,7 @@ define([ Store.setDisplayName = function (clientId, value, cb) { store.proxy[Constants.displayNameKey] = value; broadcast([clientId], "UPDATE_METADATA"); + if (store.messenger) { store.messenger.updateMyData(); } onSync(cb); }; @@ -859,6 +863,9 @@ define([ }, pinPads: function (data, cb) { Store.pinPads(null, data, cb); }, friendComplete: function (data) { + if (data.friend && store.messenger && store.messenger.onFriendAdded) { + store.messenger.onFriendAdded(data.friend); + } postMessage(clientId, "EV_FRIEND_COMPLETE", data); }, friendRequest: function (data, cb) { @@ -957,6 +964,10 @@ define([ error: e }); }); + }, + + execCommand: function (clientId, data, cb) { + store.messenger.execCommand(data, cb); } }; @@ -1317,7 +1328,6 @@ define([ } }; - var messengerEventInit = false; var sendMessengerEvent = function (q, data) { messengerEventClients.forEach(function (cId) { postMessage(cId, q, data); @@ -1327,41 +1337,47 @@ define([ if (messengerEventClients.indexOf(clientId) === -1) { messengerEventClients.push(clientId); } - if (!messengerEventInit) { - var messenger = store.messenger = Messenger.messenger(store); - messenger.on('message', function (message) { - sendMessengerEvent('CONTACTS_MESSAGE', message); + }; + var loadMessenger = function () { + var messenger = store.messenger = Messenger.messenger(store); + messenger.on('message', function (message) { + sendMessengerEvent('CONTACTS_MESSAGE', message); + }); + messenger.on('join', function (curvePublic, channel) { + sendMessengerEvent('CONTACTS_JOIN', { + curvePublic: curvePublic, + channel: channel, }); - messenger.on('join', function (curvePublic, channel) { - sendMessengerEvent('CONTACTS_JOIN', { - curvePublic: curvePublic, - channel: channel, - }); + }); + messenger.on('leave', function (curvePublic, channel) { + sendMessengerEvent('CONTACTS_LEAVE', { + curvePublic: curvePublic, + channel: channel, }); - messenger.on('leave', function (curvePublic, channel) { - sendMessengerEvent('CONTACTS_LEAVE', { - curvePublic: curvePublic, - channel: channel, - }); + }); + messenger.on('update', function (info, types) { + sendMessengerEvent('CONTACTS_UPDATE', { + types: types, + info: info, }); - messenger.on('update', function (info, curvePublic) { - sendMessengerEvent('CONTACTS_UPDATE', { - curvePublic: curvePublic, - info: info, - }); + }); + messenger.on('friend', function (curvePublic) { + sendMessengerEvent('CONTACTS_FRIEND', { + curvePublic: curvePublic, }); - messenger.on('friend', function (curvePublic) { - sendMessengerEvent('CONTACTS_FRIEND', { - curvePublic: curvePublic, - }); + }); + messenger.on('unfriend', function (curvePublic, removedByMe) { + sendMessengerEvent('CONTACTS_UNFRIEND', { + curvePublic: curvePublic, + removedByMe: removedByMe }); - messenger.on('unfriend', function (curvePublic) { - sendMessengerEvent('CONTACTS_UNFRIEND', { - curvePublic: curvePublic, - }); + }); + messenger.on('ready', function () { + console.log('here'); + sendMessengerEvent('CHAT_EVENT', { + ev: 'READY' }); - messengerEventInit = true; - } + }); }; @@ -1450,6 +1466,7 @@ define([ }); userObject.fixFiles(); loadSharedFolders(waitFor); + loadMessenger(); }).nThen(function () { var requestLogin = function () { broadcast([], "REQUEST_LOGIN"); diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 532b4c10e..a724fbd06 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -69,6 +69,8 @@ define([ CONTACTS_GET_MORE_HISTORY: Store.messenger.getMoreHistory, CONTACTS_SEND_MESSAGE: Store.messenger.sendMessage, CONTACTS_SET_CHANNEL_HEAD: Store.messenger.setChannelHead, + // Chat + CHAT_COMMAND: Store.messenger.execCommand, // Pad SEND_PAD_MSG: Store.sendPadMsg, JOIN_PAD: Store.joinPad, diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index 7266ac0c9..cb6cb4acd 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -559,6 +559,7 @@ define([ }, onLocal); var configTb = { displayed: [ + 'chat', 'userlist', 'title', 'useradmin', diff --git a/www/common/sframe-app-outer.js b/www/common/sframe-app-outer.js index b65f3f98b..cc4d5fcb3 100644 --- a/www/common/sframe-app-outer.js +++ b/www/common/sframe-app-outer.js @@ -36,7 +36,8 @@ define([ window.addEventListener('message', onMsg); }).nThen(function (/*waitFor*/) { SFCommonO.start({ - useCreationScreen: true + useCreationScreen: true, + messaging: true }); }); }); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 43728c55c..0bdabf981 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -776,6 +776,14 @@ define([ Cryptpad.messenger.setChannelHead(opt, cb); }); + sframeChan.on('Q_CHAT_COMMAND', function (data, cb) { + Cryptpad.messenger.execCommand(data, cb); + }); + Cryptpad.messenger.onEvent.reg(function (data) { + console.log(data); + sframeChan.event('EV_CHAT_EVENT', data); + }); + Cryptpad.messenger.onMessageEvent.reg(function (data) { sframeChan.event('EV_CONTACTS_MESSAGE', data); }); diff --git a/www/common/sframe-messenger-inner.js b/www/common/sframe-messenger-inner.js index 67532b015..b277a1e58 100644 --- a/www/common/sframe-messenger-inner.js +++ b/www/common/sframe-messenger-inner.js @@ -35,7 +35,7 @@ define([], function () { }); sFrameChan.on('EV_CONTACTS_UPDATE', function (data) { _handlers.update.forEach(function (f) { - f(data.info, data.curvePublic); + f(data.info, data.types); }); }); sFrameChan.on('EV_CONTACTS_FRIEND', function (data) { @@ -45,7 +45,7 @@ define([], function () { }); sFrameChan.on('EV_CONTACTS_UNFRIEND', function (data) { _handlers.unfriend.forEach(function (f) { - f(data.curvePublic); + f(data.curvePublic, data.removedByMe); }); }); diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index 02390b290..a61562887 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -171,6 +171,10 @@ define({ 'Q_CONTACTS_SET_CHANNEL_HEAD': true, 'Q_CONTACTS_CLEAR_OWNED_CHANNEL': true, + // Chat + 'EV_CHAT_EVENT': true, + 'Q_CHAT_COMMAND': true, + // Put one or more entries to the localStore which will go in localStorage. 'EV_LOCALSTORE_PUT': true, // Put one entry in the parent sessionStorage diff --git a/www/common/toolbar3.js b/www/common/toolbar3.js index 35fce658d..840afd017 100644 --- a/www/common/toolbar3.js +++ b/www/common/toolbar3.js @@ -6,8 +6,11 @@ define([ '/common/common-interface.js', '/common/common-hash.js', '/common/common-feedback.js', + '/common/sframe-messenger-inner.js', + '/contacts/messenger-ui.js', '/customize/messages.js', -], function ($, Config, ApiConfig, UIElements, UI, Hash, Feedback, Messages) { +], function ($, Config, ApiConfig, UIElements, UI, Hash, Feedback, +Messenger, MessengerUI, Messages) { var Common; var Bar = { @@ -410,6 +413,72 @@ define([ return $container; }; + var initChat = function (toolbar, config) { + var $container = $('
', {id: 'cp-app-contacts-container'}) + .prependTo(toolbar.chatContent); + var sframeChan = Common.getSframeChannel(); + var messenger = Messenger.create(sframeChan); + MessengerUI.create(messenger, $container, Common); + }; + var createChat = function (toolbar, config) { + if (!config.metadataMgr) { + throw new Error("You must provide a `metadataMgr` to display the chat"); + } + var $content = $('
', {'class': 'cp-toolbar-chat-drawer'}); + $content.on('drop dragover', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + var $closeIcon = $('', {"class": "fa fa-window-close cp-toolbar-chat-drawer-close"}).appendTo($content); + //$('

').text(Messages.users).appendTo($content); + //$('

', {'class': USERLIST_CLS}).appendTo($content); + + toolbar.chatContent = $content; + + var $container = $('', {id: 'cp-toolbar-chat-drawer-open', title: Messages.chatButton || 'CHAT'}); //XXX + + var $button = $('