Merge remote-tracking branch 'origin/communities-comments' into staging

pull/1/head
ansuz 5 years ago
commit 84909f8c30

@ -9,7 +9,7 @@
@alertify-input-bg: @colortheme_modal-input; @alertify-input-bg: @colortheme_modal-input;
@alertify-input-fg: @colortheme_modal-input-fg; @alertify-input-fg: @colortheme_modal-input-fg;
input:not(.form-control), textarea { input:not(.form-control), textarea, div.cp-textarea {
// background-color: @alertify-input-fg; // background-color: @alertify-input-fg;
color: @cryptpad_text_col; color: @cryptpad_text_col;
border: 1px solid @alertify-input-bg; border: 1px solid @alertify-input-bg;
@ -44,18 +44,29 @@
} }
} }
textarea { textarea, div.cp-textarea {
padding: 8px; padding: 8px;
&[readonly] { &[readonly] {
overflow: hidden; overflow: hidden;
resize: none; resize: none;
} }
} }
div.cp-textarea {
height: 60px;
width: 100%;
background-color: white;
cursor: text;
outline: none;
white-space: pre-wrap;
overflow-y: auto;
word-break: break-word;
resize: vertical;
}
div.cp-button-confirm { div.cp-button-confirm {
display: inline-block; display: inline-block;
button { button {
margin: 0; margin: 0 !important;
} }
.cp-button-timer { .cp-button-timer {
height: 3px; height: 3px;

@ -7,37 +7,66 @@
.comments_main() { .comments_main() {
@data-color: #888; @data-color: #888;
overflow-y: auto; overflow-y: auto;
color: @cryptpad_text_col;
&.cp-comments-readonly {
.cp-comment-actions {
display: none !important;
}
.cp-comment-form {
display: none !important;
}
.cp-comment-edit {
display: none !important;
}
}
.buttons_main(); .buttons_main();
.cp-comment-reply { .cp-comment-reply {
margin-left: 40px; margin-left: 30px;
}
.cp-comment-deleted {
background: white;
font-size: 14px;
padding: 5px;
} }
.cp-comment-form { .cp-comment-form {
&:not(:last-child) { &:not(:last-child) {
padding: 5px;
margin-bottom: 10px; margin-bottom: 10px;
} }
} }
.cp-comment-form-input { .cp-comment-form-input {
.avatar_main(40px); .avatar_main(40px);
.cp-avatar {
border: 1px solid transparent;
}
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
textarea { div.cp-textarea {
flex: 1; flex: 1;
height: 50px; min-height: 52px; // 22px per line + 8 (padding+border)
padding: 2px 8px; height: unset !important;
max-height: 140px; // 6 lines
padding: 3px 5px;
} }
margin-bottom: 5px; margin-bottom: 5px;
} }
.cp-comment-form-actions { .cp-comment-form-actions {
text-align: right; text-align: right;
margin-left: -30px;
button:not(:last-child) { button:not(:last-child) {
margin-right: 10px; margin-right: 5px;
} }
} }
#cp-comments-label {
display: none;
}
.cp-comment-container { .cp-comment-container {
outline: none; outline: none;
@ -52,6 +81,12 @@
margin-top: 5px; margin-top: 5px;
} }
padding: 5px; padding: 5px;
&:nth-child(2) {
margin-top: 10px;
};
&:last-child {
margin-bottom: 10px;
}
} }
.cp-comment { .cp-comment {
&:not(:first-child) { &:not(:first-child) {
@ -63,6 +98,9 @@
align-items: center; align-items: center;
display: flex; display: flex;
background-color: white; background-color: white;
position: relative;
padding: 5px;
box-sizing: content-box;
.avatar_main(40px); .avatar_main(40px);
.cp-comment-metadata { .cp-comment-metadata {
flex: 1; flex: 1;
@ -74,12 +112,32 @@
color: @data-color; color: @data-color;
} }
} }
.cp-comment-edit {
cursor: pointer;
outline: none;
position: absolute;
right: 0;
top: 0;
width: 20px;
height: 20px;
text-align: center;
line-height: 20px;
&:hover {
color: lighten(@cryptpad_text_col, 10%);
}
}
} }
.cp-comment-content { .cp-comment-content {
background-color: white; background-color: white;
padding: 10px 5px 5px; padding: 0px 5px 5px 5px;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-word;
}
.cp-comment-edited {
background-color: white;
font-size: 13px;
color: @data-color;
padding: 0 5px;
} }
.cp-comment-actions { .cp-comment-actions {
display: none; display: none;
@ -89,7 +147,7 @@
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
button:not(:last-child) { button:not(:last-child) {
margin-right: 10px; margin-right: 5px;
} }
} }
.cp-comment-active { .cp-comment-active {

@ -1,11 +1,30 @@
@import (reference) "./colortheme-all.less"; @import (reference) "./colortheme-all.less";
@import (reference) "./tools.less"; @import (reference) "./tools.less";
@import (reference) "./avatar.less";
/* The container <div> - needed to position the dropdown content */ /* The container <div> - needed to position the dropdown content */
.dropdown_main () { .dropdown_main () {
--LessLoader_require: LessLoader_currentFile(); --LessLoader_require: LessLoader_currentFile();
} }
& { & {
.cp-autocomplete-value {
display: flex;
align-items: center;
.cp-avatar {
.avatar_main(30px);
padding: 1px;
}
& > span:last-child {
flex: 1;
height: 32px;
line-height: 32px;
padding: 0 10px;
}
span {
margin: 0px !important;
border: none !important;
}
}
.cp-dropdown-container { .cp-dropdown-container {
@dropdown_font: @colortheme_app-font-size @colortheme_font; @dropdown_font: @colortheme_app-font-size @colortheme_font;
position: relative; position: relative;

@ -15,6 +15,7 @@
@import (reference) "./messenger.less"; @import (reference) "./messenger.less";
@import (reference) "./cursor.less"; @import (reference) "./cursor.less";
@import (reference) "./usergrid.less"; @import (reference) "./usergrid.less";
@import (reference) "./mentions.less";
@import (reference) "./modals-ui-elements.less"; @import (reference) "./modals-ui-elements.less";
.framework_main(@bg-color, @warn-color, @color) { .framework_main(@bg-color, @warn-color, @color) {
@ -44,6 +45,7 @@
.messenger_main(); .messenger_main();
.cursor_main(); .cursor_main();
.usergrid_main(); .usergrid_main();
.mentions_main();
.creation_main( .creation_main(
@bg-color: @bg-color, @bg-color: @bg-color,
@color: @color @color: @color

@ -0,0 +1,32 @@
@import (reference) "./tools.less";
@import (reference) "./avatar.less";
.mentions_main() {
--LessLoader_require: LessLoader_currentFile();
}
& {
.cp-mentions {
.avatar_main(20px);
.tools_unselectable();
display: inline-flex;
align-items: center;
vertical-align: bottom;
background-color: #eee;
span.cp-mentions-name {
max-width: 150px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 0px 3px;
}
&.cp-mentions-clickable {
outline: none;
cursor: pointer;
&:hover {
background-color: #ddd;
}
}
}
}

@ -3666,5 +3666,268 @@ define([
UI.proposal(div, todo); UI.proposal(div, todo);
}; };
var insertTextAtCursor = function (text) {
var selection = window.getSelection();
var range = selection.getRangeAt(0);
range.deleteContents();
var node = document.createTextNode(text);
range.insertNode(node);
for (var position = 0; position !== text.length; position++) {
selection.modify("move", "right", "character");
}
};
var getSource = {};
getSource['contacts'] = function (common, sources) {
var priv = common.getMetadataMgr().getPrivateData();
Object.keys(priv.friends || {}).forEach(function (key) {
if (key === 'me') { return; }
var f = priv.friends[key];
if (!f.curvePublic || sources[f.curvePublic]) { return; }
sources[f.curvePublic] = {
avatar: f.avatar,
name: f.displayName,
curvePublic: f.curvePublic,
profile: f.profile,
notifications: f.notifications
};
});
};
UIElements.addMentions = function (common, options) {
if (!options.$input) { return; }
var $t = options.$input;
var getValue = function () { return $t.val(); };
var setValue = function (val) { $t.val(val); };
var div = false;
if (options.contenteditable) {
div = true;
getValue = function () { return $t.html(); };
setValue = function () {}; // Not used, we insert data at the node level
$t.on('paste', function (e) {
try {
insertTextAtCursor(e.originalEvent.clipboardData.getData('text'));
e.preventDefault();
} catch (err) { console.error(err); }
});
// Fix backspace with "contenteditable false" children
$t.on('keydown', function (e) {
if (e.which !== 8 && e.which !== 46) { return; } // Backspace or del
var sel = document.getSelection();
if (sel.anchorNode.nodeType !== Node.TEXT_NODE) { return; } // text nodes only
// Only fix node located after mentions
var n = sel.anchorNode;
var prev = n && n.previousSibling;
// Check if our caret is just after a mention
if (!prev || !prev.classList || !prev.classList.contains('cp-mentions')) { return; }
// Del: if we're at offset 0, make sure we won't delete the text node
if (e.which === 46) {
if (!sel.anchorOffset && sel.anchorNode.length === 1) {
sel.anchorNode.nodeValue = " ";
e.preventDefault();
}
return;
}
// Backspace
// If we're not at offset 0, make sure we won't delete the text node
if (e.which === 8 && sel.anchorOffset) {
if (sel.anchorNode.length === 1) {
sel.anchorNode.nodeValue = " ";
e.preventDefault();
}
return;
}
// If we're at offset 0, We're just after a mention: delete it
prev.parentElement.removeChild(prev);
e.preventDefault();
});
}
// Add the sources
// NOTE: Sources must have a "name". They can have an "avatar".
var sources = options.sources || {};
if (!getSource[options.type]) { return; }
getSource[options.type](common, sources);
// Sort autocomplete result by label
var sort = function (a, b) {
var _a = a.label.toLowerCase();
var _b = b.label.toLowerCase();
if (_a.label < _b.label) { return -1; }
if (_b.label < _a.label) { return 1; }
return 0;
};
// Get the text between the last @ before the cursor and the cursor
var extractLast = function (term, offset) {
offset = typeof(offset) !== "undefined" ? offset : $t[0].selectionStart;
var startOffset = term.slice(0,offset).lastIndexOf('@');
return term.slice(startOffset+1, offset);
};
// Insert the autocomplete value in the input field
var insertValue = function (value, offset, content) {
offset = typeof(offset) !== "undefined" ? offset : $t[0].selectionStart;
content = content || getValue();
var startOffset = content.slice(0,offset).lastIndexOf('@');
var length = offset - startOffset;
if (length <= 0) { return; }
var result = content.slice(0,startOffset) + value + content.slice(offset);
if (content) {
return {
result: result,
startOffset: startOffset
};
}
setValue(result);
};
// Set the value to receive from the autocomplete
var toInsert = function (data, key) {
var name = data.name.replace(/[^a-zA-Z0-9]+/g, "-");
return "[@"+name+"|"+key+"]";
};
// Fix the functions when suing a contenteditable div
if (div) {
var _extractLast = extractLast;
// Use getSelection to get the cursor position in contenteditable
extractLast = function () {
var sel = document.getSelection();
if (sel.anchorNode.nodeType !== Node.TEXT_NODE) { return; }
return _extractLast(sel.anchorNode.nodeValue, sel.anchorOffset);
};
var _insertValue = insertValue;
insertValue = function (value) {
// Get the selected node
var sel = document.getSelection();
if (sel.anchorNode.nodeType !== Node.TEXT_NODE) { return; }
var node = sel.anchorNode;
// Remove the "term"
var insert =_insertValue("", sel.anchorOffset, node.nodeValue);
if (insert) {
node.nodeValue = insert.result;
}
var breakAt = insert ? insert.startOffset : sel.anchorOffset;
var el;
if (typeof(value) === "string") { el = document.createTextNode(value); }
else { el = value; }
node.parentNode.insertBefore(el, node.splitText(breakAt));
var next = el.nextSibling;
if (!next) {
next = document.createTextNode(" ");
el.parentNode.appendChild(next);
} else if (next.nodeType === Node.TEXT_NODE && !next.nodeValue) {
next.nodeValue = " ";
}
var range = document.createRange();
range.setStart(next, 0);
range.setEnd(next, 0);
var newSel = window.getSelection();
newSel.removeAllRanges();
newSel.addRange(range);
};
// Inserting contacts into contenteditable: use mention UI
if (options.type === "contacts") {
toInsert = function (data) {
var avatar = h('span.cp-avatar', {
contenteditable: false
});
common.displayAvatar($(avatar), data.avatar, data.name);
return h('span.cp-mentions', {
'data-curve': data.curvePublic,
'data-notifications': data.notifications,
'data-profile': data.profile,
'data-name': Util.fixHTML(data.name),
'data-avatar': data.avatar || "",
}, [
avatar,
h('span.cp-mentions-name', {
contenteditable: false
}, data.name)
]);
};
}
}
// don't navigate away from the field on tab when selecting an item
$t.on("keydown", function(e) {
// Tab or enter
if ((e.which === 13 || e.which === 9)) {
try {
var visible = $t.autocomplete("instance").menu.activeMenu.is(':visible');
if (visible) {
e.preventDefault();
e.stopPropagation();
}
} catch (err) { console.error(err, $t); }
}
}).autocomplete({
minLength: 0,
source: function(data, cb) {
var term = data.term;
var results = [];
if (term.indexOf("@") >= 0) {
term = extractLast(data.term) || '';
results = Object.keys(sources).filter(function (key) {
var data = sources[key];
return data.name.toLowerCase().indexOf(term.toLowerCase()) !== -1;
}).map(function (key) {
var data = sources[key];
return {
label: data.name,
value: key
};
});
results.sort(sort);
}
cb(results);
// Set max-height to the autocomplete dropdown
try {
var max = window.innerHeight;
var pos = $t[0].getBoundingClientRect();
var menu = $t.autocomplete("instance").menu.activeMenu;
menu.css({
'overflow-y': 'auto',
'max-height': (max-pos.bottom)+'px'
});
} catch (e) {}
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function(event, ui) {
// add the selected item
var key = ui.item.value;
var data = sources[key];
var value = toInsert(data, key);
insertValue(value);
return false;
}
}).autocomplete( "instance" )._renderItem = function( ul, item ) {
var key = item.value;
var obj = sources[key];
if (!obj) { return; }
var avatar = h('span.cp-avatar');
common.displayAvatar($(avatar), obj.avatar, obj.name);
var li = h('li.cp-autocomplete-value', [
avatar,
h('span', obj.name)
]);
return $(li).appendTo(ul);
};
};
return UIElements; return UIElements;
}); });

@ -315,6 +315,59 @@ define([
} }
}; };
Messages.comments_notification = 'Replies to your comment "{0}" in <b>{1}</b>'; // XXX
Messages.unknownPad = "Unknown pad"; // XXX
handlers['COMMENT_REPLY'] = function (common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
//var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var comment = Util.fixHTML(msg.content.comment).slice(0,20).trim();
if (msg.content.comment.length > 20) {
comment += '...';
}
var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
var href = msg.content.href;
content.getFormatText = function () {
return Messages._getKey('comments_notification', [comment, title]);
};
if (href) {
content.handler = function () {
common.openURL(href);
defaultDismiss(common, data)();
};
}
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
Messages.mentions_notification = '{0} has mentionned you in <b>{1}</b>'; // XXX
handlers['MENTION'] = function (common, data) {
var content = data.content;
var msg = content.msg;
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title || Messages.unknownPad);
var href = msg.content.href;
content.getFormatText = function () {
return Messages._getKey('mentions_notification', [name, title]);
};
if (href) {
content.handler = function () {
common.openURL(href);
defaultDismiss(common, data)();
};
}
if (!content.archived) {
content.dismissHandler = defaultDismiss(common, data);
}
};
// NOTE: don't forget to fixHTML everything returned by "getFormatText" // NOTE: don't forget to fixHTML everything returned by "getFormatText"

@ -528,7 +528,7 @@ define([
/////////////////////// Store //////////////////////////////////// /////////////////////// Store ////////////////////////////////////
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
var getAllStores = function () { var getAllStores = Store.getAllStores = function () {
var stores = [store]; var stores = [store];
var teamModule = store.modules['team']; var teamModule = store.modules['team'];
if (teamModule) { if (teamModule) {
@ -2307,6 +2307,7 @@ define([
return; return;
} }
store.mailbox = Mailbox.init({ store.mailbox = Mailbox.init({
Store: Store,
store: store, store: store,
updateMetadata: function () { updateMetadata: function () {
broadcast([], "UPDATE_METADATA"); broadcast([], "UPDATE_METADATA");
@ -2415,7 +2416,6 @@ define([
loadUniversal(Profile, 'profile', waitFor); loadUniversal(Profile, 'profile', waitFor);
loadUniversal(Team, 'team', waitFor); loadUniversal(Team, 'team', waitFor);
loadUniversal(History, 'history', waitFor); loadUniversal(History, 'history', waitFor);
loadMailbox(waitFor);
cleanFriendRequests(); cleanFriendRequests();
}).nThen(function () { }).nThen(function () {
var requestLogin = function () { var requestLogin = function () {
@ -2506,6 +2506,8 @@ define([
proxy.on('change', [Constants.tokenKey], function () { proxy.on('change', [Constants.tokenKey], function () {
broadcast([], "UPDATE_TOKEN", { token: proxy[Constants.tokenKey] }); broadcast([], "UPDATE_TOKEN", { token: proxy[Constants.tokenKey] });
}); });
loadMailbox();
}); });
}; };

@ -527,6 +527,117 @@ define([
}; };
// Hide duplicates when receiving a SHARE_PAD notification:
// Keep only one notification per channel: the stronger and more recent one
var comments = {};
handlers['COMMENT_REPLY'] = function (ctx, box, data, cb) {
var msg = data.msg;
var hash = data.hash;
var content = msg.content;
if (Util.find(ctx.store.proxy, ['settings', 'pad', 'disableNotif'])) {
return void cb(true);
}
var channel = content.channel;
if (!channel) { return void cb(true); }
var title, href;
ctx.Store.getAllStores().some(function (s) {
var res = s.manager.findChannel(channel);
// Check if the pad is in our drive
return res.some(function (obj) {
if (!obj.data) { return; }
if (href && !obj.data.href) { return; } // We already have the VIEW url, we need EDIT
href = obj.data.href || obj.data.roHref;
title = obj.data.filename || obj.data.title;
if (obj.data.href) { return true; } // Abort only if we have the EDIT url
});
});
// If we don't have the edit url, ignore this notification
if (!href) { return void cb(true); }
// Add the title
content.href = href;
content.title = title;
// Remove duplicates
var old = comments[channel];
var toRemove = old ? old.data : undefined;
// Update the data
comments[channel] = {
data: {
type: box.type,
hash: hash
}
};
cb(false, toRemove);
};
removeHandlers['COMMENT_REPLY'] = function (ctx, box, data, hash) {
var content = data.content;
var channel = content.channel;
var old = comments[channel];
if (old && old.data && old.data.hash === hash) {
delete comments[channel];
}
};
// Hide duplicates when receiving a SHARE_PAD notification:
// Keep only one notification per channel: the stronger and more recent one
var mentions = {};
handlers['MENTION'] = function (ctx, box, data, cb) {
var msg = data.msg;
var hash = data.hash;
var content = msg.content;
if (isMuted(ctx, data)) { return void cb(true); }
var channel = content.channel;
if (!channel) { return void cb(true); }
var title, href;
ctx.Store.getAllStores().some(function (s) {
var res = s.manager.findChannel(channel);
// Check if the pad is in our drive
return res.some(function (obj) {
if (!obj.data) { return; }
if (href && !obj.data.href) { return; } // We already have the VIEW url, we need EDIT
href = obj.data.href || obj.data.roHref;
title = obj.data.filename || obj.data.title;
if (obj.data.href) { return true; } // Abort only if we have the EDIT url
});
});
// Add the title
content.href = href;
content.title = title;
// Remove duplicates
var old = mentions[channel];
var toRemove = old ? old.data : undefined;
// Update the data
mentions[channel] = {
data: {
type: box.type,
hash: hash
}
};
cb(false, toRemove);
};
removeHandlers['MENTION'] = function (ctx, box, data, hash) {
var content = data.content;
var channel = content.channel;
var old = mentions[channel];
if (old && old.data && old.data.hash === hash) {
delete mentions[channel];
}
};
return { return {
add: function (ctx, box, data, cb) { add: function (ctx, box, data, cb) {

@ -431,6 +431,7 @@ proxy.mailboxes = {
var mailbox = {}; var mailbox = {};
var store = cfg.store; var store = cfg.store;
var ctx = { var ctx = {
Store: cfg.Store,
store: store, store: store,
pinPads: cfg.pinPads, pinPads: cfg.pinPads,
updateMetadata: cfg.updateMetadata, updateMetadata: cfg.updateMetadata,

@ -265,6 +265,7 @@ define([
if (!bool && update) { onRemote(); } if (!bool && update) { onRemote(); }
}; };
/*
var hasChanged = function (content) { var hasChanged = function (content) {
try { try {
var oldValue = JSON.parse(cpNfInner.chainpad.getUserDoc()); var oldValue = JSON.parse(cpNfInner.chainpad.getUserDoc());
@ -276,8 +277,9 @@ define([
} catch (e) {} } catch (e) {}
return false; return false;
}; };
*/
onLocal = function (padChange) { onLocal = function (/*padChange*/) {
if (state !== STATE.READY) { return; } if (state !== STATE.READY) { return; }
if (readOnly) { return; } if (readOnly) { return; }
@ -289,9 +291,11 @@ define([
throw new Error("Content must be an object or array, type is " + typeof(content)); throw new Error("Content must be an object or array, type is " + typeof(content));
} }
/*
if (padChange && hasChanged(content)) { if (padChange && hasChanged(content)) {
//cpNfInner.metadataMgr.addAuthor(); //cpNfInner.metadataMgr.addAuthor();
} }
*/
oldContent = content; oldContent = content;
if (Array.isArray(content)) { if (Array.isArray(content)) {

@ -103,6 +103,7 @@ define([
funcs.getBurnAfterReadingWarning = callWithCommon(UIElements.getBurnAfterReadingWarning); funcs.getBurnAfterReadingWarning = callWithCommon(UIElements.getBurnAfterReadingWarning);
funcs.createNewPadModal = callWithCommon(UIElements.createNewPadModal); funcs.createNewPadModal = callWithCommon(UIElements.createNewPadModal);
funcs.onServerError = callWithCommon(UIElements.onServerError); funcs.onServerError = callWithCommon(UIElements.onServerError);
funcs.addMentions = callWithCommon(UIElements.addMentions);
funcs.importMediaTagMenu = callWithCommon(MT.importMediaTagMenu); funcs.importMediaTagMenu = callWithCommon(MT.importMediaTagMenu);
funcs.getMediaTagPreview = callWithCommon(MT.getMediaTagPreview); funcs.getMediaTagPreview = callWithCommon(MT.getMediaTagPreview);

@ -221,7 +221,7 @@
"poll_admin_button": "Admin", "poll_admin_button": "Admin",
"poll_create_user": "Neuen Benutzer hinzufügen", "poll_create_user": "Neuen Benutzer hinzufügen",
"poll_create_option": "Neue Option hinzufügen", "poll_create_option": "Neue Option hinzufügen",
"poll_commit": "Einchecken", "poll_commit": "Bestätigen",
"poll_closeWizardButton": "Assistenten schließen", "poll_closeWizardButton": "Assistenten schließen",
"poll_closeWizardButtonTitle": "Assistenten schließen", "poll_closeWizardButtonTitle": "Assistenten schließen",
"poll_wizardComputeButton": "Optionen übernehmen", "poll_wizardComputeButton": "Optionen übernehmen",

@ -1353,5 +1353,17 @@
"oo_login": "Veuillez vous connecter ou vous inscrire pour améliorer la performance des feuilles de calcul.", "oo_login": "Veuillez vous connecter ou vous inscrire pour améliorer la performance des feuilles de calcul.",
"pad_usePageWidth": "Mode page", "pad_usePageWidth": "Mode page",
"pad_useFullWidth": "Mode large", "pad_useFullWidth": "Mode large",
"cba_title": "Couleurs par auteurs" "cba_title": "Couleurs par auteurs",
"settings_padNotifCheckbox": "Désactiver les notifications de commentaires",
"settings_padNotifHint": "Ignorer les notifications lorsque quelqu'un répond à l'un de vos commentaires",
"settings_padNotifTitle": "Notifications de commentaires",
"comments_comment": "Commenter",
"comments_resolve": "Fermer",
"comments_reply": "Répondre",
"comments_submit": "Valider",
"comments_edited": "Édité",
"comments_deleted": "Commentaire effacé par son auteur",
"mentions_notification": "{0} vous a mentionné dans <b>{1}</b>",
"unknownPad": "Pad inconnu",
"comments_notification": "Réponses à votre commentaire \"{0}\" sur <b>{1}</b>"
} }

@ -169,5 +169,13 @@
"user_rename": "表示名を変更", "user_rename": "表示名を変更",
"users": "ユーザー", "users": "ユーザー",
"saved": "保存しました", "saved": "保存しました",
"error": "エラー" "error": "エラー",
"deleted": "削除しました",
"notifications_title": "未読の通知があります",
"profile_editDescription": "説明を編集",
"profile_addDescription": "説明を追加",
"profileButton": "プロフィール",
"profile_urlPlaceholder": "URL",
"profile_avatar": "アバター",
"profile_upload": " 新しいアバターをアップロード"
} }

@ -1353,5 +1353,17 @@
"oo_login": "Please log in or register to improve the performance of spreadsheets.", "oo_login": "Please log in or register to improve the performance of spreadsheets.",
"pad_useFullWidth": "Full-width mode", "pad_useFullWidth": "Full-width mode",
"pad_usePageWidth": "Page mode", "pad_usePageWidth": "Page mode",
"cba_title": "Author colors" "cba_title": "Author colors",
"comments_notification": "Replies to your comment \"{0}\" in <b>{1}</b>",
"unknownPad": "Unknown pad",
"mentions_notification": "{0} has mentioned you in <b>{1}</b>",
"comments_deleted": "Comment deleted by its author",
"comments_edited": "Edited",
"comments_submit": "Submit",
"comments_reply": "Reply",
"comments_resolve": "Resolve",
"comments_comment": "Comment",
"settings_padNotifTitle": "Comment notifications",
"settings_padNotifHint": "Ignore notifications when someone replies to one of your comments",
"settings_padNotifCheckbox": "Disable comment notifications"
} }

@ -84,9 +84,9 @@ body.cp-app-pad {
} }
#cp-app-pad-comments { #cp-app-pad-comments {
order: 3; order: 3;
width: 300px; width: 330px;
//background-color: white; //background-color: white;
margin: 30px; margin: 0px 20px;
.comments_main(); .comments_main();
} }
&.cke_body_width { &.cke_body_width {

@ -2,6 +2,9 @@
var CKEDITOR = window.CKEDITOR; var CKEDITOR = window.CKEDITOR;
function isUnstylable(el) { function isUnstylable(el) {
if (el.hasClass('cke_widget_mathjax')) {
return false;
}
if (el.hasClass('cke_widget_mediatag')) { if (el.hasClass('cke_widget_mediatag')) {
return false; return false;
} }
@ -10,8 +13,8 @@
return b; return b;
} }
var color1 = 'rgba(252, 165, 3, 0.8)'; var color1 = 'rgba(249, 230, 65, 1.0)';
var color2 = 'rgba(252, 231, 3, 0.8)'; var color2 = 'rgba(252, 181, 0, 1.0)';
CKEDITOR.plugins.add('comments', { CKEDITOR.plugins.add('comments', {
onLoad: function() { onLoad: function() {

@ -25,9 +25,11 @@ define([
u: id, u: id,
m: "str", // comment m: "str", // comment
t: +new Date, t: +new Date,
v: "str" // value of the commented content v: "str", // value of the commented content
e: undefined/1, // edited
d: undefined/1, // deleted
}], }],
(deleted: undefined/true,) d: undefined/1,
} }
} }
} }
@ -66,14 +68,24 @@ define([
return uid || authorUid(existing); return uid || authorUid(existing);
}; };
var updateAuthorData = function (Env) { // Return the author ID and add/update the data for registered users
// Return the username for unregistered users
var updateAuthorData = function (Env, onChange) {
var userData = Env.metadataMgr.getUserData(); var userData = Env.metadataMgr.getUserData();
if (!Env.common.isLoggedIn()) {
return userData.name;
}
var myAuthorId = getAuthorId(Env, userData.curvePublic); var myAuthorId = getAuthorId(Env, userData.curvePublic);
var data = Env.comments.authors[myAuthorId] = Env.comments.authors[myAuthorId] || {}; var data = Env.comments.authors[myAuthorId] = Env.comments.authors[myAuthorId] || {};
var old = Sortify(data);
data.name = userData.name; data.name = userData.name;
data.avatar = userData.avatar; data.avatar = userData.avatar;
data.profile = userData.profile; data.profile = userData.profile;
data.curvePublic = userData.curvePublic; data.curvePublic = userData.curvePublic;
data.notifications = userData.notifications;
if (typeof(onChange) === "function" && Sortify(data) !== old) {
onChange();
}
return myAuthorId; return myAuthorId;
}; };
@ -83,17 +95,82 @@ define([
Env.metadataMgr.updateMetadata(md); 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
});
});
};
var cleanMentions = function ($el) {
$el.html('');
var el = $el[0];
var allowed = ['data-profile', 'data-name', 'data-avatar', 'class'];
// 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);
}
}
};
// Seletc all text of a contenteditable element
var selectAll = function (element) {
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
};
Messages.comments_deleted = "Comment deleted by its author"; // XXX
Messages.comments_edited = "Edited"; // XXX
Messages.comments_submit = "Submit"; // XXX Messages.comments_submit = "Submit"; // XXX
Messages.comments_reply = "Reply"; // XXX Messages.comments_reply = "Reply"; // XXX
Messages.comments_resolve = "Resolve"; // XXX Messages.comments_resolve = "Resolve"; // XXX
var getCommentForm = function (Env, reply, _cb, editContent) {
var getCommentForm = function (Env, reply, _cb) {
var cb = Util.once(_cb); var cb = Util.once(_cb);
var userData = Env.metadataMgr.getUserData(); var userData = Env.metadataMgr.getUserData();
var name = Util.fixHTML(userData.name || Messages.anonymous); var name = Util.fixHTML(userData.name || Messages.anonymous);
var avatar = h('span.cp-avatar'); var avatar = h('span.cp-avatar');
var textarea = h('textarea', { var textarea = h('div.cp-textarea', {
tabindex: 1 tabindex: 1,
role: 'textbox',
'aria-multiline': true,
'aria-labelledby': 'cp-comments-label',
'aria-required': true,
contenteditable: true,
}); });
Env.common.displayAvatar($(avatar), userData.avatar, name); Env.common.displayAvatar($(avatar), userData.avatar, name);
@ -110,31 +187,106 @@ define([
Messages.comments_submit Messages.comments_submit
]); ]);
// List of allowed attributes in mentions
$(submit).click(function (e) { $(submit).click(function (e) {
e.stopPropagation(); e.stopPropagation();
cb(textarea.value); 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;
});
$clone.find('br').replaceWith("\n");
$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();
Object.keys(notify).forEach(function (curve) {
if (curve === userData.curvePublic) { return; }
Env.common.mailbox.sendTo("MENTION", {
channel: privateData.channel,
}, {
channel: notify[curve],
curvePublic: curve
});
});
// Push the content
cb(content);
}); });
$(cancel).click(function (e) { $(cancel).click(function (e) {
e.stopPropagation(); e.stopPropagation();
cb(); cb();
}); });
$(textarea).keydown(function (e) { var $text = $(textarea).keydown(function (e) {
e.stopPropagation();
if (e.which === 27) { if (e.which === 27) {
$(cancel).click(); $(cancel).click();
e.stopImmediatePropagation();
} }
if (e.which === 13 && !e.shiftKey) { if (e.which === 13 && !e.shiftKey) {
// Submit form on Enter is the autocompelte menu is not visible
try {
var visible = $text.autocomplete("instance").menu.activeMenu.is(':visible');
if (visible) { return; }
} catch (err) {}
$(submit).click(); $(submit).click();
e.stopImmediatePropagation();
e.preventDefault(); e.preventDefault();
} }
}).click(function (e) {
e.stopPropagation();
});
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
});
}
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(false);
}); });
}
setTimeout(function () { setTimeout(function () {
$(textarea).focus(); $(textarea).focus();
selectAll(textarea);
}); });
return h('div.cp-comment-form' + (reply ? '.cp-comment-reply' : ''), { return h('div.cp-comment-form' + (reply ? '.cp-comment-reply' : ''), {
'data-uid': reply || undefined 'data-uid': reply || ''
}, [ }, [
h('div.cp-comment-form-input', [ h('div.cp-comment-form-input', [
avatar, avatar,
@ -142,49 +294,90 @@ define([
]), ]),
h('div.cp-comment-form-actions', [ h('div.cp-comment-form-actions', [
cancel, cancel,
deleteButton,
submit submit
]) ])
]); ]);
}; };
var isVisible = function (el, $container) {
var size = $container.outerHeight();
var pos = el.getBoundingClientRect();
return (pos.bottom < size) && (pos.y > 0);
};
var redrawComments = function (Env) { var redrawComments = function (Env) {
// Don't redraw if there were no change // Don't redraw if there were no change
var str = Sortify((Env.comments || {}).data || {}); var str = Sortify(Env.comments || {});
if (str === Env.oldComments) { return; } if (str === Env.oldComments) { return; }
Env.oldComments = str; Env.oldComments = str;
// Store the cursor position if it's located in this form
var oldSelection = window.getSelection();
var oldRangeObj;
if ($(oldSelection.anchorNode).closest('.cp-comment-form').length) {
var oldRange = oldSelection.getRangeAt && oldSelection.getRangeAt(0);
oldRangeObj = {
start: oldRange.startContainer,
startO: oldRange.startOffset,
end: oldRange.endContainer,
endO: oldRange.endOffset
};
}
// Store existing input form in memory
var $oldInput = Env.$container.find('.cp-comment-form').detach(); var $oldInput = Env.$container.find('.cp-comment-form').detach();
if ($oldInput.length !== 1) { $oldInput = undefined; } if ($oldInput.length !== 1) { $oldInput = undefined; }
// Remove everything
Env.$container.html(''); Env.$container.html('');
// "show" tells us if we need to display the "comments" column or not
var show = false; var show = false;
// Add invisible label for accessibility tools
var label = h('label#cp-comments-label', Messages.comments_comment);
Env.$container.append(label);
// If we were adding a new comment, redraw our form
if ($oldInput && !$oldInput.attr('data-uid')) { if ($oldInput && !$oldInput.attr('data-uid')) {
show = true; show = true;
Env.$container.append($oldInput); Env.$container.append($oldInput);
} }
var order = Env.$inner.find('comment').map(function (i, el) { 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) {
return el.getAttribute('data-uid'); return el.getAttribute('data-uid');
}).toArray(); }).toArray();
var done = [];
order.forEach(function (key) {
// Avoir duplicates
if (done.indexOf(key) !== -1) { return; }
done.push(key);
// Draw all comment threads
Util.deduplicateString(threads).forEach(function (key) {
// Get thread data
var obj = Env.comments.data[key]; var obj = Env.comments.data[key];
if (!obj || obj.deleted || !Array.isArray(obj.m) || !obj.m.length) { if (!obj || obj.d || !Array.isArray(obj.m) || !obj.m.length) {
return; return;
} }
// If at least one thread is visible, display the "comments" column
show = true; show = true;
var content = []; var content = [];
obj.m.forEach(function (msg, i) { var $div;
var author = (Env.comments.authors || {})[msg.u] || {}; 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 };
var name = Util.fixHTML(author.name || Messages.anonymous); var name = Util.fixHTML(author.name || Messages.anonymous);
var date = new Date(msg.t); var date = new Date(msg.t);
var avatar = h('span.cp-avatar'); var avatar = h('span.cp-avatar');
@ -196,17 +389,109 @@ define([
}); });
} }
content.push(h('div.cp-comment'+(i === 0 ? '' : '.cp-comment-reply'), [ // 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');
if (!name && !avatarUrl && !profile) {
$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();
});
}
});
// 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', {
tabindex:1,
title: Messages.clickToEdit
}, h('i.fa.fa-pencil'));
$(edit).click(function (e) {
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
$div.addClass('cp-comment-active');
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();
if (typeof(val) === "undefined") { return; }
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 === false) {
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);
});
}
// Add the comment
content.push(container = h('div.cp-comment'+replyCls, [
h('div.cp-comment-header', [ h('div.cp-comment-header', [
avatar, avatar,
h('span.cp-comment-metadata', [ h('span.cp-comment-metadata', [
h('span.cp-comment-author', name), h('span.cp-comment-author', name),
h('span.cp-comment-time', date.toLocaleString()) h('span.cp-comment-time', date.toLocaleString())
])
]), ]),
h('div.cp-comment-content', [ edit
msg.m ]),
]) m,
edited
])); ]));
}); });
@ -229,22 +514,23 @@ define([
reply, reply,
resolve resolve
])); ]));
var $actions = $(actions); $actions = $(actions);
var div; var div;
Env.$container.append(div = h('div.cp-comment-container', { Env.$container.append(div = h('div.cp-comment-container', {
'data-uid': key, 'data-uid': key,
tabindex: 1 tabindex: 1
}, content)); }, content));
var $div = $(div); $div = $(div);
$(reply).click(function (e) { $(reply).click(function (e) {
e.stopPropagation(); e.stopPropagation();
$actions.hide(); $actions.hide();
var form = getCommentForm(Env, key, function (val) { var form = getCommentForm(Env, key, function (val) {
$(form).remove(); // Show the "reply" and "resolve" buttons again
$(form).closest('.cp-comment-container') $(form).closest('.cp-comment-container')
.find('.cp-comment-actions').css('display', ''); .find('.cp-comment-actions').css('display', '');
$(form).remove();
if (!val) { return; } if (!val) { return; }
var obj = Env.comments.data[key]; var obj = Env.comments.data[key];
@ -257,23 +543,36 @@ define([
}).join('\n'); }).join('\n');
// Push the reply // Push the reply
var myId = updateAuthorData(Env); var user = updateAuthorData(Env);
obj.m.push({ obj.m.push({
u: myId, u: user, // id (number) or name (string)
t: +new Date(), t: +new Date(),
m: val, m: val,
v: value v: value
}); });
// Notify other users
sendReplyNotification(Env, key);
// Send to chainpad // Send to chainpad
updateMetadata(Env); updateMetadata(Env);
Env.framework.localChange(); Env.framework.localChange();
}); });
$div.append(form); $div.append(form);
// Make sure the submit button is visible: scroll by the height of the form
setTimeout(function () {
var yContainer = Env.$container[0].getBoundingClientRect().bottom;
var yActions = form.getBoundingClientRect().bottom;
if (yActions > yContainer) {
Env.$container.scrollTop(Env.$container.scrollTop() + 55);
}
});
}); });
UI.confirmButton(resolve, { UI.confirmButton(resolve, {
classes: 'btn-danger-alt' classes: 'btn-danger'
}, function () { }, function () {
// Delete the comment // Delete the comment
delete Env.comments.data[key]; delete Env.comments.data[key];
@ -291,21 +590,24 @@ define([
// Scroll into view // Scroll into view
if (!$last.length) { return; } if (!$last.length) { return; }
var size = Env.$inner.outerHeight(); var visible = isVisible($last[0], Env.$inner);
var pos = $last[0].getBoundingClientRect();
var visible = (pos.y + pos.height) < size;
if (!visible) { $last[0].scrollIntoView(); } if (!visible) { $last[0].scrollIntoView(); }
}; };
$div.on('click focus', function () { $div.on('click focus', function (e) {
// Prevent the click event to propagate if we're already selected
// The propagation to #cp-app-pad-inner would trigger the "unselect" handler
e.stopPropagation();
if ($div.hasClass('cp-comment-active')) { return; } if ($div.hasClass('cp-comment-active')) { return; }
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active'); Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
$div.addClass('cp-comment-active'); $div.addClass('cp-comment-active');
div.scrollIntoView();
$actions.css('display', ''); $actions.css('display', '');
Env.$container.find('.cp-comment-form').remove(); Env.$container.find('.cp-comment-form').remove();
focusContent(); focusContent();
var visible = isVisible(div, Env.$container);
if (!visible) { div.scrollIntoView(); }
}); });
if ($oldInput && $oldInput.attr('data-uid') === key) { if ($oldInput && $oldInput.attr('data-uid') === key) {
@ -317,6 +619,19 @@ define([
} }
}); });
// Restore selection
if (oldRangeObj) {
setTimeout(function () {
if (!oldRangeObj) { return; }
var range = document.createRange();
range.setStart(oldRangeObj.start, oldRangeObj.startO);
range.setEnd(oldRangeObj.end, oldRangeObj.endO);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
});
}
if (show) { if (show) {
Env.$container.show(); Env.$container.show();
} else { } else {
@ -334,10 +649,22 @@ define([
} }
if (Env.ready === 0) { if (Env.ready === 0) {
Env.ready = true; 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) { if (changed) {
updateMetadata(Env); updateMetadata(Env);
Env.framework.localChange(); Env.framework.localChange();
} }
} else if (Env.ready) {
// Everytime there is a metadata change, check if our user data have changed
// and push the updates if necessary
updateAuthorData(Env, function () {
updateMetadata(Env);
Env.framework.localChange();
});
} }
redrawComments(Env); redrawComments(Env);
}; };
@ -353,7 +680,7 @@ define([
// If there is no comment stored in the metadata, abort // If there is no comment stored in the metadata, abort
var comments = Object.keys(Env.comments.data || {}).filter(function (id) { var comments = Object.keys(Env.comments.data || {}).filter(function (id) {
return !Env.comments.data[id].deleted; return !Env.comments.data[id].d;
}); });
var changed = false; var changed = false;
@ -378,8 +705,8 @@ define([
} }
// If this comment was deleted, we're probably using "undo" to restore it: // If this comment was deleted, we're probably using "undo" to restore it:
// remove the "deleted" state and continue // remove the "deleted" state and continue
if (obj.deleted) { if (obj.d) {
delete obj.deleted; delete obj.d;
changed = true; changed = true;
} }
return id; return id;
@ -397,7 +724,7 @@ define([
// comment has been deleted // comment has been deleted
var data = Env.comments.data[uid]; var data = Env.comments.data[uid];
if (!data) { return; } if (!data) { return; }
data.deleted = true; data.d = 1;
//delete Env.comments.data[uid]; //delete Env.comments.data[uid];
changed = true; changed = true;
}); });
@ -440,7 +767,8 @@ define([
node: node, node: node,
button: button button: button
}; };
$(button).click(function () { $(button).click(function (e) {
e.stopPropagation();
Env.editor.execCommand('comment'); Env.editor.execCommand('comment');
Env.bubble = undefined; Env.bubble = undefined;
}); });
@ -462,6 +790,8 @@ define([
return; return;
} }
// Remove active class on other comments
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
Env.$container.find('.cp-comment-form').remove(); Env.$container.find('.cp-comment-form').remove();
var form = getCommentForm(Env, false, function (val) { var form = getCommentForm(Env, false, function (val) {
$(form).remove(); $(form).remove();
@ -477,10 +807,10 @@ define([
// Don't override existing data // Don't override existing data
if (Env.comments.data[uid]) { return; } if (Env.comments.data[uid]) { return; }
var myId = updateAuthorData(Env); var user = updateAuthorData(Env);
Env.comments.data[uid] = { Env.comments.data[uid] = {
m: [{ m: [{
u: myId, u: user, // Id or name
t: +new Date(), t: +new Date(),
m: val, m: val,
v: canonicalize(Env.editor.getSelection().getSelectedText()) v: canonicalize(Env.editor.getSelection().getSelectedText())
@ -533,7 +863,7 @@ define([
// Clear data // Clear data
var data = (Env.comments && Env.comments.data) || {}; var data = (Env.comments && Env.comments.data) || {};
Object.keys(data).forEach(function (uid) { Object.keys(data).forEach(function (uid) {
if (data[uid].deleted) { delete data[uid]; } if (data[uid].d) { delete data[uid]; }
}); });
// Commit // Commit
@ -545,21 +875,39 @@ define([
var Env = cfg; var Env = cfg;
Env.comments = Util.clone(COMMENTS); Env.comments = Util.clone(COMMENTS);
var ro = cfg.framework.isReadOnly();
var onEditableChange = function (unlocked) {
Env.$container.removeClass('cp-comments-readonly');
if (ro || !unlocked) {
Env.$container.addClass('cp-comments-readonly');
}
};
cfg.framework.onEditableChange(onEditableChange);
onEditableChange();
addAddCommentHandler(Env); addAddCommentHandler(Env);
// Unselect comment when clicking outside // Unselect comment when clicking outside
$(window).click(function (e) { $(window).click(function (e) {
if ($(e.target).closest('.cp-comment-container').length) { var $target = $(e.target);
return; if (!$target.length) { return; }
} if ($target.is('.cp-comment-container')) { return; }
if ($target.closest('.cp-comment-container').length) { return; }
if ($target.closest('.ui-autocomplete').length) { return; }
// Add comment button? don't remove anything because this handler is called after
// the button action
if ($target.is('.cke_button__comment')) { return; }
if ($target.closest('.cke_button__comment').length) { return; }
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active'); Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
Env.$inner.find('comment.active').removeClass('active'); Env.$inner.find('comment.active').removeClass('active');
Env.$container.find('.cp-comment-form').remove();
}); });
// Unselect comment when clicking on another part of the doc // Unselect comment when clicking on another part of the doc
Env.$inner.on('click', function (e) { Env.$inner.on('click', function (e) {
if ($(e.target).closest('comment').length) { return; } if ($(e.target).closest('comment').length) { return; }
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active'); Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
Env.$inner.find('comment.active').removeClass('active'); Env.$inner.find('comment.active').removeClass('active');
Env.$container.find('.cp-comment-form').remove();
}); });
Env.$inner.on('click', 'comment', function (e) { Env.$inner.on('click', 'comment', function (e) {
var $comment = $(e.target); var $comment = $(e.target);

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -397,7 +397,7 @@ define([
var addToolbarHideBtn = function (framework, $bar) { var addToolbarHideBtn = function (framework, $bar) {
// Expand / collapse the toolbar // Expand / collapse the toolbar
var cfg = { var cfg = {
element: $bar.find('.cke_toolbox_main') element: $bar
}; };
var onClick = function (visible) { var onClick = function (visible) {
framework._.sfCommon.setAttribute(['pad', 'showToolbar'], visible); framework._.sfCommon.setAttribute(['pad', 'showToolbar'], visible);
@ -530,6 +530,15 @@ define([
mkHelpMenu(framework); mkHelpMenu(framework);
framework._.sfCommon.getAttribute(['pad', 'width'], function (err, data) {
var active = data || typeof(data) === "undefined";
if (active) {
$contentContainer.addClass('cke_body_width');
} else {
editor.execCommand('pagemode');
}
});
framework.onEditableChange(function (unlocked) { framework.onEditableChange(function (unlocked) {
if (!framework.isReadOnly()) { if (!framework.isReadOnly()) {
$inner.attr('contenteditable', '' + Boolean(unlocked)); $inner.attr('contenteditable', '' + Boolean(unlocked));
@ -663,7 +672,7 @@ define([
}); });
if (!framework.isReadOnly()) { if (!framework.isReadOnly()) {
addToolbarHideBtn(framework, $contentContainer); addToolbarHideBtn(framework, $('.cke_toolbox_main'));
} else { } else {
$('.cke_toolbox_main').hide(); $('.cke_toolbox_main').hide();
} }
@ -717,29 +726,6 @@ define([
} }
}); });
framework._.sfCommon.getAttribute(['pad', 'width'], function (err, data) {
var active = data || typeof(data) === "undefined";
if (active) {
$contentContainer.addClass('cke_body_width');
}
var $width = framework._.sfCommon.createButton('', true, {
icon: 'fa-arrows-h',
text: active ? Messages.pad_useFullWidth : Messages.pad_usePageWidth,
name: "pad-width",
},function () {
if (active) {
$contentContainer.removeClass('cke_body_width');
} else {
$contentContainer.addClass('cke_body_width');
}
active = !active;
var key = active ? Messages.pad_useFullWidth : Messages.pad_usePageWidth;
$width.find('.cp-toolbar-drawer-element').text(key);
framework._.sfCommon.setAttribute(['pad', 'width'], active);
});
framework._.toolbar.$drawer.append($width);
});
framework._.sfCommon.isPadStored(function (err, val) { framework._.sfCommon.isPadStored(function (err, val) {
if (!val) { return; } if (!val) { return; }
var b64images = $inner.find('img[src^="data:image"]:not(.cke_reset)'); var b64images = $inner.find('img[src^="data:image"]:not(.cke_reset)');
@ -972,6 +958,30 @@ define([
module.ckeditor = editor = Ckeditor.replace('editor1', { module.ckeditor = editor = Ckeditor.replace('editor1', {
customConfig: '/customize/ckeditor-config.js', customConfig: '/customize/ckeditor-config.js',
}); });
editor.addCommand('pagemode', {
exec: function () {
if (!framework) { return; }
var $contentContainer = $('#cke_1_contents');
var $button = $('.cke_button__pagemode');
var isLarge = $button.hasClass('cke_button_on');
if (isLarge) {
$button.addClass('cke_button_off').removeClass('cke_button_on');
$contentContainer.addClass('cke_body_width');
} else {
$button.addClass('cke_button_on').removeClass('cke_button_off');
$contentContainer.removeClass('cke_body_width');
}
framework._.sfCommon.setAttribute(['pad', 'width'], isLarge);
}
});
editor.ui.addButton('PageMode', {
label: Messages.pad_useFullWidth,
command: 'pagemode',
icon: '/pad/icons/arrows-h.png',
toolbar: 'document,60'
});
editor.on('instanceReady', waitFor()); editor.on('instanceReady', waitFor());
}).nThen(function () { }).nThen(function () {
editor.plugins.mediatag.import = function ($mt) { editor.plugins.mediatag.import = function ($mt) {
@ -985,6 +995,7 @@ define([
var $ckeToolbar = $('#cke_1_top').find('.cke_toolbox_main'); var $ckeToolbar = $('#cke_1_top').find('.cke_toolbox_main');
$mainContainer.prepend($ckeToolbar.addClass('cke_reset_all')); $mainContainer.prepend($ckeToolbar.addClass('cke_reset_all'));
$contentContainer.append(h('div#cp-app-pad-comments')); $contentContainer.append(h('div#cp-app-pad-comments'));
$ckeToolbar.find('.cke_button__image_icon').parent().hide();
}).nThen(waitFor()); }).nThen(waitFor());
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {

@ -78,6 +78,7 @@ define([
'pad': [ 'pad': [
'cp-settings-pad-width', 'cp-settings-pad-width',
'cp-settings-pad-spellcheck', 'cp-settings-pad-spellcheck',
'cp-settings-pad-notif',
], ],
'code': [ 'code': [
'cp-settings-code-indent-unit', 'cp-settings-code-indent-unit',
@ -1275,6 +1276,38 @@ define([
return $div; return $div;
}; };
// XXX Messages.settings_padNotifTitle
// XXX Messages.settings_padNotifHint
// XXX Messages.settings_padNotifCheckbox ("disable comments notifications")
Messages.settings_padNotifTitle = "Comments notifications"; // XXX
Messages.settings_padNotifHint = "Block notifications when someone replies to one of your comments"; // XXX
Messages.settings_padNotifCheckbox = "Disable comment notifications";
makeBlock('pad-notif', function (cb) {
var $cbox = $(UI.createCheckbox('cp-settings-pad-notif',
Messages.settings_padNotifCheckbox,
false, { label: {class: 'noTitle'} }));
var spinner = UI.makeSpinner($cbox);
// Checkbox: "Enable safe links"
var $checkbox = $cbox.find('input').on('change', function () {
spinner.spin();
var val = $checkbox.is(':checked');
common.setAttribute(['pad', 'disableNotif'], val, function () {
spinner.done();
});
});
common.getAttribute(['pad', 'disableNotif'], function (e, val) {
if (e) { return void console.error(e); }
if (val === true) {
$checkbox.attr('checked', 'checked');
}
});
cb($cbox);
}, true);
// Code settings // Code settings
create['code-indent-unit'] = function () { create['code-indent-unit'] = function () {

Loading…
Cancel
Save