diff --git a/bower.json b/bower.json index 6abd2b2f..14a1176e 100644 --- a/bower.json +++ b/bower.json @@ -25,6 +25,7 @@ "yaml.js": "https://github.com/jeremyfa/yaml.js.git#~0.1.4", "stackedit-pagedown": "https://github.com/benweet/stackedit-pagedown.git#d81b3689a99c84832d8885f607e6de860fb7d94a", "prism": "gh-pages", - "MutationObservers": "https://github.com/Polymer/MutationObservers.git#~0.2.1" + "MutationObservers": "https://github.com/Polymer/MutationObservers.git#~0.2.1", + "rangy": "~1.2.3" } } diff --git a/public/res/editor.js b/public/res/editor.js index 91a5cc30..aa8c0d9e 100644 --- a/public/res/editor.js +++ b/public/res/editor.js @@ -62,7 +62,9 @@ define([ }); function saveEditorState() { - setTimeout + if(!inputElt.focused) { + return; + } selectionStart = inputElt.selectionStart; selectionEnd = inputElt.selectionEnd; scrollTop = inputElt.scrollTop; @@ -106,6 +108,7 @@ define([ var cursorY = 0; function saveCursorCoordinates() { saveEditorState(); + $inputElt.toggleClass('has-selection', selectionStart !== selectionEnd); var backwards = false; var selection = window.getSelection(); @@ -129,10 +132,10 @@ define([ var cursorOffset = backwards ? selectionStart : selectionEnd; var selectedChar = inputElt.textContent[cursorOffset]; if(selectedChar === undefined || selectedChar == '\n') { - selectionRange = createRange(cursorOffset - 1, cursorOffset); + selectionRange = inputElt.createRange(cursorOffset - 1, cursorOffset); } else { - selectionRange = createRange(cursorOffset, cursorOffset + 1); + selectionRange = inputElt.createRange(cursorOffset, cursorOffset + 1); } var selectionRect = selectionRange.getBoundingClientRect(); cursorY = selectionRect.top + selectionRect.height / 2 - inputElt.offsetTop + inputElt.scrollTop; @@ -195,7 +198,7 @@ define([ inputElt.focus = function() { editor.$contentElt.focus(); - this.setSelectionRange(selectionStart, selectionEnd); + this.setSelectionStartEnd(selectionStart, selectionEnd); inputElt.scrollTop = scrollTop; }; editor.$contentElt.focus(function() { @@ -234,7 +237,7 @@ define([ var replacementText = value.substring(startIndex, value.length - endIndex + 1); endIndex = currentValue.length - endIndex + 1; - var range = createRange(startIndex, endIndex); + var range = inputElt.createRange(startIndex, endIndex); range.deleteContents(); range.insertNode(document.createTextNode(replacementText)); } @@ -270,7 +273,7 @@ define([ } }, set: function (value) { - inputElt.setSelectionRange(value, selectionEnd); + inputElt.setSelectionStartEnd(value, selectionEnd); }, enumerable: true, @@ -288,23 +291,103 @@ define([ } }, set: function (value) { - inputElt.setSelectionRange(selectionStart, value); + inputElt.setSelectionStartEnd(selectionStart, value); }, enumerable: true, configurable: true }); - inputElt.setSelectionRange = function (ss, se) { + inputElt.setSelectionStartEnd = function (ss, se) { selectionStart = ss; selectionEnd = se; - var range = createRange(ss, se); + var range = inputElt.createRange(ss, se); var selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); }; + inputElt.getSelectionStartEnd = function () { + return { + selectionStart: selectionStart, + selectionEnd: selectionEnd + } + }; + + inputElt.createRange = function(ss, se) { + function findOffset(ss) { + var offset = 0, + element = editor.contentElt, + container; + + do { + container = element; + element = element.firstChild; + + if (element) { + do { + var len = element.textContent.length; + + if (offset <= ss && offset + len > ss) { + break; + } + + offset += len; + } while (element = element.nextSibling); + } + + if (!element) { + // It's the container's lastChild + break; + } + } while (element && element.hasChildNodes() && element.nodeType != 3); + + if (element) { + return { + element: element, + offset: ss - offset + }; + } else if (container) { + element = container; + + while (element && element.lastChild) { + element = element.lastChild; + } + + if (element.nodeType === 3) { + return { + element: element, + offset: element.textContent.length + }; + } else { + return { + element: element, + offset: 0 + }; + } + } + + return { + element: editor.contentElt, + offset: 0, + error: true + }; + } + + var range = document.createRange(), + offset = findOffset(ss); + + range.setStart(offset.element, offset.offset); + + if (se && se != ss) { + offset = findOffset(se); + } + + range.setEnd(offset.element, offset.offset); + return range; + }; + var clearNewline = false; editor.$contentElt.on('keydown', function (evt) { if( @@ -338,7 +421,11 @@ define([ clearNewline = false; } }) - .on('mouseup', saveCursorCoordinates) + .on('mouseup', function() { + setTimeout(function() { + saveCursorCoordinates(); + }, 0); + }) .on('paste', function () { pagedownEditor.undoManager.setMode("paste"); adjustCursorPosition(); @@ -364,7 +451,7 @@ define([ actions[action](state, options); inputElt.value = state.before + state.selection + state.after; - inputElt.setSelectionRange(state.ss, state.se); + inputElt.setSelectionStartEnd(state.ss, state.se); $inputElt.trigger('input'); }; @@ -508,7 +595,7 @@ define([ if(fileChanged === true) { editor.contentElt.innerHTML = ''; editor.contentElt.appendChild(newSectionEltList); - inputElt.setSelectionRange(selectionStart, selectionEnd); + inputElt.setSelectionStartEnd(selectionStart, selectionEnd); } else { // Remove outdated sections @@ -536,7 +623,7 @@ define([ childNode = nextNode; } - inputElt.setSelectionRange(selectionStart, selectionEnd); + inputElt.setSelectionStartEnd(selectionStart, selectionEnd); } } @@ -559,79 +646,5 @@ define([ section.highlightedContent = sectionElt; } - - function createRange(ss, se) { - function findOffset(ss) { - var offset = 0, - element = editor.contentElt, - container; - - do { - container = element; - element = element.firstChild; - - if (element) { - do { - var len = element.textContent.length; - - if (offset <= ss && offset + len > ss) { - break; - } - - offset += len; - } while (element = element.nextSibling); - } - - if (!element) { - // It's the container's lastChild - break; - } - } while (element && element.hasChildNodes() && element.nodeType != 3); - - if (element) { - return { - element: element, - offset: ss - offset - }; - } else if (container) { - element = container; - - while (element && element.lastChild) { - element = element.lastChild; - } - - if (element.nodeType === 3) { - return { - element: element, - offset: element.textContent.length - }; - } else { - return { - element: element, - offset: 0 - }; - } - } - - return { - element: editor.contentElt, - offset: 0, - error: true - }; - } - - var range = document.createRange(), - offset = findOffset(ss); - - range.setStart(offset.element, offset.offset); - - if (se && se != ss) { - offset = findOffset(se); - } - - range.setEnd(offset.element, offset.offset); - return range; - } - return editor; }); diff --git a/public/res/extensions/comments.js b/public/res/extensions/comments.js new file mode 100644 index 00000000..72b1690c --- /dev/null +++ b/public/res/extensions/comments.js @@ -0,0 +1,156 @@ +define([ + "jquery", + "underscore", + "utils", + "crel", + "rangy", + "classes/Extension", + "text!html/commentsPopoverContent.html", + "bootstrap" +], function($, _, utils, crel, rangy, Extension, commentsPopoverContentHTML) { + + var comments = new Extension("comments", 'Comments'); + + var offsetMap = {}; + + var inputElt; + var marginElt; + var newCommentElt = crel('a', { + class: 'icon-comment new' + }); + comments.onCursorCoordinates = function(cursorX, cursorY) { + var top = (cursorY - 8) + 'px'; + var right = 10 + 'px'; + newCommentElt.style.top = top; + newCommentElt.style.right = right; + }; + + var fileDesc; + comments.onFileSelected = function(selectedFileDesc) { + fileDesc = selectedFileDesc; + }; + + comments.onReady = function() { + var cssApplier = rangy.createCssClassApplier("comment-highlight", { + normalize: false + }); + var openedPopover; + var selectionRange; + var rangyRange; + var currentDiscussion; + + inputElt = document.getElementById('wmd-input'); + marginElt = document.querySelector('#wmd-input > .editor-margin'); + marginElt.appendChild(newCommentElt); + $(document.body).append($('
')).popover({ + placement: 'auto top', + container: '.comments-popover', + html: true, + title: function() { + if(!currentDiscussion) { + return '...'; + } + var titleLength = currentDiscussion.selectionEnd - currentDiscussion.selectionStart; + var title = fileDesc.content.substr(currentDiscussion.selectionStart, titleLength > 18 ? 18 : titleLength); + if(titleLength > 18) { + title += '...'; + } + return title.replace(/&/g, '&').replace(/ .editor-margin > .icon-comment' + }).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) { + $(evt.target).addClass('active'); + inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 2/3; + + // Get selected text + var inputSelection = inputElt.getSelectionStartEnd(); + var selectionStart = inputSelection.selectionStart; + var selectionEnd = inputSelection.selectionEnd; + if(selectionStart === selectionEnd) { + var after = inputElt.textContent.substring(selectionStart); + var match = /\S+/.exec(after); + if(match) { + selectionStart += match.index; + if(match.index === 0) { + while(selectionStart && /\S/.test(inputElt.textContent[selectionStart-1])) { + selectionStart--; + } + } + selectionEnd += match.index + match[0].length; + } + } + selectionRange = inputElt.createRange(selectionStart, selectionEnd); + currentDiscussion = { + selectionStart: selectionStart, + selectionEnd: selectionEnd, + comments: [] + }; + }).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) { + + // Move the popover in the margin + var popoverElt = document.querySelector('.comments-popover .popover'); + var left = -10; + if(popoverElt.offsetWidth < marginElt.offsetWidth) { + left = marginElt.offsetWidth - popoverElt.offsetWidth - 10; + } + popoverElt.style.left = left + 'px'; + 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'); + $(popoverElt.querySelector('.action-add-comment')).click(function(evt) { + var author = utils.getInputTextValue(popoverElt.querySelector('.input-comment-author')); + var content = utils.getInputTextValue(textarea, evt); + if(evt.isPropagationStopped()) { + return; + } + + var fileDiscussions = fileDesc.discussions || {}; + if(!currentDiscussion.discussionIndex) { + do { + currentDiscussion.discussionIndex = utils.randomString(); + } while(_.has(fileDiscussions, currentDiscussion.discussionIndex)); + } + currentDiscussion.push({ + author: author, + content: content + }); + openedPopover.popover('toggle').popover('destroy'); + }); + + // Prevent from closing on click inside the popover + $(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); + + // Focus on textarea + textarea.focus(); + }, 10); + }).on('hide.bs.popover', '#wmd-input > .editor-margin', function(evt) { + $(evt.target).removeClass('active'); + + // 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'); + }); + }; + + return comments; +}); diff --git a/public/res/extensions/scrollLink.js b/public/res/extensions/scrollLink.js index aa16044d..6b0d995b 100644 --- a/public/res/extensions/scrollLink.js +++ b/public/res/extensions/scrollLink.js @@ -195,11 +195,9 @@ define([ var isPreviewVisible = true; function setPreviewHidden() { isPreviewVisible = false; - console.log(isPreviewVisible); } function setPreviewVisible() { isPreviewVisible = true; - console.log(isPreviewVisible); } scrollLink.onLayoutConfigure = function(layoutGlobalConfig) { diff --git a/public/res/html/commentsPopoverContent.html b/public/res/html/commentsPopoverContent.html new file mode 100644 index 00000000..190de8c2 --- /dev/null +++ b/public/res/html/commentsPopoverContent.html @@ -0,0 +1,9 @@ +
+
+ + +
+
+ +
+
diff --git a/public/res/main.js b/public/res/main.js index 342b6f32..1de71861 100644 --- a/public/res/main.js +++ b/public/res/main.js @@ -56,7 +56,9 @@ requirejs.config({ prism: 'bower-libs/prism/prism', 'prism-core': 'bower-libs/prism/components/prism-core', MutationObservers: 'bower-libs/MutationObservers/MutationObserver', - WeakMap: 'bower-libs/WeakMap/weakmap' + WeakMap: 'bower-libs/WeakMap/weakmap', + rangy: 'bower-libs/rangy/rangy-core', + 'rangy-cssclassapplier': 'bower-libs/rangy/rangy-cssclassapplier' }, shim: { underscore: { @@ -71,6 +73,12 @@ requirejs.config({ ], exports: 'jQuery.jGrowl' }, + rangy: { + exports: 'rangy' + }, + 'rangy-cssclassapplier': [ + 'rangy' + ], mousetrap: { exports: 'Mousetrap' }, @@ -184,14 +192,16 @@ if (window.baseDir.indexOf('-min') !== -1) { // RequireJS entry point. By requiring synchronizer, publisher and // media-importer, we are actually loading all the modules -require(["jquery", "core", "eventMgr", "synchronizer", "publisher", "mediaImporter", "css", -themeModule, ], function($, core, eventMgr) { +require(["jquery", "rangy", "core", "eventMgr", "synchronizer", "publisher", "mediaImporter", "css", "rangy-cssclassapplier", +themeModule, ], function($, rangy, core, eventMgr) { if(window.noStart) { return; } $(function() { + rangy.init(); + // Here, all the modules are loaded and the DOM is ready core.onReady(); diff --git a/public/res/styles/main.less b/public/res/styles/main.less index ca76b629..b52424e1 100644 --- a/public/res/styles/main.less +++ b/public/res/styles/main.less @@ -152,7 +152,7 @@ body { .user-select(none); } -.dropdown-menu, .modal-content, .panel-content, .search-bar { +.dropdown-menu, .modal-content, .panel-content, .search-bar .popover { .box-shadow(0 4px 12px rgba(0,0,0,.125)); } @@ -1057,15 +1057,25 @@ a { position: absolute; top: 0; .icon-comment { + .opacity(0.4); position: absolute; - color: fade(@tertiary-color, 12%); + color: fade(@tertiary-color, 25%); cursor: pointer; - &:hover { - color: fade(@tertiary-color, 30%); + &:hover, &.active { + .opacity(1); + color: fade(@tertiary-color, 35%); text-decoration: none; } } } + &.has-selection > .editor-margin .icon-comment { + .opacity(1); + } + + .comment-highlight { + background-color: fade(@label-danger-bg, 25%); + //border-radius: @border-radius-base; + } .code, .pre { @@ -1378,12 +1388,13 @@ input[type="file"] { .popover { max-width: 350px; - padding: 15px; - .box-shadow(0 5px 30px rgba(0,0,0,.175)); + padding: 10px 20px 0; + //.box-shadow(0 5px 30px rgba(0,0,0,.175)); .popover-title { font-weight: @headings-font-weight; font-size: 24px; - padding: 10px 15px; + padding: 5px 0 15px; + border-bottom: 1px solid @hr-border; } .icon-lock { font-size: 38px; @@ -1392,9 +1403,43 @@ input[type="file"] { .disabled { display: none; } + .popover-content { + padding-bottom: 0; + .btn { + padding: 6px 11px; + } + .input-comment-author { + border: none; + background: none; + .box-shadow(none); + font-weight: bold; + height: 32px; + } + } + + .comments-popover & { + &.top, + &.bottom { + .arrow { + margin-right: 1px; + border-right-width: 0; + &:after { + margin-left: -11px; + border-right-width: 0; + } + } + } + &.top .arrow:after { + bottom: 2px; + } + &.bottom .arrow:after { + top: 2px; + } + } } + /******************** * jGrowl ********************/ diff --git a/public/res/themes/original.less b/public/res/themes/original.less index 7560bf00..60ec923c 100644 --- a/public/res/themes/original.less +++ b/public/res/themes/original.less @@ -37,7 +37,7 @@ .ace_cursor { border-left-color: #fff; } -} +} */ @primary: #9d9d9d; @@ -73,7 +73,7 @@ @tertiary-bg: #fcfcfc; -@secondary-bg: #f9f9f9; +@secondary-bg: @tertiary-bg; @secondary-bg-light: #f6f6f6; @secondary-bg-lighter: @tertiary-bg; @secondary-border-color: #e2e2e2; @@ -93,7 +93,7 @@ &:hover, &:focus, &:active, - .open &.dropdown-toggle { + .open &.dropdown-toggle { color: #fff; } } @@ -125,7 +125,7 @@ a.list-group-item, .ace_cursor { border-left-color: #555; } -} +} .ui-layout-resizer-north { background-color: fade(@navbar-default-bg, 75%); @@ -157,4 +157,4 @@ a.list-group-item, background-color: @navbar-default-bg; } } -*/ \ No newline at end of file +*/ diff --git a/public/res/utils.js b/public/res/utils.js index e66334c4..e536753d 100644 --- a/public/res/utils.js +++ b/public/res/utils.js @@ -24,7 +24,7 @@ define([ // Transform a selector into a jQuery object function jqElt(element) { - if(_.isString(element)) { + if(_.isString(element) || !element.val) { return $(element); } return element;