diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index efd2e2463..41e78bf6d 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1179,7 +1179,18 @@ define([ // getPinnedUsage updates common.account.usage, and other values // so we can just use those and only check for errors var $container = $('', {'class':'cp-limit-container'}); + var to; var todo = function (err, data) { + if (to) { + clearTimeout(to); + to = undefined; + } + if (err === 'RPC_NOT_READY') { + to = setTimeout(function () { + common.getPinUsage(teamId, todo); + }, 1000); + return; + } if (err || !data) { return void console.error(err || 'No data'); } var usage = data.usage; @@ -1849,6 +1860,13 @@ define([ var oldUrl = ''; var updateButton = function () { var myData = metadataMgr.getUserData(); + var privateData = metadataMgr.getPrivateData(); + if (!priv.plan && privateData.plan) { + config.$initBlock.empty(); + metadataMgr.off('change', updateButton); + UIElements.createUserAdminMenu(Common, config); + return; + } if (!myData) { return; } if (loadingAvatar) { // Try again in 200ms @@ -2087,6 +2105,16 @@ define([ var sframeChan = common.getSframeChannel(); var metadataMgr = common.getMetadataMgr(); var privateData = metadataMgr.getPrivateData(); + + if (privateData.offline) { + metadataMgr.onChange(function () { + var privateData = metadataMgr.getPrivateData(); + if (privateData.offline) { return; } + UIElements.getPadCreationScreen(common, cfg, appCfg, cb); + }); + return; + } + var type = metadataMgr.getMetadataLazy().type || privateData.app; var fromFileData = privateData.fromFileData; diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 965578577..7715b39b8 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -3259,6 +3259,7 @@ define([ var $spinnerContainer = $(h('div.cp-app-drive-search-spinner')); var spinner = UI.makeSpinner($spinnerContainer); + var searching = true; var $input = APP.Search.$input = $('', { id: 'cp-app-drive-search-input', placeholder: Messages.fm_searchName, @@ -3266,21 +3267,19 @@ define([ draggable: false, tabindex: 1, }).keyup(function (e) { - var lastValue = search.value; - search.value = $input.val().trim(); - if (lastValue === search.value) { return; } - - if (search.to) { window.clearTimeout(search.to); } - if (search.value === "") { - search.cursor = 0; - APP.displayDirectory([SEARCH]); + if (searching) { + e.preventDefault(); + e.stopPropagation(); return; } - spinner.spin(); + var currentValue = $input.val().trim(); + if (search.to) { window.clearTimeout(search.to); } if (e.which === 13) { + spinner.spin(); var newLocation = [SEARCH, $input.val()]; search.cursor = $input[0].selectionStart; if (!manager.comparePath(newLocation, currentPath.slice())) { + searching = true; APP.displayDirectory(newLocation); } return; @@ -3288,27 +3287,30 @@ define([ if (e.which === 27) { $input.val(''); search.cursor = 0; + searching = true; APP.displayDirectory([SEARCH]); return; } - if ($input.val()) { - if (!$input.hasClass('cp-app-drive-search-active')) { - $input.addClass('cp-app-drive-search-active'); - } - } else { - $input.removeClass('cp-app-drive-search-active'); + + if (currentValue === "") { + search.cursor = 0; + APP.displayDirectory([SEARCH]); + return; } + + if (currentValue.length < 2) { return; } // Don't autosearch 1 character search.to = window.setTimeout(function () { var newLocation = [SEARCH, $input.val()]; search.cursor = $input[0].selectionStart; + if (currentValue === search.value) { return; } if (!manager.comparePath(newLocation, currentPath.slice())) { + searching = true; APP.displayDirectory(newLocation); } }, 500); }).on('click mousedown mouseup', function (e) { e.stopPropagation(); }).val(value || '').appendTo($div); - if (value) { $input.addClass('cp-app-drive-search-active'); } $input[0].selectionStart = search.cursor || 0; $input[0].selectionEnd = search.cursor || 0; @@ -3329,6 +3331,7 @@ define([ if (typeof(value) === "string" && value.trim()) { spinner.spin(); } else { + searching = false; return; } @@ -3338,6 +3341,7 @@ define([ if (!filesList.length) { $list.append(h('div.cp-app-drive-search-noresult', Messages.fm_noResult)); spinner.hide(); + searching = false; return; } var sortable = {}; @@ -3402,6 +3406,7 @@ define([ }); setTimeout(collapseDrivePath); spinner.hide(); + searching = false; }); }; diff --git a/www/common/inner/access.js b/www/common/inner/access.js index 4d0cba17f..cea4544e2 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -992,8 +992,10 @@ define([ // Also stop for shared folders if (parsed.hashData.type !== 'pad' || parsed.type === 'drive') { return h('div', content); } + var owned = Modal.isOwned(Env, data); + // Request edit access - if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref)) { + if (common.isLoggedIn() && ((data.roHref && !data.href) || data.fakeHref) && !owned) { var requestButton = h('button.btn.btn-secondary.no-margin.cp-access-margin-right', Messages.requestEdit_button); var requestBlock = h('p', requestButton); @@ -1028,7 +1030,6 @@ define([ // Mute access requests var edPublic = priv.edPublic; - var owned = Modal.isOwned(Env, data); var canMute = data.mailbox && owned === true && ( (typeof (data.mailbox) === "string" && data.owners[0] === edPublic) || data.mailbox[edPublic]); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index bb6f02938..900172f16 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1229,6 +1229,7 @@ define([ var data = obj.data; if (channels.indexOf(data.channel) !== -1) { return; } var id = obj.id; + if (data.channel) { channels.push(data.channel); } var parsed = Hash.parsePadUrl(data.href || data.roHref); if ((!types || types.length === 0 || types.indexOf(parsed.type) !== -1) && !isFiltered(parsed.type, data)) { diff --git a/www/kanban/app-kanban.less b/www/kanban/app-kanban.less index 6bb7e0ff4..92be4eada 100644 --- a/www/kanban/app-kanban.less +++ b/www/kanban/app-kanban.less @@ -141,6 +141,9 @@ border: 0; background: transparent; align-self: flex-start; + @media (hover: none) { + margin-right: 20px; + } } .cp-kanban-cursors { @@ -166,7 +169,10 @@ flex-wrap: wrap; touch-action: none; background: @cp_kanban-item-bg; + .tools_unselectable(); + touch-action: none; cursor: move; + cursor: grab; margin-bottom: 10px; &:last-child { @@ -259,12 +265,14 @@ .kanban-board { position: relative; transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1); - margin: 10px; vertical-align: top; display: flex; flex-flow: column; width: 300px; margin: 10px 5px; + @media (hover: none) { + margin-bottom: 30px; + } &.is-moving.gu-mirror { transform: rotate(3deg); @@ -282,12 +290,17 @@ overflow-y: auto; justify-content: space-around; min-height: 38px; // Size of one card + @media (hover: none) { + padding-right: 30px; + } } header { display: flex; flex-wrap: wrap; align-items: center; padding: 5px 10px; + cursor: move; + cursor: grab; .kanban-title-board { flex: 1; min-width: 0; @@ -305,9 +318,6 @@ #kanban-edit { font-weight: bold; } - &:hover { - cursor: move; - } } footer { margin: 10px; diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 5ad3d1dc8..18ed526f1 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -134,6 +134,24 @@ define([ }; var addEditItemButton = function () {}; + + var now = function () { return +new Date(); }; + var _lastUpdate = 0; + var _updateBoards = function (framework, kanban, boards) { + _lastUpdate = now(); + kanban.setBoards(Util.clone(boards)); + kanban.inEditMode = false; + addEditItemButton(framework, kanban); + }; + var _updateBoardsThrottle = Util.throttle(_updateBoards, 1000); + var updateBoards = function (framework, kanban, boards) { + if ((now() - _lastUpdate) > 5000 || framework.isLocked()) { + _updateBoards(framework, kanban, boards); + return; + } + _updateBoardsThrottle(framework, kanban, boards); + }; + var onRemoteChange = Util.mkEvent(); var editModal; var PROPERTIES = ['title', 'body', 'tags', 'color']; @@ -146,10 +164,9 @@ define([ var isBoard, id; var offline = false; - var update = Util.throttle(function () { - kanban.setBoards(kanban.options.boards); - addEditItemButton(framework, kanban); - }, 400); + var update = function () { + updateBoards(framework, kanban, kanban.options.boards); + }; var commit = function () { framework.localChange(); @@ -400,7 +417,9 @@ define([ framework.onEditableChange(function (unlocked) { editor.setOption('readOnly', !unlocked); $title.prop('disabled', unlocked ? '' : 'disabled'); - $(_field.element).tokenfield(unlocked ? 'enable' : 'disable'); + if (_field) { + $(_field.element).tokenfield(unlocked ? 'enable' : 'disable'); + } $modal.find('nav button.danger').prop('disabled', unlocked ? '' : 'disabled'); offline = !unlocked; @@ -831,7 +850,8 @@ define([ openLink: openLink, getTags: getExistingTags, cursors: remoteCursors, - boards: boards + boards: boards, + _boards: Util.clone(boards), }); framework._.cpNfInner.metadataMgr.onChange(function () { @@ -842,7 +862,7 @@ define([ // If the rendering has changed, update the value and redraw kanban.options.tagsAnd = tagsAnd; _tagsAnd = tagsAnd; - kanban.setBoards(kanban.options.boards); + updateBoards(framework, kanban, kanban.options.boards); }); if (migrated) { framework.localChange(); } @@ -1167,9 +1187,8 @@ define([ if (Sortify(currentContent) !== Sortify(remoteContent)) { var cursor = getCursor(); verbose("Content is different.. Applying content"); - kanban.setBoards(remoteContent); - kanban.inEditMode = false; - addEditItemButton(framework, kanban); + kanban.options.boards = remoteContent; + updateBoards(framework, kanban, remoteContent); restoreCursor(cursor); onRemoteChange.fire(); } @@ -1191,8 +1210,17 @@ define([ var items = boards.items || {}; var data = boards.data || {}; var list = boards.list || []; + + // Remove duplicate boards + list = boards.list = Util.deduplicateString(list); + Object.keys(data).forEach(function (id) { - if (list.indexOf(Number(id)) === -1) { delete data[id]; } + if (list.indexOf(Number(id)) === -1) { + list.push(Number(id)); + } + // Remove duplicate items + var b = data[id]; + b.item = Util.deduplicateString(b.item || []); }); Object.keys(items).forEach(function (eid) { var exists = Object.keys(data).some(function (id) { diff --git a/www/kanban/jkanban_cp.js b/www/kanban/jkanban_cp.js index f122eed59..2a3c6991b 100644 --- a/www/kanban/jkanban_cp.js +++ b/www/kanban/jkanban_cp.js @@ -26,13 +26,14 @@ define([ element: '', gutter: '15px', widthBoard: '250px', - responsive: '700', + responsive: 0, //'700', responsivePercentage: false, boards: { data: {}, items: {}, list: [] - }, + }, // The realtime kanban + _boards: {}, // The displayed kanban. We need to remember the old columns when we redraw getAvatar: function () {}, openLink: function () {}, getTags: function () {}, @@ -298,7 +299,9 @@ define([ // Move to trash? if (target.classList.contains('kanban-trash')) { list.splice(index1, 1); - delete self.options.boards.data[id]; + if (list.indexOf(id) === -1) { + delete self.options.boards.data[id]; + } self.onChange(); return; } @@ -393,10 +396,11 @@ define([ console.log("In drop"); var id1 = Number($(el).attr('data-eid')); + var boardId = Number($(source).closest('.kanban-board').data('id')); // Move to trash? if (target.classList.contains('kanban-trash')) { - self.moveItem(id1); + self.moveItem(boardId, id1); self.onChange(); return; } @@ -416,7 +420,7 @@ define([ } // Move the item - self.moveItem(id1, board2, pos2); + self.moveItem(boardId, id1, board2, pos2); // send event that board has changed self.onChange(); @@ -443,21 +447,44 @@ define([ }); return res; }; - this.moveItem = function (eid, board, pos) { + this.checkItem = function (eid) { var boards = self.options.boards; - var same = -1; - var from = findItem(eid); - // Remove the item from its board - from.forEach(function (obj) { - obj.board.item.splice(obj.pos, 1); - if (obj.board === board) { same = obj.pos; } + var data = boards.data || {}; + var exists = Object.keys(data).some(function (id) { + return (data[id].item || []).indexOf(Number(eid)) !== -1; }); - // If it's a deletion, remove the item data + return exists; + }; + this.moveItem = function (source, eid, board, pos) { + var boards = self.options.boards; + var same = -1; + console.error(source, eid, board, pos); + if (source && boards.data[source]) { + // Remove from this board only + var l = boards.data[source].item; + var idx = l.indexOf(eid); + if (idx !== -1) { l.splice(idx, 1); } + if (source === board) { same = idx; } + } else { + // Remove the item from all its board + var from = findItem(eid); + from.forEach(function (obj) { + obj.board.item.splice(obj.pos, 1); + if (obj.board === board) { same = obj.pos; } + }); + } + // If it's a deletion and not a duplicate, remove the item data if (!board) { - delete boards.items[eid]; - delete self.cache[eid]; - removeUnusedTags(boards); - self.options.refresh(); + if (!self.checkItem(eid)) { + delete boards.items[eid]; + delete self.cache[eid]; + removeUnusedTags(boards); + self.options.refresh(); + } + return; + } + // If the item already exists in the target board, abort (duplicate) + if (board.item.indexOf(eid) !== -1) { return; } // If it's moved to the same board at a bigger index, decrement the index by one @@ -707,21 +734,29 @@ define([ }; this.addBoard = function (board) { if (!board || !board.id) { return; } + // We need to store all the columns in _boards too because it's used to + // remember what columns were already displayed when we redraw (in order to + // preserve their scroll value) var boards = self.options.boards; boards.data = boards.data || {}; boards.list = boards.list || []; + var _boards = self.options._boards; + _boards.data = _boards.data || {}; + _boards.list = _boards.list || []; // If it already there, abort boards.data[board.id] = board; + _boards.data[board.id] = board; if (boards.list.indexOf(board.id) !== -1) { return; } boards.list.push(board.id); + _boards.list.push(board.id); var boardNode = getBoardNode(board); self.container.appendChild(boardNode); }; this.addBoards = function() { //for on all the boards - var boards = self.options.boards; + var boards = self.options._boards; boards.list = boards.list || []; boards.data = boards.data || {}; var toRemove = []; @@ -759,10 +794,10 @@ define([ var $el = $(self.element); var scrollLeft = $el.scrollLeft(); // Get existing boards list - var list = Util.clone(this.options.boards.list); + var list = Util.clone(this.options._boards.list); // Update memory - this.options.boards = boards; + this.options._boards = Util.clone(boards); // If the tab is not focused but a handler already exists: abort if (!Visible.currently() && onVisibleHandler) { return; } @@ -779,7 +814,7 @@ define([ self.addBoards(); self.options.refresh(); // Preserve scroll - self.options.boards.list.forEach(function (id) { + self.options._boards.list.forEach(function (id) { if (!scroll[id]) { return; } $('.kanban-board[data-id="'+id+'"] .kanban-drag').scrollTop(scroll[id]); }); diff --git a/www/lib/fabric-history.min.js b/www/lib/fabric-history.min.js new file mode 100644 index 000000000..5dd7ebd4f --- /dev/null +++ b/www/lib/fabric-history.min.js @@ -0,0 +1 @@ +fabric.Canvas.prototype.initialize=function(t){return function(...i){return t.call(this,...i),this._historyInit(),this}}(fabric.Canvas.prototype.initialize),fabric.Canvas.prototype.dispose=function(t){return function(...i){return t.call(this,...i),this._historyDispose(),this}}(fabric.Canvas.prototype.dispose),fabric.Canvas.prototype._historyNext=function(){return JSON.stringify(this.toDatalessJSON(this.extraProps))},fabric.Canvas.prototype._historyEvents=function(){return{"object:added":this._historySaveAction,"object:removed":this._historySaveAction,"object:modified":this._historySaveAction,"object:skewing":this._historySaveAction}},fabric.Canvas.prototype._historyInit=function(){this.historyUndo=[],this.historyRedo=[],this.extraProps=["selectable"],this.historyNextState=this._historyNext(),this.on(this._historyEvents())},fabric.Canvas.prototype._historyDispose=function(){this.off(this._historyEvents())},fabric.Canvas.prototype._historySaveAction=function(){if(this.historyProcessing)return;const t=this.historyNextState;this.historyUndo.push(t),this.historyNextState=this._historyNext(),this.fire("history:append",{json:t})},fabric.Canvas.prototype.undo=function(t){this.historyProcessing=!0;const i=this.historyUndo.pop();i?(this.historyRedo.push(this._historyNext()),this.historyNextState=i,this._loadHistory(i,"history:undo",t)):this.historyProcessing=!1},fabric.Canvas.prototype.redo=function(t){this.historyProcessing=!0;const i=this.historyRedo.pop();i?(this.historyUndo.push(this._historyNext()),this.historyNextState=i,this._loadHistory(i,"history:redo",t)):this.historyProcessing=!1},fabric.Canvas.prototype._loadHistory=function(t,i,s){var o=this;this.loadFromJSON(t,function(){o.renderAll(),o.fire(i),o.historyProcessing=!1,s&&"function"==typeof s&&s()})},fabric.Canvas.prototype.clearHistory=function(){this.historyUndo=[],this.historyRedo=[],this.fire("history:clear")},fabric.Canvas.prototype.offHistory=function(){this.historyProcessing=!0},fabric.Canvas.prototype.onHistory=function(){this.historyProcessing=!1,this._historySaveAction()}; \ No newline at end of file diff --git a/www/whiteboard/app-whiteboard.less b/www/whiteboard/app-whiteboard.less index c6a8c104a..cdb7f618d 100644 --- a/www/whiteboard/app-whiteboard.less +++ b/www/whiteboard/app-whiteboard.less @@ -88,7 +88,7 @@ #cp-app-whiteboard-delete { min-width: 40px; } - .cp-whiteboard-type { + .cp-whiteboard-type, .cp-whiteboard-history { button { min-width: 40px; text-align: center; diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index fb67d8dd5..91ae12162 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -53,12 +53,15 @@ define([ var $type = $('.cp-whiteboard-type'); var $brush = $('.cp-whiteboard-type .brush'); var $move = $('.cp-whiteboard-type .move'); + var $undo = $('.cp-whiteboard-history .undo'); + var $redo = $('.cp-whiteboard-history .redo'); + var $text = $('.cp-whiteboard-text button'); var $deleteButton = $('#cp-app-whiteboard-delete'); var metadataMgr = framework._.cpNfInner.metadataMgr; var brush = { - color: '#000000', + color: window.CryptPad_theme === "dark" ? '#FFFFFF' : '#000000', opacity: 1 }; @@ -137,6 +140,35 @@ define([ $deleteButton.prop('disabled', ''); }); + $text.click(function () { + $move.click(); + canvas.add(new Fabric.Textbox('My Text', { + fill: brush.color, + top: 5, + left: 5 + })); + }); + $undo.click(function () { + if (typeof(APP.canvas.undo) !== "function") { return; } + APP.canvas.undo(); + APP.onLocal(); + }); + $redo.click(function () { + if (typeof(APP.canvas.undo) !== "function") { return; } + APP.canvas.redo(); + APP.onLocal(); + }); + $('body').on('keydown', function (e) { + if (e.which === 90 && e.ctrlKey) { + $undo.click(); + return; + } + if (e.which === 89 && e.ctrlKey) { + $redo.click(); + return; + } + }); + var deleteSelection = function () { if (APP.draw) { return; } if (canvas.getActiveObject()) { @@ -175,6 +207,16 @@ define([ c = Colors.rgb2hex(c); brush.color = c; canvas.freeDrawingBrush.color = Colors.hex2rgba(brush.color, brush.opacity); + if (!APP.draw) { + var active = canvas.getActiveObject(); + if (active) { + var col = Colors.hex2rgba(brush.color, brush.opacity); + if (active.text) { active.set('fill', col); } + else { active.set('stroke', col); } + canvas.renderAll(); + APP.onLocal(); + } + } createCursor(); }; @@ -436,7 +478,15 @@ define([ }; }); + var cleanHistory = function () { + if (Array.isArray(canvas.historyUndo)) { + canvas.historyUndo = canvas.historyUndo.slice(-100); + canvas.historyRedo = canvas.historyRedo.slice(-100); + } + }; + framework.onContentUpdate(function (newContent, waitFor) { + cleanHistory(); var content = newContent.content; canvas.loadFromJSON(content, waitFor(function () { canvas.renderAll(); @@ -445,6 +495,7 @@ define([ }); framework.setContentGetter(function () { + cleanHistory(); var content = canvas.toDatalessJSON(); return { content: content @@ -475,6 +526,8 @@ define([ }; + Messages.undo = "Undo"; // XXX + Messages.redo = "Redo"; // XXX var initialContent = function () { return [ h('div#cp-toolbar.cp-toolbar-container'), @@ -494,6 +547,13 @@ define([ h('button.btn.brush.fa.fa-paint-brush.btn-primary', {title: Messages.canvas_brush}), h('button.btn.move.fa.fa-arrows', {title: Messages.canvas_select}), ]), + h('div.cp-whiteboard-history', [ + h('button.btn.undo.fa.fa-undo', {title: Messages.undo}), + h('button.btn.redo.fa.fa-repeat', {title: Messages.redo}), + ]), + h('div.cp-whiteboard-text', [ + h('button.btn.fa.fa-font') + ]), h('button.btn.fa.fa-trash#cp-app-whiteboard-delete', { disabled: 'disabled', title: Messages.canvas_delete @@ -560,6 +620,7 @@ define([ $('body').append($div.html()); })); }).nThen(function (waitFor) { + require(['/lib/fabric-history.min.js'], waitFor()); // Framework initialization Framework.create({