cryptpad/www/pad/comments.js

870 lines
31 KiB
JavaScript
Raw Normal View History

2020-04-20 13:22:45 +00:00
define([
'jquery',
2020-04-20 13:22:45 +00:00
'json.sortify',
'/common/common-util.js',
2020-04-24 15:53:33 +00:00
'/common/common-hash.js',
2020-04-22 14:23:45 +00:00
'/common/hyperscript.js',
2020-04-20 13:22:45 +00:00
'/common/common-interface.js',
'/customize/messages.js'
2020-04-24 15:53:33 +00:00
], function ($, Sortify, Util, Hash, h, UI, Messages) {
2020-04-20 13:22:45 +00:00
var Comments = {};
2020-04-22 14:23:45 +00:00
/*
{
authors: {
"id": {
name: "",
curvePublic: "",
avatar: "",
profile: ""
}
},
data: {
"uid": {
m: [{
u: id,
m: "str", // comment
t: +new Date,
2020-04-29 14:45:42 +00:00
v: "str", // value of the commented content
e: undefined/1, // edited
d: undefined/1, // deleted
2020-04-22 14:23:45 +00:00
}],
2020-04-29 14:45:42 +00:00
d: undefined/1,
2020-04-22 14:23:45 +00:00
}
}
}
*/
2020-04-20 13:22:45 +00:00
var COMMENTS = {
authors: {},
2020-04-22 14:23:45 +00:00
data: {}
2020-04-20 13:22:45 +00:00
};
2020-04-22 14:23:45 +00:00
var canonicalize = function (t) { return t.replace(/\r\n/g, '\n'); };
2020-04-20 13:22:45 +00:00
// XXX function duplicated from www/code/markers.js
var authorUid = function (existing) {
if (!Array.isArray(existing)) { existing = []; }
var n;
var i = 0;
while (!n || existing.indexOf(n) !== -1 && i++ < 1000) {
n = Math.floor(Math.random() * 1000000);
}
// If we can't find a valid number in 1000 iterations, use 0...
if (existing.indexOf(n) !== -1) { n = 0; }
return n;
};
var getAuthorId = function (Env, curve) {
var existing = Object.keys(Env.comments.authors || {}).map(Number);
if (!Env.common.isLoggedIn()) { return authorUid(existing); }
var uid;
existing.some(function (id) {
var author = Env.comments.authors[id] || {};
if (author.curvePublic !== curve) { return; }
2020-04-20 13:22:45 +00:00
uid = Number(id);
return true;
});
return uid || authorUid(existing);
};
// Return the author ID and add/update the data for registered users
// Return the username for unregistered users
var updateAuthorData = function (Env, onChange) {
2020-04-20 13:22:45 +00:00
var userData = Env.metadataMgr.getUserData();
if (!Env.common.isLoggedIn()) {
return userData.name;
}
2020-04-20 13:22:45 +00:00
var myAuthorId = getAuthorId(Env, userData.curvePublic);
var data = Env.comments.authors[myAuthorId] = Env.comments.authors[myAuthorId] || {};
var old = Sortify(data);
2020-04-20 13:22:45 +00:00
data.name = userData.name;
data.avatar = userData.avatar;
data.profile = userData.profile;
data.curvePublic = userData.curvePublic;
data.notifications = userData.notifications;
if (typeof(onChange) === "function" && Sortify(data) !== old) {
onChange();
}
2020-04-20 13:22:45 +00:00
return myAuthorId;
};
2020-04-22 14:23:45 +00:00
var updateMetadata = function (Env) {
var md = Util.clone(Env.metadataMgr.getMetadata());
md.comments = Util.clone(Env.comments);
Env.metadataMgr.updateMetadata(md);
};
var sendReplyNotification = function (Env, uid) {
if (!Env.comments || !Env.comments.data || !Env.comments.authors) { return; }
if (!Env.common.isLoggedIn()) { return; }
var thread = Env.comments.data[uid];
if (!thread || !Array.isArray(thread.m)) { return; }
var userData = Env.metadataMgr.getUserData();
var privateData = Env.metadataMgr.getPrivateData();
var others = {};
// Get all the other registered users with a mailbox
thread.m.forEach(function (obj) {
var u = obj.u;
if (typeof(u) !== "number") { return; }
var author = Env.comments.authors[u];
if (!author || others[u] || !author.notifications || !author.curvePublic) { return; }
if (author.curvePublic === userData.curvePublic) { return; } // don't send to yourself
others[u] = {
curvePublic: author.curvePublic,
comment: obj.m,
content: obj.v,
notifications: author.notifications
};
});
// Send the notification
Object.keys(others).forEach(function (id) {
var data = others[id];
Env.common.mailbox.sendTo("COMMENT_REPLY", {
channel: privateData.channel,
comment: data.comment,
content: data.content
}, {
channel: data.notifications,
curvePublic: data.curvePublic
});
});
};
2020-04-29 14:45:42 +00:00
var cleanMentions = function ($el) {
2020-04-28 13:54:12 +00:00
$el.html('');
var el = $el[0];
2020-04-29 14:45:42 +00:00
var allowed = ['data-profile', 'data-name', 'data-avatar', 'class'];
2020-04-28 13:54:12 +00:00
// Remove unnecessary/unsafe attributes
for (var i=el.attributes.length-1; i>0; i--) {
var name = el.attributes[i] && el.attributes[i].name;
if (allowed.indexOf(name) === -1) {
$el.removeAttr(name);
}
}
};
2020-04-29 14:45:42 +00:00
Messages.comments_deleted = "Comment deleted by its author"; // XXX
Messages.comments_edited = "Edited"; // XXX
2020-04-22 14:23:45 +00:00
Messages.comments_submit = "Submit"; // XXX
Messages.comments_reply = "Reply"; // XXX
Messages.comments_resolve = "Resolve"; // XXX
2020-04-29 14:45:42 +00:00
var getCommentForm = function (Env, reply, _cb, editContent) {
2020-04-22 14:23:45 +00:00
var cb = Util.once(_cb);
var userData = Env.metadataMgr.getUserData();
var name = Util.fixHTML(userData.name || Messages.anonymous);
var avatar = h('span.cp-avatar');
2020-04-28 13:54:12 +00:00
var textarea = h('div.cp-textarea', {
tabindex: 1,
role: 'textbox',
'aria-multiline': true,
'aria-labelledby': 'cp-comments-label',
'aria-required': true,
contenteditable: true,
2020-04-23 15:42:28 +00:00
});
2020-04-22 14:23:45 +00:00
Env.common.displayAvatar($(avatar), userData.avatar, name);
2020-04-23 15:42:28 +00:00
var cancel = h('button.btn.btn-cancel', {
tabindex: 1
}, [
2020-04-22 14:23:45 +00:00
h('i.fa.fa-times'),
Messages.cancel
]);
2020-04-23 15:42:28 +00:00
var submit = h('button.btn.btn-primary', {
tabindex: 1
}, [
2020-04-22 14:23:45 +00:00
h('i.fa.fa-paper-plane-o'),
Messages.comments_submit
]);
2020-04-28 13:54:12 +00:00
// List of allowed attributes in mentions
2020-04-22 14:23:45 +00:00
$(submit).click(function (e) {
e.stopPropagation();
2020-04-28 13:54:12 +00:00
var clone = textarea.cloneNode(true);
var notify = {};
var $clone = $(clone);
$clone.find('span.cp-mentions').each(function (i, el) {
var $el = $(el);
var curve = $el.attr('data-curve');
var notif = $el.attr('data-notifications');
cleanMentions($el, true);
if (!curve || !notif) { return; }
notify[curve] = notif;
});
2020-04-29 14:54:02 +00:00
$clone.find('br').replaceWith("\n");
2020-04-28 13:54:12 +00:00
$clone.find('> *:not(.cp-mentions)').remove();
var content = clone.innerHTML.trim();
if (!content) { return; }
// Send notification
var privateData = Env.metadataMgr.getPrivateData();
var userData = Env.metadataMgr.getUserData();
2020-04-28 13:54:12 +00:00
Object.keys(notify).forEach(function (curve) {
if (curve === userData.curvePublic) { return; }
2020-04-28 13:54:12 +00:00
Env.common.mailbox.sendTo("MENTION", {
channel: privateData.channel,
}, {
channel: notify[curve],
curvePublic: curve
});
});
// Push the content
cb(content);
2020-04-22 14:23:45 +00:00
});
$(cancel).click(function (e) {
e.stopPropagation();
cb();
});
2020-04-28 13:54:12 +00:00
var $text = $(textarea).keydown(function (e) {
e.stopPropagation();
2020-04-22 14:23:45 +00:00
if (e.which === 27) {
$(cancel).click();
2020-04-29 14:45:42 +00:00
e.stopImmediatePropagation();
2020-04-22 14:23:45 +00:00
}
if (e.which === 13 && !e.shiftKey) {
2020-04-28 13:54:12 +00:00
// Submit form on Enter is the autocompelte menu is not visible
try {
var visible = $text.autocomplete("instance").menu.activeMenu.is(':visible');
if (visible) { return; }
2020-04-28 16:16:21 +00:00
} catch (err) {}
2020-04-22 14:23:45 +00:00
$(submit).click();
2020-04-29 14:45:42 +00:00
e.stopImmediatePropagation();
e.preventDefault();
2020-04-22 14:23:45 +00:00
}
2020-04-28 13:54:12 +00:00
}).click(function (e) {
e.stopPropagation();
2020-04-22 14:23:45 +00:00
});
2020-04-28 13:54:12 +00:00
if (Env.common.isLoggedIn()) {
var authors = {};
Object.keys((Env.comments && Env.comments.authors) || {}).forEach(function (id) {
var obj = Util.clone(Env.comments.authors[id]);
authors[obj.curvePublic] = obj;
});
Env.common.addMentions({
$input: $text,
contenteditable: true,
type: 'contacts',
sources: authors
});
}
2020-04-29 14:45:42 +00:00
var deleteButton;
// Edit? start with the old content
// Add a space to make sure we won't end with a mention and a bad cursor
if (editContent) {
textarea.innerHTML = editContent + " ";
deleteButton = h('button.btn.btn-danger', {
tabindex: 1
}, [
h('i.fa.fa-times'),
Messages.kanban_delete
]);
$(deleteButton).click(function (e) {
e.stopPropagation();
cb();
});
}
2020-04-28 13:54:12 +00:00
setTimeout(function () {
$(textarea).focus();
});
return h('div.cp-comment-form' + (reply ? '.cp-comment-reply' : ''), {
'data-uid': reply || undefined
}, [
2020-04-22 14:23:45 +00:00
h('div.cp-comment-form-input', [
avatar,
textarea
]),
h('div.cp-comment-form-actions', [
cancel,
2020-04-29 14:45:42 +00:00
deleteButton,
2020-04-22 14:23:45 +00:00
submit
])
]);
};
var redrawComments = function (Env) {
// Don't redraw if there were no change
var str = Sortify(Env.comments || {});
2020-04-22 14:23:45 +00:00
if (str === Env.oldComments) { return; }
Env.oldComments = str;
2020-04-29 14:45:42 +00:00
// Store existing input form in memory
var $oldInput = Env.$container.find('.cp-comment-form').detach();
2020-04-22 14:23:45 +00:00
if ($oldInput.length !== 1) { $oldInput = undefined; }
2020-04-29 14:45:42 +00:00
// Remove everything
2020-04-22 14:23:45 +00:00
Env.$container.html('');
2020-04-29 14:45:42 +00:00
// "show" tells us if we need to display the "comments" column or not
var show = false;
// Add invisible label for accessibility tools
2020-04-28 13:54:12 +00:00
var label = h('label#cp-comments-label', Messages.comments_comment);
Env.$container.append(label);
2020-04-29 14:45:42 +00:00
// If we were adding a new comment, redraw our form
2020-04-22 14:23:45 +00:00
if ($oldInput && !$oldInput.attr('data-uid')) {
show = true;
2020-04-22 14:23:45 +00:00
Env.$container.append($oldInput);
}
2020-04-29 14:45:42 +00:00
var userData = Env.metadataMgr.getUserData();
// Get all the comment threads in their order in the pad
var threads = Env.$inner.find('comment').map(function (i, el) {
2020-04-22 14:23:45 +00:00
return el.getAttribute('data-uid');
}).toArray();
2020-04-29 14:45:42 +00:00
// Draw all comment threads
Util.deduplicateString(threads).forEach(function (key) {
// Get thread data
2020-04-22 14:23:45 +00:00
var obj = Env.comments.data[key];
2020-04-29 14:45:42 +00:00
if (!obj || obj.d || !Array.isArray(obj.m) || !obj.m.length) {
2020-04-22 14:23:45 +00:00
return;
}
2020-04-29 14:45:42 +00:00
// If at least one thread is visible, display the "comments" column
2020-04-22 14:23:45 +00:00
show = true;
var content = [];
2020-04-29 14:45:42 +00:00
var $div;
var $actions;
// Draw all messages for this thread
(obj.m || []).forEach(function (msg, i) {
var replyCls = i === 0 ? '' : '.cp-comment-reply';
if (msg.d) {
content.push(h('div.cp-comment.cp-comment-deleted'+replyCls,
Messages.comments_deleted));
return;
}
var author = typeof(msg.u) === "number" ?
((Env.comments.authors || {})[msg.u] || {}) :
{ name: msg.u };
2020-04-22 14:23:45 +00:00
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);
2020-04-24 15:53:33 +00:00
if (author.profile) {
$(avatar).click(function (e) {
Env.common.openURL(Hash.hashToHref(author.profile, 'profile'));
e.stopPropagation();
});
}
2020-04-22 14:23:45 +00:00
2020-04-28 13:54:12 +00:00
// Build sanitized html with mentions
var m = h('div.cp-comment-content');
m.innerHTML = msg.m;
var $m = $(m);
$m.find('> *:not(span.cp-mentions)').remove();
$m.find('span.cp-mentions').each(function (i, el) {
var $el = $(el);
var name = $el.attr('data-name');
var avatarUrl = $el.attr('data-avatar');
var profile = $el.attr('data-profile');
2020-04-28 16:16:21 +00:00
if (!name && !avatarUrl && !profile) {
2020-04-28 13:54:12 +00:00
$el.remove();
return;
}
cleanMentions($el);
var avatar = h('span.cp-avatar');
Env.common.displayAvatar($(avatar), avatarUrl, name);
$el.append([
avatar,
h('span.cp-mentions-name', name)
]);
if (profile) {
$el.attr('tabindex', 1);
$el.addClass('cp-mentions-clickable').click(function (e) {
e.preventDefault();
e.stopPropagation();
Env.common.openURL(Hash.hashToHref(profile, 'profile'));
}).focus(function (e) {
e.stopPropagation();
});
}
});
2020-04-29 14:45:42 +00:00
// edited state
var edited;
if (msg.e) {
edited = h('div.cp-comment-edited', Messages.comments_edited);
}
var container;
// Add edit button when applicable (last message of the thread, written by ourselves)
var edit;
if (i === (obj.m.length -1) && author.curvePublic === userData.curvePublic) {
edit = h('span.cp-comment-edit', {
title: Messages.clickToEdit
}, h('i.fa.fa-pencil'));
$(edit).click(function (e) {
e.stopPropagation();
Env.$container.find('.cp-comment-form').remove();
if ($actions) { $actions.hide(); }
var form = getCommentForm(Env, key, function (val) {
// Show the "reply" and "resolve" buttons again
$(form).closest('.cp-comment-container')
.find('.cp-comment-actions').css('display', '');
$(form).remove();
var obj = Env.comments.data[key];
if (!obj || !Array.isArray(obj.m)) { return; }
var msg = obj.m[i];
if (!msg) { return; }
// i is our index
if (!val) {
msg.d = 1;
if (container) {
$(container).addClass('cp-comment-deleted')
.html(Messages.comments_deleted);
}
if (obj.m.length === 1) {
delete Env.comments.data[key];
}
} else {
msg.e = 1;
msg.m = val;
}
// Send to chainpad
updateMetadata(Env);
Env.framework.localChange();
}, m.innerHTML);
if (!$div) { return; }
$div.append(form);
});
}
2020-04-28 13:54:12 +00:00
// Add the comment
2020-04-29 14:45:42 +00:00
content.push(container = h('div.cp-comment'+replyCls, [
2020-04-22 14:23:45 +00:00
h('div.cp-comment-header', [
avatar,
h('span.cp-comment-metadata', [
h('span.cp-comment-author', name),
h('span.cp-comment-time', date.toLocaleString())
2020-04-29 14:45:42 +00:00
]),
edit
2020-04-22 14:23:45 +00:00
]),
2020-04-29 14:45:42 +00:00
m,
edited
2020-04-22 14:23:45 +00:00
]));
});
2020-04-23 15:42:28 +00:00
var reply = h('button.btn.btn-secondary', {
tabindex: 1
}, [
2020-04-22 14:23:45 +00:00
h('i.fa.fa-reply'),
Messages.comments_reply
]);
2020-04-23 15:42:28 +00:00
var resolve = h('button.btn.btn-primary', {
tabindex: 1
}, [
2020-04-22 14:23:45 +00:00
h('i.fa.fa-check'),
Messages.comments_resolve
]);
var actions;
content.push(actions = h('div.cp-comment-actions', [
reply,
resolve
]));
2020-04-29 14:45:42 +00:00
$actions = $(actions);
2020-04-22 14:23:45 +00:00
var div;
Env.$container.append(div = h('div.cp-comment-container', {
'data-uid': key,
tabindex: 1
}, content));
2020-04-29 14:45:42 +00:00
$div = $(div);
2020-04-22 14:23:45 +00:00
$(reply).click(function (e) {
e.stopPropagation();
$actions.hide();
var form = getCommentForm(Env, key, function (val) {
2020-04-29 14:45:42 +00:00
// Show the "reply" and "resolve" buttons again
$(form).closest('.cp-comment-container')
.find('.cp-comment-actions').css('display', '');
2020-04-28 13:54:12 +00:00
$(form).remove();
2020-04-22 14:23:45 +00:00
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 user = updateAuthorData(Env);
2020-04-22 14:23:45 +00:00
obj.m.push({
u: user, // id (number) or name (string)
2020-04-22 14:23:45 +00:00
t: +new Date(),
m: val,
v: value
});
// Notify other users
sendReplyNotification(Env, key);
2020-04-22 14:23:45 +00:00
// Send to chainpad
updateMetadata(Env);
Env.framework.localChange();
});
2020-04-22 14:23:45 +00:00
$div.append(form);
// Make sure the submit button is visible: scroll by the height of the form
setTimeout(function () {
2020-04-29 14:45:42 +00:00
var yContainer = Env.$container[0].getBoundingClientRect().bottom;
var yActions = form.getBoundingClientRect().bottom;
if (yActions > yContainer) {
Env.$container.scrollTop(Env.$container.scrollTop() + 55);
}
});
2020-04-22 14:23:45 +00:00
});
UI.confirmButton(resolve, {
classes: 'btn-danger-alt'
}, function () {
// Delete the comment
delete Env.comments.data[key];
// Send to chainpad
updateMetadata(Env);
Env.framework.localChange();
});
var focusContent = function () {
// Add class "active"
Env.$inner.find('comment.active').removeClass('active');
Env.$inner.find('comment[data-uid="'+key+'"]').addClass('active');
var $last = Env.$inner.find('comment[data-uid="'+key+'"]').last();
// Scroll into view
if (!$last.length) { return; }
var size = Env.$inner.outerHeight();
var pos = $last[0].getBoundingClientRect();
var visible = (pos.y + pos.height) < size;
if (!visible) { $last[0].scrollIntoView(); }
};
2020-04-23 15:42:28 +00:00
$div.on('click focus', function () {
2020-04-22 14:46:36 +00:00
if ($div.hasClass('cp-comment-active')) { return; }
2020-04-22 14:23:45 +00:00
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
$div.addClass('cp-comment-active');
div.scrollIntoView();
2020-04-22 14:23:45 +00:00
$actions.css('display', '');
Env.$container.find('.cp-comment-form').remove();
focusContent();
2020-04-22 14:23:45 +00:00
});
if ($oldInput && $oldInput.attr('data-uid') === key) {
$div.addClass('cp-comment-active');
$actions.hide();
$div.append($oldInput);
$oldInput.find('textarea').focus();
focusContent();
}
2020-04-22 14:23:45 +00:00
});
if (show) {
Env.$container.show();
} else {
Env.$container.hide();
}
};
2020-04-20 13:22:45 +00:00
var onChange = function (Env) {
var md = Util.clone(Env.metadataMgr.getMetadata());
Env.comments = md.comments;
var changed = false;
if (!Env.comments || !Env.comments.data) {
changed = true;
Env.comments = Util.clone(COMMENTS);
}
2020-04-22 14:23:45 +00:00
if (Env.ready === 0) {
Env.ready = true;
updateAuthorData(Env, function () {
changed = true;
});
// On ready, if our user data have changed or if we've added the initial structure
// of the comments, push the changes
if (changed) {
updateMetadata(Env);
Env.framework.localChange();
}
} else if (Env.ready) {
// Everytime there is a metadata change, check if our user data have changed
2020-04-27 14:47:03 +00:00
// and push the updates if necessary
updateAuthorData(Env, function () {
updateMetadata(Env);
Env.framework.localChange();
});
2020-04-22 14:23:45 +00:00
}
redrawComments(Env);
2020-04-20 13:22:45 +00:00
};
2020-04-22 14:23:45 +00:00
// Check if comments have been deleted from the document but not from metadata
var checkDeleted = function (Env) {
if (!Env.comments || !Env.comments.data) { return; }
// Don't recheck if there were no change
var str = Env.$inner[0].innerHTML;
if (str === Env.oldCheck) { return; }
Env.oldCheck = str;
2020-04-22 14:23:45 +00:00
// If there is no comment stored in the metadata, abort
var comments = Object.keys(Env.comments.data || {}).filter(function (id) {
2020-04-29 14:45:42 +00:00
return !Env.comments.data[id].d;
2020-04-22 14:23:45 +00:00
});
2020-04-20 13:22:45 +00:00
2020-04-22 14:23:45 +00:00
var changed = false;
// Get the comments from the document
2020-04-24 09:16:56 +00:00
var toUncomment = {};
2020-04-22 14:23:45 +00:00
var uids = Env.$inner.find('comment').map(function (i, el) {
var id = el.getAttribute('data-uid');
// Empty comment: remove from dom
2020-04-24 08:50:07 +00:00
if (!el.innerHTML && el.parentElement) {
2020-04-22 14:23:45 +00:00
el.parentElement.removeChild(el);
changed = true;
return;
}
// Comment not in the metadata: uncomment (probably an undo)
2020-04-24 09:16:56 +00:00
var obj = Env.comments.data[id];
if (!obj) {
toUncomment[id] = toUncomment[id] || [];
toUncomment[id].push(el);
2020-04-22 14:23:45 +00:00
changed = true;
return;
}
2020-04-24 09:16:56 +00:00
// If this comment was deleted, we're probably using "undo" to restore it:
// remove the "deleted" state and continue
2020-04-29 14:45:42 +00:00
if (obj.d) {
delete obj.d;
2020-04-24 09:16:56 +00:00
changed = true;
}
2020-04-22 14:23:45 +00:00
return id;
}).toArray();
2020-04-24 09:16:56 +00:00
if (Object.keys(toUncomment).length) {
Object.keys(toUncomment).forEach(function (id) {
Env.editor.plugins.comments.uncomment(id, toUncomment[id]);
});
}
2020-04-22 14:23:45 +00:00
// 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; }
2020-04-29 14:45:42 +00:00
data.d = 1;
2020-04-24 09:16:56 +00:00
//delete Env.comments.data[uid];
2020-04-22 14:23:45 +00:00
changed = true;
});
if (changed) {
updateMetadata(Env);
}
};
2020-04-24 15:53:33 +00:00
var removeCommentBubble = function (Env) {
Env.bubble = undefined;
Env.$contentContainer.find('.cp-comment-bubble').remove();
};
var updateBubble = function (Env) {
if (!Env.bubble) { return; }
var pos = Env.bubble.node.getBoundingClientRect();
if (pos.y < 0 || pos.y > Env.$inner.outerHeight()) {
//removeCommentBubble(Env);
}
Env.bubble.button.setAttribute('style', 'top:'+pos.y+'px');
};
var addCommentBubble = function (Env) {
var ranges = Env.editor.getSelectedRanges();
if (!ranges.length) { return; }
var el = ranges[0].endContainer || ranges[0].startContainer;
var node = el && el.$;
if (!node) { return; }
if (node.nodeType === Node.TEXT_NODE) {
node = node.parentNode;
if (!node) { return; }
}
var pos = node.getBoundingClientRect();
var y = pos.y;
if (y < 0 || y > Env.$inner.outerHeight()) { return; }
var button = h('button.btn.btn-secondary', {
style: 'top:'+y+'px;',
title: Messages.comments_comment
},h('i.fa.fa-comment'));
Env.bubble = {
node: node,
button: button
};
$(button).click(function () {
Env.editor.execCommand('comment');
Env.bubble = undefined;
});
Env.$contentContainer.append(h('div.cp-comment-bubble', button));
};
2020-04-22 14:23:45 +00:00
var addAddCommentHandler = function (Env) {
2020-04-20 13:22:45 +00:00
Env.editor.plugins.comments.addComment = function (uid, addMark) {
2020-04-24 15:53:33 +00:00
if (!Env.ready) { return; }
2020-04-20 13:22:45 +00:00
if (!Env.comments) { Env.comments = Util.clone(COMMENTS); }
2020-04-22 14:23:45 +00:00
// Get all comments ID contained within the selection
2020-04-24 15:53:33 +00:00
var applicable = Env.editor.plugins.comments.isApplicable();
if (!applicable) {
2020-04-22 14:23:45 +00:00
// Abort if our selection contains a comment
2020-04-24 15:53:33 +00:00
console.error("Can't add a comment here");
2020-04-22 14:23:45 +00:00
// XXX show error
2020-04-24 15:53:33 +00:00
UI.warn(Messages.error);
2020-04-22 14:23:45 +00:00
return;
}
Env.$container.find('.cp-comment-form').remove();
var form = getCommentForm(Env, false, function (val) {
$(form).remove();
Env.$inner.focus();
2020-04-20 13:22:45 +00:00
if (!val) { return; }
2020-04-24 15:53:33 +00:00
var applicable = Env.editor.plugins.comments.isApplicable();
if (!applicable) {
2020-04-20 13:22:45 +00:00
// text has been deleted by another user while we were typing our comment?
return void UI.warn(Messages.error);
}
2020-04-24 15:53:33 +00:00
// Don't override existing data
if (Env.comments.data[uid]) { return; }
var user = updateAuthorData(Env);
2020-04-22 14:23:45 +00:00
Env.comments.data[uid] = {
m: [{
u: user, // Id or name
2020-04-22 14:23:45 +00:00
t: +new Date(),
m: val,
2020-04-23 11:52:21 +00:00
v: canonicalize(Env.editor.getSelection().getSelectedText())
2020-04-22 14:23:45 +00:00
}]
2020-04-20 13:22:45 +00:00
};
// There may be a race condition between updateMetadata and addMark that causes
// * updateMetadata first: comment not rendered (redrawComments called
// before addMark)
// * addMark first: comment deleted (checkDeleted called before updateMetadata)
// ==> we're going to call updateMetadata first, and we'll invalidate the cache
// of rendered comments to display them properly in redrawComments
2020-04-22 14:23:45 +00:00
updateMetadata(Env);
2020-04-20 13:22:45 +00:00
addMark();
Env.framework.localChange();
Env.oldComments = undefined;
2020-04-20 13:22:45 +00:00
});
2020-04-23 11:52:21 +00:00
Env.$container.prepend(form).show();
2020-04-20 13:22:45 +00:00
};
2020-04-24 15:53:33 +00:00
Env.$iframe.on('scroll', function () {
updateBubble(Env);
});
$(Env.ifrWindow.document).on('selectionchange', function () {
removeCommentBubble(Env);
var applicable = Env.editor.plugins.comments.isApplicable();
if (!applicable) { return; }
addCommentBubble(Env);
});
2020-04-22 14:23:45 +00:00
};
var onContentUpdate = function (Env) {
if (!Env.ready) { return; }
// Check deleted
onChange(Env);
2020-04-22 14:23:45 +00:00
checkDeleted(Env);
};
var ready = function (Env) {
Env.ready = 0;
// If you're the only edit user online, clear "deleted" comments
if (!Env.common.isLoggedIn()) { return; }
var users = Env.metadataMgr.getMetadata().users || {};
var isNotAlone = Object.keys(users).length > 1;
if (isNotAlone) { return; }
// Clear data
var data = (Env.comments && Env.comments.data) || {};
Object.keys(data).forEach(function (uid) {
2020-04-29 14:45:42 +00:00
if (data[uid].d) { delete data[uid]; }
});
// Commit
updateMetadata(Env);
Env.framework.localChange();
2020-04-22 14:23:45 +00:00
};
Comments.create = function (cfg) {
var Env = cfg;
Env.comments = Util.clone(COMMENTS);
addAddCommentHandler(Env);
// Unselect comment when clicking outside
2020-04-22 14:23:45 +00:00
$(window).click(function (e) {
if ($(e.target).closest('.cp-comment-container').length) {
return;
}
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
Env.$inner.find('comment.active').removeClass('active');
});
// Unselect comment when clicking on another part of the doc
Env.$inner.on('click', function (e) {
if ($(e.target).closest('comment').length) { return; }
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
Env.$inner.find('comment.active').removeClass('active');
2020-04-22 14:23:45 +00:00
});
Env.$inner.on('click', 'comment', function (e) {
var $comment = $(e.target);
var uid = $comment.attr('data-uid');
if (!uid) { return; }
Env.$container.find('.cp-comment-container[data-uid="'+uid+'"]').click();
});
2020-04-20 13:22:45 +00:00
var call = function (f) {
return function () {
try {
[].unshift.call(arguments, Env);
return f.apply(null, arguments);
} catch (e) {
console.error(e);
}
};
};
Env.metadataMgr.onChange(call(onChange));
return {
2020-04-22 14:23:45 +00:00
onContentUpdate: call(onContentUpdate),
ready: call(ready)
2020-04-20 13:22:45 +00:00
};
};
return Comments;
});