manual merge of staging
This commit is contained in:
@@ -30,6 +30,7 @@ body {
|
||||
min-width: 20%;
|
||||
max-width: 80%;
|
||||
resize: horizontal;
|
||||
font-size: initial;
|
||||
}
|
||||
.CodeMirror.fullPage {
|
||||
//min-width: 100%;
|
||||
@@ -84,7 +85,6 @@ body {
|
||||
width: 8px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 9999;
|
||||
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
@@ -61,19 +61,28 @@ define([
|
||||
$iframe.find('.CodeMirror').addClass('fullPage');
|
||||
editor = CodeMirror.editor;
|
||||
|
||||
var setIndentation = APP.setIndentation = function (units) {
|
||||
var setIndentation = APP.setIndentation = function (units, useTabs) {
|
||||
if (typeof(units) !== 'number') { return; }
|
||||
editor.setOption('indentUnit', units);
|
||||
editor.setOption('tabSize', units);
|
||||
//editor.setOption('indentWithTabs', true);
|
||||
editor.setOption('indentWithTabs', useTabs);
|
||||
};
|
||||
|
||||
var indentKey = 'cryptpad.indentUnit';
|
||||
var useTabsKey = 'cryptpad.indentWithTabs';
|
||||
|
||||
var proxy = Cryptpad.getProxy();
|
||||
proxy.on('change', [indentKey], function (o, n) {
|
||||
APP.setIndentation(n);
|
||||
});
|
||||
setIndentation(proxy[indentKey]);
|
||||
|
||||
var updateIndentSettings = function () {
|
||||
var indentUnit = proxy[indentKey];
|
||||
var useTabs = proxy[useTabsKey];
|
||||
setIndentation(
|
||||
typeof(indentUnit) === 'number'? indentUnit: 2,
|
||||
typeof(useTabs) === 'boolean'? useTabs: false);
|
||||
};
|
||||
|
||||
proxy.on('change', [indentKey], updateIndentSettings);
|
||||
proxy.on('change', [useTabsKey], updateIndentSettings);
|
||||
|
||||
var $bar = $('#pad-iframe')[0].contentWindow.$('#cme_toolbox');
|
||||
|
||||
@@ -401,6 +410,7 @@ define([
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
// add the splitter
|
||||
if (!$iframe.has('.cp-splitter').length) {
|
||||
var $preview = $iframe.find('#previewContainer');
|
||||
@@ -428,6 +438,7 @@ define([
|
||||
});
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
Cryptpad.removeLoadingScreen();
|
||||
setEditable(true);
|
||||
|
||||
@@ -301,7 +301,16 @@ define([
|
||||
// Tooltips
|
||||
|
||||
UI.clearTooltips = function () {
|
||||
$('.tippy-popper').remove();
|
||||
// If an element is removed from the UI while a tooltip is applied on that element, the tooltip will get hung
|
||||
// forever, this is a solution which just searches for tooltips which have no corrisponding element and removes
|
||||
// them.
|
||||
var win;
|
||||
$('.tippy-popper').each(function (i, el) {
|
||||
win = win || $('#pad-iframe')[0].contentWindow;
|
||||
if (win.$('[aria-describedby=' + el.getAttribute('id') + ']').length === 0) {
|
||||
el.remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
UI.addTooltips = function () {
|
||||
|
||||
@@ -2,8 +2,42 @@ define([
|
||||
'jquery',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/common/curve.js',
|
||||
'/common/common-hash.js',
|
||||
|
||||
'/bower_components/marked/marked.min.js',
|
||||
], function ($, Crypto, Curve, Marked) {
|
||||
'/common/common-realtime.js',
|
||||
|
||||
// displayAvatar
|
||||
// whenRealtimeSyncs
|
||||
// getRealtime -> removeFromFriendList
|
||||
/* UI
|
||||
Messages
|
||||
confirm
|
||||
fixHTML
|
||||
displayAvatar
|
||||
clearOwnedChannel
|
||||
alert
|
||||
|
||||
|
||||
pushMsg
|
||||
removeFromFriendList
|
||||
|
||||
onDirectMessage
|
||||
getNetwork
|
||||
getProxy
|
||||
pushMsg
|
||||
|
||||
Init
|
||||
getNetwork
|
||||
getProxy
|
||||
onDirectMessage
|
||||
removeFromFriendList
|
||||
notify
|
||||
onMessage
|
||||
|
||||
*/
|
||||
|
||||
], function ($, Crypto, Curve, Hash, Marked, Realtime) {
|
||||
var Msg = {
|
||||
inputs: [],
|
||||
};
|
||||
@@ -28,11 +62,10 @@ define([
|
||||
return Marked(content);
|
||||
};
|
||||
|
||||
var createData = Msg.createData = function (common, hash) {
|
||||
var proxy = common.getProxy();
|
||||
var createData = Msg.createData = function (proxy, hash) {
|
||||
return {
|
||||
channel: hash || common.createChannelId(),
|
||||
displayName: proxy[common.displayNameKey],
|
||||
channel: hash || Hash.createChannelId(),
|
||||
displayName: proxy['cryptpad.username'],
|
||||
profile: proxy.profile && proxy.profile.view,
|
||||
edPublic: proxy.edPublic,
|
||||
curvePublic: proxy.curvePublic,
|
||||
@@ -40,39 +73,39 @@ define([
|
||||
};
|
||||
};
|
||||
|
||||
var getFriend = function (common, pubkey) {
|
||||
var proxy = common.getProxy();
|
||||
var getFriend = function (proxy, pubkey) {
|
||||
if (pubkey === proxy.curvePublic) {
|
||||
var data = createData(common);
|
||||
var data = createData(proxy);
|
||||
delete data.channel;
|
||||
return data;
|
||||
}
|
||||
return proxy.friends ? proxy.friends[pubkey] : undefined;
|
||||
};
|
||||
|
||||
var removeFromFriendList = Msg.removeFromFriendList = function (common, curvePublic, cb) {
|
||||
var proxy = common.getProxy();
|
||||
if (!proxy.friends) {
|
||||
return;
|
||||
}
|
||||
var removeFromFriendList = function (proxy, realtime, curvePublic, cb) {
|
||||
if (!proxy.friends) { return; }
|
||||
var friends = proxy.friends;
|
||||
delete friends[curvePublic];
|
||||
common.whenRealtimeSyncs(common.getRealtime(), cb);
|
||||
Realtime.whenRealtimeSyncs(realtime, cb);
|
||||
};
|
||||
|
||||
// TODO set this up as an observable data structure
|
||||
var getFriendList = Msg.getFriendList = function (common) {
|
||||
var proxy = common.getProxy();
|
||||
var getFriendList = Msg.getFriendList = function (proxy) {
|
||||
if (!proxy.friends) { proxy.friends = {}; }
|
||||
return proxy.friends;
|
||||
};
|
||||
|
||||
Msg.getFriendChannelsList = function (common) {
|
||||
var friends = getFriendList(common);
|
||||
var eachFriend = function (friends, cb) {
|
||||
Object.keys(friends).forEach(function (id) {
|
||||
if (id === 'me') { return; }
|
||||
cb(friends[id], id, friends);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Msg.getFriendChannelsList = function (proxy) {
|
||||
var list = [];
|
||||
Object.keys(friends).forEach(function (key) {
|
||||
if (key === "me") { return; }
|
||||
list.push(friends[key].channel);
|
||||
eachFriend(proxy, function (friend) {
|
||||
list.push(friend.channel);
|
||||
});
|
||||
return list;
|
||||
};
|
||||
@@ -85,131 +118,6 @@ define([
|
||||
|
||||
var UI = Msg.UI = {};
|
||||
|
||||
// TODO extract into UI method
|
||||
var createChatBox = function (common, $container, curvePublic, ui) {
|
||||
var data = getFriend(common, curvePublic);
|
||||
|
||||
// Input
|
||||
var channel = channels[data.channel];
|
||||
|
||||
var $header = $('<div>', {
|
||||
'class': 'header',
|
||||
}).appendTo($container);
|
||||
|
||||
var $avatar = $('<div>', {'class': 'avatar'}).appendTo($header);
|
||||
|
||||
// more history...
|
||||
$('<span>', {
|
||||
'class': 'more-history',
|
||||
})
|
||||
.text('get more history')
|
||||
.click(function () {
|
||||
console.log("GETTING HISTORY");
|
||||
channel.getPreviousMessages();
|
||||
})
|
||||
.appendTo($header);
|
||||
|
||||
var $removeHistory = $('<span>', {
|
||||
'class': 'remove-history fa fa-eraser',
|
||||
title: common.Messages.contacts_removeHistoryTitle
|
||||
})
|
||||
.click(function () {
|
||||
common.confirm(common.Messages.contacts_confirmRemoveHistory, function (yes) {
|
||||
if (!yes) { return; }
|
||||
common.clearOwnedChannel(data.channel, function (e) {
|
||||
if (e) {
|
||||
console.error(e);
|
||||
common.alert(common.Messages.contacts_removeHistoryServerError);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$removeHistory.appendTo($header);
|
||||
|
||||
$('<div>', {'class': 'messages'}).appendTo($container);
|
||||
var $inputBlock = $('<div>', {'class': 'input'}).appendTo($container);
|
||||
|
||||
var $input = $('<textarea>').appendTo($inputBlock);
|
||||
$input.attr('placeholder', common.Messages.contacts_typeHere);
|
||||
ui.input = $input[0];
|
||||
|
||||
var send = function () {
|
||||
// TODO implement sending queue
|
||||
// TODO separate message logic from UI
|
||||
var channel = channels[data.channel];
|
||||
if (channel.sending) {
|
||||
console.error("still sending");
|
||||
return;
|
||||
}
|
||||
if (!$input.val()) {
|
||||
console.error("nothing to send");
|
||||
return;
|
||||
}
|
||||
if ($input.attr('disabled')) {
|
||||
console.error("input is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = $input.val();
|
||||
// Send the message
|
||||
channel.sending = true;
|
||||
channel.send(payload, function (e) {
|
||||
if (e) {
|
||||
channel.sending = false;
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
$input.val('');
|
||||
channel.refresh();
|
||||
channel.sending = false;
|
||||
});
|
||||
};
|
||||
$('<button>', {
|
||||
'class': 'btn btn-primary fa fa-paper-plane',
|
||||
title: common.Messages.contacts_send,
|
||||
}).appendTo($inputBlock).click(send);
|
||||
|
||||
var onKeyDown = function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
if (e.ctrlKey || e.shiftKey) {
|
||||
var val = this.value;
|
||||
if (typeof this.selectionStart === "number" && typeof this.selectionEnd === "number") {
|
||||
var start = this.selectionStart;
|
||||
this.value = val.slice(0, start) + "\n" + val.slice(this.selectionEnd);
|
||||
this.selectionStart = this.selectionEnd = start + 1;
|
||||
} else if (document.selection && document.selection.createRange) {
|
||||
this.focus();
|
||||
var range = document.selection.createRange();
|
||||
range.text = "\r\n";
|
||||
range.collapse(false);
|
||||
range.select();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
send();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
$input.on('keydown', onKeyDown);
|
||||
|
||||
// Header
|
||||
var $rightCol = $('<span>', {'class': 'right-col'});
|
||||
$('<span>', {'class': 'name'}).text(data.displayName).appendTo($rightCol);
|
||||
if (data.avatar && avatars[data.avatar]) {
|
||||
$avatar.append(avatars[data.avatar]);
|
||||
$avatar.append($rightCol);
|
||||
} else {
|
||||
common.displayAvatar($avatar, data.avatar, data.displayName, function ($img) {
|
||||
if (data.avatar && $img) {
|
||||
avatars[data.avatar] = $img[0].outerHTML;
|
||||
}
|
||||
$avatar.append($rightCol);
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
UI.init = function (common, $listContainer, $msgContainer) {
|
||||
var ui = {
|
||||
containers: {
|
||||
@@ -260,9 +168,8 @@ define([
|
||||
|
||||
ui.createFriendList = function (friends, display, remove) {
|
||||
var $block = ui.containers.friendBlock = $('<div>');
|
||||
Object.keys(friends).forEach(function (f) {
|
||||
if (f === 'me') { return; }
|
||||
ui.addToFriendList(friends[f], display, remove);
|
||||
eachFriend(friends, function (friend) {
|
||||
ui.addToFriendList(friend, display, remove);
|
||||
});
|
||||
$block.appendTo($listContainer);
|
||||
};
|
||||
@@ -282,7 +189,8 @@ define([
|
||||
};
|
||||
|
||||
ui.update = function (curvePublic, types) {
|
||||
var data = getFriend(common, curvePublic);
|
||||
var proxy = common.getProxy();
|
||||
var data = getFriend(proxy, curvePublic);
|
||||
var chan = channels[data.channel];
|
||||
if (!chan.ready) {
|
||||
chan.updateOnReady = (chan.updateOnReady || []).concat(types);
|
||||
@@ -311,7 +219,7 @@ define([
|
||||
|
||||
ui.updateStatus = function (curvePublic, online) {
|
||||
ui.getFriend(curvePublic).find('.status')
|
||||
.attr('class', 'status ' + online? 'online' : 'offline');
|
||||
.attr('class', 'status ' + (online? 'online' : 'offline'));
|
||||
};
|
||||
|
||||
ui.getChannel = function (curvePublic) {
|
||||
@@ -384,6 +292,129 @@ define([
|
||||
}
|
||||
};
|
||||
|
||||
ui.createChatBox = function (proxy, $container, curvePublic) {
|
||||
var data = getFriend(proxy, curvePublic);
|
||||
|
||||
// Input
|
||||
var channel = channels[data.channel];
|
||||
|
||||
var $header = $('<div>', {
|
||||
'class': 'header',
|
||||
}).appendTo($container);
|
||||
|
||||
var $avatar = $('<div>', {'class': 'avatar'}).appendTo($header);
|
||||
|
||||
// more history...
|
||||
$('<span>', {
|
||||
'class': 'more-history',
|
||||
})
|
||||
.text('get more history')
|
||||
.click(function () {
|
||||
console.log("GETTING HISTORY");
|
||||
channel.getPreviousMessages();
|
||||
})
|
||||
.appendTo($header);
|
||||
|
||||
var $removeHistory = $('<span>', {
|
||||
'class': 'remove-history fa fa-eraser',
|
||||
title: common.Messages.contacts_removeHistoryTitle
|
||||
})
|
||||
.click(function () {
|
||||
common.confirm(common.Messages.contacts_confirmRemoveHistory, function (yes) {
|
||||
if (!yes) { return; }
|
||||
common.clearOwnedChannel(data.channel, function (e) {
|
||||
if (e) {
|
||||
console.error(e);
|
||||
common.alert(common.Messages.contacts_removeHistoryServerError);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
$removeHistory.appendTo($header);
|
||||
|
||||
$('<div>', {'class': 'messages'}).appendTo($container);
|
||||
var $inputBlock = $('<div>', {'class': 'input'}).appendTo($container);
|
||||
|
||||
var $input = $('<textarea>').appendTo($inputBlock);
|
||||
$input.attr('placeholder', common.Messages.contacts_typeHere);
|
||||
ui.input = $input[0];
|
||||
|
||||
var send = function () {
|
||||
// TODO implement sending queue
|
||||
// TODO separate message logic from UI
|
||||
var channel = channels[data.channel];
|
||||
if (channel.sending) {
|
||||
console.error("still sending");
|
||||
return;
|
||||
}
|
||||
if (!$input.val()) {
|
||||
console.error("nothing to send");
|
||||
return;
|
||||
}
|
||||
if ($input.attr('disabled')) {
|
||||
console.error("input is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = $input.val();
|
||||
// Send the message
|
||||
channel.sending = true;
|
||||
channel.send(payload, function (e) {
|
||||
if (e) {
|
||||
channel.sending = false;
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
$input.val('');
|
||||
channel.refresh();
|
||||
channel.sending = false;
|
||||
});
|
||||
};
|
||||
$('<button>', {
|
||||
'class': 'btn btn-primary fa fa-paper-plane',
|
||||
title: common.Messages.contacts_send,
|
||||
}).appendTo($inputBlock).click(send);
|
||||
|
||||
var onKeyDown = function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
if (e.ctrlKey || e.shiftKey) {
|
||||
var val = this.value;
|
||||
if (typeof this.selectionStart === "number" && typeof this.selectionEnd === "number") {
|
||||
var start = this.selectionStart;
|
||||
this.value = val.slice(0, start) + "\n" + val.slice(this.selectionEnd);
|
||||
this.selectionStart = this.selectionEnd = start + 1;
|
||||
} else if (document.selection && document.selection.createRange) {
|
||||
this.focus();
|
||||
var range = document.selection.createRange();
|
||||
range.text = "\r\n";
|
||||
range.collapse(false);
|
||||
range.select();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
send();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
$input.on('keydown', onKeyDown);
|
||||
|
||||
// Header
|
||||
var $rightCol = $('<span>', {'class': 'right-col'});
|
||||
$('<span>', {'class': 'name'}).text(data.displayName).appendTo($rightCol);
|
||||
if (data.avatar && avatars[data.avatar]) {
|
||||
$avatar.append(avatars[data.avatar]);
|
||||
$avatar.append($rightCol);
|
||||
} else {
|
||||
common.displayAvatar($avatar, data.avatar, data.displayName, function ($img) {
|
||||
if (data.avatar && $img) {
|
||||
avatars[data.avatar] = $img[0].outerHTML;
|
||||
}
|
||||
$avatar.append($rightCol);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return ui;
|
||||
};
|
||||
|
||||
@@ -393,7 +424,8 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
var pushMsg = function (common, channel, cryptMsg) {
|
||||
// TODO remove dependency on common
|
||||
var pushMsg = function (realtime, proxy, common, channel, cryptMsg) {
|
||||
var msg = channel.encryptor.decrypt(cryptMsg);
|
||||
|
||||
var sig = cryptMsg.slice(0, 64);
|
||||
@@ -413,12 +445,10 @@ define([
|
||||
channel.messages.push(res);
|
||||
return true;
|
||||
}
|
||||
var proxy;
|
||||
if (parsedMsg[0] === Types.update) {
|
||||
proxy = common.getProxy();
|
||||
if (parsedMsg[1] === common.getProxy().curvePublic) { return; }
|
||||
if (parsedMsg[1] === proxy.curvePublic) { return; }
|
||||
var newdata = parsedMsg[3];
|
||||
var data = getFriend(common, parsedMsg[1]);
|
||||
var data = getFriend(proxy, parsedMsg[1]);
|
||||
var types = [];
|
||||
Object.keys(newdata).forEach(function (k) {
|
||||
if (data[k] !== newdata[k]) {
|
||||
@@ -430,11 +460,7 @@ define([
|
||||
return;
|
||||
}
|
||||
if (parsedMsg[0] === Types.unfriend) {
|
||||
proxy = common.getProxy();
|
||||
|
||||
// FIXME pushMsg shouldn't need access to common
|
||||
// implement this as a callback or use some other API
|
||||
removeFromFriendList(common, channel.friendEd, function () {
|
||||
removeFromFriendList(proxy, realtime, channel.friendEd, function () {
|
||||
channel.wc.leave(Types.unfriend);
|
||||
channel.removeUI();
|
||||
});
|
||||
@@ -444,10 +470,10 @@ define([
|
||||
|
||||
/* Broadcast a display name, profile, or avatar change to all contacts
|
||||
*/
|
||||
var updateMyData = function (common) {
|
||||
var friends = getFriendList(common);
|
||||
var updateMyData = function (proxy) {
|
||||
var friends = getFriendList(proxy);
|
||||
var mySyncData = friends.me;
|
||||
var myData = createData(common);
|
||||
var myData = createData(proxy);
|
||||
if (!mySyncData || mySyncData.displayName !== myData.displayName
|
||||
|| mySyncData.profile !== myData.profile
|
||||
|| mySyncData.avatar !== myData.avatar) {
|
||||
@@ -467,20 +493,20 @@ define([
|
||||
}
|
||||
};
|
||||
|
||||
var onChannelReady = function (common, chanId) {
|
||||
var onChannelReady = function (proxy, chanId) {
|
||||
if (ready.indexOf(chanId) !== -1) { return; }
|
||||
ready.push(chanId);
|
||||
channels[chanId].updateStatus(); // c'est quoi?
|
||||
var friends = getFriendList(common);
|
||||
var friends = getFriendList(proxy);
|
||||
if (ready.length === Object.keys(friends).length) {
|
||||
// All channels are ready
|
||||
updateMyData(common);
|
||||
updateMyData(proxy);
|
||||
}
|
||||
return ready.length;
|
||||
};
|
||||
|
||||
// Id message allows us to map a netfluxId with a public curve key
|
||||
var onIdMessage = function (common, msg, sender) {
|
||||
var onIdMessage = function (proxy, network, msg, sender) {
|
||||
var channel;
|
||||
var isId = Object.keys(channels).some(function (chanId) {
|
||||
if (channels[chanId].userList.indexOf(sender) !== -1) {
|
||||
@@ -516,6 +542,9 @@ define([
|
||||
return;
|
||||
}
|
||||
if (parsed[0] !== Types.mapId && parsed[0] !== Types.mapIdAck) { return; }
|
||||
|
||||
// check that the responding peer's encrypted netflux id matches
|
||||
// the sender field. This is to prevent replay attacks.
|
||||
if (parsed[2] !== sender || !parsed[1]) { return; }
|
||||
channel.mapId[sender] = parsed[1];
|
||||
|
||||
@@ -523,15 +552,19 @@ define([
|
||||
|
||||
if (parsed[0] !== Types.mapId) { return; } // Don't send your key if it's already an ACK
|
||||
// Answer with your own key
|
||||
var proxy = common.getProxy();
|
||||
var network = common.getNetwork();
|
||||
var rMsg = [Types.mapIdAck, proxy.curvePublic, channel.wc.myID];
|
||||
var rMsgStr = JSON.stringify(rMsg);
|
||||
var cryptMsg = channel.encryptor.encrypt(rMsgStr);
|
||||
network.sendto(sender, cryptMsg);
|
||||
};
|
||||
|
||||
// HERE
|
||||
var onDirectMessage = function (common, msg, sender) {
|
||||
if (sender !== Msg.hk) { return void onIdMessage(common, msg, sender); }
|
||||
var proxy = common.getProxy();
|
||||
var network = common.getNetwork();
|
||||
var realtime = common.getRealtime();
|
||||
|
||||
if (sender !== Msg.hk) { return void onIdMessage(proxy, network, msg, sender); }
|
||||
var parsed = JSON.parse(msg);
|
||||
if ((parsed.validateKey || parsed.owners) && parsed.channel) {
|
||||
return;
|
||||
@@ -542,7 +575,7 @@ define([
|
||||
// TODO: call a function that shows that the channel is ready? (remove a spinner, ...)
|
||||
// channel[parsed.channel].ready();
|
||||
channels[parsed.channel].ready = true;
|
||||
onChannelReady(common, parsed.channel);
|
||||
onChannelReady(proxy, parsed.channel);
|
||||
var updateTypes = channels[parsed.channel].updateOnReady;
|
||||
if (updateTypes) {
|
||||
channels[parsed.channel].updateUI(updateTypes);
|
||||
@@ -552,12 +585,16 @@ define([
|
||||
}
|
||||
var chan = parsed[3];
|
||||
if (!chan || !channels[chan]) { return; }
|
||||
pushMsg(common, channels[chan], parsed[4]);
|
||||
pushMsg(realtime, proxy, common, channels[chan], parsed[4]);
|
||||
channels[chan].refresh();
|
||||
};
|
||||
var onMessage = function (common, msg, sender, chan) {
|
||||
if (!channels[chan.id]) { return; }
|
||||
var isMessage = pushMsg(common, channels[chan.id], msg);
|
||||
|
||||
var realtime = common.getRealtime();
|
||||
var proxy = common.getProxy();
|
||||
|
||||
var isMessage = pushMsg(realtime, proxy, common, channels[chan.id], msg);
|
||||
if (isMessage) {
|
||||
// Don't notify for your own messages
|
||||
if (channels[chan.id].wc.myID !== sender) {
|
||||
@@ -602,12 +639,15 @@ define([
|
||||
});
|
||||
};
|
||||
|
||||
/* TODO remove dependency on common
|
||||
*/
|
||||
Msg.init = function (common, ui) {
|
||||
// declare common variables
|
||||
var network = common.getNetwork();
|
||||
var proxy = common.getProxy();
|
||||
var realtime = common.getRealtime();
|
||||
Msg.hk = network.historyKeeper;
|
||||
var friends = getFriendList(common);
|
||||
var friends = getFriendList(proxy);
|
||||
|
||||
// listen for messages...
|
||||
network.on('message', function(msg, sender) {
|
||||
@@ -645,7 +685,7 @@ define([
|
||||
for (var i = last + 1; i<messages.length; i++) {
|
||||
msg = messages[i];
|
||||
name = (msg.channel !== channel.lastSender)?
|
||||
getFriend(common, msg.channel).displayName: undefined;
|
||||
getFriend(proxy, msg.channel).displayName: undefined;
|
||||
|
||||
ui.createMessage(msg, name).appendTo($messages);
|
||||
channel.lastSender = msg.channel;
|
||||
@@ -667,7 +707,7 @@ define([
|
||||
var $chat = ui.getChannel(curvePublic);
|
||||
if (!$chat) {
|
||||
$chat = ui.createChat(curvePublic);
|
||||
createChatBox(common, $chat, curvePublic, ui);
|
||||
ui.createChatBox(proxy, $chat, curvePublic);
|
||||
}
|
||||
// Show the correct div
|
||||
ui.hideChat();
|
||||
@@ -682,9 +722,8 @@ define([
|
||||
|
||||
// TODO take a callback
|
||||
var remove = function (curvePublic) {
|
||||
var data = getFriend(common, curvePublic);
|
||||
var data = getFriend(proxy, curvePublic);
|
||||
var channel = channels[data.channel];
|
||||
//var newdata = createData(common, data.channel);
|
||||
var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
|
||||
var msgStr = JSON.stringify(msg);
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
@@ -704,9 +743,7 @@ define([
|
||||
// Open the channels
|
||||
|
||||
// TODO extract this into an external function
|
||||
var openFriendChannel = function (f) {
|
||||
if (f === "me") { return; }
|
||||
var data = friends[f];
|
||||
var openFriendChannel = function (data, f) {
|
||||
var keys = Curve.deriveKeys(data.curvePublic, proxy.curvePrivate);
|
||||
var encryptor = Curve.createEncryptor(keys);
|
||||
network.join(data.channel).then(function (chan) {
|
||||
@@ -719,13 +756,14 @@ define([
|
||||
refresh: function () { refresh(data.curvePublic); },
|
||||
notify: function () {
|
||||
ui.notify(data.curvePublic);
|
||||
common.notify();
|
||||
common.notify(); // HERE
|
||||
},
|
||||
unnotify: function () { ui.unnotify(data.curvePublic); },
|
||||
removeUI: function () { ui.remove(data.curvePublic); },
|
||||
updateUI: function (types) { ui.update(data.curvePublic, types); },
|
||||
updateStatus: function () {
|
||||
ui.updateStatus(data.curvePublic, channel.getStatus(data.curvePublic));
|
||||
ui.updateStatus(data.curvePublic,
|
||||
channel.getStatus(data.curvePublic));
|
||||
},
|
||||
setLastMessageRead: function (hash) {
|
||||
data.lastKnownHash = hash;
|
||||
@@ -774,7 +812,7 @@ define([
|
||||
var cryptMsg = channel.encryptor.encrypt(msgStr);
|
||||
|
||||
channel.wc.bcast(cryptMsg).then(function () {
|
||||
pushMsg(common, channel, cryptMsg);
|
||||
pushMsg(realtime, proxy, common, channel, cryptMsg);
|
||||
cb();
|
||||
}, function (err) {
|
||||
cb(err);
|
||||
@@ -823,7 +861,7 @@ define([
|
||||
};
|
||||
|
||||
var openFriendChannels = messenger.openFriendChannels = function () {
|
||||
Object.keys(friends).forEach(openFriendChannel);
|
||||
eachFriend(friends, openFriendChannel);
|
||||
};
|
||||
|
||||
messenger.setEditable = ui.setEditable;
|
||||
@@ -832,13 +870,10 @@ define([
|
||||
|
||||
// TODO split loop innards into ui methods
|
||||
var checkNewFriends = function () {
|
||||
Object.keys(friends).forEach(function (f) {
|
||||
var $friend = ui.getFriend(f);
|
||||
|
||||
eachFriend(friends, function (friend, id) {
|
||||
var $friend = ui.getFriend(id);
|
||||
if (!$friend.length) {
|
||||
openFriendChannel(f);
|
||||
if (f === 'me') { return; }
|
||||
var friend = friends[f];
|
||||
openFriendChannel(friend, id);
|
||||
ui.addToFriendList(friend, display, remove);
|
||||
}
|
||||
});
|
||||
@@ -846,7 +881,7 @@ define([
|
||||
|
||||
common.onDisplayNameChanged(function () {
|
||||
checkNewFriends();
|
||||
updateMyData(common);
|
||||
updateMyData(proxy);
|
||||
});
|
||||
|
||||
return messenger;
|
||||
@@ -856,17 +891,14 @@ define([
|
||||
// FIXME there are too many functions with this name
|
||||
var addToFriendList = Msg.addToFriendList = function (common, data, cb) {
|
||||
var proxy = common.getProxy();
|
||||
if (!proxy.friends) {
|
||||
proxy.friends = {};
|
||||
}
|
||||
var friends = proxy.friends;
|
||||
var friends = getFriendList(proxy);
|
||||
var pubKey = data.curvePublic;
|
||||
|
||||
if (pubKey === proxy.curvePublic) { return void cb("E_MYKEY"); }
|
||||
|
||||
friends[pubKey] = data;
|
||||
|
||||
common.whenRealtimeSyncs(common.getRealtime(), function () {
|
||||
Realtime.whenRealtimeSyncs(common.getRealtime(), function () {
|
||||
cb();
|
||||
common.pinPads([data.channel]);
|
||||
});
|
||||
@@ -876,6 +908,7 @@ define([
|
||||
/* Used to accept friend requests within apps other than /contacts/ */
|
||||
Msg.addDirectMessageHandler = function (common) {
|
||||
var network = common.getNetwork();
|
||||
var proxy = common.getProxy();
|
||||
if (!network) { return void console.error('Network not ready'); }
|
||||
network.on('message', function (message, sender) {
|
||||
var msg;
|
||||
@@ -910,7 +943,7 @@ define([
|
||||
msgStr = Crypto.encrypt(JSON.stringify(msg), key);
|
||||
network.sendto(sender, msgStr);
|
||||
};
|
||||
var existing = getFriend(common, msgData.curvePublic);
|
||||
var existing = getFriend(proxy, msgData.curvePublic);
|
||||
if (existing) {
|
||||
todo(true);
|
||||
return;
|
||||
@@ -941,7 +974,6 @@ define([
|
||||
var i = pendingRequests.indexOf(sender);
|
||||
if (i !== -1) { pendingRequests.splice(i, 1); }
|
||||
common.log(common.Messages.contacts_rejected);
|
||||
var proxy = common.getProxy();
|
||||
common.changeDisplayName(proxy[common.displayNameKey]);
|
||||
return;
|
||||
}
|
||||
|
||||
42
www/common/common-realtime.js
Normal file
42
www/common/common-realtime.js
Normal file
@@ -0,0 +1,42 @@
|
||||
define([
|
||||
'/customize/application_config.js',
|
||||
'/customize/messages.js',
|
||||
], function (AppConfig, Messages) {
|
||||
var common = {};
|
||||
|
||||
common.infiniteSpinnerDetected = false;
|
||||
var BAD_STATE_TIMEOUT = typeof(AppConfig.badStateTimeout) === 'number'?
|
||||
AppConfig.badStateTimeout: 30000;
|
||||
|
||||
/*
|
||||
TODO make this not blow up when disconnected or lagging...
|
||||
*/
|
||||
common.whenRealtimeSyncs = function (realtime, cb) {
|
||||
realtime.sync();
|
||||
|
||||
window.setTimeout(function () {
|
||||
if (realtime.getAuthDoc() === realtime.getUserDoc()) {
|
||||
return void cb();
|
||||
}
|
||||
|
||||
var to = setTimeout(function () {
|
||||
realtime.abort();
|
||||
// don't launch more than one popup
|
||||
if (common.infiniteSpinnerDetected) { return; }
|
||||
|
||||
// inform the user their session is in a bad state
|
||||
common.confirm(Messages.realtime_unrecoverableError, function (yes) {
|
||||
if (!yes) { return; }
|
||||
window.location.reload();
|
||||
});
|
||||
common.infiniteSpinnerDetected = true;
|
||||
}, BAD_STATE_TIMEOUT);
|
||||
realtime.onSettle(function () {
|
||||
clearTimeout(to);
|
||||
cb();
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return common;
|
||||
});
|
||||
@@ -14,13 +14,15 @@ define([
|
||||
'/common/common-codemirror.js',
|
||||
'/common/common-file.js',
|
||||
'/file/file-crypto.js',
|
||||
'/common/common-realtime.js',
|
||||
|
||||
'/common/clipboard.js',
|
||||
'/common/pinpad.js',
|
||||
'/customize/application_config.js',
|
||||
'/common/media-tag.js',
|
||||
], function ($, Config, Messages, Store, Util, Hash, UI, History, UserList, Title, Metadata,
|
||||
Messaging, CodeMirror, Files, FileCrypto, Clipboard, Pinpad, AppConfig, MediaTag) {
|
||||
Messaging, CodeMirror, Files, FileCrypto, Realtime, Clipboard,
|
||||
Pinpad, AppConfig, MediaTag) {
|
||||
|
||||
// Configure MediaTags to use our local viewer
|
||||
if (MediaTag && MediaTag.PdfPlugin) {
|
||||
@@ -129,6 +131,9 @@ define([
|
||||
common.getLatestMessages = Messaging.getLatestMessages;
|
||||
common.initMessagingUI = Messaging.UI.init;
|
||||
|
||||
// Realtime
|
||||
var whenRealtimeSyncs = common.whenRealtimeSyncs = Realtime.whenRealtimeSyncs;
|
||||
|
||||
// Userlist
|
||||
common.createUserList = UserList.create;
|
||||
|
||||
@@ -244,36 +249,6 @@ define([
|
||||
return;
|
||||
};
|
||||
|
||||
common.infiniteSpinnerDetected = false;
|
||||
var BAD_STATE_TIMEOUT = typeof(AppConfig.badStateTimeout) === 'number'?
|
||||
AppConfig.badStateTimeout: 30000;
|
||||
var whenRealtimeSyncs = common.whenRealtimeSyncs = function (realtime, cb) {
|
||||
realtime.sync();
|
||||
|
||||
window.setTimeout(function () {
|
||||
if (realtime.getAuthDoc() === realtime.getUserDoc()) {
|
||||
return void cb();
|
||||
}
|
||||
|
||||
var to = setTimeout(function () {
|
||||
realtime.abort();
|
||||
// don't launch more than one popup
|
||||
if (common.infiniteSpinnerDetected) { return; }
|
||||
|
||||
// inform the user their session is in a bad state
|
||||
common.confirm(Messages.realtime_unrecoverableError, function (yes) {
|
||||
if (!yes) { return; }
|
||||
window.location.reload();
|
||||
});
|
||||
common.infiniteSpinnerDetected = true;
|
||||
}, BAD_STATE_TIMEOUT);
|
||||
realtime.onSettle(function () {
|
||||
clearTimeout(to);
|
||||
cb();
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
common.getWebsocketURL = function () {
|
||||
if (!Config.websocketPath) { return Config.websocketURL; }
|
||||
var path = Config.websocketPath;
|
||||
|
||||
@@ -70,8 +70,8 @@ define([
|
||||
'IFRAME',
|
||||
'OBJECT',
|
||||
'APPLET',
|
||||
//'VIDEO',
|
||||
'AUDIO',
|
||||
//'VIDEO', // privacy implications of videos are the same as images
|
||||
//'AUDIO', // same with audio
|
||||
];
|
||||
var unsafeTag = function (info) {
|
||||
/*if (info.node && $(info.node).parents('media-tag').length) {
|
||||
@@ -79,7 +79,7 @@ define([
|
||||
return true;
|
||||
}*/
|
||||
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) {
|
||||
if (/^on/.test(info.diff.name)) {
|
||||
if (/^on/i.test(info.diff.name)) {
|
||||
console.log("Rejecting forbidden element attribute with name", info.diff.name);
|
||||
return true;
|
||||
}
|
||||
@@ -101,10 +101,25 @@ define([
|
||||
return Array.prototype.slice.call(coll);
|
||||
};
|
||||
|
||||
var removeNode = function (node) {
|
||||
if (!(node && node.parentElement)) { return; }
|
||||
var parent = node.parentElement;
|
||||
if (!parent) { return; }
|
||||
console.log('removing %s tag', node.nodeName);
|
||||
parent.removeChild(node);
|
||||
};
|
||||
|
||||
var removeForbiddenTags = function (root) {
|
||||
if (!root) { return; }
|
||||
if (forbiddenTags.indexOf(root.nodeName) !== -1) { removeNode(root); }
|
||||
slice(root.children).forEach(removeForbiddenTags);
|
||||
};
|
||||
|
||||
/* remove listeners from the DOM */
|
||||
var removeListeners = function (root) {
|
||||
slice(root.attributes).map(function (attr) {
|
||||
if (/^on/.test(attr.name)) {
|
||||
if (/^on/i.test(attr.name)) {
|
||||
console.log('removing attribute', attr.name, root.attributes[attr.name]);
|
||||
root.attributes.removeNamedItem(attr.name);
|
||||
}
|
||||
});
|
||||
@@ -114,6 +129,7 @@ define([
|
||||
|
||||
var domFromHTML = function (html) {
|
||||
var Dom = new DOMParser().parseFromString(html, "text/html");
|
||||
removeForbiddenTags(Dom.body);
|
||||
removeListeners(Dom.body);
|
||||
return Dom;
|
||||
};
|
||||
@@ -148,7 +164,8 @@ define([
|
||||
var id = $content.attr('id');
|
||||
if (!id) { throw new Error("The element must have a valid id"); }
|
||||
var pattern = /(<media-tag src="([^"]*)" data-crypto-key="([^"]*)">)<\/media-tag>/g;
|
||||
var newHtmlFixed = newHtml.replace(pattern, function (all, tag, src) {
|
||||
|
||||
var unsafe_newHtmlFixed = newHtml.replace(pattern, function (all, tag, src) {
|
||||
var mt = tag;
|
||||
if (mediaMap[src]) {
|
||||
mediaMap[src].forEach(function (n) {
|
||||
@@ -157,7 +174,10 @@ define([
|
||||
}
|
||||
return mt + '</media-tag>';
|
||||
});
|
||||
var $div = $('<div>', {id: id}).append(newHtmlFixed);
|
||||
|
||||
var safe_newHtmlFixed = domFromHTML(unsafe_newHtmlFixed).body.outerHTML;
|
||||
var $div = $('<div>', {id: id}).append(safe_newHtmlFixed);
|
||||
|
||||
var Dom = domFromHTML($('<div>').append($div).html());
|
||||
var oldDom = domFromHTML($content[0].outerHTML);
|
||||
var patch = makeDiff(oldDom, Dom, id);
|
||||
|
||||
@@ -624,7 +624,7 @@ define([
|
||||
}).appendTo(toolbar.$top);
|
||||
|
||||
// We need to override the "a" tag action here because it is inside the iframe!
|
||||
var inDrive = /^\/drive/;
|
||||
var inDrive = /^\/drive/.test(window.location.pathname);
|
||||
|
||||
var href = inDrive ? '/index.html' : '/drive/';
|
||||
var buttonTitle = inDrive ? Messages.header_homeTitle : Messages.header_logoTitle;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@content-bg: #fff;
|
||||
@content-bg-ro: darken(@content-bg, 10%);
|
||||
@content-fg: @tree-fg;
|
||||
@info-box-bg: #ddddff;
|
||||
@info-box-bg: #d2e1f2;
|
||||
@info-box-border: #bbb;
|
||||
@table-header-fg: #555;
|
||||
@table-header-bg: #e8e8e8;
|
||||
@@ -380,19 +380,16 @@ span {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.info-box {
|
||||
line-height: 40px;
|
||||
padding-left: 10px;
|
||||
margin: 10px auto;
|
||||
line-height: 2em;
|
||||
padding: 0.25em 0.75em;
|
||||
margin: 1em;
|
||||
background: @info-box-bg;
|
||||
border: 1px solid @info-box-border;
|
||||
border-radius: 5px;
|
||||
span {
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
float: right;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
&.noclose {
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
li {
|
||||
|
||||
3
www/pad/ckeditor-inner.html
Normal file
3
www/pad/ckeditor-inner.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html dir="ltr" lang="en"><head><title>Rich Text Editor, editor1</title><style data-cke-temp="1">html{cursor:text;*cursor:auto}
|
||||
img,input,textarea{cursor:default}</style><link type="text/css" rel="stylesheet" href="/customize/ckeditor-contents.css"><link type="text/css" rel="stylesheet" href="/bower_components/ckeditor/plugins/tableselection/styles/tableselection.css"></head><body><p><br></p></body></html>
|
||||
@@ -7,6 +7,7 @@
|
||||
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.min.css">
|
||||
<script async data-bootload="/pad/inner.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.1.15"></script>
|
||||
<script src="/bower_components/ckeditor/ckeditor.js"></script>
|
||||
<script src="/pad/wysiwygarea-plugin.js"></script>
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0px;
|
||||
|
||||
@@ -43,7 +43,7 @@ define([
|
||||
|
||||
var removeListeners = function (root) {
|
||||
slice(root.attributes).map(function (attr) {
|
||||
if (/^on/.test(attr.name)) {
|
||||
if (/^on/i.test(attr.name)) {
|
||||
root.attributes.removeNamedItem(attr.name);
|
||||
}
|
||||
});
|
||||
@@ -313,6 +313,10 @@ define([
|
||||
if (!readOnly && !initializing) {
|
||||
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
|
||||
}
|
||||
$(userDocStateDom).find('script, applet, object, iframe').remove();
|
||||
$(userDocStateDom).find('a').filter(function (i, x) {
|
||||
return ! /^(https|http|ftp):\/\/[^\s\n]*$/.test(x.getAttribute('href'));
|
||||
}).remove();
|
||||
var patch = (DD).diff(inner, userDocStateDom);
|
||||
(DD).apply(inner, patch);
|
||||
if (readOnly) {
|
||||
@@ -625,8 +629,10 @@ define([
|
||||
if (stringify(hjson2) !== stringify(hjson)) {
|
||||
console.log('err');
|
||||
console.error("shjson2 !== shjson");
|
||||
Cryptpad.errorLoadingScreen(Messages.wrongApp);
|
||||
throw new Error();
|
||||
// TODO(cjd): This is removed because the XSS filter in applyHjson()
|
||||
// is applied on incoming content so it causes this to fail.
|
||||
//Cryptpad.errorLoadingScreen(Messages.wrongApp);
|
||||
//throw new Error();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
735
www/pad/wysiwygarea-plugin.js
Normal file
735
www/pad/wysiwygarea-plugin.js
Normal file
@@ -0,0 +1,735 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved.
|
||||
* For licensing, see LICENSE.md or http://ckeditor.com/license
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fileOverview The WYSIWYG Area plugin. It registers the "wysiwyg" editing
|
||||
* mode, which handles the main editing area space.
|
||||
*/
|
||||
|
||||
( function() {
|
||||
var framedWysiwyg;
|
||||
var iframe;
|
||||
|
||||
CKEDITOR.plugins.registered.wysiwygarea.init = function( editor ) {
|
||||
if ( editor.config.fullPage ) {
|
||||
editor.addFeature( {
|
||||
allowedContent: 'html head title; style [media,type]; body (*)[id]; meta link [*]',
|
||||
requiredContent: 'body'
|
||||
} );
|
||||
}
|
||||
|
||||
editor.addMode( 'wysiwyg', function( callback ) {
|
||||
var src = 'document.open();' +
|
||||
// In IE, the document domain must be set any time we call document.open().
|
||||
( CKEDITOR.env.ie ? '(' + CKEDITOR.tools.fixDomain + ')();' : '' ) +
|
||||
'document.close();';
|
||||
|
||||
// With IE, the custom domain has to be taken care at first,
|
||||
// for other browers, the 'src' attribute should be left empty to
|
||||
// trigger iframe's 'load' event.
|
||||
// Microsoft Edge throws "Permission Denied" if treated like an IE (http://dev.ckeditor.com/ticket/13441).
|
||||
if ( CKEDITOR.env.air ) {
|
||||
src = 'javascript:void(0)'; // jshint ignore:line
|
||||
} else if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
|
||||
src = 'javascript:void(function(){' + encodeURIComponent( src ) + '}())'; // jshint ignore:line
|
||||
} else {
|
||||
src = '';
|
||||
}
|
||||
|
||||
// CryptPad
|
||||
src = '/pad/ckeditor-inner.html';
|
||||
|
||||
iframe = CKEDITOR.dom.element.createFromHtml( '<iframe src="' + src + '" frameBorder="0"></iframe>' );
|
||||
iframe.setStyles( { width: '100%', height: '100%' } );
|
||||
iframe.addClass( 'cke_wysiwyg_frame' ).addClass( 'cke_reset' );
|
||||
|
||||
// CryptPad
|
||||
// this is impossible because ckeditor uses some (non-inline) script inside of the iframe...
|
||||
//iframe.setAttribute('sandbox', 'allow-same-origin');
|
||||
|
||||
var contentSpace = editor.ui.space( 'contents' );
|
||||
contentSpace.append( iframe );
|
||||
|
||||
|
||||
// Asynchronous iframe loading is only required in IE>8 and Gecko (other reasons probably).
|
||||
// Do not use it on WebKit as it'll break the browser-back navigation.
|
||||
var useOnloadEvent = ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) || CKEDITOR.env.gecko;
|
||||
if ( useOnloadEvent )
|
||||
iframe.on( 'load', onLoad );
|
||||
|
||||
var frameLabel = editor.title,
|
||||
helpLabel = editor.fire( 'ariaEditorHelpLabel', {} ).label;
|
||||
|
||||
if ( frameLabel ) {
|
||||
if ( CKEDITOR.env.ie && helpLabel )
|
||||
frameLabel += ', ' + helpLabel;
|
||||
|
||||
iframe.setAttribute( 'title', frameLabel );
|
||||
}
|
||||
|
||||
if ( helpLabel ) {
|
||||
var labelId = CKEDITOR.tools.getNextId(),
|
||||
desc = CKEDITOR.dom.element.createFromHtml( '<span id="' + labelId + '" class="cke_voice_label">' + helpLabel + '</span>' );
|
||||
|
||||
contentSpace.append( desc, 1 );
|
||||
iframe.setAttribute( 'aria-describedby', labelId );
|
||||
}
|
||||
|
||||
// Remove the ARIA description.
|
||||
editor.on( 'beforeModeUnload', function( evt ) {
|
||||
evt.removeListener();
|
||||
if ( desc )
|
||||
desc.remove();
|
||||
} );
|
||||
|
||||
iframe.setAttributes( {
|
||||
tabIndex: editor.tabIndex,
|
||||
allowTransparency: 'true'
|
||||
} );
|
||||
|
||||
// Execute onLoad manually for all non IE||Gecko browsers.
|
||||
!useOnloadEvent && onLoad();
|
||||
|
||||
editor.fire( 'ariaWidget', iframe );
|
||||
|
||||
function onLoad( evt ) {
|
||||
evt && evt.removeListener();
|
||||
var fw = new framedWysiwyg( editor, iframe.$.contentWindow.document.body );
|
||||
editor.editable( fw );
|
||||
editor.setData( editor.getData( 1 ), callback );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds the path to a stylesheet file to the exisiting {@link CKEDITOR.config#contentsCss} value.
|
||||
*
|
||||
* **Note:** This method is available only with the `wysiwygarea` plugin and only affects
|
||||
* classic editors based on it (so it does not affect inline editors).
|
||||
*
|
||||
* editor.addContentsCss( 'assets/contents.css' );
|
||||
*
|
||||
* @since 4.4
|
||||
* @param {String} cssPath The path to the stylesheet file which should be added.
|
||||
* @member CKEDITOR.editor
|
||||
*/
|
||||
CKEDITOR.editor.prototype.addContentsCss = function( cssPath ) {
|
||||
var cfg = this.config,
|
||||
curContentsCss = cfg.contentsCss;
|
||||
|
||||
// Convert current value into array.
|
||||
if ( !CKEDITOR.tools.isArray( curContentsCss ) )
|
||||
cfg.contentsCss = curContentsCss ? [ curContentsCss ] : [];
|
||||
|
||||
cfg.contentsCss.push( cssPath );
|
||||
};
|
||||
|
||||
function onDomReady( win ) {
|
||||
var editor = this.editor,
|
||||
doc = win.document,
|
||||
body = doc.body;
|
||||
|
||||
// Remove helper scripts from the DOM.
|
||||
var script = doc.getElementById( 'cke_actscrpt' );
|
||||
script && script.parentNode.removeChild( script );
|
||||
script = doc.getElementById( 'cke_shimscrpt' );
|
||||
script && script.parentNode.removeChild( script );
|
||||
script = doc.getElementById( 'cke_basetagscrpt' );
|
||||
script && script.parentNode.removeChild( script );
|
||||
|
||||
body.contentEditable = true;
|
||||
|
||||
if ( CKEDITOR.env.ie ) {
|
||||
// Don't display the focus border.
|
||||
body.hideFocus = true;
|
||||
|
||||
// Disable and re-enable the body to avoid IE from
|
||||
// taking the editing focus at startup. (http://dev.ckeditor.com/ticket/141 / http://dev.ckeditor.com/ticket/523)
|
||||
body.disabled = true;
|
||||
body.removeAttribute( 'disabled' );
|
||||
}
|
||||
|
||||
delete this._.isLoadingData;
|
||||
|
||||
// Play the magic to alter element reference to the reloaded one.
|
||||
this.$ = body;
|
||||
|
||||
doc = new CKEDITOR.dom.document( doc );
|
||||
|
||||
this.setup();
|
||||
this.fixInitialSelection();
|
||||
|
||||
var editable = this;
|
||||
|
||||
// Without it IE8 has problem with removing selection in nested editable. (http://dev.ckeditor.com/ticket/13785)
|
||||
if ( CKEDITOR.env.ie && !CKEDITOR.env.edge ) {
|
||||
doc.getDocumentElement().addClass( doc.$.compatMode );
|
||||
}
|
||||
|
||||
// Prevent IE/Edge from leaving a new paragraph/div after deleting all contents in body. (http://dev.ckeditor.com/ticket/6966, http://dev.ckeditor.com/ticket/13142)
|
||||
if ( CKEDITOR.env.ie && !CKEDITOR.env.edge && editor.enterMode != CKEDITOR.ENTER_P ) {
|
||||
removeSuperfluousElement( 'p' );
|
||||
} else if ( CKEDITOR.env.edge && editor.enterMode != CKEDITOR.ENTER_DIV ) {
|
||||
removeSuperfluousElement( 'div' );
|
||||
}
|
||||
|
||||
// Fix problem with cursor not appearing in Webkit and IE11+ when clicking below the body (http://dev.ckeditor.com/ticket/10945, http://dev.ckeditor.com/ticket/10906).
|
||||
// Fix for older IEs (8-10 and QM) is placed inside selection.js.
|
||||
if ( CKEDITOR.env.webkit || ( CKEDITOR.env.ie && CKEDITOR.env.version > 10 ) ) {
|
||||
doc.getDocumentElement().on( 'mousedown', function( evt ) {
|
||||
if ( evt.data.getTarget().is( 'html' ) ) {
|
||||
// IE needs this timeout. Webkit does not, but it does not cause problems too.
|
||||
setTimeout( function() {
|
||||
editor.editable().focus();
|
||||
} );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
// Config props: disableObjectResizing and disableNativeTableHandles handler.
|
||||
objectResizeDisabler( editor );
|
||||
|
||||
// Enable dragging of position:absolute elements in IE.
|
||||
try {
|
||||
editor.document.$.execCommand( '2D-position', false, true );
|
||||
} catch ( e ) {}
|
||||
|
||||
if ( CKEDITOR.env.gecko || CKEDITOR.env.ie && editor.document.$.compatMode == 'CSS1Compat' ) {
|
||||
this.attachListener( this, 'keydown', function( evt ) {
|
||||
var keyCode = evt.data.getKeystroke();
|
||||
|
||||
// PageUp OR PageDown
|
||||
if ( keyCode == 33 || keyCode == 34 ) {
|
||||
// PageUp/PageDown scrolling is broken in document
|
||||
// with standard doctype, manually fix it. (http://dev.ckeditor.com/ticket/4736)
|
||||
if ( CKEDITOR.env.ie ) {
|
||||
setTimeout( function() {
|
||||
editor.getSelection().scrollIntoView();
|
||||
}, 0 );
|
||||
}
|
||||
// Page up/down cause editor selection to leak
|
||||
// outside of editable thus we try to intercept
|
||||
// the behavior, while it affects only happen
|
||||
// when editor contents are not overflowed. (http://dev.ckeditor.com/ticket/7955)
|
||||
else if ( editor.window.$.innerHeight > this.$.offsetHeight ) {
|
||||
var range = editor.createRange();
|
||||
range[ keyCode == 33 ? 'moveToElementEditStart' : 'moveToElementEditEnd' ]( this );
|
||||
range.select();
|
||||
evt.data.preventDefault();
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
if ( CKEDITOR.env.ie ) {
|
||||
// [IE] Iframe will still keep the selection when blurred, if
|
||||
// focus is moved onto a non-editing host, e.g. link or button, but
|
||||
// it becomes a problem for the object type selection, since the resizer
|
||||
// handler attached on it will mark other part of the UI, especially
|
||||
// for the dialog. (http://dev.ckeditor.com/ticket/8157)
|
||||
// [IE<8 & Opera] Even worse For old IEs, the cursor will not vanish even if
|
||||
// the selection has been moved to another text input in some cases. (http://dev.ckeditor.com/ticket/4716)
|
||||
//
|
||||
// Now the range restore is disabled, so we simply force IE to clean
|
||||
// up the selection before blur.
|
||||
this.attachListener( doc, 'blur', function() {
|
||||
// Error proof when the editor is not visible. (http://dev.ckeditor.com/ticket/6375)
|
||||
try {
|
||||
doc.$.selection.empty();
|
||||
} catch ( er ) {}
|
||||
} );
|
||||
}
|
||||
|
||||
if ( CKEDITOR.env.iOS ) {
|
||||
// [iOS] If touch is bound to any parent of the iframe blur happens on any touch
|
||||
// event and body becomes the focused element (http://dev.ckeditor.com/ticket/10714).
|
||||
this.attachListener( doc, 'touchend', function() {
|
||||
win.focus();
|
||||
} );
|
||||
}
|
||||
|
||||
var title = editor.document.getElementsByTag( 'title' ).getItem( 0 );
|
||||
// document.title is malfunctioning on Chrome, so get value from the element (http://dev.ckeditor.com/ticket/12402).
|
||||
title.data( 'cke-title', title.getText() );
|
||||
|
||||
// [IE] JAWS will not recognize the aria label we used on the iframe
|
||||
// unless the frame window title string is used as the voice label,
|
||||
// backup the original one and restore it on output.
|
||||
if ( CKEDITOR.env.ie )
|
||||
editor.document.$.title = this._.docTitle;
|
||||
|
||||
CKEDITOR.tools.setTimeout( function() {
|
||||
// Editable is ready after first setData.
|
||||
if ( this.status == 'unloaded' )
|
||||
this.status = 'ready';
|
||||
|
||||
editor.fire( 'contentDom' );
|
||||
|
||||
if ( this._.isPendingFocus ) {
|
||||
editor.focus();
|
||||
this._.isPendingFocus = false;
|
||||
}
|
||||
|
||||
setTimeout( function() {
|
||||
editor.fire( 'dataReady' );
|
||||
}, 0 );
|
||||
}, 0, this );
|
||||
|
||||
function removeSuperfluousElement( tagName ) {
|
||||
var lockRetain = false;
|
||||
|
||||
// Superfluous elements appear after keydown
|
||||
// and before keyup, so the procedure is as follows:
|
||||
// 1. On first keydown mark all elements with
|
||||
// a specified tag name as non-superfluous.
|
||||
editable.attachListener( editable, 'keydown', function() {
|
||||
var body = doc.getBody(),
|
||||
retained = body.getElementsByTag( tagName );
|
||||
|
||||
if ( !lockRetain ) {
|
||||
for ( var i = 0; i < retained.count(); i++ ) {
|
||||
retained.getItem( i ).setCustomData( 'retain', true );
|
||||
}
|
||||
lockRetain = true;
|
||||
}
|
||||
}, null, null, 1 );
|
||||
|
||||
// 2. On keyup remove all elements that were not marked
|
||||
// as non-superfluous (which means they must have had appeared in the meantime).
|
||||
// Also we should preserve all temporary elements inserted by editor – otherwise we'd likely
|
||||
// leak fake selection's content into editable due to removing hidden selection container (http://dev.ckeditor.com/ticket/14831).
|
||||
editable.attachListener( editable, 'keyup', function() {
|
||||
var elements = doc.getElementsByTag( tagName );
|
||||
if ( lockRetain ) {
|
||||
if ( elements.count() == 1 && !elements.getItem( 0 ).getCustomData( 'retain' ) &&
|
||||
!elements.getItem( 0 ).hasAttribute( 'data-cke-temp' ) ) {
|
||||
elements.getItem( 0 ).remove( 1 );
|
||||
}
|
||||
lockRetain = false;
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
framedWysiwyg = CKEDITOR.tools.createClass( {
|
||||
$: function() {
|
||||
this.base.apply( this, arguments );
|
||||
|
||||
this._.frameLoadedHandler = CKEDITOR.tools.addFunction( function( win ) {
|
||||
// Avoid opening design mode in a frame window thread,
|
||||
// which will cause host page scrolling.(http://dev.ckeditor.com/ticket/4397)
|
||||
CKEDITOR.tools.setTimeout( onDomReady, 0, this, win );
|
||||
}, this );
|
||||
|
||||
this._.docTitle = this.getWindow().getFrame().getAttribute( 'title' );
|
||||
},
|
||||
|
||||
base: CKEDITOR.editable,
|
||||
|
||||
proto: {
|
||||
setData: function( data, isSnapshot ) {
|
||||
var editor = this.editor;
|
||||
|
||||
if ( isSnapshot ) {
|
||||
this.setHtml( data );
|
||||
this.fixInitialSelection();
|
||||
|
||||
// Fire dataReady for the consistency with inline editors
|
||||
// and because it makes sense. (http://dev.ckeditor.com/ticket/10370)
|
||||
editor.fire( 'dataReady' );
|
||||
}
|
||||
else {
|
||||
this._.isLoadingData = true;
|
||||
editor._.dataStore = { id: 1 };
|
||||
|
||||
var config = editor.config,
|
||||
fullPage = config.fullPage,
|
||||
docType = config.docType;
|
||||
|
||||
// Build the additional stuff to be included into <head>.
|
||||
var headExtra = CKEDITOR.tools.buildStyleHtml( iframeCssFixes() ).replace( /<style>/, '<style data-cke-temp="1">' );
|
||||
|
||||
if ( !fullPage )
|
||||
headExtra += CKEDITOR.tools.buildStyleHtml( editor.config.contentsCss );
|
||||
|
||||
var baseTag = config.baseHref ? '<base href="' + config.baseHref + '" data-cke-temp="1" />' : '';
|
||||
|
||||
if ( fullPage ) {
|
||||
// Search and sweep out the doctype declaration.
|
||||
data = data.replace( /<!DOCTYPE[^>]*>/i, function( match ) {
|
||||
editor.docType = docType = match;
|
||||
return '';
|
||||
} ).replace( /<\?xml\s[^\?]*\?>/i, function( match ) {
|
||||
editor.xmlDeclaration = match;
|
||||
return '';
|
||||
} );
|
||||
}
|
||||
|
||||
// Get the HTML version of the data.
|
||||
data = editor.dataProcessor.toHtml( data );
|
||||
|
||||
if ( fullPage ) {
|
||||
// Check if the <body> tag is available.
|
||||
if ( !( /<body[\s|>]/ ).test( data ) )
|
||||
data = '<body>' + data;
|
||||
|
||||
// Check if the <html> tag is available.
|
||||
if ( !( /<html[\s|>]/ ).test( data ) )
|
||||
data = '<html>' + data + '</html>';
|
||||
|
||||
// Check if the <head> tag is available.
|
||||
if ( !( /<head[\s|>]/ ).test( data ) )
|
||||
data = data.replace( /<html[^>]*>/, '$&<head><title></title></head>' );
|
||||
else if ( !( /<title[\s|>]/ ).test( data ) )
|
||||
data = data.replace( /<head[^>]*>/, '$&<title></title>' );
|
||||
|
||||
// The base must be the first tag in the HEAD, e.g. to get relative
|
||||
// links on styles.
|
||||
baseTag && ( data = data.replace( /<head[^>]*?>/, '$&' + baseTag ) );
|
||||
|
||||
// Inject the extra stuff into <head>.
|
||||
// Attention: do not change it before testing it well. (V2)
|
||||
// This is tricky... if the head ends with <meta ... content type>,
|
||||
// Firefox will break. But, it works if we place our extra stuff as
|
||||
// the last elements in the HEAD.
|
||||
data = data.replace( /<\/head\s*>/, headExtra + '$&' );
|
||||
|
||||
// Add the DOCTYPE back to it.
|
||||
data = docType + data;
|
||||
} else {
|
||||
data = config.docType +
|
||||
'<html dir="' + config.contentsLangDirection + '"' +
|
||||
' lang="' + ( config.contentsLanguage || editor.langCode ) + '">' +
|
||||
'<head>' +
|
||||
'<title>' + this._.docTitle + '</title>' +
|
||||
baseTag +
|
||||
headExtra +
|
||||
'</head>' +
|
||||
'<body' + ( config.bodyId ? ' id="' + config.bodyId + '"' : '' ) +
|
||||
( config.bodyClass ? ' class="' + config.bodyClass + '"' : '' ) +
|
||||
'>' +
|
||||
data +
|
||||
'</body>' +
|
||||
'</html>';
|
||||
}
|
||||
|
||||
if ( CKEDITOR.env.gecko ) {
|
||||
// Hack to make Fx put cursor at the start of doc on fresh focus.
|
||||
data = data.replace( /<body/, '<body contenteditable="true" ' );
|
||||
|
||||
// Another hack which is used by onDomReady to remove a leading
|
||||
// <br> which is inserted by Firefox 3.6 when document.write is called.
|
||||
// This additional <br> is present because of contenteditable="true"
|
||||
if ( CKEDITOR.env.version < 20000 )
|
||||
data = data.replace( /<body[^>]*>/, '$&<!-- cke-content-start -->' );
|
||||
}
|
||||
|
||||
// The script that launches the bootstrap logic on 'domReady', so the document
|
||||
// is fully editable even before the editing iframe is fully loaded (http://dev.ckeditor.com/ticket/4455).
|
||||
var bootstrapCode =
|
||||
'<script id="cke_actscrpt" type="text/javascript"' + ( CKEDITOR.env.ie ? ' defer="defer" ' : '' ) + '>' +
|
||||
'var wasLoaded=0;' + // It must be always set to 0 as it remains as a window property.
|
||||
'function onload(){' +
|
||||
'if(!wasLoaded)' + // FF3.6 calls onload twice when editor.setData. Stop that.
|
||||
'window.parent.CKEDITOR.tools.callFunction(' + this._.frameLoadedHandler + ',window);' +
|
||||
'wasLoaded=1;' +
|
||||
'}' +
|
||||
( CKEDITOR.env.ie ? 'onload();' : 'document.addEventListener("DOMContentLoaded", onload, false );' ) +
|
||||
'</script>';
|
||||
|
||||
// For IE<9 add support for HTML5's elements.
|
||||
// Note: this code must not be deferred.
|
||||
if ( CKEDITOR.env.ie && CKEDITOR.env.version < 9 ) {
|
||||
bootstrapCode +=
|
||||
'<script id="cke_shimscrpt">' +
|
||||
'window.parent.CKEDITOR.tools.enableHtml5Elements(document)' +
|
||||
'</script>';
|
||||
}
|
||||
|
||||
// IE<10 needs this hack to properly enable <base href="...">.
|
||||
// See: http://stackoverflow.com/a/13373180/1485219 (http://dev.ckeditor.com/ticket/11910).
|
||||
if ( baseTag && CKEDITOR.env.ie && CKEDITOR.env.version < 10 ) {
|
||||
bootstrapCode +=
|
||||
'<script id="cke_basetagscrpt">' +
|
||||
'var baseTag = document.querySelector( "base" );' +
|
||||
'baseTag.href = baseTag.href;' +
|
||||
'</script>';
|
||||
}
|
||||
|
||||
data = data.replace( /(?=\s*<\/(:?head)>)/, bootstrapCode );
|
||||
|
||||
// Current DOM will be deconstructed by document.write, cleanup required.
|
||||
this.clearCustomData();
|
||||
this.clearListeners();
|
||||
|
||||
editor.fire( 'contentDomUnload' );
|
||||
|
||||
var doc = this.getDocument();
|
||||
|
||||
// CryptPad
|
||||
var _iframe = window._iframe = iframe.$;
|
||||
var fw = this;
|
||||
_iframe.contentWindow.onload = function () {}
|
||||
var intr = setInterval(function () {
|
||||
//console.log(_iframe.contentWindow.document.body);
|
||||
if (!_iframe.contentWindow) { return; }
|
||||
if (!_iframe.contentWindow.document) { return; }
|
||||
if (_iframe.contentWindow.document.readyState !== 'complete') { return; }
|
||||
if (!_iframe.contentWindow.document.getElementsByTagName('title').length) { return; }
|
||||
clearInterval(intr);
|
||||
CKEDITOR.tools.callFunction(fw._.frameLoadedHandler, _iframe.contentWindow);
|
||||
}, 10);
|
||||
return;
|
||||
|
||||
// Work around Firefox bug - error prune when called from XUL (http://dev.ckeditor.com/ticket/320),
|
||||
// defer it thanks to the async nature of this method.
|
||||
try {
|
||||
doc.write( data );
|
||||
} catch ( e ) {
|
||||
setTimeout( function() {
|
||||
doc.write( data );
|
||||
}, 0 );
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getData: function( isSnapshot ) {
|
||||
if ( isSnapshot )
|
||||
return this.getHtml();
|
||||
else {
|
||||
var editor = this.editor,
|
||||
config = editor.config,
|
||||
fullPage = config.fullPage,
|
||||
docType = fullPage && editor.docType,
|
||||
xmlDeclaration = fullPage && editor.xmlDeclaration,
|
||||
doc = this.getDocument();
|
||||
|
||||
var data = fullPage ? doc.getDocumentElement().getOuterHtml() : doc.getBody().getHtml();
|
||||
|
||||
// BR at the end of document is bogus node for Mozilla. (http://dev.ckeditor.com/ticket/5293).
|
||||
// Prevent BRs from disappearing from the end of the content
|
||||
// while enterMode is ENTER_BR (http://dev.ckeditor.com/ticket/10146).
|
||||
if ( CKEDITOR.env.gecko && config.enterMode != CKEDITOR.ENTER_BR )
|
||||
data = data.replace( /<br>(?=\s*(:?$|<\/body>))/, '' );
|
||||
|
||||
data = editor.dataProcessor.toDataFormat( data );
|
||||
|
||||
if ( xmlDeclaration )
|
||||
data = xmlDeclaration + '\n' + data;
|
||||
if ( docType )
|
||||
data = docType + '\n' + data;
|
||||
|
||||
return data;
|
||||
}
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
if ( this._.isLoadingData )
|
||||
this._.isPendingFocus = true;
|
||||
else
|
||||
framedWysiwyg.baseProto.focus.call( this );
|
||||
},
|
||||
|
||||
detach: function() {
|
||||
var editor = this.editor,
|
||||
doc = editor.document,
|
||||
iframe,
|
||||
onResize;
|
||||
|
||||
// Trying to access window's frameElement property on Edge throws an exception
|
||||
// when frame was already removed from DOM. (http://dev.ckeditor.com/ticket/13850, http://dev.ckeditor.com/ticket/13790)
|
||||
try {
|
||||
iframe = editor.window.getFrame();
|
||||
} catch ( e ) {}
|
||||
|
||||
framedWysiwyg.baseProto.detach.call( this );
|
||||
|
||||
// Memory leak proof.
|
||||
this.clearCustomData();
|
||||
doc.getDocumentElement().clearCustomData();
|
||||
CKEDITOR.tools.removeFunction( this._.frameLoadedHandler );
|
||||
|
||||
// On IE, iframe is returned even after remove() method is called on it.
|
||||
// Checking if parent is present fixes this issue. (http://dev.ckeditor.com/ticket/13850)
|
||||
if ( iframe && iframe.getParent() ) {
|
||||
iframe.clearCustomData();
|
||||
onResize = iframe.removeCustomData( 'onResize' );
|
||||
onResize && onResize.removeListener();
|
||||
|
||||
// IE BUG: When destroying editor DOM with the selection remains inside
|
||||
// editing area would break IE7/8's selection system, we have to put the editing
|
||||
// iframe offline first. (http://dev.ckeditor.com/ticket/3812 and http://dev.ckeditor.com/ticket/5441)
|
||||
iframe.remove();
|
||||
} else {
|
||||
CKEDITOR.warn( 'editor-destroy-iframe' );
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
||||
function objectResizeDisabler( editor ) {
|
||||
if ( CKEDITOR.env.gecko ) {
|
||||
// FF allows to change resizing preferences by calling execCommand.
|
||||
try {
|
||||
var doc = editor.document.$;
|
||||
doc.execCommand( 'enableObjectResizing', false, !editor.config.disableObjectResizing );
|
||||
doc.execCommand( 'enableInlineTableEditing', false, !editor.config.disableNativeTableHandles );
|
||||
} catch ( e ) {}
|
||||
} else if ( CKEDITOR.env.ie && CKEDITOR.env.version < 11 && editor.config.disableObjectResizing ) {
|
||||
// It's possible to prevent resizing up to IE10.
|
||||
blockResizeStart( editor );
|
||||
}
|
||||
|
||||
// Disables resizing by preventing default action on resizestart event.
|
||||
function blockResizeStart() {
|
||||
var lastListeningElement;
|
||||
|
||||
// We'll attach only one listener at a time, instead of adding it to every img, input, hr etc.
|
||||
// Listener will be attached upon selectionChange, we'll also check if there was any element that
|
||||
// got listener before (lastListeningElement) - if so we need to remove previous listener.
|
||||
editor.editable().attachListener( editor, 'selectionChange', function() {
|
||||
var selectedElement = editor.getSelection().getSelectedElement();
|
||||
|
||||
if ( selectedElement ) {
|
||||
if ( lastListeningElement ) {
|
||||
lastListeningElement.detachEvent( 'onresizestart', resizeStartListener );
|
||||
lastListeningElement = null;
|
||||
}
|
||||
|
||||
// IE requires using attachEvent, because it does not work using W3C compilant addEventListener,
|
||||
// tested with IE10.
|
||||
selectedElement.$.attachEvent( 'onresizestart', resizeStartListener );
|
||||
lastListeningElement = selectedElement.$;
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
function resizeStartListener( evt ) {
|
||||
evt.returnValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
function iframeCssFixes() {
|
||||
var css = [];
|
||||
|
||||
// IE>=8 stricts mode doesn't have 'contentEditable' in effect
|
||||
// on element unless it has layout. (http://dev.ckeditor.com/ticket/5562)
|
||||
if ( CKEDITOR.document.$.documentMode >= 8 ) {
|
||||
css.push( 'html.CSS1Compat [contenteditable=false]{min-height:0 !important}' );
|
||||
|
||||
var selectors = [];
|
||||
|
||||
for ( var tag in CKEDITOR.dtd.$removeEmpty )
|
||||
selectors.push( 'html.CSS1Compat ' + tag + '[contenteditable=false]' );
|
||||
|
||||
css.push( selectors.join( ',' ) + '{display:inline-block}' );
|
||||
}
|
||||
// Set the HTML style to 100% to have the text cursor in affect (http://dev.ckeditor.com/ticket/6341)
|
||||
else if ( CKEDITOR.env.gecko ) {
|
||||
css.push( 'html{height:100% !important}' );
|
||||
css.push( 'img:-moz-broken{-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}' );
|
||||
}
|
||||
|
||||
// http://dev.ckeditor.com/ticket/6341: The text cursor must be set on the editor area.
|
||||
// http://dev.ckeditor.com/ticket/6632: Avoid having "text" shape of cursor in IE7 scrollbars.
|
||||
css.push( 'html{cursor:text;*cursor:auto}' );
|
||||
|
||||
// Use correct cursor for these elements
|
||||
css.push( 'img,input,textarea{cursor:default}' );
|
||||
|
||||
return css.join( '\n' );
|
||||
}
|
||||
} )();
|
||||
|
||||
/**
|
||||
* Disables the ability to resize objects (images and tables) in the editing area.
|
||||
*
|
||||
* config.disableObjectResizing = true;
|
||||
*
|
||||
* **Note:** Because of incomplete implementation of editing features in browsers
|
||||
* this option does not work for inline editors (see ticket [#10197](http://dev.ckeditor.com/ticket/10197)),
|
||||
* does not work in Internet Explorer 11+ (see [#9317](http://dev.ckeditor.com/ticket/9317#comment:16) and
|
||||
* [IE11+ issue](https://connect.microsoft.com/IE/feedback/details/742593/please-respect-execcommand-enableobjectresizing-in-contenteditable-elements)).
|
||||
* In Internet Explorer 8-10 this option only blocks resizing, but it is unable to hide the resize handles.
|
||||
*
|
||||
* @cfg
|
||||
* @member CKEDITOR.config
|
||||
*/
|
||||
CKEDITOR.config.disableObjectResizing = false;
|
||||
|
||||
/**
|
||||
* Disables the "table tools" offered natively by the browser (currently
|
||||
* Firefox only) to perform quick table editing operations, like adding or
|
||||
* deleting rows and columns.
|
||||
*
|
||||
* config.disableNativeTableHandles = false;
|
||||
*
|
||||
* @cfg
|
||||
* @member CKEDITOR.config
|
||||
*/
|
||||
CKEDITOR.config.disableNativeTableHandles = true;
|
||||
|
||||
/**
|
||||
* Disables the built-in spell checker if the browser provides one.
|
||||
*
|
||||
* **Note:** Although word suggestions provided natively by the browsers will
|
||||
* not appear in CKEditor's default context menu,
|
||||
* users can always reach the native context menu by holding the
|
||||
* *Ctrl* key when right-clicking if {@link #browserContextMenuOnCtrl}
|
||||
* is enabled or you are simply not using the
|
||||
* [context menu](http://ckeditor.com/addon/contextmenu) plugin.
|
||||
*
|
||||
* config.disableNativeSpellChecker = false;
|
||||
*
|
||||
* @cfg
|
||||
* @member CKEDITOR.config
|
||||
*/
|
||||
CKEDITOR.config.disableNativeSpellChecker = true;
|
||||
|
||||
/**
|
||||
* Language code of the writing language which is used to author the editor
|
||||
* content. This option accepts one single entry value in the format defined in the
|
||||
* [Tags for Identifying Languages (BCP47)](http://www.ietf.org/rfc/bcp/bcp47.txt)
|
||||
* IETF document and is used in the `lang` attribute.
|
||||
*
|
||||
* config.contentsLanguage = 'fr';
|
||||
*
|
||||
* @cfg {String} [contentsLanguage=same value with editor's UI language]
|
||||
* @member CKEDITOR.config
|
||||
*/
|
||||
|
||||
/**
|
||||
* The base href URL used to resolve relative and absolute URLs in the
|
||||
* editor content.
|
||||
*
|
||||
* config.baseHref = 'http://www.example.com/path/';
|
||||
*
|
||||
* @cfg {String} [baseHref='']
|
||||
* @member CKEDITOR.config
|
||||
*/
|
||||
|
||||
/**
|
||||
* Whether to automatically create wrapping blocks around inline content inside the document body.
|
||||
* This helps to ensure the integrity of the block *Enter* mode.
|
||||
*
|
||||
* **Note:** This option is deprecated. Changing the default value might introduce unpredictable usability issues and is
|
||||
* highly unrecommended.
|
||||
*
|
||||
* config.autoParagraph = false;
|
||||
*
|
||||
* @deprecated
|
||||
* @since 3.6
|
||||
* @cfg {Boolean} [autoParagraph=true]
|
||||
* @member CKEDITOR.config
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when some elements are added to the document.
|
||||
*
|
||||
* @event ariaWidget
|
||||
* @member CKEDITOR.editor
|
||||
* @param {CKEDITOR.editor} editor This editor instance.
|
||||
* @param {CKEDITOR.dom.element} data The element being added.
|
||||
*/
|
||||
@@ -116,7 +116,7 @@ table#table {
|
||||
|
||||
min-width: 80%;
|
||||
width: 80%;
|
||||
min-height: 5em;
|
||||
min-height: 7em;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
border: 1px solid black;
|
||||
|
||||
@@ -39,7 +39,6 @@ define([
|
||||
'account': [
|
||||
'infoBlock',
|
||||
'displayName',
|
||||
'indentUnit',
|
||||
'languageSelector',
|
||||
'logoutEverywhere',
|
||||
'resetTips',
|
||||
@@ -49,6 +48,10 @@ define([
|
||||
'backupDrive',
|
||||
'importLocalPads',
|
||||
'resetDrive'
|
||||
],
|
||||
'code': [
|
||||
'indentUnit',
|
||||
'indentType'
|
||||
]
|
||||
};
|
||||
|
||||
@@ -129,10 +132,10 @@ define([
|
||||
var createIndentUnitSelector = function (obj) {
|
||||
var proxy = obj.proxy;
|
||||
|
||||
console.log('create indent unit selector');
|
||||
var $div = $('<div>', {
|
||||
'class': 'indentUnit element'
|
||||
});
|
||||
$('<label>').text(Messages.settings_codeIndentation).appendTo($div);
|
||||
|
||||
var $inputBlock = $('<div>', {
|
||||
'class': 'inputBlock',
|
||||
@@ -144,7 +147,6 @@ define([
|
||||
type: 'number',
|
||||
}).on('change', function () {
|
||||
var val = parseInt($input.val());
|
||||
console.log(val, typeof(val));
|
||||
if (typeof(val) !== 'number') { return; }
|
||||
proxy['cryptpad.indentUnit'] = val;
|
||||
}).appendTo($inputBlock);
|
||||
@@ -162,6 +164,41 @@ define([
|
||||
return $div;
|
||||
};
|
||||
|
||||
var createIndentTypeSelector = function (obj) {
|
||||
var proxy = obj.proxy;
|
||||
|
||||
var key = 'cryptpad.indentWithTabs';
|
||||
|
||||
var $div = $('<div>', {
|
||||
'class': 'indentType element'
|
||||
});
|
||||
$('<label>').text(Messages.settings_codeUseTabs).appendTo($div);
|
||||
|
||||
var $inputBlock = $('<div>', {
|
||||
'class': 'inputBlock',
|
||||
}).appendTo($div);
|
||||
|
||||
var $input = $('<input>', {
|
||||
type: 'checkbox',
|
||||
}).on('change', function () {
|
||||
var val = $input.is(':checked');
|
||||
if (typeof(val) !== 'boolean') { return; }
|
||||
proxy[key] = val;
|
||||
}).appendTo($inputBlock);
|
||||
|
||||
proxy.on('change', [key], function (o, n) { $input.val(n); });
|
||||
|
||||
Cryptpad.getAttribute('indentUnit', function (e, val) {
|
||||
if (e) { return void console.error(e); }
|
||||
if (typeof(val) !== 'number') {
|
||||
$input.val(2);
|
||||
} else {
|
||||
$input.val(val);
|
||||
}
|
||||
});
|
||||
return $div;
|
||||
};
|
||||
|
||||
var createResetTips = function () {
|
||||
var $div = $('<div>', {'class': 'resetTips element'});
|
||||
$('<label>', {'for' : 'resetTips'}).text(Messages.settings_resetTips).appendTo($div);
|
||||
@@ -380,6 +417,7 @@ define([
|
||||
var $category = $('<div>', {'class': 'category'}).appendTo($categories);
|
||||
if (key === 'account') { $category.append($('<span>', {'class': 'fa fa-user-o'})); }
|
||||
if (key === 'drive') { $category.append($('<span>', {'class': 'fa fa-hdd-o'})); }
|
||||
if (key === 'code') { $category.append($('<span>', {'class': 'fa fa-file-code-o' })); }
|
||||
|
||||
if (key === active) {
|
||||
$category.addClass('active');
|
||||
@@ -421,6 +459,7 @@ define([
|
||||
$rightside.append(createDisplayNameInput(obj));
|
||||
$rightside.append(createLanguageSelector());
|
||||
$rightside.append(createIndentUnitSelector(obj));
|
||||
$rightside.append(createIndentTypeSelector(obj));
|
||||
|
||||
if (Cryptpad.isLoggedIn()) {
|
||||
$rightside.append(createLogoutEverywhere(obj));
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
&[type="number"] {
|
||||
border-right: 1px solid #adadad;
|
||||
}
|
||||
&[type="checkbox"] {
|
||||
margin-right: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
.infoBlock {
|
||||
|
||||
@@ -36,6 +36,7 @@ h6 { font-size: 24px; }
|
||||
body {
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
font-size: initial;
|
||||
}
|
||||
.CodeMirror-focused .cm-matchhighlight {
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
|
||||
|
||||
Reference in New Issue
Block a user