Add comments UI
parent
9d03d0effb
commit
31081eac46
|
@ -1,4 +1,5 @@
|
|||
@import (reference) "../../customize/src/less2/include/framework.less";
|
||||
@import (reference) "../../customize/src/less2/include/comments.less";
|
||||
|
||||
body.cp-app-pad {
|
||||
.framework_main(
|
||||
|
@ -71,9 +72,10 @@ body.cp-app-pad {
|
|||
min-width: 0;
|
||||
}
|
||||
#cp-app-pad-comments {
|
||||
width: 400px;
|
||||
background-color: white;
|
||||
width: 300px;
|
||||
//background-color: white;
|
||||
margin: 30px;
|
||||
.comments_main();
|
||||
}
|
||||
&.cke_body_width {
|
||||
iframe {
|
||||
|
|
|
@ -36,14 +36,9 @@
|
|||
childRule: isUnstylable
|
||||
};
|
||||
|
||||
// XXX define default style
|
||||
// XXX we can't uncomment if nothing has been added yet
|
||||
// XXX "styles" is useless because not rebuilt on reload
|
||||
// XXX and one style can remove all the other ones so no need to store all of them?
|
||||
|
||||
// Register the command.
|
||||
var removeStyle = new CKEDITOR.style(styleDef, { 'uid': '' });
|
||||
editor.addCommand(pluginName, {
|
||||
editor.addCommand('comment', {
|
||||
exec: function (editor, data) {
|
||||
if (editor.readOnly) { return; }
|
||||
editor.focus();
|
||||
|
@ -57,19 +52,15 @@
|
|||
|
||||
var uid = CKEDITOR.tools.getUniqueId();
|
||||
editor.plugins.comments.addComment(uid, function () {
|
||||
// XXX call cryptpad code here
|
||||
// Make an undo spnashot
|
||||
editor.fire('saveSnapshot');
|
||||
// Make sure comments won't overlap
|
||||
editor.removeStyle(removeStyle);
|
||||
/*
|
||||
Object.keys(styles).forEach(function (id) {
|
||||
editor.removeStyle(styles[id]);
|
||||
});
|
||||
*/
|
||||
styles[uid] = new CKEDITOR.style(styleDef, { 'uid': uid });
|
||||
editor.applyStyle(styles[uid]);
|
||||
|
||||
//editor.removeStyle(removeStyle); // XXX to remove comment on the selection
|
||||
//editor.plugins.comments.addComment();
|
||||
// Add the comment marker
|
||||
var s = new CKEDITOR.style(styleDef, { 'uid': uid });
|
||||
editor.applyStyle(s);
|
||||
|
||||
// Save the undo snapshot after all changes are affected.
|
||||
setTimeout( function() {
|
||||
editor.fire('saveSnapshot');
|
||||
|
@ -79,13 +70,34 @@
|
|||
}
|
||||
});
|
||||
|
||||
// XXX Uncomment selection, remove on prod, only used for dev
|
||||
editor.addCommand('uncomment', {
|
||||
exec: function (editor, data) {
|
||||
if (editor.readOnly) { return; }
|
||||
editor.focus();
|
||||
editor.fire('saveSnapshot');
|
||||
editor.removeStyle(removeStyle);
|
||||
if (!data || !data.id) {
|
||||
// XXX Uncomment the selection, remove on prod, only used for dev
|
||||
editor.focus();
|
||||
editor.removeStyle(removeStyle);
|
||||
setTimeout( function() {
|
||||
editor.fire('saveSnapshot');
|
||||
}, 0 );
|
||||
return;
|
||||
}
|
||||
// Uncomment provided element
|
||||
|
||||
//Create style for this id
|
||||
var style = new CKEDITOR.style({
|
||||
element: 'comment',
|
||||
attributes: {
|
||||
'data-uid': data.id
|
||||
},
|
||||
});
|
||||
// Create range for the entire document
|
||||
var range = editor.createRange();
|
||||
range.selectNodeContents( editor.document.getBody() );
|
||||
// Remove style for the document
|
||||
style.removeFromRange(range, editor);
|
||||
|
||||
setTimeout( function() {
|
||||
editor.fire('saveSnapshot');
|
||||
}, 0 );
|
||||
|
@ -93,6 +105,7 @@
|
|||
});
|
||||
|
||||
// Register the toolbar button.
|
||||
// XXX Uncomment selection, remove on prod, only used for dev
|
||||
editor.ui.addButton && editor.ui.addButton('UnComment', {
|
||||
label: 'UNCOMMENT',
|
||||
command: 'uncomment',
|
||||
|
|
|
@ -1,16 +1,43 @@
|
|||
define([
|
||||
'json.sortify',
|
||||
'/common/common-util.js',
|
||||
'/common/hyperscript.js',
|
||||
'/common/common-interface.js',
|
||||
'/customize/messages.js'
|
||||
], function (Sortify, Util, UI, Messages) {
|
||||
], function (Sortify, Util, h, UI, Messages) {
|
||||
var Comments = {};
|
||||
|
||||
/*
|
||||
{
|
||||
authors: {
|
||||
"id": {
|
||||
name: "",
|
||||
curvePublic: "",
|
||||
avatar: "",
|
||||
profile: ""
|
||||
}
|
||||
},
|
||||
data: {
|
||||
"uid": {
|
||||
m: [{
|
||||
u: id,
|
||||
m: "str", // comment
|
||||
t: +new Date,
|
||||
v: "str" // value of the commented content
|
||||
}],
|
||||
(deleted: undefined/true,)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
var COMMENTS = {
|
||||
authors: {},
|
||||
messages: {}
|
||||
data: {}
|
||||
};
|
||||
|
||||
var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); };
|
||||
|
||||
// XXX function duplicated from www/code/markers.js
|
||||
var authorUid = function (existing) {
|
||||
if (!Array.isArray(existing)) { existing = []; }
|
||||
|
@ -49,40 +76,353 @@ define([
|
|||
return myAuthorId;
|
||||
};
|
||||
|
||||
var updateMetadata = function (Env) {
|
||||
var md = Util.clone(Env.metadataMgr.getMetadata());
|
||||
md.comments = Util.clone(Env.comments);
|
||||
Env.metadataMgr.updateMetadata(md);
|
||||
};
|
||||
|
||||
Messages.comments_submit = "Submit"; // XXX
|
||||
Messages.comments_reply = "Reply"; // XXX
|
||||
Messages.comments_resolve = "Resolve"; // XXX
|
||||
|
||||
var getCommentForm = function (Env, reply, _cb) {
|
||||
var cb = Util.once(_cb);
|
||||
var userData = Env.metadataMgr.getUserData();
|
||||
var name = Util.fixHTML(userData.name || Messages.anonymous);
|
||||
var avatar = h('span.cp-avatar');
|
||||
var textarea = h('textarea');
|
||||
Env.common.displayAvatar($(avatar), userData.avatar, name);
|
||||
|
||||
var cancel = h('button.btn.btn-cancel', [
|
||||
h('i.fa.fa-times'),
|
||||
Messages.cancel
|
||||
]);
|
||||
var submit = h('button.btn.btn-primary', [
|
||||
h('i.fa.fa-paper-plane-o'),
|
||||
Messages.comments_submit
|
||||
]);
|
||||
|
||||
var done = false;
|
||||
|
||||
$(submit).click(function (e) {
|
||||
e.stopPropagation();
|
||||
cb(textarea.value);
|
||||
});
|
||||
$(cancel).click(function (e) {
|
||||
e.stopPropagation();
|
||||
cb();
|
||||
});
|
||||
|
||||
$(textarea).keydown(function (e) {
|
||||
if (e.which === 27) {
|
||||
$(cancel).click();
|
||||
}
|
||||
if (e.which === 13 && e.ctrlKey) {
|
||||
$(submit).click();
|
||||
}
|
||||
});
|
||||
|
||||
return h('div.cp-comment-form' + (reply ? '.cp-comment-reply' : ''), [
|
||||
h('div.cp-comment-form-input', [
|
||||
avatar,
|
||||
textarea
|
||||
]),
|
||||
h('div.cp-comment-form-actions', [
|
||||
cancel,
|
||||
submit
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
var redrawComments = function (Env) {
|
||||
// Don't redraw if there were no change
|
||||
var str = Sortify(Env.comments || {});
|
||||
if (str === Env.oldComments) { return; }
|
||||
Env.oldComments = str;
|
||||
|
||||
// XXX don't wipe inputs?
|
||||
|
||||
var $oldInput = Env.$container.find('.cp-comment-form');
|
||||
if ($oldInput.length !== 1) { $oldInput = undefined; }
|
||||
|
||||
Env.$container.html('');
|
||||
|
||||
if ($oldInput && !$oldInput.attr('data-uid')) {
|
||||
Env.$container.append($oldInput);
|
||||
}
|
||||
|
||||
var order = Env.$inner.find('comment').map(function (i, el) {
|
||||
return el.getAttribute('data-uid');
|
||||
}).toArray();
|
||||
var done = [];
|
||||
|
||||
|
||||
var show = false;
|
||||
order.forEach(function (key) {
|
||||
// Avoir duplicates
|
||||
if (done.indexOf(key) !== -1) { return; }
|
||||
done.push(key);
|
||||
|
||||
var obj = Env.comments.data[key];
|
||||
if (!obj || obj.deleted || !Array.isArray(obj.m) || !obj.m.length) {
|
||||
return;
|
||||
}
|
||||
show = true;
|
||||
|
||||
var content = [];
|
||||
obj.m.forEach(function (msg, i) {
|
||||
var author = (Env.comments.authors || {})[msg.u] || {};
|
||||
var name = Util.fixHTML(author.name || Messages.anonymous);
|
||||
var date = new Date(msg.t);
|
||||
var avatar = h('span.cp-avatar');
|
||||
Env.common.displayAvatar($(avatar), author.avatar, name);
|
||||
|
||||
content.push(h('div.cp-comment'+(i === 0 ? '' : '.cp-comment-reply'), [
|
||||
h('div.cp-comment-header', [
|
||||
avatar,
|
||||
h('span.cp-comment-metadata', [
|
||||
h('span.cp-comment-author', name),
|
||||
h('span.cp-comment-time', date.toLocaleString())
|
||||
])
|
||||
]),
|
||||
h('div.cp-comment-content', [
|
||||
msg.m
|
||||
])
|
||||
]));
|
||||
|
||||
});
|
||||
|
||||
var reply = h('button.btn.btn-secondary', [
|
||||
h('i.fa.fa-reply'),
|
||||
Messages.comments_reply
|
||||
]);
|
||||
var resolve = h('button.btn.btn-primary', [
|
||||
h('i.fa.fa-check'),
|
||||
Messages.comments_resolve
|
||||
]);
|
||||
|
||||
var actions;
|
||||
content.push(actions = h('div.cp-comment-actions', [
|
||||
reply,
|
||||
resolve
|
||||
]));
|
||||
var $actions = $(actions);
|
||||
|
||||
var div;
|
||||
Env.$container.append(div = h('div.cp-comment-container', {
|
||||
'data-uid': key,
|
||||
tabindex: 1
|
||||
}, content));
|
||||
var $div = $(div);
|
||||
|
||||
$(reply).click(function (e) {
|
||||
e.stopPropagation();
|
||||
$actions.hide();
|
||||
var form = getCommentForm(Env, true, function (val) {
|
||||
$(form).remove();
|
||||
$actions.css('display', '');
|
||||
if (!val) { return; }
|
||||
var obj = Env.comments.data[key];
|
||||
if (!obj || !Array.isArray(obj.m)) { return; }
|
||||
|
||||
// Get the value of the commented text
|
||||
var res = Env.$inner.find('comment[data-uid="'+key+'"]').toArray();
|
||||
var value = res.map(function (el) {
|
||||
return el.innerText;
|
||||
}).join('\n');
|
||||
|
||||
// Push the reply
|
||||
var myId = updateAuthorData(Env);
|
||||
obj.m.push({
|
||||
u: myId,
|
||||
t: +new Date(),
|
||||
m: val,
|
||||
v: value
|
||||
});
|
||||
|
||||
// Send to chainpad
|
||||
updateMetadata(Env);
|
||||
Env.framework.localChange();
|
||||
});
|
||||
$div.append(form);
|
||||
});
|
||||
|
||||
UI.confirmButton(resolve, {
|
||||
classes: 'btn-danger-alt'
|
||||
}, function () {
|
||||
// Delete the comment
|
||||
delete Env.comments.data[key];
|
||||
|
||||
// Send to chainpad
|
||||
updateMetadata(Env);
|
||||
Env.framework.localChange();
|
||||
});
|
||||
|
||||
$div.click(function () {
|
||||
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
|
||||
$div.addClass('cp-comment-active');
|
||||
$actions.css('display', '');
|
||||
Env.$container.find('.cp-comment-form').remove();
|
||||
// XXX highlight (and scroll to) the comment in the doc?
|
||||
});
|
||||
});
|
||||
|
||||
if (show) {
|
||||
Env.$container.show();
|
||||
} else {
|
||||
Env.$container.hide();
|
||||
}
|
||||
};
|
||||
|
||||
var onChange = function (Env) {
|
||||
var md = Util.clone(Env.metadataMgr.getMetadata());
|
||||
Env.comments = md.comments;
|
||||
if (!Env.comments) { Env.comments = Util.clone(COMMENTS); }
|
||||
if (!Env.comments || !Env.comments.data) { Env.comments = Util.clone(COMMENTS); }
|
||||
if (Env.ready === 0) {
|
||||
Env.ready = true;
|
||||
}
|
||||
redrawComments(Env);
|
||||
};
|
||||
|
||||
Comments.create = function (cfg) {
|
||||
var Env = cfg;
|
||||
Env.comments = Util.clone(COMMENTS);
|
||||
// Check if comments have been deleted from the document but not from metadata
|
||||
var checkDeleted = function (Env) {
|
||||
if (!Env.comments || !Env.comments.data) { return; }
|
||||
|
||||
// If there is no comment stored in the metadata, abort
|
||||
var comments = Object.keys(Env.comments.data || {}).filter(function (id) {
|
||||
return !Env.comments.data[id].deleted;
|
||||
});
|
||||
|
||||
var changed = false;
|
||||
|
||||
// Get the comments from the document
|
||||
var uids = Env.$inner.find('comment').map(function (i, el) {
|
||||
var id = el.getAttribute('data-uid');
|
||||
// Empty comment: remove from dom
|
||||
if (!el.innerText && el.parentElement) {
|
||||
el.parentElement.removeChild(el);
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
// Comment not in the metadata: uncomment (probably an undo)
|
||||
if (comments.indexOf(id) === -1) {
|
||||
console.error(id, el);
|
||||
Env.editor.execCommand('uncomment', {id:id});
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
return id;
|
||||
}).toArray();
|
||||
|
||||
// Check if a comment has been deleted
|
||||
comments.forEach(function (uid) {
|
||||
if (uids.indexOf(uid) !== -1) { return; }
|
||||
// comment has been deleted
|
||||
var data = Env.comments.data[uid];
|
||||
if (!data) { return; }
|
||||
//data.deleted = true;
|
||||
delete Env.comments.data[uid];
|
||||
changed = true;
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
updateMetadata(Env);
|
||||
}
|
||||
};
|
||||
|
||||
var addAddCommentHandler = function (Env) {
|
||||
Env.editor.plugins.comments.addComment = function (uid, addMark) {
|
||||
if (!Env.comments) { Env.comments = Util.clone(COMMENTS); }
|
||||
|
||||
UI.prompt("Message", "", function (val) { // XXX
|
||||
// Get all comments ID contained within the selection
|
||||
var sel = Env.editor.getSelectedHtml().$.querySelectorAll('comment');
|
||||
if (sel.length) {
|
||||
// Abort if our selection contains a comment
|
||||
console.error("Your selection contains a comment");
|
||||
UI.warn(Messages.error);
|
||||
// XXX show error
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
sel.forEach(function (el) {
|
||||
// For each comment ID, check if the comment will be deleted
|
||||
// if we add a comment on our selection
|
||||
var id = el.getAttribute('data-uid');
|
||||
|
||||
// Get all nodes for this comment
|
||||
var all = Env.$inner.find('comment[data-uid="'+id+'"]');
|
||||
// Get our selection
|
||||
var sel = Env.ifrWindow.getSelection();
|
||||
if (!sel.containsNode) {
|
||||
// IE doesn't support this method, always allow comments for them...
|
||||
sel.containsNode = function () { return false; };
|
||||
}
|
||||
|
||||
var notDeleted = all.some(function (i, el) {
|
||||
// If this node is completely outside of the selection, continue
|
||||
if (!sel.containsNode(el, true)) { return true; }
|
||||
});
|
||||
|
||||
// only continue if notDeleted is true (at least one node for
|
||||
// this comment won't be deleted)
|
||||
});
|
||||
*/
|
||||
Env.$container.find('.cp-comment-form').remove();
|
||||
var form = getCommentForm(Env, false, function (val) {
|
||||
$(form).remove();
|
||||
if (!val) { return; }
|
||||
if (!editor.getSelection().getSelectedText()) {
|
||||
// text has been deleted by another user while we were typing our comment?
|
||||
return void UI.warn(Messages.error);
|
||||
}
|
||||
var myId = updateAuthorData(Env);
|
||||
Env.comments.messages[uid] = {
|
||||
user: myId,
|
||||
time: +new Date(),
|
||||
message: val
|
||||
Env.comments.data[uid] = {
|
||||
m: [{
|
||||
u: myId,
|
||||
t: +new Date(),
|
||||
m: val,
|
||||
v: canonicalize(editor.getSelection().getSelectedText())
|
||||
}]
|
||||
};
|
||||
var md = Util.clone(Env.metadataMgr.getMetadata());
|
||||
md.comments = Util.clone(Env.comments);
|
||||
Env.metadataMgr.updateMetadata(md);
|
||||
updateMetadata(Env);
|
||||
|
||||
addMark();
|
||||
|
||||
Env.framework.localChange();
|
||||
});
|
||||
Env.$container.prepend(form);
|
||||
};
|
||||
};
|
||||
|
||||
var onContentUpdate = function (Env) {
|
||||
if (!Env.ready) { return; }
|
||||
// Check deleted
|
||||
checkDeleted(Env);
|
||||
};
|
||||
var localChange = function (Env) {
|
||||
if (!Env.ready) { return; }
|
||||
// Check deleted
|
||||
checkDeleted(Env);
|
||||
};
|
||||
|
||||
var ready = function (Env) {
|
||||
Env.ready = 0;
|
||||
};
|
||||
|
||||
Comments.create = function (cfg) {
|
||||
var Env = cfg;
|
||||
Env.comments = Util.clone(COMMENTS);
|
||||
|
||||
addAddCommentHandler(Env);
|
||||
|
||||
$(window).click(function (e) {
|
||||
if ($(e.target).closest('.cp-comment-container').length) {
|
||||
return;
|
||||
}
|
||||
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
|
||||
});
|
||||
|
||||
var call = function (f) {
|
||||
return function () {
|
||||
|
@ -98,6 +438,9 @@ define([
|
|||
Env.metadataMgr.onChange(call(onChange));
|
||||
|
||||
return {
|
||||
onContentUpdate: call(onContentUpdate),
|
||||
localChange: call(localChange),
|
||||
ready: call(ready)
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -494,7 +494,9 @@ define([
|
|||
metadataMgr: metadataMgr,
|
||||
common: common,
|
||||
editor: editor,
|
||||
container: $('#cp-app-pad-comments')[0]
|
||||
ifrWindow: ifrWindow,
|
||||
$inner: $inner,
|
||||
$container: $('#cp-app-pad-comments')
|
||||
});
|
||||
|
||||
var onLinkClicked = function (e) {
|
||||
|
@ -662,11 +664,7 @@ define([
|
|||
$links.off('click', openLink).on('click', openLink);
|
||||
}
|
||||
|
||||
// XXX check comments
|
||||
// new comments
|
||||
// deleted comments
|
||||
// check comment authors too
|
||||
|
||||
comments.onContentUpdate();
|
||||
});
|
||||
|
||||
framework.setTextContentGetter(function () {
|
||||
|
@ -689,6 +687,8 @@ define([
|
|||
// the text nodes and OT/ChainPad would freak out
|
||||
cursors.removeCursors(inner);
|
||||
|
||||
comments.onContentUpdate();
|
||||
|
||||
displayMediaTags(framework, inner, mediaTagMap);
|
||||
inner.normalize();
|
||||
var hjson = Hyperjson.fromDOM(inner, shouldSerialize, hjsonFilters);
|
||||
|
@ -805,6 +805,9 @@ define([
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
comments.ready();
|
||||
|
||||
/*setTimeout(function () {
|
||||
$('iframe.cke_wysiwyg_frame').focus();
|
||||
editor.focus();
|
||||
|
|
Loading…
Reference in New Issue