Comments support
This commit is contained in:
		
							parent
							
								
									e310d234cf
								
							
						
					
					
						commit
						8b565e32ce
					
				| @ -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; | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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,8 +308,10 @@ define([ | ||||
|         eventMgr.onCursorCoordinates(coordinates.x, coordinates.y); | ||||
|     } | ||||
| 
 | ||||
|     function adjustCursorPosition() { | ||||
|         inputElt && setTimeout(function() { | ||||
|     var adjustCursorPosition = _.debounce(function() { | ||||
|         if(inputElt === undefined) { | ||||
|             return; | ||||
|         } | ||||
|         updateCursorCoordinates(); | ||||
| 
 | ||||
|         var adjust = inputElt.offsetHeight / 2; | ||||
| @ -251,7 +327,6 @@ define([ | ||||
|             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; | ||||
|                 } | ||||
|             }, | ||||
|             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; | ||||
|                 } | ||||
|             }, | ||||
|             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; | ||||
|  | ||||
| @ -210,6 +210,11 @@ define([ | ||||
|     addEventHook("onSectionsCreated"); | ||||
|     addEventHook("onCursorCoordinates"); | ||||
| 
 | ||||
|     // Operations on comments
 | ||||
|     addEventHook("onDiscussionCreated"); | ||||
|     addEventHook("onDiscussionRemoved"); | ||||
|     addEventHook("onCommentAdded"); | ||||
| 
 | ||||
|     // Refresh twitter buttons
 | ||||
|     addEventHook("onTweet"); | ||||
| 
 | ||||
|  | ||||
| @ -2,15 +2,28 @@ 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 = [ | ||||
|         '<div class="comment-block">', | ||||
|         '    <div class="comment-author"><%= author %></div>', | ||||
|         '    <div class="comment-content"><%= content %></div>', | ||||
|         '</div>', | ||||
|     ].join(''); | ||||
| 
 | ||||
|     var eventMgr; | ||||
|     comments.onEventMgrCreated = function(eventMgrParam) { | ||||
|         eventMgr = eventMgrParam; | ||||
|     }; | ||||
| 
 | ||||
|     var offsetMap = {}; | ||||
|     function setCommentEltCoordinates(commentElt, y) { | ||||
|         var lineIndex = Math.round(y / 10); | ||||
| @ -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() { | ||||
|             var discussion; | ||||
|             do { | ||||
|                 if(discussionList.length === 0) { | ||||
|                     return; | ||||
|                 } | ||||
|             var discussion = discussionList.pop(); | ||||
|                 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(/</g, '<').replace(/\u00a0/g, ' '); | ||||
|                 title = title.replace(/&/g, '&').replace(/</g, '<').replace(/\u00a0/g, ' '); | ||||
|                 return '<a href="#" class="action-remove-discussion pull-right"><i class="icon-trash"></i></a>' + 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); | ||||
|             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); | ||||
|             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; | ||||
|         }); | ||||
|     }; | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <div class="form-horizontal"> | ||||
|     <div class="discussion-history"></div> | ||||
| <div class="discussion-comment-list"><%= commentList %></div> | ||||
| <div class="new-comment-block"> | ||||
|     <div class="form-group"> | ||||
|         <input class="form-control input-comment-author" placeholder="Your name"></input> | ||||
|         <textarea class="form-control input-comment-content"></textarea> | ||||
| @ -8,3 +8,11 @@ | ||||
|         <button class="btn btn-primary action-add-comment">Add</button> | ||||
|     </div> | ||||
| </div> | ||||
| <div class="remove-discussion-confirm hide"> | ||||
|     <br/> | ||||
|     <blockquote>Remove this discussion, really?</blockquote> | ||||
|     <div class="form-group text-right"> | ||||
|         <button class="btn btn-default action-remove-discussion-cancel">No</button> | ||||
|         <button class="btn btn-primary action-remove-discussion-confirm">Yes</button> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| @ -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'); | ||||
|  | ||||
| @ -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; | ||||
| 				} | ||||
| 			} | ||||
| 			position: absolute; | ||||
| 			&.added { | ||||
| 				color: fade(@label-warning-bg, 40%); | ||||
| 			cursor: pointer; | ||||
| 				&:hover, &.active, &.active:hover { | ||||
| 					color: fade(@label-warning-bg, 60%) !important; | ||||
| 				} | ||||
| 			} | ||||
| 			.transition(~"color ease-in-out .25s"); | ||||
| 			position: absolute; | ||||
| 			color: fade(#fff, 0%); | ||||
| 			cursor: pointer; | ||||
| 			&: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; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 benweet
						benweet