diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less index 695125dad..f84c7c49b 100644 --- a/www/kanban/app-kanban.less +++ b/www/kanban/app-kanban.less @@ -2,6 +2,7 @@ @import (reference) "../../customize/src/less2/include/framework.less"; @import (reference) "../../customize/src/less2/include/tools.less"; @import (reference) "../../customize/src/less2/include/markdown.less"; +@import (reference) "../../customize/src/less2/include/avatar.less"; // body &.cp-app-kanban { @@ -99,6 +100,16 @@ flex-flow: column; overflow: hidden; } + #cp-kanban-edit-conflicts { + padding: 5px; + background: #eee; + color: @cryptpad_text_col; + font-size: 14px; + .cp-kanban-cursors { + margin-top: 5px; + } + margin-bottom: 5px; + } #cp-kanban-edit-body { border: 1px solid @colortheme_modal-input; .CodeMirror { @@ -154,12 +165,30 @@ padding: 5px; } + .cp-kanban-cursors { + &:empty { display: none; } + order: 2; + width: 100%; + &> span { + display: inline-block; + width: 20px; + height: 20px; + text-align: center; + line-height: 20px; + margin-right: 5px; + .tools_unselectable(); + cursor: default; + } + } .kanban-item { display: flex; align-items: center; justify-content: space-between; padding: 5px; flex-wrap: wrap; + .cp-kanban-cursors { + margin-top: 10px; + } .kanban-item-body, .kanban-item-tags { .tools_unselectable(); width: 100%; @@ -216,6 +245,7 @@ } header { display: flex; + flex-wrap: wrap; align-items: center; padding: 5px 10px; .kanban-title-board { diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 17e0e278e..f1eaa7a45 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -8,6 +8,7 @@ define([ '/common/common-util.js', '/common/common-hash.js', '/common/common-interface.js', + '/common/common-ui-elements.js', '/common/modes.js', '/customize/messages.js', '/common/hyperscript.js', @@ -38,6 +39,7 @@ define([ Util, Hash, UI, + UIElements, Modes, Messages, h, @@ -51,6 +53,8 @@ define([ var verbose = function (x) { console.log(x); }; verbose = function () {}; // comment out to enable verbose logging var onRedraw = Util.mkEvent(); + var onCursorUpdate = Util.mkEvent(); + var remoteCursors = {}; Messages.kanban_title = "Title"; // XXX Messages.kanban_body = "Body"; // XXX @@ -59,6 +63,7 @@ define([ Messages.kanban_delete = "Delete"; // XXX Messages.kanban_tags = "Filter tags"; // XXX Messages.kanban_noTags = "No tags"; // XXX + Messages.kanban_conflicts = "Currently editing:"; // XXX // XXX // Conflicts @@ -83,6 +88,46 @@ define([ input.selectionEnd = selects[1]; }; + var getTextColor = function (hex) { + if (hex && /^#/.test(hex)) { hex = hex.slice(1); } + if (!/^[0-9a-f]{6}$/i.test(hex)) { + return '#000000'; + } + var r = parseInt(hex.slice(0,2), 16); + var g = parseInt(hex.slice(2,4), 16); + var b = parseInt(hex.slice(4,6), 16); + if ((r*0.213 + g*0.715 + b*0.072) > 255/2) { + return '#000000'; + } + return '#FFFFFF'; + }; + + var getAvatar = function (cursor, noClear) { + // Tippy + var html = ''; + if (cursor.avatar && UIElements.getAvatar(cursor.avatar)) { + html += UIElements.getAvatar(cursor.avatar); + } + html += cursor.name + ''; + + var l = (cursor.name || Messages.anonymous).slice(0,1).toUpperCase(); + + var text = ''; + if (cursor.color) { + text = 'color:'+getTextColor(cursor.color)+';'; + } + var avatar = h('span.cp-cursor.cp-tippy-html', { + style: "background-color: " + (cursor.color || 'red') + ";"+text, + title: html + }, l); + if (!noClear) { + cursor.clear = function () { + $(avatar).remove(); + }; + } + return avatar; + }; + var getExistingTags = function (boards) { var tags = []; boards = boards || {}; @@ -112,8 +157,12 @@ define([ addEditItemButton(framework, kanban); }; if (editModal) { return editModal; } - var titleInput, tagsDiv, colors, text; + var conflicts, conflictContainer, titleInput, tagsDiv, colors, text; var content = h('div', [ + conflictContainer = h('div#cp-kanban-edit-conflicts', [ + h('div', Messages.kanban_conflicts), + conflicts = h('div.cp-kanban-cursors') + ]), h('label', {for:'cp-kanban-edit-title'}, Messages.kanban_title), titleInput = h('input#cp-kanban-edit-title'), h('label', {for:'cp-kanban-edit-body'}, Messages.kanban_body), @@ -126,6 +175,27 @@ define([ colors = h('div#cp-kanban-edit-colors'), ]); + + var $conflict = $(conflicts); + var $cc = $(conflictContainer); + var conflict = { + setValue: function () { + $conflict.empty(); + var i = 0; + $cc.hide(); + Object.keys(remoteCursors).forEach(function (nid) { + var c = remoteCursors[nid]; + var avatar = getAvatar(c, true); + if (Number(c.item) === Number(id) || Number(c.board) === Number(id)) { + $conflict.append(avatar); + i++; + } + }); + if (!i) { return; } + $cc.show(); + } + }; + // Title var $title = $(titleInput); $title.on('change keyup', function () { @@ -248,11 +318,17 @@ define([ isBoard = _isBoard; id = _id; if (_isBoard) { + onCursorUpdate.fire({ + board: _id + }); dataObject = kanban.getBoardJSON(id); $(content) .find('#cp-kanban-edit-body, #cp-kanban-edit-tags, [for="cp-kanban-edit-body"], [for="cp-kanban-edit-tags"]') .hide(); } else { + onCursorUpdate.fire({ + item: _id + }); dataObject = kanban.getItemJSON(id); $(content) .find('#cp-kanban-edit-body, #cp-kanban-edit-tags, [for="cp-kanban-edit-body"], [for="cp-kanban-edit-tags"]') @@ -287,6 +363,7 @@ define([ className: 'primary', name: Messages.filePicker_close, onClick: function () { + onCursorUpdate.fire({}); }, keys: [] }]; @@ -310,6 +387,7 @@ define([ return; } // Not deleted, apply updates + editModal.conflict.setValue(); PROPERTIES.forEach(function (type) { editModal[type].setValue(dataObject[type], true); }); @@ -321,7 +399,8 @@ define([ title: title, body: body, tags: tags, - color: color + color: color, + conflict: conflict }; }; var getItemEditModal = function (framework, kanban, eid) { @@ -331,6 +410,7 @@ define([ var boards = kanban.options.boards || {}; var item = (boards.items || {})[eid]; if (!item) { return void UI.warn(Messages.error); } + editModal.conflict.setValue(); PROPERTIES.forEach(function (type) { if (!editModal[type]) { return; } editModal[type].setValue(item[type]); @@ -345,6 +425,7 @@ define([ var boards = kanban.options.boards || {}; var board = (boards.data || {})[id]; if (!board) { return void UI.warn(Messages.error); } + editModal.conflict.setValue(); BOARD_PROPERTIES.forEach(function (type) { if (!editModal[type]) { return; } editModal[type].setValue(board[type]); @@ -482,6 +563,12 @@ define([ } var eid = $(el).attr('data-eid'); kanban.inEditMode = eid; + setTimeout(function () { + // Make sure the click is sent after the "blur" in case we move from a card to another + onCursorUpdate.fire({ + item: eid + }); + }); var name = $(el).text(); $(el).html(''); @@ -500,8 +587,9 @@ define([ kanban.onChange(); // Unlock edit mode kanban.inEditMode = false; + onCursorUpdate.fire({}); }; - $input.blur(save); + //$input.blur(save); $input.keydown(function (e) { if (e.which === 13) { e.preventDefault(); @@ -515,10 +603,7 @@ define([ if (e.which === 27) { e.preventDefault(); e.stopPropagation(); - $(el).text(name); - kanban.inEditMode = false; - addEditItemButton(framework, kanban); - return; + save(); } }); $input.on('change keyup', function () { @@ -540,6 +625,12 @@ define([ } var boardId = $(el).closest('.kanban-board').attr("data-id"); kanban.inEditMode = boardId; + setTimeout(function () { + // Make sure the click is sent after the "blur" in case we move from a card to another + onCursorUpdate.fire({ + board: boardId + }); + }); var name = $(el).text(); $(el).html(''); @@ -559,6 +650,7 @@ define([ kanban.onChange(); // Unlock edit mode kanban.inEditMode = false; + onCursorUpdate.fire({}); }; $input.blur(save); $input.keydown(function (e) { @@ -571,8 +663,7 @@ define([ if (e.which === 27) { e.preventDefault(); e.stopPropagation(); - $(el).text(name); - kanban.inEditMode = false; + save(); return; } }); @@ -604,6 +695,7 @@ define([ var save = function () { $item.remove(); kanban.inEditMode = false; + onCursorUpdate.fire({}); if (!$input.val()) { return; } var id = Util.createRandomInteger(); var item = { @@ -630,6 +722,7 @@ define([ e.stopPropagation(); $item.remove(); kanban.inEditMode = false; + onCursorUpdate.fire({}); return; } }); @@ -638,6 +731,9 @@ define([ return DiffMd.render(md, true, false); }, addItemButton: true, + getTextColor: getTextColor, + getAvatar: getAvatar, + cursors: remoteCursors, boards: boards }); @@ -664,9 +760,6 @@ define([ }); var $container = $('#cp-app-kanban-content'); - $container[0].onclick = function (e) { - console.warn(e); - }; var $cContainer = $('#cp-app-kanban-container'); var addControls = function () { // Quick or normal mode @@ -986,6 +1079,47 @@ define([ kanban = initKanban(framework); }); + var myCursor = {}; + onCursorUpdate.reg(function (data) { + myCursor = data; + framework.updateCursor(); + }); + framework.onCursorUpdate(function (data) { + if (!data) { return; } + var id = data.id; + + // Clear existing cursor + if (remoteCursors[id] && remoteCursors[id].clear) { + remoteCursors[id].clear(); + } + delete remoteCursors[id]; + + var cursor = data.cursor; + if (data.leave || !cursor) { return; } + if (!cursor.item && !cursor.board) { return; } + + // Add new cursor + var avatar = getAvatar(cursor); + var $item = $('.kanban-item[data-eid="'+cursor.item+'"]'); + var $board = $('.kanban-board[data-id="'+cursor.board+'"]'); + if ($item.length) { + remoteCursors[id] = cursor; + $item.find('.cp-kanban-cursors').append(avatar); + return; + } + if ($board.length) { + remoteCursors[id] = cursor; + $board.find('header .cp-kanban-cursors').append(avatar); + } + }); + framework.onCursorUpdate(function () { + if (!editModal || !editModal.conflict) { return; } + editModal.conflict.setValue(); + }); + framework.setCursorGetter(function () { + return myCursor; + }); + framework.start(); }; diff --git a/www/kanban/jkanban.js b/www/kanban/jkanban.js index 340fe66f4..a6606c787 100644 --- a/www/kanban/jkanban.js +++ b/www/kanban/jkanban.js @@ -56,6 +56,9 @@ items: {}, list: [] }, + getAvatar: function () {}, + getTextColor: function () { return '#000'; }, + cursors: {}, tags: [], dragBoards: true, addItemButton: false, @@ -351,19 +354,6 @@ } }; - var getTextColor = function (hex) { - if (!/^[0-9a-f]{6}$/.test(hex)) { - return '#000000'; - } - var r = parseInt(hex.slice(0,2), 16); - var g = parseInt(hex.slice(2,4), 16); - var b = parseInt(hex.slice(4,6), 16); - if ((r*0.213 + g*0.715 + b*0.072) > 255/2) { - return '#000000'; - } - return '#FFFFFF'; - }; - var getElementNode = function (element) { var nodeItem = document.createElement('div'); nodeItem.classList.add('kanban-item'); @@ -374,10 +364,18 @@ nodeItem.classList.add('cp-kanban-palette-'+element.color); } else { // Hex color code - var textColor = getTextColor(element.color); + var textColor = self.options.getTextColor(element.color); nodeItem.setAttribute('style', 'background-color:#'+element.color+';color:'+textColor+';'); } } + var nodeCursors = document.createElement('div'); + nodeCursors.classList.add('cp-kanban-cursors'); + Object.keys(self.options.cursors).forEach(function (id) { + var c = self.options.cursors[id]; + if (Number(c.item) !== Number(element.id)) { return; } + var el = self.options.getAvatar(c); + nodeCursors.appendChild(el); + }); var nodeItemText = document.createElement('div'); nodeItemText.classList.add('kanban-item-text'); nodeItemText.dataset.eid = element.id; @@ -413,6 +411,7 @@ }); nodeItem.appendChild(nodeTags); } + nodeItem.appendChild(nodeCursors); //add function nodeItem.clickfn = element.click; nodeItem.dragfn = element.drag; @@ -482,10 +481,11 @@ headerBoard.classList.add("kanban-header-" + board.color); } else { // Hex color code - var textColor = getTextColor(board.color); + var textColor = self.options.getTextColor(board.color); headerBoard.setAttribute('style', 'background-color:#'+board.color+';color:'+textColor+';'); } } + titleBoard = document.createElement('div'); titleBoard.classList.add('kanban-title-board'); titleBoard.innerText = board.title; @@ -494,6 +494,16 @@ __onboardTitleClickHandler(titleBoard); headerBoard.appendChild(titleBoard); + var nodeCursors = document.createElement('div'); + nodeCursors.classList.add('cp-kanban-cursors'); + Object.keys(self.options.cursors).forEach(function (id) { + var c = self.options.cursors[id]; + if (Number(c.board) !== Number(board.id)) { return; } + var el = self.options.getAvatar(c); + nodeCursors.appendChild(el); + }); + headerBoard.appendChild(nodeCursors); + //content board var contentBoard = document.createElement('main'); contentBoard.classList.add('kanban-drag');