From 492f5ffbc0f2bc0c27320f6d71b7b63d6ef7305b Mon Sep 17 00:00:00 2001 From: benweet Date: Fri, 23 May 2014 23:58:18 +0100 Subject: [PATCH] Fixed className leak in undo/redo buttons --- public/res/editor.js | 14 +- public/res/extensions/comments.js | 892 +++++++++++++++-------------- public/res/libs/Markdown.Editor.js | 2 +- 3 files changed, 461 insertions(+), 447 deletions(-) diff --git a/public/res/editor.js b/public/res/editor.js index 273bff3b..335196ef 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -27,6 +27,7 @@ define([ var pagedownEditor; var refreshPreviewLater = (function() { var elapsedTime = 0; + var timeoutId; var refreshPreview = function() { var startTime = Date.now(); pagedownEditor.refreshPreview(); @@ -36,7 +37,8 @@ define([ return _.debounce(refreshPreview, 500); } return function() { - setTimeout(refreshPreview, elapsedTime < 2000 ? elapsedTime : 2000); + clearTimeout(timeoutId); + timeoutId = setTimeout(refreshPreview, elapsedTime < 2000 ? elapsedTime : 2000); }; })(); eventMgr.addListener('onPagedownConfigure', function(pagedownEditorParam) { @@ -174,9 +176,10 @@ define([ var min = Math.min(this.selectionStart, this.selectionEnd); var max = Math.max(this.selectionStart, this.selectionEnd); var range = this.createRange(min, max); - var selection = document.getSelection(); + var selection = rangy.getSelection(); selection.removeAllRanges(); - selection.addRange(range); + selection.addRange(range, this.selectionStart > this.selectionEnd); + selection.detach(); range.detach(); }; this.setSelectionStartEnd = function(start, end) { @@ -203,7 +206,7 @@ define([ var selectionStart = self.selectionStart; var selectionEnd = self.selectionEnd; var range; - var selection = document.getSelection(); + var selection = rangy.getSelection(); if(selection.rangeCount > 0) { var selectionRange = selection.getRangeAt(0); var element = selectionRange.startContainer; @@ -220,7 +223,7 @@ define([ element = container = container.parentNode; } while(element && element != inputElt); - if(false) { + if(selection.isBackwards()) { selectionStart = offset + (range + '').length; selectionEnd = offset; } @@ -231,6 +234,7 @@ define([ } selectionRange.detach(); } + selection.detach(); self.setSelectionStartEnd(selectionStart, selectionEnd); } undoMgr.saveSelectionState(); diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js index 4c5c14c4..0057c504 100644 --- a/public/res/extensions/comments.js +++ b/public/res/extensions/comments.js @@ -1,479 +1,489 @@ define([ - "jquery", - "underscore", - "utils", - "storage", - "crel", - "rangy", - "classes/Extension", - "text!html/commentsPopoverContent.html", - "bootstrap" + "jquery", + "underscore", + "utils", + "storage", + "crel", + "rangy", + "classes/Extension", + "text!html/commentsPopoverContent.html", + "bootstrap" ], function($, _, utils, storage, crel, rangy, Extension, commentsPopoverContentHTML) { - var comments = new Extension("comments", 'Comments', false, true); + var comments = new Extension("comments", 'Comments', false, true); - var commentTmpl = [ - '
', - '
<%= author %>
', - '
<%= content %>
', - '
' - ].join(''); - var popoverTitleTmpl = [ - '', - ' ', - ' ', - ' ', - ' “<%- title %>”', - '' - ].join(''); + var commentTmpl = [ + '
', + '
<%= author %>
', + '
<%= content %>
', + '
' + ].join(''); + var popoverTitleTmpl = [ + '', + ' ', + ' ', + ' ', + ' “<%- title %>”', + '' + ].join(''); - var eventMgr; - comments.onEventMgrCreated = function(eventMgrParam) { - eventMgr = eventMgrParam; - }; + var eventMgr; + comments.onEventMgrCreated = function(eventMgrParam) { + eventMgr = eventMgrParam; + }; - var editor; - var selectionMgr; - comments.onEditorCreated = function(editorParam) { - editor = editorParam; - selectionMgr = editor.selectionMgr; - }; + var editor; + var selectionMgr; + comments.onEditorCreated = function(editorParam) { + editor = editorParam; + selectionMgr = editor.selectionMgr; + }; - var yList = []; - function setCommentEltCoordinates(commentElt, y, isNew) { - y = Math.round(y); - var yListIndex = y - 21; - // Avoid overlap of comment icons - while(yListIndex < y + 22) { - if(yList[yListIndex]) { - y = yListIndex + 22; - } - yListIndex++; - } - !isNew && (yList[y] = 1); - var yOffset = -8; - if(commentElt.className.indexOf(' icon-split') !== -1) { - yOffset = -12; - } - var top = y + yOffset; - commentElt.style.top = top + 'px'; - commentElt.style.right = '12px'; - } + var yList = []; - var inputElt; - var marginElt; - var newCommentElt = crel('a', { - class: 'discussion icon-comment new' - }); + function setCommentEltCoordinates(commentElt, y, isNew) { + y = Math.round(y); + var yListIndex = y - 21; + // Avoid overlap of comment icons + while(yListIndex < y + 22) { + if(yList[yListIndex]) { + y = yListIndex + 22; + } + yListIndex++; + } + !isNew && (yList[y] = 1); + var yOffset = -8; + if(commentElt.className.indexOf(' icon-split') !== -1) { + yOffset = -12; + } + var top = y + yOffset; + commentElt.style.top = top + 'px'; + commentElt.style.right = '12px'; + } - function Context(commentElt, fileDesc) { - this.commentElt = commentElt; - this.$commentElt = $(commentElt).addClass('active'); - this.fileDesc = fileDesc; - this.discussionIndex = commentElt.discussionIndex; - } - Context.prototype.getDiscussion = function() { - if(!this.discussionIndex) { - return this.fileDesc.newDiscussion; - } - return this.fileDesc.discussionList[this.discussionIndex]; - }; - Context.prototype.getPopoverElt = function() { - return document.querySelector('.comments-popover .popover:last-child'); - }; - var currentContext; - function movePopover(commentElt) { - // Move popover in the margin - var popoverElt = currentContext.getPopoverElt(); - var left = 0; - if(popoverElt.offsetWidth < marginElt.offsetWidth - 10) { - left = marginElt.offsetWidth - 10 - popoverElt.offsetWidth; - } - popoverElt.style.left = left + 'px'; - popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(commentElt.style.right) - commentElt.offsetWidth / 2 - left) + 'px'; - var popoverTopOffset = window.innerHeight - currentContext.hr.getBoundingClientRect().top; - if(popoverTopOffset < 0) { - popoverElt.style.top = (parseInt(popoverElt.style.top) + popoverTopOffset) + 'px'; - } - } + var inputElt; + var marginElt; + var newCommentElt = crel('a', { + class: 'discussion icon-comment new' + }); - var cssApplier; - var currentFileDesc; - var refreshTimeoutId; - var commentEltMap = {}; - var sortedCommentEltList = []; - var someReplies = false; - var $openDiscussionElt; - var $openDiscussionIconElt; - var refreshDiscussions = _.debounce(function() { - if(currentFileDesc === undefined) { - return; - } - someReplies = false; - sortedCommentEltList = []; - var author = storage['author.name']; - yList = []; - var discussionList = _.sortBy(currentFileDesc.discussionList, function(discussion) { - return discussion.selectionEnd; - }); - function refreshOne() { - var coordinates; - if(discussionList.length === 0) { - // Remove outdated commentElt - _.filter(commentEltMap, function(commentElt, discussionIndex) { - return !_.has(currentFileDesc.discussionList, discussionIndex); - }).forEach(function(commentElt) { - marginElt.removeChild(commentElt); - delete commentEltMap[commentElt.discussionIndex]; - }); - // Move newCommentElt - if(currentContext && !currentContext.discussionIndex) { - coordinates = selectionMgr.getCoordinates(currentContext.getDiscussion().selectionEnd); - setCommentEltCoordinates(newCommentElt, coordinates.y, true); - inputElt.scrollTop += parseInt(newCommentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; - movePopover(newCommentElt); - } - sortedCommentEltList = _.sortBy(commentEltMap, function(commentElt) { - return commentElt.selectionEnd; - }); - $openDiscussionElt.toggleClass('some', sortedCommentEltList.length !== 0); - $openDiscussionElt.toggleClass('replied', someReplies); - $openDiscussionIconElt.toggleClass('icon-chat', sortedCommentEltList.length !== 0); - return; - } - var discussion = discussionList.shift(); - var commentElt = commentEltMap[discussion.discussionIndex]; - if(!commentElt) { - commentElt = crel('a'); - } - var className = 'discussion'; - var isReplied = !discussion.commentList || !author || _.last(discussion.commentList).author != author; - isReplied && (someReplies = true); - if(discussion.type == 'conflict') { - className += ' icon-split'; - } - else { - className += ' icon-comment'; - } - className += isReplied ? ' replied' : ' added'; - commentElt.className = className; - commentElt.discussionIndex = discussion.discussionIndex; - commentElt.selectionEnd = discussion.selectionEnd; - coordinates = selectionMgr.getCoordinates(discussion.selectionEnd); - setCommentEltCoordinates(commentElt, coordinates.y); + function Context(commentElt, fileDesc) { + this.commentElt = commentElt; + this.$commentElt = $(commentElt).addClass('active'); + this.fileDesc = fileDesc; + this.discussionIndex = commentElt.discussionIndex; + } - marginElt.appendChild(commentElt); - commentEltMap[discussion.discussionIndex] = commentElt; + Context.prototype.getDiscussion = function() { + if(!this.discussionIndex) { + return this.fileDesc.newDiscussion; + } + return this.fileDesc.discussionList[this.discussionIndex]; + }; + Context.prototype.getPopoverElt = function() { + return document.querySelector('.comments-popover .popover:last-child'); + }; + var currentContext; - if(currentContext && currentContext.getDiscussion() === discussion) { - inputElt.scrollTop += parseInt(commentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; - movePopover(commentElt); - } - refreshTimeoutId = setTimeout(refreshOne, 5); - } - clearTimeout(refreshTimeoutId); - refreshTimeoutId = setTimeout(refreshOne, 5); - }, 50); - comments.onLayoutResize = refreshDiscussions; + function movePopover(commentElt) { + // Move popover in the margin + var popoverElt = currentContext.getPopoverElt(); + var left = 0; + if(popoverElt.offsetWidth < marginElt.offsetWidth - 10) { + left = marginElt.offsetWidth - 10 - popoverElt.offsetWidth; + } + popoverElt.style.left = left + 'px'; + popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(commentElt.style.right) - commentElt.offsetWidth / 2 - left) + 'px'; + var popoverTopOffset = window.innerHeight - currentContext.hr.getBoundingClientRect().top; + if(popoverTopOffset < 0) { + popoverElt.style.top = (parseInt(popoverElt.style.top) + popoverTopOffset) + 'px'; + } + } - comments.onFileOpen = function(fileDesc) { - currentFileDesc = fileDesc; - refreshDiscussions(); - }; + var cssApplier; + var currentFileDesc; + var refreshTimeoutId; + var commentEltMap = {}; + var sortedCommentEltList = []; + var someReplies = false; + var $openDiscussionElt; + var $openDiscussionIconElt; + var refreshDiscussions = _.debounce(function() { + if(currentFileDesc === undefined) { + return; + } + someReplies = false; + sortedCommentEltList = []; + var author = storage['author.name']; + yList = []; + var discussionList = _.sortBy(currentFileDesc.discussionList, function(discussion) { + return discussion.selectionEnd; + }); - comments.onContentChanged = function(fileDesc) { - currentFileDesc === fileDesc && refreshDiscussions(); - }; + function refreshOne() { + var coordinates; + if(discussionList.length === 0) { + // Remove outdated commentElt + _.filter(commentEltMap, function(commentElt, discussionIndex) { + return !_.has(currentFileDesc.discussionList, discussionIndex); + }).forEach(function(commentElt) { + marginElt.removeChild(commentElt); + delete commentEltMap[commentElt.discussionIndex]; + }); + // Move newCommentElt + if(currentContext && !currentContext.discussionIndex) { + coordinates = selectionMgr.getCoordinates(currentContext.getDiscussion().selectionEnd); + setCommentEltCoordinates(newCommentElt, coordinates.y, true); + inputElt.scrollTop += parseInt(newCommentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; + movePopover(newCommentElt); + } + sortedCommentEltList = _.sortBy(commentEltMap, function(commentElt) { + return commentElt.selectionEnd; + }); + $openDiscussionElt.toggleClass('some', sortedCommentEltList.length !== 0); + $openDiscussionElt.toggleClass('replied', someReplies); + $openDiscussionIconElt.toggleClass('icon-chat', sortedCommentEltList.length !== 0); + return; + } + var discussion = discussionList.shift(); + var commentElt = commentEltMap[discussion.discussionIndex]; + if(!commentElt) { + commentElt = crel('a'); + } + var className = 'discussion'; + var isReplied = !discussion.commentList || !author || _.last(discussion.commentList).author != author; + isReplied && (someReplies = true); + if(discussion.type == 'conflict') { + className += ' icon-split'; + } + else { + className += ' icon-comment'; + } + className += isReplied ? ' replied' : ' added'; + commentElt.className = className; + commentElt.discussionIndex = discussion.discussionIndex; + commentElt.selectionEnd = discussion.selectionEnd; + coordinates = selectionMgr.getCoordinates(discussion.selectionEnd); + setCommentEltCoordinates(commentElt, coordinates.y); - comments.onCommentsChanged = function(fileDesc) { - if(currentFileDesc !== fileDesc) { - return; - } - if(currentContext !== undefined) { - // Refresh conversation if popover is open - var context = currentContext; - if(context.discussionIndex) { - context.getPopoverElt().querySelector('.discussion-comment-list').innerHTML = getDiscussionComments(); - } - try { - cssApplier.undoToRange(context.rangyRange); - context.rangyRange.detach(); - } - catch(e) {} - var discussion = context.getDiscussion(); - context.selectionRange = selectionMgr.createRange(discussion.selectionStart, discussion.selectionEnd); + marginElt.appendChild(commentElt); + commentEltMap[discussion.discussionIndex] = commentElt; - // 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); - } - refreshDiscussions(); - }; + if(currentContext && currentContext.getDiscussion() === discussion) { + inputElt.scrollTop += parseInt(commentElt.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; + movePopover(commentElt); + } + refreshTimeoutId = setTimeout(refreshOne, 5); + } - function closeCurrentPopover() { - currentContext && currentContext.$commentElt.popover('toggle').popover('destroy'); - } + clearTimeout(refreshTimeoutId); + refreshTimeoutId = setTimeout(refreshOne, 5); + }, 50); + comments.onLayoutResize = refreshDiscussions; - comments.onDiscussionCreated = function(fileDesc) { - currentFileDesc === fileDesc && refreshDiscussions(); - }; + comments.onFileOpen = function(fileDesc) { + currentFileDesc = fileDesc; + refreshDiscussions(); + }; - comments.onDiscussionRemoved = function(fileDesc, discussion) { - if(currentFileDesc === fileDesc) { - // Close popover if the discussion has been removed - if(currentContext !== undefined && currentContext.discussionIndex == discussion.discussionIndex) { - closeCurrentPopover(); - } - refreshDiscussions(); - } - }; + comments.onContentChanged = function(fileDesc) { + currentFileDesc === fileDesc && refreshDiscussions(); + }; - function getDiscussionComments() { - var discussion = currentContext.getDiscussion(); - var author = storage['author.name']; - var result = []; - if(discussion.commentList) { - result = discussion.commentList.map(function(comment) { - var commentAuthor = comment.author || 'Anonymous'; - return _.template(commentTmpl, { - author: commentAuthor, - content: comment.content, - reply: comment.author != author - }); - }); - } - if(discussion.type == 'conflict') { - result.unshift(_.template(commentTmpl, { - author: 'StackEdit', - content: 'Multiple users have made conflicting modifications.', - reply: true - })); - } - return result.join(''); - } + comments.onCommentsChanged = function(fileDesc) { + if(currentFileDesc !== fileDesc) { + return; + } + if(currentContext !== undefined) { + // Refresh conversation if popover is open + var context = currentContext; + if(context.discussionIndex) { + context.getPopoverElt().querySelector('.discussion-comment-list').innerHTML = getDiscussionComments(); + } + try { + cssApplier.undoToRange(context.rangyRange); + context.rangyRange.detach(); + } + catch(e) { + } + var discussion = context.getDiscussion(); + context.selectionRange = selectionMgr.createRange(discussion.selectionStart, discussion.selectionEnd); - comments.onReady = function() { - cssApplier = rangy.createCssClassApplier("comment-highlight", { - normalize: false - }); - var previousContent = ''; + // 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); + } + refreshDiscussions(); + }; - inputElt = document.getElementById('wmd-input'); - marginElt = document.querySelector('#wmd-input > .editor-margin'); - marginElt.appendChild(newCommentElt); - var $popoverContainer = $(crel('div', { - class: 'comments-popover' - })); - $(document.body).append($popoverContainer).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(!currentContext) { - return true; - } - var discussion = currentContext.getDiscussion(); - var titleLength = discussion.selectionEnd - discussion.selectionStart; - var title = editor.getValue().substr(discussion.selectionStart, titleLength > 20 ? 20 : titleLength); - if(titleLength > 20) { - title += '...'; - } - return _.template(popoverTitleTmpl, { - title: title - }); - }, - content: function() { - var content = _.template(commentsPopoverContentHTML, { - commentList: getDiscussionComments() - }); - return content; - }, - selector: '#wmd-input > .editor-margin > .discussion' - }); - $(marginElt).on('show.bs.popover', function(evt) { - closeCurrentPopover(); - var context = new Context(evt.target, currentFileDesc); - currentContext = context; + function closeCurrentPopover() { + currentContext && currentContext.$commentElt.popover('toggle').popover('destroy'); + } - // If it's not an existing discussion - var discussion = context.getDiscussion(); - if(!discussion) { - // Get selected text - var selectionStart = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd); - var selectionEnd = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd); - if(selectionStart === selectionEnd) { - var offset = selectionMgr.getClosestWordOffset(selectionStart); - selectionStart = offset.start; - selectionEnd = offset.end; - } - discussion = { - selectionStart: selectionStart, - selectionEnd: selectionEnd, - commentList: [] - }; - currentFileDesc.newDiscussion = discussion; - var coordinates = selectionMgr.getCoordinates(selectionStart); - setCommentEltCoordinates(newCommentElt, coordinates.y, true); - } - context.selectionRange = selectionMgr.createRange(discussion.selectionStart, discussion.selectionEnd); - inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; + comments.onDiscussionCreated = function(fileDesc) { + currentFileDesc === fileDesc && refreshDiscussions(); + }; - }).on('shown.bs.popover', function(evt) { - var context = currentContext; - var popoverElt = context.getPopoverElt(); - context.$authorInputElt = $(popoverElt.querySelector('.input-comment-author')).val(storage['author.name']); - context.$contentInputElt = $(popoverElt.querySelector('.input-comment-content')); - context.hr = popoverElt.querySelector('hr'); - movePopover(context.commentElt); + comments.onDiscussionRemoved = function(fileDesc, discussion) { + if(currentFileDesc === fileDesc) { + // Close popover if the discussion has been removed + if(currentContext !== undefined && currentContext.discussionIndex == discussion.discussionIndex) { + closeCurrentPopover(); + } + refreshDiscussions(); + } + }; - // Scroll to the bottom of the discussion - popoverElt.querySelector('.scrollport').scrollTop = 9999999; + function getDiscussionComments() { + var discussion = currentContext.getDiscussion(); + var author = storage['author.name']; + var result = []; + if(discussion.commentList) { + result = discussion.commentList.map(function(comment) { + var commentAuthor = comment.author || 'Anonymous'; + return _.template(commentTmpl, { + author: commentAuthor, + content: comment.content, + reply: comment.author != author + }); + }); + } + if(discussion.type == 'conflict') { + result.unshift(_.template(commentTmpl, { + author: 'StackEdit', + content: 'Multiple users have made conflicting modifications.', + reply: true + })); + } + return result.join(''); + } - var $addButton = $(popoverElt.querySelector('.action-add-comment')); - $().add(context.$contentInputElt).add(context.$authorInputElt).keydown(function(evt) { - // Enter key - switch(evt.which) { - case 13: - evt.preventDefault(); - $addButton.click(); - return; - case 27: - evt.preventDefault(); - closeCurrentPopover(); - editor.focus(); - editor.adjustCursorPosition(); - return; - } - }); - $addButton.click(function(evt) { - var author = utils.getInputTextValue(context.$authorInputElt); - var content = utils.getInputTextValue(context.$contentInputElt, evt); - if(evt.isPropagationStopped()) { - return; - } + comments.onReady = function() { + cssApplier = rangy.createCssClassApplier("comment-highlight", { + normalize: false + }); + var previousContent = ''; - var discussion = context.getDiscussion(); - context.$contentInputElt.val(''); - closeCurrentPopover(); + inputElt = document.getElementById('wmd-input'); + marginElt = document.querySelector('#wmd-input > .editor-margin'); + marginElt.appendChild(newCommentElt); + var $popoverContainer = $(crel('div', { + class: 'comments-popover' + })); + $(document.body).append($popoverContainer).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(!currentContext) { + return true; + } + var discussion = currentContext.getDiscussion(); + var titleLength = discussion.selectionEnd - discussion.selectionStart; + var title = editor.getValue().substr(discussion.selectionStart, titleLength > 20 ? 20 : titleLength); + if(titleLength > 20) { + title += '...'; + } + return _.template(popoverTitleTmpl, { + title: title + }); + }, + content: function() { + var content = _.template(commentsPopoverContentHTML, { + commentList: getDiscussionComments() + }); + return content; + }, + selector: '#wmd-input > .editor-margin > .discussion' + }); + $(marginElt).on('show.bs.popover', function(evt) { + closeCurrentPopover(); + var context = new Context(evt.target, currentFileDesc); + currentContext = context; - discussion.commentList = discussion.commentList || []; - discussion.commentList.push({ - author: author, - content: content - }); - var discussionList = context.fileDesc.discussionList || {}; - if(!discussion.discussionIndex) { - // Create discussion index - var discussionIndex; - do { - discussionIndex = utils.randomString(); - } while(_.has(discussionList, discussionIndex)); - discussion.discussionIndex = discussionIndex; - discussionList[discussionIndex] = discussion; - context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage - eventMgr.onDiscussionCreated(context.fileDesc, discussion); - } - else { - context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage - eventMgr.onCommentsChanged(context.fileDesc); - } - inputElt.focus(); - }); + // If it's not an existing discussion + var discussion = context.getDiscussion(); + if(!discussion) { + // Get selected text + var selectionStart = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd); + var selectionEnd = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd); + if(selectionStart === selectionEnd) { + var offset = selectionMgr.getClosestWordOffset(selectionStart); + selectionStart = offset.start; + selectionEnd = offset.end; + } + discussion = { + selectionStart: selectionStart, + selectionEnd: selectionEnd, + commentList: [] + }; + currentFileDesc.newDiscussion = discussion; + var coordinates = selectionMgr.getCoordinates(selectionStart); + setCommentEltCoordinates(newCommentElt, coordinates.y, true); + } + context.selectionRange = selectionMgr.createRange(discussion.selectionStart, discussion.selectionEnd); + inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4; - var $removeButton = $(popoverElt.querySelector('.action-remove-discussion')); - if(evt.target.discussionIndex) { - // If it's an existing discussion - $removeButton.click(function() { - closeCurrentPopover(); - var discussion = context.getDiscussion(); - delete context.fileDesc.discussionList[discussion.discussionIndex]; - context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage - eventMgr.onDiscussionRemoved(context.fileDesc, discussion); - inputElt.focus(); - }); - } - else { - // Otherwise hide the remove button - $removeButton.hide(); - } + }).on('shown.bs.popover', function(evt) { + var context = currentContext; + var popoverElt = context.getPopoverElt(); + context.$authorInputElt = $(popoverElt.querySelector('.input-comment-author')).val(storage['author.name']); + context.$contentInputElt = $(popoverElt.querySelector('.input-comment-content')); + context.hr = popoverElt.querySelector('hr'); + movePopover(context.commentElt); - // 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); + // Scroll to the bottom of the discussion + popoverElt.querySelector('.scrollport').scrollTop = 9999999; - // Focus on textarea - context.$contentInputElt.focus().val(previousContent); - }).on('hide.bs.popover', function() { - if(!currentContext) { - return; - } - currentContext.$commentElt.removeClass('active'); + var $addButton = $(popoverElt.querySelector('.action-add-comment')); + $().add(context.$contentInputElt).add(context.$authorInputElt).keydown(function(evt) { + // Enter key + switch(evt.which) { + case 13: + evt.preventDefault(); + $addButton.click(); + return; + case 27: + evt.preventDefault(); + closeCurrentPopover(); + editor.focus(); + editor.adjustCursorPosition(); + return; + } + }); + $addButton.click(function(evt) { + var author = utils.getInputTextValue(context.$authorInputElt); + var content = utils.getInputTextValue(context.$contentInputElt, evt); + if(evt.isPropagationStopped()) { + return; + } - // Save content and author for later - previousContent = currentContext.$contentInputElt.val(); - storage['author.name'] = currentContext.$authorInputElt.val(); + var discussion = context.getDiscussion(); + context.$contentInputElt.val(''); + closeCurrentPopover(); - // Remove highlight - cssApplier.undoToRange(currentContext.rangyRange); - currentContext.rangyRange.detach(); - currentContext = undefined; - delete currentFileDesc.newDiscussion; - }); + discussion.commentList = discussion.commentList || []; + discussion.commentList.push({ + author: author, + content: content + }); + var discussionList = context.fileDesc.discussionList || {}; + if(!discussion.discussionIndex) { + // Create discussion index + var discussionIndex; + do { + discussionIndex = utils.randomString(); + } while(_.has(discussionList, discussionIndex)); + discussion.discussionIndex = discussionIndex; + discussionList[discussionIndex] = discussion; + context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage + eventMgr.onDiscussionCreated(context.fileDesc, discussion); + } + else { + context.fileDesc.discussionList = discussionList; // Write discussionList in localStorage + eventMgr.onCommentsChanged(context.fileDesc); + } + inputElt.focus(); + }); - // Prevent from closing on click inside the popover - $popoverContainer.on('click', '.popover', function(evt) { - evt.stopPropagation(); - }); + var $removeButton = $(popoverElt.querySelector('.action-remove-discussion')); + if(evt.target.discussionIndex) { + // If it's an existing discussion + $removeButton.click(function() { + closeCurrentPopover(); + var discussion = context.getDiscussion(); + delete context.fileDesc.discussionList[discussion.discussionIndex]; + context.fileDesc.discussionList = context.fileDesc.discussionList; // Write discussionList in localStorage + eventMgr.onDiscussionRemoved(context.fileDesc, discussion); + inputElt.focus(); + }); + } + else { + // Otherwise hide the remove button + $removeButton.hide(); + } + + // 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 + context.$contentInputElt.focus().val(previousContent); + }).on('hide.bs.popover', function() { + 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 + try { + cssApplier.undoToRange(currentContext.rangyRange); + currentContext.rangyRange.detach(); + } + catch(e) { + } + currentContext = undefined; + delete currentFileDesc.newDiscussion; + }); + + // Prevent from closing on click inside the popover + $popoverContainer.on('click', '.popover', function(evt) { + evt.stopPropagation(); + }); - var $newCommentElt = $(newCommentElt); - $openDiscussionElt = $('.button-open-discussion').click(function(evt) { - var $commentElt = $newCommentElt; - if(currentContext) { - if(!currentContext.discussionIndex) { - $commentElt = $(_.first(sortedCommentEltList)); - } - else { - var curentIndex = -1; - sortedCommentEltList.some(function(elt, index) { - if(elt === currentContext.commentElt) { - curentIndex = index; - return true; - } - }); - $commentElt = $(sortedCommentEltList[(curentIndex + 1)]); - } - } - if($commentElt.length === 0) { - // Close the popover properly - closeCurrentPopover(); - editor.focus(); - editor.adjustCursorPosition(); - } - else { - $commentElt.click(); - } - evt.stopPropagation(); - }); - $openDiscussionIconElt = $openDiscussionElt.find('i'); - }; + var $newCommentElt = $(newCommentElt); + $openDiscussionElt = $('.button-open-discussion').click(function(evt) { + var $commentElt = $newCommentElt; + if(currentContext) { + if(!currentContext.discussionIndex) { + $commentElt = $(_.first(sortedCommentEltList)); + } + else { + var curentIndex = -1; + sortedCommentEltList.some(function(elt, index) { + if(elt === currentContext.commentElt) { + curentIndex = index; + return true; + } + }); + $commentElt = $(sortedCommentEltList[(curentIndex + 1)]); + } + } + if($commentElt.length === 0) { + // Close the popover properly + closeCurrentPopover(); + editor.focus(); + editor.adjustCursorPosition(); + } + else { + $commentElt.click(); + } + evt.stopPropagation(); + }); + $openDiscussionIconElt = $openDiscussionElt.find('i'); + }; - return comments; + return comments; }); diff --git a/public/res/libs/Markdown.Editor.js b/public/res/libs/Markdown.Editor.js index 742d94e7..d95b0fcb 100644 --- a/public/res/libs/Markdown.Editor.js +++ b/public/res/libs/Markdown.Editor.js @@ -1425,6 +1425,7 @@ var disabledYShift = "-20px"; var highlightYShift = "-40px"; var image = button.getElementsByTagName("span")[0]; + button.className = button.className.replace(/ disabled/g, ""); if (isEnabled) { image.style.backgroundPosition = button.XShift + " " + normalYShift; button.onmouseover = function () { @@ -1454,7 +1455,6 @@ return false; } } - button.className = button.className.replace(/ disabled/g, ""); } else { image.style.backgroundPosition = button.XShift + " " + disabledYShift;