New comments extension

This commit is contained in:
benweet 2014-03-22 01:57:31 +00:00
parent 8ee8cbaaae
commit 9039cac7f5
9 changed files with 338 additions and 106 deletions

View File

@ -25,6 +25,7 @@
"yaml.js": "https://github.com/jeremyfa/yaml.js.git#~0.1.4", "yaml.js": "https://github.com/jeremyfa/yaml.js.git#~0.1.4",
"stackedit-pagedown": "https://github.com/benweet/stackedit-pagedown.git#d81b3689a99c84832d8885f607e6de860fb7d94a", "stackedit-pagedown": "https://github.com/benweet/stackedit-pagedown.git#d81b3689a99c84832d8885f607e6de860fb7d94a",
"prism": "gh-pages", "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"
} }
} }

View File

@ -62,7 +62,9 @@ define([
}); });
function saveEditorState() { function saveEditorState() {
setTimeout if(!inputElt.focused) {
return;
}
selectionStart = inputElt.selectionStart; selectionStart = inputElt.selectionStart;
selectionEnd = inputElt.selectionEnd; selectionEnd = inputElt.selectionEnd;
scrollTop = inputElt.scrollTop; scrollTop = inputElt.scrollTop;
@ -106,6 +108,7 @@ define([
var cursorY = 0; var cursorY = 0;
function saveCursorCoordinates() { function saveCursorCoordinates() {
saveEditorState(); saveEditorState();
$inputElt.toggleClass('has-selection', selectionStart !== selectionEnd);
var backwards = false; var backwards = false;
var selection = window.getSelection(); var selection = window.getSelection();
@ -129,10 +132,10 @@ define([
var cursorOffset = backwards ? selectionStart : selectionEnd; var cursorOffset = backwards ? selectionStart : selectionEnd;
var selectedChar = inputElt.textContent[cursorOffset]; var selectedChar = inputElt.textContent[cursorOffset];
if(selectedChar === undefined || selectedChar == '\n') { if(selectedChar === undefined || selectedChar == '\n') {
selectionRange = createRange(cursorOffset - 1, cursorOffset); selectionRange = inputElt.createRange(cursorOffset - 1, cursorOffset);
} }
else { else {
selectionRange = createRange(cursorOffset, cursorOffset + 1); selectionRange = inputElt.createRange(cursorOffset, cursorOffset + 1);
} }
var selectionRect = selectionRange.getBoundingClientRect(); var selectionRect = selectionRange.getBoundingClientRect();
cursorY = selectionRect.top + selectionRect.height / 2 - inputElt.offsetTop + inputElt.scrollTop; cursorY = selectionRect.top + selectionRect.height / 2 - inputElt.offsetTop + inputElt.scrollTop;
@ -195,7 +198,7 @@ define([
inputElt.focus = function() { inputElt.focus = function() {
editor.$contentElt.focus(); editor.$contentElt.focus();
this.setSelectionRange(selectionStart, selectionEnd); this.setSelectionStartEnd(selectionStart, selectionEnd);
inputElt.scrollTop = scrollTop; inputElt.scrollTop = scrollTop;
}; };
editor.$contentElt.focus(function() { editor.$contentElt.focus(function() {
@ -234,7 +237,7 @@ define([
var replacementText = value.substring(startIndex, value.length - endIndex + 1); var replacementText = value.substring(startIndex, value.length - endIndex + 1);
endIndex = currentValue.length - endIndex + 1; endIndex = currentValue.length - endIndex + 1;
var range = createRange(startIndex, endIndex); var range = inputElt.createRange(startIndex, endIndex);
range.deleteContents(); range.deleteContents();
range.insertNode(document.createTextNode(replacementText)); range.insertNode(document.createTextNode(replacementText));
} }
@ -270,7 +273,7 @@ define([
} }
}, },
set: function (value) { set: function (value) {
inputElt.setSelectionRange(value, selectionEnd); inputElt.setSelectionStartEnd(value, selectionEnd);
}, },
enumerable: true, enumerable: true,
@ -288,23 +291,103 @@ define([
} }
}, },
set: function (value) { set: function (value) {
inputElt.setSelectionRange(selectionStart, value); inputElt.setSelectionStartEnd(selectionStart, value);
}, },
enumerable: true, enumerable: true,
configurable: true configurable: true
}); });
inputElt.setSelectionRange = function (ss, se) { inputElt.setSelectionStartEnd = function (ss, se) {
selectionStart = ss; selectionStart = ss;
selectionEnd = se; selectionEnd = se;
var range = createRange(ss, se); var range = inputElt.createRange(ss, se);
var selection = window.getSelection(); var selection = window.getSelection();
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); 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; var clearNewline = false;
editor.$contentElt.on('keydown', function (evt) { editor.$contentElt.on('keydown', function (evt) {
if( if(
@ -338,7 +421,11 @@ define([
clearNewline = false; clearNewline = false;
} }
}) })
.on('mouseup', saveCursorCoordinates) .on('mouseup', function() {
setTimeout(function() {
saveCursorCoordinates();
}, 0);
})
.on('paste', function () { .on('paste', function () {
pagedownEditor.undoManager.setMode("paste"); pagedownEditor.undoManager.setMode("paste");
adjustCursorPosition(); adjustCursorPosition();
@ -364,7 +451,7 @@ define([
actions[action](state, options); actions[action](state, options);
inputElt.value = state.before + state.selection + state.after; inputElt.value = state.before + state.selection + state.after;
inputElt.setSelectionRange(state.ss, state.se); inputElt.setSelectionStartEnd(state.ss, state.se);
$inputElt.trigger('input'); $inputElt.trigger('input');
}; };
@ -508,7 +595,7 @@ define([
if(fileChanged === true) { if(fileChanged === true) {
editor.contentElt.innerHTML = ''; editor.contentElt.innerHTML = '';
editor.contentElt.appendChild(newSectionEltList); editor.contentElt.appendChild(newSectionEltList);
inputElt.setSelectionRange(selectionStart, selectionEnd); inputElt.setSelectionStartEnd(selectionStart, selectionEnd);
} }
else { else {
// Remove outdated sections // Remove outdated sections
@ -536,7 +623,7 @@ define([
childNode = nextNode; childNode = nextNode;
} }
inputElt.setSelectionRange(selectionStart, selectionEnd); inputElt.setSelectionStartEnd(selectionStart, selectionEnd);
} }
} }
@ -559,79 +646,5 @@ define([
section.highlightedContent = sectionElt; 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; return editor;
}); });

View File

@ -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($('<div class="comments-popover">')).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, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
},
content: function() {
var content = _.template(commentsPopoverContentHTML, {
});
return content;
},
selector: '#wmd-input > .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;
});

View File

@ -195,11 +195,9 @@ define([
var isPreviewVisible = true; var isPreviewVisible = true;
function setPreviewHidden() { function setPreviewHidden() {
isPreviewVisible = false; isPreviewVisible = false;
console.log(isPreviewVisible);
} }
function setPreviewVisible() { function setPreviewVisible() {
isPreviewVisible = true; isPreviewVisible = true;
console.log(isPreviewVisible);
} }
scrollLink.onLayoutConfigure = function(layoutGlobalConfig) { scrollLink.onLayoutConfigure = function(layoutGlobalConfig) {

View File

@ -0,0 +1,9 @@
<div class="form-horizontal">
<div class="form-group">
<input class="form-control input-comment-author" placeholder="Anonymous"></input>
<textarea class="form-control input-comment-content"></textarea>
</div>
<div class="form-group text-right">
<button class="btn btn-primary action-add-comment">Add</button>
</div>
</div>

View File

@ -56,7 +56,9 @@ requirejs.config({
prism: 'bower-libs/prism/prism', prism: 'bower-libs/prism/prism',
'prism-core': 'bower-libs/prism/components/prism-core', 'prism-core': 'bower-libs/prism/components/prism-core',
MutationObservers: 'bower-libs/MutationObservers/MutationObserver', 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: { shim: {
underscore: { underscore: {
@ -71,6 +73,12 @@ requirejs.config({
], ],
exports: 'jQuery.jGrowl' exports: 'jQuery.jGrowl'
}, },
rangy: {
exports: 'rangy'
},
'rangy-cssclassapplier': [
'rangy'
],
mousetrap: { mousetrap: {
exports: 'Mousetrap' exports: 'Mousetrap'
}, },
@ -184,14 +192,16 @@ if (window.baseDir.indexOf('-min') !== -1) {
// RequireJS entry point. By requiring synchronizer, publisher and // RequireJS entry point. By requiring synchronizer, publisher and
// media-importer, we are actually loading all the modules // media-importer, we are actually loading all the modules
require(["jquery", "core", "eventMgr", "synchronizer", "publisher", "mediaImporter", "css", require(["jquery", "rangy", "core", "eventMgr", "synchronizer", "publisher", "mediaImporter", "css", "rangy-cssclassapplier",
themeModule, ], function($, core, eventMgr) { themeModule, ], function($, rangy, core, eventMgr) {
if(window.noStart) { if(window.noStart) {
return; return;
} }
$(function() { $(function() {
rangy.init();
// Here, all the modules are loaded and the DOM is ready // Here, all the modules are loaded and the DOM is ready
core.onReady(); core.onReady();

View File

@ -152,7 +152,7 @@ body {
.user-select(none); .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)); .box-shadow(0 4px 12px rgba(0,0,0,.125));
} }
@ -1057,15 +1057,25 @@ a {
position: absolute; position: absolute;
top: 0; top: 0;
.icon-comment { .icon-comment {
.opacity(0.4);
position: absolute; position: absolute;
color: fade(@tertiary-color, 12%); color: fade(@tertiary-color, 25%);
cursor: pointer; cursor: pointer;
&:hover { &:hover, &.active {
color: fade(@tertiary-color, 30%); .opacity(1);
color: fade(@tertiary-color, 35%);
text-decoration: none; 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, .code,
.pre { .pre {
@ -1378,12 +1388,13 @@ input[type="file"] {
.popover { .popover {
max-width: 350px; max-width: 350px;
padding: 15px; padding: 10px 20px 0;
.box-shadow(0 5px 30px rgba(0,0,0,.175)); //.box-shadow(0 5px 30px rgba(0,0,0,.175));
.popover-title { .popover-title {
font-weight: @headings-font-weight; font-weight: @headings-font-weight;
font-size: 24px; font-size: 24px;
padding: 10px 15px; padding: 5px 0 15px;
border-bottom: 1px solid @hr-border;
} }
.icon-lock { .icon-lock {
font-size: 38px; font-size: 38px;
@ -1392,7 +1403,41 @@ input[type="file"] {
.disabled { .disabled {
display: none; 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;
}
}
}
/******************** /********************

View File

@ -73,7 +73,7 @@
@tertiary-bg: #fcfcfc; @tertiary-bg: #fcfcfc;
@secondary-bg: #f9f9f9; @secondary-bg: @tertiary-bg;
@secondary-bg-light: #f6f6f6; @secondary-bg-light: #f6f6f6;
@secondary-bg-lighter: @tertiary-bg; @secondary-bg-lighter: @tertiary-bg;
@secondary-border-color: #e2e2e2; @secondary-border-color: #e2e2e2;

View File

@ -24,7 +24,7 @@ define([
// Transform a selector into a jQuery object // Transform a selector into a jQuery object
function jqElt(element) { function jqElt(element) {
if(_.isString(element)) { if(_.isString(element) || !element.val) {
return $(element); return $(element);
} }
return element; return element;