From 8b565e32ce142fb3b1161f44ebb2b0fe3e4607ed Mon Sep 17 00:00:00 2001 From: benweet Date: Mon, 24 Mar 2014 00:22:46 +0000 Subject: [PATCH] Comments support --- public/res/constants.js | 12 +- public/res/core.js | 6 +- public/res/editor.js | 228 ++++++++------- public/res/eventMgr.js | 5 + public/res/extensions/comments.js | 262 ++++++++++++------ public/res/html/commentsPopoverContent.html | 12 +- public/res/providers/gdriveProviderBuilder.js | 12 + public/res/styles/main.less | 44 ++- 8 files changed, 374 insertions(+), 207 deletions(-) diff --git a/public/res/constants.js b/public/res/constants.js index f4e816e4..a5922c62 100644 --- a/public/res/constants.js +++ b/public/res/constants.js @@ -1,7 +1,7 @@ define([], function() { var constants = {}; constants.VERSION = "3.1.9"; - + constants.MAIN_URL = "https://stackedit.io/"; constants.GOOGLE_ANALYTICS_ACCOUNT_ID = "UA-39556145-1"; constants.GOOGLE_API_KEY = "AIzaSyAeCU8CGcSkn0z9js6iocHuPBX4f_mMWkw"; @@ -14,7 +14,7 @@ define([], function() { constants.DEFAULT_FILE_TITLE = "Title"; constants.DEFAULT_FOLDER_NAME = "New folder"; constants.GDRIVE_DEFAULT_FILE_TITLE = "New Markdown document"; - constants.EDITOR_DEFAULT_PADDING = 30; + constants.EDITOR_DEFAULT_PADDING = 35; constants.CHECK_ONLINE_PERIOD = 120000; constants.AJAX_TIMEOUT = 30000; constants.ASYNC_TASK_DEFAULT_TIMEOUT = 60000; @@ -28,7 +28,7 @@ define([], function() { constants.PICASA_PROXY_URL = "https://stackedit-picasa-proxy.herokuapp.com/"; constants.SSH_PROXY_URL = "https://stackedit-ssh-proxy.herokuapp.com/"; constants.HTMLTOPDF_URL = "https://stackedit-htmltopdf.herokuapp.com/"; - + // Site dependent constants.BASE_URL = "http://localhost/"; constants.GOOGLE_CLIENT_ID = '241271498917-lev37kef013q85avc91am1gccg5g8lrb.apps.googleusercontent.com'; @@ -37,7 +37,7 @@ define([], function() { constants.TUMBLR_PROXY_URL = "https://stackedit-tumblr-proxy-local.herokuapp.com/"; constants.WORDPRESS_CLIENT_ID = '23361'; constants.WORDPRESS_PROXY_URL = "https://stackedit-io-wordpress-proxy.herokuapp.com/"; - + if(location.hostname.indexOf("stackedit.io") === 0) { constants.BASE_URL = constants.MAIN_URL; constants.GOOGLE_CLIENT_ID = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com'; @@ -61,7 +61,7 @@ define([], function() { constants.GATEKEEPER_URL = "https://stackedit-gatekeeper-insomnia.herokuapp.com/"; constants.TUMBLR_PROXY_URL = "https://stackedit-tumblr-proxy-beta.herokuapp.com/"; } - + constants.THEME_LIST = { "default": "Default", "gray": "Gray", @@ -69,6 +69,6 @@ define([], function() { "night": "Night", "school": "School", }; - + return constants; }); diff --git a/public/res/core.js b/public/res/core.js index 1d91a174..749b1e0c 100644 --- a/public/res/core.js +++ b/public/res/core.js @@ -397,7 +397,7 @@ define([ if(pagedownEditor !== undefined) { // If the editor is already created - $editorElt.val(initDocumentContent); + editor.contentElt.textContent = initDocumentContent; pagedownEditor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop); $editorElt.focus(); return; @@ -455,7 +455,7 @@ define([ eventMgr.onPagedownConfigure(pagedownEditor); pagedownEditor.hooks.chain("onPreviewRefresh", eventMgr.onAsyncPreview); pagedownEditor.run(); - $editorElt.val(initDocumentContent); + editor.contentElt.textContent = initDocumentContent; pagedownEditor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop); $editorElt.focus(); @@ -799,7 +799,7 @@ define([ }); $(".action-import-docs-settings-confirm").click(function() { storage.clear(); - var allowedKeys = /^file\.|^folder\.|^publish\.|^settings$|^sync\.|^google\.|^themeV3$|^version$/; + var allowedKeys = /^file\.|^folder\.|^publish\.|^settings$|^sync\.|^google\.|^author\.|^themeV3$|^version$/; _.each(newstorage, function(value, key) { if(allowedKeys.test(key)) { storage[key] = value; diff --git a/public/res/editor.js b/public/res/editor.js index 3603c492..0524d9a0 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -61,23 +61,66 @@ define([ fileDesc = selectedFileDesc; }); - function saveEditorState() { - if(!inputElt.focused) { - return; + function saveSelectionState() { + var selection = window.getSelection(); + if (selection.rangeCount > 0) { + var range = selection.getRangeAt(0); + var element = range.startContainer; + + if ((inputElt.compareDocumentPosition(element) & 0x10)) { + var container = element; + var offset = range.startOffset; + do { + while (element = element.previousSibling) { + if (element.textContent) { + offset += element.textContent.length; + } + } + + element = container = container.parentNode; + } while (element && element != inputElt); + selectionStart = offset; + selectionEnd = offset + (range + '').length; + } } - selectionStart = inputElt.selectionStart; - selectionEnd = inputElt.selectionEnd; - scrollTop = inputElt.scrollTop; if(fileChanged === false) { fileDesc.editorStart = selectionStart; fileDesc.editorEnd = selectionEnd; - fileDesc.editorScrollTop = scrollTop; } } var previousTextContent; + function getContentChange(textContent) { + // Find the first modified char + var startIndex = 0; + var startIndexMax = Math.min(previousTextContent.length, textContent.length); + while (startIndex < startIndexMax) { + if (previousTextContent.charCodeAt(startIndex) !== textContent.charCodeAt(startIndex)) { + break; + } + startIndex++; + } + // Find the last modified char + var endIndex = 1; + var endIndexMax = Math.min(previousTextContent.length - startIndex, textContent.length - startIndex); + while (endIndex <= endIndexMax) { + if (previousTextContent.charCodeAt(previousTextContent.length - endIndex) !== textContent.charCodeAt(textContent.length - endIndex)) { + break; + } + endIndex++; + } + + var replacement = textContent.substring(startIndex, textContent.length - endIndex + 1); + endIndex = previousTextContent.length - endIndex + 1; + return { + startIndex: startIndex, + endIndex: endIndex, + replacement: replacement + }; + } + function checkContentChange() { - saveEditorState(); + saveSelectionState(); var currentTextContent = inputElt.textContent; if(fileChanged === false) { if(currentTextContent == previousTextContent) { @@ -86,6 +129,37 @@ define([ if(!/\n$/.test(currentTextContent)) { currentTextContent += '\n'; } + var change = getContentChange(currentTextContent); + var endOffset = change.startIndex + change.replacement.length - change.endIndex; + + // Move comments according to change + var updateDiscussionList = false; + _.each(fileDesc.discussionList, function(discussion) { + if(discussion.isRemoved === true) { + return; + } + // selectionEnd + if(discussion.selectionEnd >= change.endIndex) { + discussion.selectionEnd += endOffset; + updateDiscussionList = true; + } + else if(discussion.selectionEnd > change.startIndex) { + discussion.selectionEnd = change.startIndex; + updateDiscussionList = true; + } + // selectionStart + if(discussion.selectionStart >= change.endIndex) { + discussion.selectionStart += endOffset; + updateDiscussionList = true; + } + else if(discussion.selectionStart > change.startIndex) { + discussion.selectionStart = change.startIndex; + updateDiscussionList = true; + } + }); + if(updateDiscussionList === true) { + fileDesc.discussionList = fileDesc.discussionList; // Write discussionList in localStorage + } fileDesc.content = currentTextContent; eventMgr.onContentChanged(fileDesc, currentTextContent); } @@ -199,7 +273,7 @@ define([ var cursorY = 0; var isBackwardSelection = false; function updateCursorCoordinates() { - saveEditorState(); + saveSelectionState(); $inputElt.toggleClass('has-selection', selectionStart !== selectionEnd); var element; @@ -234,24 +308,25 @@ define([ eventMgr.onCursorCoordinates(coordinates.x, coordinates.y); } - function adjustCursorPosition() { - inputElt && setTimeout(function() { - updateCursorCoordinates(); + var adjustCursorPosition = _.debounce(function() { + if(inputElt === undefined) { + return; + } + updateCursorCoordinates(); - var adjust = inputElt.offsetHeight / 2; - if(adjust > 130) { - adjust = 130; - } - var cursorMinY = inputElt.scrollTop + adjust; - var cursorMaxY = inputElt.scrollTop + inputElt.offsetHeight - adjust; - if(cursorY < cursorMinY) { - inputElt.scrollTop += cursorY - cursorMinY; - } - else if(cursorY > cursorMaxY) { - inputElt.scrollTop += cursorY - cursorMaxY; - } - }, 0); - } + var adjust = inputElt.offsetHeight / 2; + if(adjust > 130) { + adjust = 130; + } + var cursorMinY = inputElt.scrollTop + adjust; + var cursorMaxY = inputElt.scrollTop + inputElt.offsetHeight - adjust; + if(cursorY < cursorMinY) { + inputElt.scrollTop += cursorY - cursorMinY; + } + else if(cursorY > cursorMaxY) { + inputElt.scrollTop += cursorY - cursorMaxY; + } + }, 0); eventMgr.addListener('onLayoutResize', adjustCursorPosition); editor.init = function(elt1, elt2) { @@ -279,7 +354,12 @@ define([ characterData: true }); - $(inputElt).scroll(saveEditorState); + $(inputElt).scroll(function() { + scrollTop = inputElt.scrollTop; + if(fileChanged === false) { + fileDesc.editorScrollTop = scrollTop; + } + }); $(previewElt).scroll(function() { if(fileChanged === false) { fileDesc.previewScrollTop = previewElt.scrollTop; @@ -303,64 +383,16 @@ define([ return this.textContent; }, set: function (value) { - var currentValue = this.textContent; - - // Find the first modified char - var startIndex = 0; - var startIndexMax = Math.min(currentValue.length, value.length); - while (startIndex < startIndexMax) { - if (currentValue.charCodeAt(startIndex) !== value.charCodeAt(startIndex)) { - break; - } - startIndex++; - } - // Find the last modified char - var endIndex = 1; - var endIndexMax = Math.min(currentValue.length - startIndex, value.length - startIndex); - while (endIndex <= endIndexMax) { - if (currentValue.charCodeAt(currentValue.length - endIndex) !== value.charCodeAt(value.length - endIndex)) { - break; - } - endIndex++; - } - - var replacementText = value.substring(startIndex, value.length - endIndex + 1); - endIndex = currentValue.length - endIndex + 1; - - var range = inputElt.createRange(startIndex, endIndex); + var contentChange = getContentChange(value); + var range = inputElt.createRange(contentChange.startIndex, contentChange.endIndex); range.deleteContents(); - range.insertNode(document.createTextNode(replacementText)); + range.insertNode(document.createTextNode(contentChange.replacement)); } }); Object.defineProperty(inputElt, 'selectionStart', { get: function () { - var selection = window.getSelection(); - - if (selection.rangeCount) { - var range = selection.getRangeAt(0), - element = range.startContainer, - container = element, - offset = range.startOffset; - - if (!(this.compareDocumentPosition(element) & 0x10)) { - return selectionStart; - } - - do { - while (element = element.previousSibling) { - if (element.textContent) { - offset += element.textContent.length; - } - } - - element = container = container.parentNode; - } while (element && element != this); - - return offset; - } else { - return selectionStart; - } + return selectionStart; }, set: function (value) { inputElt.setSelectionStartEnd(value, selectionEnd); @@ -372,13 +404,7 @@ define([ Object.defineProperty(inputElt, 'selectionEnd', { get: function () { - var selection = window.getSelection(); - - if (selection.rangeCount) { - return this.selectionStart + (selection.getRangeAt(0) + '').length; - } else { - return selectionEnd; - } + return selectionEnd; }, set: function (value) { inputElt.setSelectionStartEnd(selectionStart, value); @@ -388,34 +414,24 @@ define([ configurable: true }); - inputElt.setSelectionStartEnd = function (ss, se) { - selectionStart = ss; - selectionEnd = se; - var range = inputElt.createRange(ss, se); - + inputElt.setSelectionStartEnd = function (start, end) { + selectionStart = start; + selectionEnd = end; + var range = inputElt.createRange(start, end); var selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); }; - inputElt.getSelectionStartEnd = function () { - return { - selectionStart: selectionStart, - selectionEnd: selectionEnd - }; - }; - - inputElt.createRange = function(ss, se) { - - var range = document.createRange(), - offset = _.isObject(ss) ? ss : findOffset(ss); + inputElt.createRange = function(start, end) { + var range = document.createRange(); + var offset = _.isObject(start) ? start : findOffset(start); range.setStart(offset.element, offset.offset); - if (se && se != ss) { - offset = _.isObject(se) ? se : findOffset(se); + if (end && end != start) { + offset = _.isObject(end) ? end : findOffset(end); } - range.setEnd(offset.element, offset.offset); return range; }; @@ -435,7 +451,7 @@ define([ ) { return; } - saveEditorState(); + saveSelectionState(); adjustCursorPosition(); var cmdOrCtrl = evt.metaKey || evt.ctrlKey; diff --git a/public/res/eventMgr.js b/public/res/eventMgr.js index 245e30eb..c7c80d7c 100644 --- a/public/res/eventMgr.js +++ b/public/res/eventMgr.js @@ -210,6 +210,11 @@ define([ addEventHook("onSectionsCreated"); addEventHook("onCursorCoordinates"); + // Operations on comments + addEventHook("onDiscussionCreated"); + addEventHook("onDiscussionRemoved"); + addEventHook("onCommentAdded"); + // Refresh twitter buttons addEventHook("onTweet"); diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js index be1ce56e..08d7e070 100644 --- a/public/res/extensions/comments.js +++ b/public/res/extensions/comments.js @@ -2,18 +2,31 @@ define([ "jquery", "underscore", "utils", + "storage", "crel", "rangy", "classes/Extension", "text!html/commentsPopoverContent.html", "bootstrap" -], function($, _, utils, crel, rangy, Extension, commentsPopoverContentHTML) { +], function($, _, utils, storage, crel, rangy, Extension, commentsPopoverContentHTML) { var comments = new Extension("comments", 'Comments'); + var commentTmpl = [ + '
', + '
<%= author %>
', + '
<%= content %>
', + '
', + ].join(''); + + var eventMgr; + comments.onEventMgrCreated = function(eventMgrParam) { + eventMgr = eventMgrParam; + }; + var offsetMap = {}; function setCommentEltCoordinates(commentElt, y) { - var lineIndex = Math.round(y/10); + var lineIndex = Math.round(y / 10); var top = (y - 8) + 'px'; var right = ((offsetMap[lineIndex] || 0) * 25 + 10) + 'px'; commentElt.style.top = top; @@ -33,32 +46,28 @@ define([ setCommentEltCoordinates(newCommentElt, cursorY); }; - var fileDesc; - comments.onFileSelected = function(selectedFileDesc) { - fileDesc = selectedFileDesc; - refreshDiscussions(); - }; - - var openedPopover; - comments.onLayoutResize = function() { - openedPopover && openedPopover.popover('toggle').popover('destroy'); - refreshDiscussions(); - }; - var refreshId; + var currentFileDesc; function refreshDiscussions() { + if(currentFileDesc === undefined) { + return; + } + var author = storage['author.name']; clearTimeout(refreshId); commentEltList.forEach(function(commentElt) { marginElt.removeChild(commentElt); }); commentEltList = []; offsetMap = {}; - var discussionList = _.map(fileDesc.discussionList, _.identity); + var discussionList = _.map(currentFileDesc.discussionList, _.identity); function refreshOne() { - if(discussionList.length === 0) { - return; - } - var discussion = discussionList.pop(); + var discussion; + do { + if(discussionList.length === 0) { + return; + } + discussion = discussionList.pop(); + } while(discussion.isRemoved); var commentElt = crel('a', { class: 'icon-comment' }); @@ -69,63 +78,115 @@ define([ marginElt.appendChild(commentElt); commentEltList.push(commentElt); - // Replace newCommentElt + // Move newCommentElt setCommentEltCoordinates(newCommentElt, cursorY); - refreshId = setTimeout(refreshOne, 0); + // Apply class later for fade effect + commentElt.offsetWidth; // Refresh + var isReplied = _.last(discussion.commentList).author != author; + commentElt.className += isReplied ? ' replied' : ' added'; + refreshId = setTimeout(refreshOne, 50); } refreshId = setTimeout(refreshOne, 50); } + var debouncedRefreshDiscussions = _.debounce(refreshDiscussions, 2000); + + comments.onFileOpen = function(fileDesc) { + currentFileDesc = fileDesc; + refreshDiscussions(); + }; + + comments.onContentChanged = function(fileDesc, content) { + currentFileDesc === fileDesc && debouncedRefreshDiscussions(); + }; + + var currentContext; + function closeCurrentPopover() { + currentContext && currentContext.$commentElt.popover('toggle').popover('destroy'); + } + comments.onLayoutResize = function() { + closeCurrentPopover(); + refreshDiscussions(); + }; + + comments.onDiscussionCreated = function() { + refreshDiscussions(); + }; + + comments.onDiscussionRemoved = function() { + refreshDiscussions(); + }; + + comments.onCommentAdded = function() { + refreshDiscussions(); + }; + + function getDiscussionComments() { + return currentContext.discussion.commentList.map(function(comment) { + return _.template(commentTmpl, { + author: comment.author || 'Anonymous', + content: comment.content + }); + }).join(''); + } comments.onReady = function() { var cssApplier = rangy.createCssClassApplier("comment-highlight", { normalize: false }); - var selectionRange; - var rangyRange; - var currentDiscussion; + var previousContent = ''; inputElt = document.getElementById('wmd-input'); marginElt = document.querySelector('#wmd-input > .editor-margin'); marginElt.appendChild(newCommentElt); $(document.body).append(crel('div', { class: 'comments-popover' - })).popover({ + })).on('click', function(evt) { + // Close on click outside the popover + if(currentContext && currentContext.$commentElt[0] !== evt.target) { + closeCurrentPopover(); + } + }).popover({ placement: 'auto top', container: '.comments-popover', html: true, title: function() { - if(!currentDiscussion) { - return '...'; + if(!currentContext) { + return true; } - var titleLength = currentDiscussion.selectionEnd - currentDiscussion.selectionStart; - var title = inputElt.textContent.substr(currentDiscussion.selectionStart, titleLength > 20 ? 20 : titleLength); + var titleLength = currentContext.discussion.selectionEnd - currentContext.discussion.selectionStart; + var title = inputElt.textContent.substr(currentContext.discussion.selectionStart, titleLength > 20 ? 20 : titleLength); if(titleLength > 20) { title += '...'; } - return title.replace(/&/g, '&').replace(/' + title; }, content: function() { var content = _.template(commentsPopoverContentHTML, { + commentList: getDiscussionComments() }); return content; }, selector: '#wmd-input > .editor-margin > .icon-comment' }).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) { - $(evt.target).addClass('active'); + closeCurrentPopover(); + var context = { + $commentElt: $(evt.target).addClass('active') + }; + currentContext = context; inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; // If it's an existing discussion if(evt.target.discussion) { - currentDiscussion = evt.target.discussion; - selectionRange = inputElt.createRange(currentDiscussion.selectionStart, currentDiscussion.selectionEnd); + context.discussion = evt.target.discussion; + context.selectionRange = inputElt.createRange(context.discussion.selectionStart, context.discussion.selectionEnd); return; } // Get selected text - var inputSelection = inputElt.getSelectionStartEnd(); - var selectionStart = inputSelection.selectionStart; - var selectionEnd = inputSelection.selectionEnd; + var selectionStart = inputElt.selectionStart; + var selectionEnd = inputElt.selectionEnd; if(selectionStart === selectionEnd) { var after = inputElt.textContent.substring(selectionStart); var match = /\S+/.exec(after); @@ -139,26 +200,30 @@ define([ selectionEnd += match.index + match[0].length; } } - selectionRange = inputElt.createRange(selectionStart, selectionEnd); - currentDiscussion = { + context.selectionRange = inputElt.createRange(selectionStart, selectionEnd); + context.discussion = { selectionStart: selectionStart, selectionEnd: selectionEnd, commentList: [] }; }).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) { - // Move the popover in the margin - var popoverElt = document.querySelector('.comments-popover .popover:last-child'); - var left = -10; - if(popoverElt.offsetWidth < marginElt.offsetWidth) { - left = marginElt.offsetWidth - popoverElt.offsetWidth - 10; + var context = currentContext; + context.popoverElt = document.querySelector('.comments-popover .popover:last-child'); + var left = -5; + if(context.popoverElt.offsetWidth < marginElt.offsetWidth - 5) { + left = marginElt.offsetWidth - 10 - context.popoverElt.offsetWidth; } - popoverElt.style.left = left + 'px'; - popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(evt.target.style.right) - evt.target.offsetWidth / 2 - left) + 'px'; + context.popoverElt.style.left = left + 'px'; + context.popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(evt.target.style.right) - evt.target.offsetWidth / 2 - left) + 'px'; - var $textarea = $(popoverElt.querySelector('.input-comment-content')); - var $addButton = $(popoverElt.querySelector('.action-add-comment')); - $textarea.keydown(function(evt) { + // Scroll to the bottom of the discussion + context.popoverElt.querySelector('.popover-content').scrollTop = 9999999; + + context.$authorInputElt = $(context.popoverElt.querySelector('.input-comment-author')).val(storage['author.name']); + context.$contentInputElt = $(context.popoverElt.querySelector('.input-comment-content')); + var $addButton = $(context.popoverElt.querySelector('.action-add-comment')); + context.$contentInputElt.keydown(function(evt) { // Enter key switch(evt.which) { case 13: @@ -167,65 +232,106 @@ define([ return; case 27: evt.preventDefault(); - openedPopover && openedPopover.popover('toggle').popover('destroy'); + closeCurrentPopover(); inputElt.focus(); return; } }); $addButton.click(function(evt) { - var author = utils.getInputTextValue(popoverElt.querySelector('.input-comment-author')); - var content = utils.getInputTextValue($textarea, evt); + var author = utils.getInputTextValue(context.$authorInputElt); + var content = utils.getInputTextValue(context.$contentInputElt, evt); if(evt.isPropagationStopped()) { return; } - var discussionList = fileDesc.discussionList || {}; - if(!currentDiscussion.discussionIndex) { + context.$contentInputElt.val(''); + closeCurrentPopover(); + + var discussionList = currentFileDesc.discussionList || {}; + var isNew = false; + if(!context.discussion.discussionIndex) { + isNew = true; // Create discussion index var discussionIndex; do { discussionIndex = utils.randomString(); } while(_.has(discussionList, discussionIndex)); - currentDiscussion.discussionIndex = discussionIndex; - discussionList[discussionIndex] = currentDiscussion; + context.discussion.discussionIndex = discussionIndex; + discussionList[discussionIndex] = context.discussion; } - currentDiscussion.commentList.push({ + context.discussion.commentList.push({ author: author, content: content }); - fileDesc.discussionList = discussionList; - openedPopover.popover('toggle').popover('destroy'); - refreshDiscussions(); + currentFileDesc.discussionList = discussionList; // Write discussionList in localStorage + isNew ? + eventMgr.onDiscussionCreated(currentFileDesc, context.discussion) : + eventMgr.onCommentAdded(currentFileDesc, context.discussion); inputElt.focus(); }); + var $removeButton = $(context.popoverElt.querySelector('.action-remove-discussion')); + if(evt.target.discussion) { + // If it's an existing discussion + var $removeCancelButton = $(context.popoverElt.querySelector('.action-remove-discussion-cancel')); + var $removeConfirmButton = $(context.popoverElt.querySelector('.action-remove-discussion-confirm')); + $removeButton.click(function() { + $(context.popoverElt.querySelector('.new-comment-block')).addClass('hide'); + $(context.popoverElt.querySelector('.remove-discussion-confirm')).removeClass('hide'); + context.popoverElt.querySelector('.popover-content').scrollTop = 9999999; + }); + $removeCancelButton.click(function() { + $(context.popoverElt.querySelector('.new-comment-block')).removeClass('hide'); + $(context.popoverElt.querySelector('.remove-discussion-confirm')).addClass('hide'); + context.popoverElt.querySelector('.popover-content').scrollTop = 9999999; + context.$contentInputElt.focus(); + }); + $removeConfirmButton.click(function() { + closeCurrentPopover(); + context.discussion.isRemoved = true; + delete context.discussion.selectionStart; + delete context.discussion.selectionEnd; + delete context.discussion.commentList; + currentFileDesc.discussionList = currentFileDesc.discussionList; // Write discussionList in localStorage + eventMgr.onDiscussionRemoved(currentFileDesc, context.discussion); + inputElt.focus(); + }); + } + else { + // Otherwise hide the remove button + $removeButton.hide(); + } + // Prevent from closing on click inside the popover - $(popoverElt).on('click', function(evt) { + $(context.popoverElt).on('click', function(evt) { evt.stopPropagation(); }); - setTimeout(function() { - openedPopover = $(evt.target); - // Highlight selected text - rangyRange = rangy.createRange(); - rangyRange.setStart(selectionRange.startContainer, selectionRange.startOffset); - rangyRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); - cssApplier.applyToRange(rangyRange); + // Highlight selected text + context.rangyRange = rangy.createRange(); + context.rangyRange.setStart(context.selectionRange.startContainer, context.selectionRange.startOffset); + context.rangyRange.setEnd(context.selectionRange.endContainer, context.selectionRange.endOffset); + setTimeout(function() { // Need to delay this because it's not refreshed properly + if(currentContext === context) { + cssApplier.applyToRange(context.rangyRange); + } + }, 50); - // Focus on textarea - $textarea.focus(); - }, 10); + // Focus on textarea + context.$contentInputElt.focus().val(previousContent); }).on('hide.bs.popover', '#wmd-input > .editor-margin', function(evt) { - $(evt.target).removeClass('active'); + if(!currentContext) { + return; + } + currentContext.$commentElt.removeClass('active'); + + // Save content and author for later + previousContent = currentContext.$contentInputElt.val(); + storage['author.name'] = currentContext.$authorInputElt.val(); // Remove highlight - rangyRange && cssApplier.undoToRange(rangyRange); - openedPopover = undefined; - rangyRange = undefined; - currentDiscussion = undefined; - }).on('click', function() { - // Close on click outside the popover - openedPopover && openedPopover.popover('toggle').popover('destroy'); + cssApplier.undoToRange(currentContext.rangyRange); + currentContext = undefined; }); }; diff --git a/public/res/html/commentsPopoverContent.html b/public/res/html/commentsPopoverContent.html index 1e56b649..bec86cd9 100644 --- a/public/res/html/commentsPopoverContent.html +++ b/public/res/html/commentsPopoverContent.html @@ -1,5 +1,5 @@ -
-
+
<%= commentList %>
+
@@ -8,3 +8,11 @@
+
+
+
Remove this discussion, really?
+
+ + +
+
diff --git a/public/res/providers/gdriveProviderBuilder.js b/public/res/providers/gdriveProviderBuilder.js index 7388c3b0..d40537c7 100644 --- a/public/res/providers/gdriveProviderBuilder.js +++ b/public/res/providers/gdriveProviderBuilder.js @@ -451,6 +451,18 @@ define([ }; function mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer) { + if(localDiscussion.isRemoved === true) { + realtimeDiscussion.set('isRemoved'); + realtimeDiscussion.delete('selectionStart'); + realtimeDiscussion.delete('selectionEnd'); + return realtimeDiscussion.delete('commentList'); + } + if(realtimeDiscussion.get('isRemoved') === true) { + localDiscussion.isRemoved = true; + delete localDiscussion.selectionStart; + delete localDiscussion.selectionEnd; + return delete localDiscussion.commentList; + } if(takeServer) { localDiscussion.selectionStart = realtimeDiscussion.get('selectionStart'); localDiscussion.selectionEnd = realtimeDiscussion.get('selectionEnd'); diff --git a/public/res/styles/main.less b/public/res/styles/main.less index 72cce427..b1d1bf6e 100644 --- a/public/res/styles/main.less +++ b/public/res/styles/main.less @@ -127,7 +127,7 @@ @popover-arrow-outer-color: @secondary-border-color; @popover-title-bg: @transparent; @alert-border-radius: 0; -@label-warning-bg: #d90; +@label-warning-bg: #da0; @label-danger-bg: #d00; @@ -140,7 +140,7 @@ body { } #preview-contents { - padding: 30px; + padding: 35px; margin: 0 auto 200px; text-align: justify; } @@ -362,7 +362,7 @@ a { .form-control.error { border-color: @error-border; - .box-shadow(~"@{form-control-inset-shadow}, 0 0 8px rgba(255, 134, 97, 0.6)"); + .box-shadow(~"@{form-control-inset-shadow}, 0 0 8px rgba(255, 0, 0, 0.6)"); } .help-block { @@ -773,7 +773,7 @@ a { position: absolute; z-index: 1; margin-top: 6px; - right: 30px; + right: 35px; .ui-layout-resizer-south-closed & { display: none !important; } @@ -1060,22 +1060,28 @@ a { top: 0; .icon-comment { &.new { - color: fade(@tertiary-color, 12%); - &:hover { + color: fade(@tertiary-color, 10%); + &:hover, &.active, &.active:hover { color: fade(@tertiary-color, 35%) !important; } } &.replied { - color: fade(@label-danger-bg, 30%); + color: fade(@label-danger-bg, 35%); &:hover, &.active, &.active:hover { color: fade(@label-danger-bg, 45%) !important; } } + &.added { + color: fade(@label-warning-bg, 40%); + &:hover, &.active, &.active:hover { + color: fade(@label-warning-bg, 60%) !important; + } + } + .transition(~"color ease-in-out .25s"); position: absolute; - color: fade(@label-warning-bg, 40%); + color: fade(#fff, 0%); cursor: pointer; - &:hover, &.active, &.active:hover { - color: fade(@label-warning-bg, 60%) !important; + &:hover, &.active { text-decoration: none; } } @@ -1399,7 +1405,7 @@ input[type="file"] { *********************/ .popover { - max-width: 240px; + max-width: 230px; padding: 10px 20px 0; //.box-shadow(0 5px 30px rgba(0,0,0,.175)); .popover-title { @@ -1408,6 +1414,10 @@ input[type="file"] { padding: 5px 0 15px; border-bottom: 1px solid @hr-border; line-height: @headings-line-height; + .action-remove-discussion { + font-size: 16px; + line-height: 22px; + } } .icon-lock { font-size: 38px; @@ -1417,10 +1427,20 @@ input[type="file"] { display: none; } .popover-content { - padding-bottom: 0; + padding: 10px 20px 0; + overflow: auto; + max-height: 230px; + margin: 0 -20px; .btn { padding: 6px 11px; } + .comment-block { + margin-bottom: 5px; + } + .comment-author { + padding-left: 12px; + font-weight: bold; + } .input-comment-author { border: none; background: none;