Add comments UI

pull/1/head
yflory 5 years ago
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…
Cancel
Save