Merge remote-tracking branch 'origin/lessonce' into staging

pull/1/head
ansuz 5 years ago
commit 9714783818

@ -21,7 +21,7 @@
"jquery": "2.2.4", "jquery": "2.2.4",
"tweetnacl": "0.12.2", "tweetnacl": "0.12.2",
"components-font-awesome": "^4.6.3", "components-font-awesome": "^4.6.3",
"ckeditor": "4.7.3", "ckeditor": "4.14.0",
"codemirror": "^5.19.0", "codemirror": "^5.19.0",
"requirejs": "2.3.5", "requirejs": "2.3.5",
"marked": "0.5.0", "marked": "0.5.0",

@ -10,9 +10,7 @@ CKEDITOR.editorConfig = function( config ) {
// document itself and causes problems when it's sent across the wire and reflected back // document itself and causes problems when it's sent across the wire and reflected back
config.removePlugins= 'resize,elementspath'; config.removePlugins= 'resize,elementspath';
config.resize_enabled= false; //bottom-bar config.resize_enabled= false; //bottom-bar
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax,wordcount'; config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax,wordcount,comments';
// FIXME translation for default? updating to a newer CKEditor seems like it will add 'default' by default
config.fontSize_sizes = '(Default)/unset;8/8px;9/9px;10/10px;11/11px;12/12px;14/14px;16/16px;18/18px;20/20px;22/22px;24/24px;26/26px;28/28px;36/36px;48/48px;72/72px';
config.toolbarGroups= [ config.toolbarGroups= [
// {"name":"clipboard","groups":["clipboard","undo"]}, // {"name":"clipboard","groups":["clipboard","undo"]},
//{"name":"editing","groups":["find","selection"]}, //{"name":"editing","groups":["find","selection"]},

@ -99,7 +99,7 @@
margin: 0; margin: 0;
} }
&:hover, &:active { &:hover, &:active, &:focus {
background-color: lighten(@alertify-fore, 35%); background-color: lighten(@alertify-fore, 35%);
} }
@ -113,7 +113,7 @@
background-color: @colortheme_alertify-red; background-color: @colortheme_alertify-red;
border-color: @colortheme_alertify-red-border; border-color: @colortheme_alertify-red-border;
color: @colortheme_alertify-red-color; color: @colortheme_alertify-red-color;
&:hover, &:active { &:hover, &:active, &:focus {
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%)); background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%));
} }
} }
@ -121,7 +121,7 @@
&.danger-alt, &.btn-danger-alt { &.danger-alt, &.btn-danger-alt {
border-color: @colortheme_alertify-red; border-color: @colortheme_alertify-red;
color: @colortheme_alertify-red; color: @colortheme_alertify-red;
&:hover, &:active { &:hover, &:active, &:focus {
color: @colortheme_alertify-red-color; color: @colortheme_alertify-red-color;
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%)); background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-red, 10%), lighten(@colortheme_alertify-red, 10%));
} }
@ -131,7 +131,7 @@
background-color: @colortheme_alertify-green; background-color: @colortheme_alertify-green;
border-color: @colortheme_alertify-green-border; border-color: @colortheme_alertify-green-border;
color: @colortheme_alertify-green-color; color: @colortheme_alertify-green-color;
&:hover, &:active { &:hover, &:active, &:focus {
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-green, 10%), lighten(@colortheme_alertify-green, 10%)); background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-green, 10%), lighten(@colortheme_alertify-green, 10%));
} }
} }
@ -141,7 +141,7 @@
color: @colortheme_alertify-primary-text; color: @colortheme_alertify-primary-text;
border-color: @colortheme_alertify-primary-border; border-color: @colortheme_alertify-primary-border;
font-weight: bold; font-weight: bold;
&:hover, &:active { &:hover, &:active, &:focus {
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-primary, 10%), lighten(@colortheme_alertify-primary, 10%)); background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-primary, 10%), lighten(@colortheme_alertify-primary, 10%));
} }
} }
@ -149,7 +149,7 @@
&.cancel, &.btn-cancel { &.cancel, &.btn-cancel {
border-color: @colortheme_alertify-cancel-border; border-color: @colortheme_alertify-cancel-border;
color: @colortheme_alertify-cancel-border; color: @colortheme_alertify-cancel-border;
&:hover, &:hover { &:hover, &:hover, &:focus {
background-color: fade(@colortheme_alertify-cancel-border, 25%); background-color: fade(@colortheme_alertify-cancel-border, 25%);
} }
} }

@ -0,0 +1,102 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
@import (reference) "./avatar.less";
@import (reference) "./buttons.less";
@import (reference) "./tools.less";
.comments_main() {
@data-color: #888;
overflow-y: auto;
.buttons_main();
.cp-comment-reply {
margin-left: 40px;
}
.cp-comment-form {
&:not(:last-child) {
margin-bottom: 10px;
}
}
.cp-comment-form-input {
.avatar_main(40px);
display: flex;
align-items: flex-start;
textarea {
flex: 1;
height: 50px;
padding: 2px 8px;
}
margin-bottom: 5px;
}
.cp-comment-form-actions {
text-align: right;
button:not(:last-child) {
margin-right: 10px;
}
}
.cp-comment-container {
outline: none;
&:not(:focus) {
cursor: pointer;
.tools_unselectable();
}
//&:not(:last-child) {
// margin-bottom: 10px;
//}
.cp-comment-form {
margin-top: 5px;
}
padding: 5px;
}
.cp-comment {
&:not(:first-child) {
margin-top: 5px;
}
}
.cp-comment-header {
height: 40px;
align-items: center;
display: flex;
background-color: white;
.avatar_main(40px);
.cp-comment-metadata {
flex: 1;
display: flex;
flex-flow: column;
margin-left: 5px;
.cp-comment-time {
font-size: 13px;
color: @data-color;
}
}
}
.cp-comment-content {
background-color: white;
padding: 10px 5px 5px;
white-space: pre-wrap;
word-break: break-all;
}
.cp-comment-actions {
display: none;
text-align: right;
margin-top: 5px;
button {
margin-bottom: 0 !important;
}
button:not(:last-child) {
margin-right: 10px;
}
}
.cp-comment-active {
background-color: rgba(0,0,0,0.2);
.cp-comment-actions {
display: block;
}
}
}

@ -99,7 +99,7 @@ define([
if (lessEngine) { if (lessEngine) {
cb(lessEngine); cb(lessEngine);
} else { } else {
require(['/bower_components/less/dist/less.min.js'], function (Less) { require(['/common/less.min.js'], function (Less) {
if (lessEngine) { return void cb(lessEngine); } if (lessEngine) { return void cb(lessEngine); }
lessEngine = Less; lessEngine = Less;
Less.functions.functionRegistry.add('LessLoader_currentFile', function () { Less.functions.functionRegistry.add('LessLoader_currentFile', function () {

@ -3,7 +3,7 @@ define([], function () {
var indexOfNode = tree.indexOfNode = function (el) { var indexOfNode = tree.indexOfNode = function (el) {
if (!(el && el.parentNode)) { if (!(el && el.parentNode)) {
console.log("No parentNode found!"); console.log("No parentNode found!", el);
throw new Error('No parentNode found!'); throw new Error('No parentNode found!');
} }
return Array.prototype.indexOf.call(el.parentNode.childNodes, el); return Array.prototype.indexOf.call(el.parentNode.childNodes, el);
@ -26,6 +26,7 @@ define([], function () {
leaf nodes of a tree leaf nodes of a tree
*/ */
var rightmostNode = tree.rightmostNode = function (el) { var rightmostNode = tree.rightmostNode = function (el) {
if (!el) { return null; }
var childNodeCount = childCount(el); var childNodeCount = childCount(el);
if (!childNodeCount) { // no children if (!childNodeCount) { // no children
return el; // return the element return el; // return the element
@ -53,7 +54,7 @@ define([], function () {
if (!el.parentNode) { return null; } if (!el.parentNode) { return null; }
if (i === 0) { if (i === 0) {
if (root && el.parentNode === root.childNodes[0]) { return null; } if (root && el.parentNode === root.childNodes[0]) { return null; }
return rightmostNode(previousNode(el.parentNode)); return rightmostNode(previousNode(el.parentNode, root));
} else { } else {
return rightmostNode(el.parentNode.childNodes[i-1]); return rightmostNode(el.parentNode.childNodes[i-1]);
} }
@ -77,6 +78,10 @@ define([], function () {
// a and b might be the same element // a and b might be the same element
if (a === b) { return 0; } if (a === b) { return 0; }
// If we're selecting an entire node containing a single text node,
// b can be the child of a. Order is correct.
if (b.parentNode && b.parentNode === a) { return 1; }
var cur = b; var cur = b;
while (cur) { while (cur) {
cur = previousNode(cur, root); cur = previousNode(cur, root);

File diff suppressed because one or more lines are too long

@ -1,4 +1,6 @@
@import (reference) "../../customize/src/less2/include/framework.less"; @import (reference) "../../customize/src/less2/include/framework.less";
@import (reference) "../../customize/src/less2/include/comments.less";
@import (reference) "../../customize/src/less2/include/buttons.less";
body.cp-app-pad { body.cp-app-pad {
.framework_main( .framework_main(
@ -7,10 +9,21 @@ body.cp-app-pad {
@color: @colortheme_pad-color @color: @colortheme_pad-color
); );
#cke_1_top { @bg-color: #e3e3e3;
overflow: visible;
padding: 0px; display: flex;
flex-flow: column;
max-height: 100%;
min-height: auto;
#cp-app-pad-editor {
flex: 1;
display: flex;
flex-flow: row;
height: 100%;
overflow: hidden;
} }
.cke_toolbox_main { .cke_toolbox_main {
background-color: @colortheme_pad-toolbar-bg; background-color: @colortheme_pad-toolbar-bg;
.cke_toolbar { .cke_toolbar {
@ -33,20 +46,58 @@ body.cp-app-pad {
flex-flow: column; flex-flow: column;
height: 100%; height: 100%;
border: 0; border: 0;
flex: 1;
> .cke_inner { > .cke_inner {
overflow: hidden; overflow: hidden;
flex: 1; flex: 1;
position: unset; position: unset;
display: flex; display: flex;
flex-flow: column;
margin-top: -1px; margin-top: -1px;
#cke_1_contents { }
flex: 1; #cke_1_top {
display: flex; display: none;
flex-flow: column; }
height: auto !important; }
iframe { #cke_1_contents {
flex: 1; flex: 1;
display: flex;
height: auto !important;
background-color: @bg-color;
justify-content: center;
iframe {
flex: 1;
min-width: 0;
order: 1;
}
div.cp-comment-bubble {
.buttons_main();
position: relative;
order: 2;
button {
.fa {
margin: 0 !important;
} }
right: 20px;
position: absolute;
}
}
#cp-app-pad-comments {
order: 3;
width: 300px;
//background-color: white;
margin: 30px;
.comments_main();
}
&.cke_body_width {
div.cp-comment-bubble {
button {
right: 0px;
}
}
iframe {
margin: 0 30px;
max-width: 800px;
} }
} }
} }

@ -0,0 +1,173 @@
(function () {
var CKEDITOR = window.CKEDITOR;
function isUnstylable (el) {
if (el.hasClass('cke_widget_mediatag')) {
return false;
}
var b = el.getAttribute( 'contentEditable' ) === 'false' ||
el.getAttribute( 'data-nostyle' );
return b;
}
var color1 = 'rgba(252, 165, 3, 0.8)';
var color2 = 'rgba(252, 231, 3, 0.8)';
CKEDITOR.plugins.add('comments', {
onLoad: function () {
CKEDITOR.addCss('comment { background-color: '+color1+'; }' +
'@keyframes color { 0% { background-color: '+color2+'; } 50% { background-color: '+color1+'; } 100% { background-color: '+color2+'; } }' +
'comment.active { animation-name: color; animation-duration: 1s; animation-iteration-count: 2; background-color: '+color2+'; outline: none;}' +
'comment media-tag { border: 2px solid '+color1+' !important; }' +
'comment.active media-tag { border: 2px solid '+color2+' !important; }' +
'comment * { background-color: transparent !important; }');
},
init: function (editor) {
var Messages = CKEDITOR._commentsTranslations;
var styleDef = {
element: 'comment',
attributes: {
'data-uid': '#(uid)',
},
overrides: [ {
element: 'comment'
} ],
childRule: isUnstylable
};
var removeStyle = new CKEDITOR.style(styleDef, { 'uid': '' });
var isApplicable = editor.plugins.comments.isApplicable = function (path, sel) {
path = path || editor.elementPath();
sel = sel || editor.getSelection();
var applicable = removeStyle.checkApplicable(path, editor);
var hasComments = editor.getSelectedHtml().$.querySelectorAll('comment').length;
var isComment = removeStyle.checkActive(path, editor);
var empty = !sel.getSelectedText();
return applicable && !empty && !hasComments && !isComment;
};
// Register the command.
editor.addCommand('comment', {
exec: function (editor) {
if (editor.readOnly) { return; }
editor.focus();
var uid = CKEDITOR.tools.getUniqueId();
editor.plugins.comments.addComment(uid, function () {
// Make an undo spnashot
editor.fire('saveSnapshot');
// Make sure comments won't overlap
editor.removeStyle(removeStyle);
// 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');
}, 0 );
});
}
});
// Uncomment provided element
editor.plugins.comments.uncomment = function (id, els) {
if (editor.readOnly) { return; }
editor.fire('saveSnapshot');
//Create style for this id
var style = new CKEDITOR.style({
element: 'comment',
attributes: {
'data-uid': id,
},
});
style.alwaysRemoveElement = true;
els.forEach(function (el) {
// Create range for this element
el.removeAttribute('class');
var node = new CKEDITOR.dom.node(el);
var range = editor.createRange();
range.setStart(node, 0);
range.setEnd(node, Number.MAX_SAFE_INTEGER);
// Remove style for the comment
try {
style.removeFromRange(range, editor);
} catch (e) {
console.error(e);
}
});
setTimeout( function() {
editor.fire('saveSnapshot');
}, 0 );
};
// Uncomment from context menu, disabled for now...
editor.addCommand('uncomment', {
exec: function (editor, data) {
if (editor.readOnly) { return; }
editor.fire('saveSnapshot');
if (!data || !data.id) {
editor.focus();
editor.removeStyle(removeStyle);
setTimeout( function() {
editor.fire('saveSnapshot');
}, 0 );
return;
}
}
});
// Register the toolbar button.
if (editor.ui.addButton) {
editor.ui.addButton('Comment', {
label: Messages.comment,
command: 'comment',
icon : '/pad/icons/comment.png',
toolbar: 'insert,10'
});
}
if (editor.addMenuItems) {
editor.addMenuGroup('comments');
editor.addMenuItem('comment', {
label: Messages.comment,
icon : '/pad/icons/comment.png',
command: 'comment',
group: 'comments'
});
/*
editor.addMenuItem('uncomment', {
label: Messages.uncomment,
icon : '/pad/icons/uncomment.png',
command: 'uncomment',
group: 'comments'
});
*/
}
if (editor.contextMenu) {
/*
editor.contextMenu.addListener(function (element, sel, path) {
var isComment = removeStyle.checkActive(path, editor);
if (!isComment) { return; }
return {
uncomment: CKEDITOR.TRISTATE_OFF,
};
});
*/
editor.contextMenu.addListener(function (element, sel, path) {
var applicable = isApplicable(path, sel);
if (!applicable) { return; }
return {
comment: CKEDITOR.TRISTATE_OFF,
};
});
}
}
});
})();

@ -0,0 +1,591 @@
define([
'jquery',
'json.sortify',
'/common/common-util.js',
'/common/common-hash.js',
'/common/hyperscript.js',
'/common/common-interface.js',
'/customize/messages.js'
], function ($, Sortify, Util, Hash, 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: {},
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 = []; }
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; }
uid = Number(id);
return true;
});
return uid || authorUid(existing);
};
var updateAuthorData = function (Env) {
var userData = Env.metadataMgr.getUserData();
var myAuthorId = getAuthorId(Env, userData.curvePublic);
var data = Env.comments.authors[myAuthorId] = Env.comments.authors[myAuthorId] || {};
data.name = userData.name;
data.avatar = userData.avatar;
data.profile = userData.profile;
data.curvePublic = userData.curvePublic;
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', {
tabindex: 1
});
Env.common.displayAvatar($(avatar), userData.avatar, name);
var cancel = h('button.btn.btn-cancel', {
tabindex: 1
}, [
h('i.fa.fa-times'),
Messages.cancel
]);
var submit = h('button.btn.btn-primary', {
tabindex: 1
}, [
h('i.fa.fa-paper-plane-o'),
Messages.comments_submit
]);
$(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.shiftKey) {
$(submit).click();
e.preventDefault();
}
});
setTimeout(function () {
$(textarea).focus();
});
return h('div.cp-comment-form' + (reply ? '.cp-comment-reply' : ''), {
'data-uid': reply || undefined
}, [
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 || {}).data || {});
if (str === Env.oldComments) { return; }
Env.oldComments = str;
var $oldInput = Env.$container.find('.cp-comment-form').detach();
if ($oldInput.length !== 1) { $oldInput = undefined; }
Env.$container.html('');
var show = false;
if ($oldInput && !$oldInput.attr('data-uid')) {
show = true;
Env.$container.append($oldInput);
}
var order = Env.$inner.find('comment').map(function (i, el) {
return el.getAttribute('data-uid');
}).toArray();
var done = [];
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);
if (author.profile) {
$(avatar).click(function (e) {
Env.common.openURL(Hash.hashToHref(author.profile, 'profile'));
e.stopPropagation();
});
}
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', {
tabindex: 1
}, [
h('i.fa.fa-reply'),
Messages.comments_reply
]);
var resolve = h('button.btn.btn-primary', {
tabindex: 1
}, [
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, key, function (val) {
$(form).remove();
$(form).closest('.cp-comment-container')
.find('.cp-comment-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();
});
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(); }
};
$div.on('click focus', function () {
if ($div.hasClass('cp-comment-active')) { return; }
Env.$container.find('.cp-comment-active').removeClass('cp-comment-active');
$div.addClass('cp-comment-active');
div.scrollIntoView();
$actions.css('display', '');
Env.$container.find('.cp-comment-form').remove();
focusContent();
});
if ($oldInput && $oldInput.attr('data-uid') === key) {
$div.addClass('cp-comment-active');
$actions.hide();
$div.append($oldInput);
$oldInput.find('textarea').focus();
focusContent();
}
});
if (show) {
Env.$container.show();
} else {
Env.$container.hide();
}
};
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);
}
if (Env.ready === 0) {
Env.ready = true;
if (changed) {
updateMetadata(Env);
Env.framework.localChange();
}
}
redrawComments(Env);
};
// 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;
// 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 toUncomment = {};
var uids = Env.$inner.find('comment').map(function (i, el) {
var id = el.getAttribute('data-uid');
// Empty comment: remove from dom
if (!el.innerHTML && el.parentElement) {
el.parentElement.removeChild(el);
changed = true;
return;
}
// Comment not in the metadata: uncomment (probably an undo)
var obj = Env.comments.data[id];
if (!obj) {
toUncomment[id] = toUncomment[id] || [];
toUncomment[id].push(el);
changed = true;
return;
}
// If this comment was deleted, we're probably using "undo" to restore it:
// remove the "deleted" state and continue
if (obj.deleted) {
delete obj.deleted;
changed = true;
}
return id;
}).toArray();
if (Object.keys(toUncomment).length) {
Object.keys(toUncomment).forEach(function (id) {
Env.editor.plugins.comments.uncomment(id, toUncomment[id]);
});
}
// 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 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));
};
var addAddCommentHandler = function (Env) {
Env.editor.plugins.comments.addComment = function (uid, addMark) {
if (!Env.ready) { return; }
if (!Env.comments) { Env.comments = Util.clone(COMMENTS); }
// Get all comments ID contained within the selection
var applicable = Env.editor.plugins.comments.isApplicable();
if (!applicable) {
// Abort if our selection contains a comment
console.error("Can't add a comment here");
// XXX show error
UI.warn(Messages.error);
return;
}
Env.$container.find('.cp-comment-form').remove();
var form = getCommentForm(Env, false, function (val) {
$(form).remove();
Env.$inner.focus();
if (!val) { return; }
var applicable = Env.editor.plugins.comments.isApplicable();
if (!applicable) {
// text has been deleted by another user while we were typing our comment?
return void UI.warn(Messages.error);
}
// Don't override existing data
if (Env.comments.data[uid]) { return; }
var myId = updateAuthorData(Env);
Env.comments.data[uid] = {
m: [{
u: myId,
t: +new Date(),
m: val,
v: canonicalize(Env.editor.getSelection().getSelectedText())
}]
};
// 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
updateMetadata(Env);
addMark();
Env.framework.localChange();
Env.oldComments = undefined;
});
Env.$container.prepend(form).show();
};
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);
});
};
var onContentUpdate = function (Env) {
if (!Env.ready) { return; }
// Check deleted
onChange(Env);
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) {
if (data[uid].deleted) { delete data[uid]; }
});
// Commit
updateMetadata(Env);
Env.framework.localChange();
};
Comments.create = function (cfg) {
var Env = cfg;
Env.comments = Util.clone(COMMENTS);
addAddCommentHandler(Env);
// Unselect comment when clicking outside
$(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');
});
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();
});
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 {
onContentUpdate: call(onContentUpdate),
ready: call(ready)
};
};
return Comments;
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -42,7 +42,11 @@
</style> </style>
</head> </head>
<body class="cp-app-pad"> <body class="cp-app-pad">
<div id="cp-app-pad-toolbar" class="cp-toolbar-container"></div>
<div id="cp-app-pad-editor">
<textarea style="display:none" id="editor1" name="editor1"></textarea> <textarea style="display:none" id="editor1" name="editor1"></textarea>
<div id="cp-app-pad-comments"></div>
</div>
</body> </body>
</html> </html>

@ -25,6 +25,7 @@ define([
'/common/TypingTests.js', '/common/TypingTests.js',
'/customize/messages.js', '/customize/messages.js',
'/pad/links.js', '/pad/links.js',
'/pad/comments.js',
'/pad/export.js', '/pad/export.js',
'/pad/cursor.js', '/pad/cursor.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
@ -51,6 +52,7 @@ define([
TypingTest, TypingTest,
Messages, Messages,
Links, Links,
Comments,
Exporter, Exporter,
Cursors, Cursors,
nThen, nThen,
@ -153,8 +155,13 @@ define([
if (hj[0] === 'MEDIA-TAG') { hj[2] = []; } if (hj[0] === 'MEDIA-TAG') { hj[2] = []; }
return hj; return hj;
}; };
var commentActiveFilter = function (hj) {
if (hj[0] === 'COMMENT') { delete (hj[1] || {}).class; }
return hj;
};
brFilter(hj); brFilter(hj);
mediatagContentFilter(hj); mediatagContentFilter(hj);
commentActiveFilter(hj);
widgetFilter(hj); widgetFilter(hj);
return hj; return hj;
}; };
@ -278,6 +285,14 @@ define([
} }
} }
// Don't remote the "active" class of our comments
if (info.node && info.node.tagName === 'COMMENT') {
if (info.diff.action === 'removeAttribute' &&
['class'].indexOf(info.diff.name) !== -1) {
return true;
}
}
if (info.node && info.node.tagName === 'BODY') { if (info.node && info.node.tagName === 'BODY') {
if (info.diff.action === 'removeAttribute' && if (info.diff.action === 'removeAttribute' &&
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) { ['class', 'spellcheck'].indexOf(info.diff.name) !== -1) {
@ -438,18 +453,20 @@ define([
var andThen2 = function (editor, Ckeditor, framework) { var andThen2 = function (editor, Ckeditor, framework) {
var mediaTagMap = {}; var mediaTagMap = {};
var $bar = $('#cke_1_toolbox');
var $contentContainer = $('#cke_1_contents'); var $contentContainer = $('#cke_1_contents');
var $html = $bar.closest('html'); var $html = $('html');
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]');
if ($faLink.length) { if ($faLink.length) {
$html.find('iframe').contents().find('head').append($faLink.clone()); $html.find('iframe').contents().find('head').append($faLink.clone());
} }
var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$;
var ml = editor._.magiclineBackdoor.that.line.$;
[ml, ml.parentElement].forEach(function (el) { [ml, ml.parentElement].forEach(function (el) {
el.setAttribute('class', 'non-realtime'); el.setAttribute('class', 'non-realtime');
}); });
window.editor = editor;
var $iframe = $('html').find('iframe').contents(); var $iframe = $('html').find('iframe').contents();
var ifrWindow = $html.find('iframe')[0].contentWindow; var ifrWindow = $html.find('iframe')[0].contentWindow;
@ -458,9 +475,9 @@ define([
framework._.sfCommon.addShortcuts(ifrWindow); framework._.sfCommon.addShortcuts(ifrWindow);
var privateData = framework._.sfCommon.getMetadataMgr().getPrivateData();
var documentBody = ifrWindow.document.body; var documentBody = ifrWindow.document.body;
var inner = window.inner = documentBody;
var $inner = $(inner);
var observer = new MutationObserver(function (muts) { var observer = new MutationObserver(function (muts) {
muts.forEach(function (mut) { muts.forEach(function (mut) {
@ -480,55 +497,20 @@ define([
childList: true childList: true
}); });
var inner = window.inner = documentBody; var metadataMgr = framework._.sfCommon.getMetadataMgr();
var $inner = $(inner); var privateData = metadataMgr.getPrivateData();
var common = framework._.sfCommon;
var onLinkClicked = function (e) {
var $target = $(e.target); var comments = Comments.create({
if (!$target.is('a')) { return; } framework: framework,
var href = $target.attr('href'); metadataMgr: metadataMgr,
if (!href || href[0] === '#') { return; } common: common,
e.preventDefault(); editor: editor,
e.stopPropagation(); ifrWindow: ifrWindow,
$iframe: $iframe,
var rect = e.target.getBoundingClientRect(); $inner: $inner,
var rect0 = inner.getBoundingClientRect(); $contentContainer: $contentContainer,
var l = (rect.left - rect0.left)+'px'; $container: $('#cp-app-pad-comments')
var t = rect.bottom + $iframe.scrollTop() +'px';
var a = h('a', { href: href}, href);
var link = h('div.cp-link-clicked.non-realtime', {
contenteditable: false,
style: 'top:'+t+';left:'+l
}, [ a ]);
var $link = $(link);
$inner.append(link);
if (rect.left + $link.outerWidth() - rect0.left > $inner.width()) {
$link.css('left', 'unset');
$link.css('right', 0);
}
$(a).click(function (ee) {
ee.preventDefault();
ee.stopPropagation();
framework._.sfCommon.openUnsafeURL(href);
$link.remove();
});
$link.on('mouseleave', function () {
$link.remove();
});
};
var removeClickedLink = function () {
$inner.find('.cp-link-clicked').remove();
};
$inner.click(function (e) {
if (e.target.nodeName.toUpperCase() === 'A') {
removeClickedLink();
return void onLinkClicked(e);
}
removeClickedLink();
}); });
// My cursor // My cursor
@ -647,6 +629,8 @@ define([
// off so that we don't end up with multiple identical handlers // off so that we don't end up with multiple identical handlers
$links.off('click', openLink).on('click', openLink); $links.off('click', openLink).on('click', openLink);
} }
comments.onContentUpdate();
}); });
framework.setTextContentGetter(function () { framework.setTextContentGetter(function () {
@ -669,6 +653,8 @@ define([
// the text nodes and OT/ChainPad would freak out // the text nodes and OT/ChainPad would freak out
cursors.removeCursors(inner); cursors.removeCursors(inner);
comments.onContentUpdate();
displayMediaTags(framework, inner, mediaTagMap); displayMediaTags(framework, inner, mediaTagMap);
inner.normalize(); inner.normalize();
var hjson = Hyperjson.fromDOM(inner, shouldSerialize, hjsonFilters); var hjson = Hyperjson.fromDOM(inner, shouldSerialize, hjsonFilters);
@ -676,7 +662,6 @@ define([
return hjson; return hjson;
}); });
$bar.find('#cke_1_toolbar_collapser').hide();
if (!framework.isReadOnly()) { if (!framework.isReadOnly()) {
addToolbarHideBtn(framework, $contentContainer); addToolbarHideBtn(framework, $contentContainer);
} else { } else {
@ -735,7 +720,7 @@ define([
framework._.sfCommon.getAttribute(['pad', 'width'], function (err, data) { framework._.sfCommon.getAttribute(['pad', 'width'], function (err, data) {
var active = data || typeof(data) === "undefined"; var active = data || typeof(data) === "undefined";
if (active) { if (active) {
$iframe.find('html').addClass('cke_body_width'); $contentContainer.addClass('cke_body_width');
} }
var $width = framework._.sfCommon.createButton('', true, { var $width = framework._.sfCommon.createButton('', true, {
icon: 'fa-arrows-h', icon: 'fa-arrows-h',
@ -743,9 +728,9 @@ define([
name: "pad-width", name: "pad-width",
},function () { },function () {
if (active) { if (active) {
$iframe.find('html').removeClass('cke_body_width'); $contentContainer.removeClass('cke_body_width');
} else { } else {
$iframe.find('html').addClass('cke_body_width'); $contentContainer.addClass('cke_body_width');
} }
active = !active; active = !active;
var key = active ? Messages.pad_useFullWidth : Messages.pad_usePageWidth; var key = active ? Messages.pad_useFullWidth : Messages.pad_usePageWidth;
@ -786,6 +771,9 @@ define([
}); });
} }
}); });
comments.ready();
/*setTimeout(function () { /*setTimeout(function () {
$('iframe.cke_wysiwyg_frame').focus(); $('iframe.cke_wysiwyg_frame').focus();
editor.focus(); editor.focus();
@ -918,8 +906,8 @@ define([
nThen(function (waitFor) { nThen(function (waitFor) {
Framework.create({ Framework.create({
toolbarContainer: '#cke_1_toolbox', toolbarContainer: '#cp-app-pad-toolbar',
contentContainer: '#cke_editor1 > .cke_inner', contentContainer: '#cp-app-pad-editor',
patchTransformer: ChainPad.NaiveJSONTransformer, patchTransformer: ChainPad.NaiveJSONTransformer,
/*thumbnail: { /*thumbnail: {
getContainer: function () { return $('iframe').contents().find('html')[0]; }, getContainer: function () { return $('iframe').contents().find('html')[0]; },
@ -963,14 +951,6 @@ define([
} }
// Used in ckeditor-config.js // Used in ckeditor-config.js
Ckeditor.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs; Ckeditor.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs;
var backColor = AppConfig.appBackgroundColor;
var newCss = '.cke_body_width { background: '+ backColor +'; height: 100%; overflow: auto;}' +
'.cke_body_width body {' +
'max-width: 50em; padding: 20px 30px; margin: 0 auto; min-height: 100%;'+
'box-sizing: border-box; overflow: auto;'+
'}' +
'.cke_body_width body > *:first-child { margin-top: 0; }';
Ckeditor.addCss(newCss);
Ckeditor._mediatagTranslations = { Ckeditor._mediatagTranslations = {
title: Messages.pad_mediatagTitle, title: Messages.pad_mediatagTitle,
width: Messages.pad_mediatagWidth, width: Messages.pad_mediatagWidth,
@ -981,8 +961,13 @@ define([
'import': Messages.pad_mediatagImport, 'import': Messages.pad_mediatagImport,
options: Messages.pad_mediatagOptions options: Messages.pad_mediatagOptions
}; };
Messages.comments_comment = "COMMENT"; // XXX
Ckeditor._commentsTranslations = {
comment: Messages.comments_comment,
};
Ckeditor.plugins.addExternal('mediatag','/pad/', 'mediatag-plugin.js'); Ckeditor.plugins.addExternal('mediatag','/pad/', 'mediatag-plugin.js');
Ckeditor.plugins.addExternal('blockbase64','/pad/', 'disable-base64.js'); Ckeditor.plugins.addExternal('blockbase64','/pad/', 'disable-base64.js');
Ckeditor.plugins.addExternal('comments','/pad/', 'comment.js');
Ckeditor.plugins.addExternal('wordcount','/pad/wordcount/', 'plugin.js'); Ckeditor.plugins.addExternal('wordcount','/pad/wordcount/', 'plugin.js');
module.ckeditor = editor = Ckeditor.replace('editor1', { module.ckeditor = editor = Ckeditor.replace('editor1', {
customConfig: '/customize/ckeditor-config.js', customConfig: '/customize/ckeditor-config.js',
@ -992,16 +977,14 @@ define([
editor.plugins.mediatag.import = function ($mt) { editor.plugins.mediatag.import = function ($mt) {
framework._.sfCommon.importMediaTag($mt); framework._.sfCommon.importMediaTag($mt);
}; };
Links.addSupportForOpeningLinksInNewTab(Ckeditor)({editor: editor}); Links.init(Ckeditor, editor);
}).nThen(function () { }).nThen(function () {
// Move ckeditor parts to have a structure like the other apps // Move ckeditor parts to have a structure like the other apps
var $toolbarContainer = $('#cke_1_top');
var $contentContainer = $('#cke_1_contents'); var $contentContainer = $('#cke_1_contents');
var $mainContainer = $('#cke_editor1'); var $mainContainer = $('#cke_editor1 > .cke_inner');
$contentContainer.prepend($toolbarContainer.find('.cke_toolbox_main')); var $ckeToolbar = $('#cke_1_top').find('.cke_toolbox_main');
$mainContainer.prepend($toolbarContainer); $mainContainer.prepend($ckeToolbar.addClass('cke_reset_all'));
$contentContainer.find('.cke_toolbox_main').addClass('cke_reset_all'); $contentContainer.append(h('div#cp-app-pad-comments'));
$toolbarContainer.removeClass('cke_reset_all');
}).nThen(waitFor()); }).nThen(waitFor());
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {

@ -1,14 +1,74 @@
define(['/customize/messages.js'], function (Messages) { define([
// Adds a context menu entry to open the selected link in a new tab. 'jquery',
// See https://github.com/xwiki-contrib/application-ckeditor/commit/755d193497bf23ed874d874b4ae92fbee887fc10 '/common/hyperscript.js',
'/customize/messages.js'
], function ($, h, Messages) {
var onLinkClicked = function (e, inner) {
var $target = $(e.target);
if (!$target.is('a')) { return; }
var href = $target.attr('href');
if (!href || href[0] === '#') { return; }
e.preventDefault();
e.stopPropagation();
var $iframe = $('html').find('iframe').contents();
var $inner = $(inner);
var rect = e.target.getBoundingClientRect();
var rect0 = inner.getBoundingClientRect();
var l = (rect.left - rect0.left)+'px';
var t = rect.bottom + $iframe.scrollTop() +'px';
var a = h('a', { href: href}, href);
var link = h('div.cp-link-clicked.non-realtime', {
contenteditable: false,
style: 'top:'+t+';left:'+l
}, [ a ]);
var $link = $(link);
$inner.append(link);
if (rect.left + $link.outerWidth() - rect0.left > $inner.width()) {
$link.css('left', 'unset');
$link.css('right', 0);
}
$(a).click(function (ee) {
ee.preventDefault();
ee.stopPropagation();
var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(href);
window.open(bounceHref);
$link.remove();
});
$link.on('mouseleave', function () {
$link.remove();
});
};
var removeClickedLink = function ($inner) {
$inner.find('.cp-link-clicked').remove();
};
return { return {
addSupportForOpeningLinksInNewTab : function (Ckeditor) { init : function (Ckeditor, editor) {
// Returns the DOM element of the active (currently focused) link. It has also support for linked image widgets. if (!Ckeditor.plugins.link) { return; }
// @return {CKEDITOR.dom.element}
var getActiveLink = function(editor) { var inner = editor.document.$.body;
var anchor = Ckeditor.plugins.link.getSelectedLink(editor), var $inner = $(inner);
// We need to do some special checking against widgets availability. // Bubble to open the link in a new tab
activeWidget = editor.widgets && editor.widgets.focused; $inner.click(function (e) {
removeClickedLink($inner);
if (e.target.nodeName.toUpperCase() === 'A') {
return void onLinkClicked(e, inner);
}
});
// Adds a context menu entry to open the selected link in a new tab.
var getActiveLink = function() {
var anchor = Ckeditor.plugins.link.getSelectedLink(editor);
// We need to do some special checking against widgets availability.
var activeWidget = editor.widgets && editor.widgets.focused;
// If default way of getting links didn't return anything useful.. // If default way of getting links didn't return anything useful..
if (!anchor && activeWidget && activeWidget.name === 'image' && activeWidget.parts.link) { if (!anchor && activeWidget && activeWidget.name === 'image' && activeWidget.parts.link) {
// Since CKEditor 4.4.0 image widgets may be linked. // Since CKEditor 4.4.0 image widgets may be linked.
@ -17,44 +77,38 @@ define(['/customize/messages.js'], function (Messages) {
return anchor; return anchor;
}; };
return function(event) { editor.addCommand( 'openLink', {
var editor = event.editor; exec: function() {
if (!Ckeditor.plugins.link) { var anchor = getActiveLink();
return; if (anchor) {
} var href = anchor.getAttribute('href');
editor.addCommand( 'openLink', { if (href) {
exec: function(editor) { var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(href);
var anchor = getActiveLink(editor); window.open(bounceHref);
if (anchor) {
var href = anchor.getAttribute('href');
if (href) {
var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(href);
window.open(bounceHref);
}
} }
} }
});
if (typeof editor.addMenuItem === 'function') {
editor.addMenuItem('openLink', {
label: Messages.openLinkInNewTab,
command: 'openLink',
group: 'link',
order: -1
});
} }
if (editor.contextMenu) { });
editor.contextMenu.addListener(function(startElement) { if (typeof editor.addMenuItem === 'function') {
if (startElement) { editor.addMenuItem('openLink', {
var anchor = getActiveLink(editor); label: Messages.openLinkInNewTab,
if (anchor && anchor.getAttribute('href')) { command: 'openLink',
return {openLink: Ckeditor.TRISTATE_OFF}; group: 'link',
} order: -1
});
}
if (editor.contextMenu) {
editor.contextMenu.addListener(function(startElement) {
if (startElement) {
var anchor = getActiveLink();
if (anchor && anchor.getAttribute('href')) {
return {openLink: Ckeditor.TRISTATE_OFF};
} }
}); }
editor.contextMenu._.panelDefinition.css.push('.cke_button__openLink_icon {' + });
Ckeditor.skin.getIconStyle('link') + '}'); editor.contextMenu._.panelDefinition.css.push('.cke_button__openLink_icon {' +
} Ckeditor.skin.getIconStyle('link') + '}');
}; }
} }
}; };
}); });

Loading…
Cancel
Save