15 changed files with 1039 additions and 2647 deletions
Split View
Diff Options
-
15www/bounce/index.html
-
10www/bounce/main.js
-
9www/bounce/readme.md
-
9www/pad/index.html
-
9www/pad/inner.html
-
792www/pad/inner.js
-
943www/pad/main.js
-
15www/pad/wysiwygarea-plugin.js
-
3www/pad2/ckeditor-inner.html
-
30www/pad2/index.html
-
37www/pad2/inner.html
-
60www/pad2/links.js
-
790www/pad2/main.js
-
224www/pad2/outer.js
-
740www/pad2/wysiwygarea-plugin.js
@ -0,0 +1,15 @@ |
|||
<!DOCTYPE html> |
|||
<html class="cp"> |
|||
<head> |
|||
<title data-localization="main_title">CryptPad: Zero Knowledge, Collaborative Real Time Editing</title> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> |
|||
<link rel="icon" type="image/png" href="/customize/main-favicon.png" id="favicon"/> |
|||
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script> |
|||
</head> |
|||
<body class="html"> |
|||
<noscript> |
|||
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p> |
|||
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p> |
|||
</noscript> |
|||
</html> |
|||
@ -0,0 +1,10 @@ |
|||
define([], function () { |
|||
if (window.localStorage && window.localStorage.FS_hash) { |
|||
window.alert('The bounce application must only be used from the sandbox domain, ' + |
|||
'please report this issue on https://github.com/xwiki-labs/cryptpad'); |
|||
return; |
|||
} |
|||
var bounceTo = decodeURIComponent(window.location.hash.slice(1)); |
|||
if (!bounceTo) { return; } |
|||
window.location.href = bounceTo; |
|||
}); |
|||
@ -0,0 +1,9 @@ |
|||
# Bounce app |
|||
|
|||
This app redirects you to a new URL. |
|||
This app must only be served from CryptPad's safe origin, if this app detects that it is being |
|||
served from the unsafe origin, it will throw an alert that it is misconfigured and it will refuse |
|||
to redirect. |
|||
|
|||
If the URL is a javascript: URL, it will be trapped by CryptPad's Content Security Policy rules |
|||
or in the worst case, it will run in the context of the sandboxed origin. |
|||
@ -1,3 +1,791 @@ |
|||
require(['/api/config'], function (ApiConfig) { |
|||
// see ckeditor_base.js getUrl()
|
|||
window.CKEDITOR_GETURL = function (resource) { |
|||
if (resource.indexOf( '/' ) === 0) { |
|||
resource = window.CKEDITOR.basePath.replace(/\/bower_components\/.*/, '') + resource; |
|||
} else if (resource.indexOf(':/') === -1) { |
|||
resource = window.CKEDITOR.basePath + resource; |
|||
} |
|||
if (resource[resource.length - 1] !== '/' && resource.indexOf('ver=') === -1) { |
|||
var args = ApiConfig.requireConf.urlArgs; |
|||
if (resource.indexOf('/bower_components/') !== -1) { |
|||
args = 'ver=' + window.CKEDITOR.timestamp; |
|||
} |
|||
resource += (resource.indexOf('?') >= 0 ? '&' : '?') + args; |
|||
} |
|||
return resource; |
|||
}; |
|||
require(['/bower_components/ckeditor/ckeditor.js']); |
|||
}); |
|||
define([ |
|||
'less!/customize/src/less/toolbar.less', |
|||
], function () {}); |
|||
'jquery', |
|||
'/bower_components/chainpad-crypto/crypto.js', |
|||
'/bower_components/hyperjson/hyperjson.js', |
|||
'/common/toolbar3.js', |
|||
'/common/cursor.js', |
|||
'/bower_components/chainpad-json-validator/json-ot.js', |
|||
'/common/TypingTests.js', |
|||
'json.sortify', |
|||
'/bower_components/textpatcher/TextPatcher.js', |
|||
'/common/cryptpad-common.js', |
|||
'/common/cryptget.js', |
|||
'/pad/links.js', |
|||
'/bower_components/nthen/index.js', |
|||
'/common/sframe-common.js', |
|||
'/api/config', |
|||
'/common/common-realtime.js', |
|||
|
|||
'/bower_components/file-saver/FileSaver.min.js', |
|||
'/bower_components/diff-dom/diffDOM.js', |
|||
|
|||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', |
|||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css', |
|||
'less!/customize/src/less/cryptpad.less', |
|||
'less!/customize/src/less/toolbar.less' |
|||
], function ( |
|||
$, |
|||
Crypto, |
|||
Hyperjson, |
|||
Toolbar, |
|||
Cursor, |
|||
JsonOT, |
|||
TypingTest, |
|||
JSONSortify, |
|||
TextPatcher, |
|||
Cryptpad, |
|||
Cryptget, |
|||
Links, |
|||
nThen, |
|||
SFCommon, |
|||
ApiConfig, |
|||
CommonRealtime) |
|||
{ |
|||
var saveAs = window.saveAs; |
|||
var Messages = Cryptpad.Messages; |
|||
var DiffDom = window.diffDOM; |
|||
|
|||
var stringify = function (obj) { return JSONSortify(obj); }; |
|||
|
|||
window.Toolbar = Toolbar; |
|||
window.Hyperjson = Hyperjson; |
|||
|
|||
var slice = function (coll) { |
|||
return Array.prototype.slice.call(coll); |
|||
}; |
|||
|
|||
var removeListeners = function (root) { |
|||
slice(root.attributes).map(function (attr) { |
|||
if (/^on/.test(attr.name)) { |
|||
root.attributes.removeNamedItem(attr.name); |
|||
} |
|||
}); |
|||
slice(root.children).forEach(removeListeners); |
|||
}; |
|||
|
|||
var hjsonToDom = function (H) { |
|||
var dom = Hyperjson.toDOM(H); |
|||
removeListeners(dom); |
|||
return dom; |
|||
}; |
|||
|
|||
var module = window.REALTIME_MODULE = window.APP = { |
|||
Hyperjson: Hyperjson, |
|||
TextPatcher: TextPatcher, |
|||
logFights: true, |
|||
fights: [], |
|||
Cryptpad: Cryptpad, |
|||
Cursor: Cursor, |
|||
}; |
|||
|
|||
var emitResize = module.emitResize = function () { |
|||
var evt = window.document.createEvent('UIEvents'); |
|||
evt.initUIEvent('resize', true, false, window, 0); |
|||
window.dispatchEvent(evt); |
|||
}; |
|||
|
|||
var toolbar; |
|||
|
|||
var isNotMagicLine = function (el) { |
|||
return !(el && typeof(el.getAttribute) === 'function' && |
|||
el.getAttribute('class') && |
|||
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1); |
|||
}; |
|||
|
|||
/* catch `type="_moz"` before it goes over the wire */ |
|||
var brFilter = function (hj) { |
|||
if (hj[1].type === '_moz') { hj[1].type = undefined; } |
|||
return hj; |
|||
}; |
|||
|
|||
var onConnectError = function () { |
|||
Cryptpad.errorLoadingScreen(Messages.websocketError); |
|||
}; |
|||
|
|||
var domFromHTML = function (html) { |
|||
return new DOMParser().parseFromString(html, 'text/html'); |
|||
}; |
|||
|
|||
var forbiddenTags = [ |
|||
'SCRIPT', |
|||
'IFRAME', |
|||
'OBJECT', |
|||
'APPLET', |
|||
'VIDEO', |
|||
'AUDIO' |
|||
]; |
|||
|
|||
var getHTML = function (inner) { |
|||
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML); |
|||
}; |
|||
|
|||
var CKEDITOR_CHECK_INTERVAL = 100; |
|||
var ckEditorAvailable = function (cb) { |
|||
var intr; |
|||
var check = function () { |
|||
if (window.CKEDITOR) { |
|||
clearTimeout(intr); |
|||
cb(window.CKEDITOR); |
|||
} |
|||
}; |
|||
intr = setInterval(function () { |
|||
console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL); |
|||
check(); |
|||
}, CKEDITOR_CHECK_INTERVAL); |
|||
check(); |
|||
}; |
|||
|
|||
var mkDiffOptions = function (cursor, readOnly) { |
|||
return { |
|||
preDiffApply: function (info) { |
|||
/* |
|||
Don't accept attributes that begin with 'on' |
|||
these are probably listeners, and we don't want to |
|||
send scripts over the wire. |
|||
*/ |
|||
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { |
|||
if (info.diff.name === 'href') { |
|||
// console.log(info.diff);
|
|||
//var href = info.diff.newValue;
|
|||
|
|||
// TODO normalize HTML entities
|
|||
if (/javascript *: */.test(info.diff.newValue)) { |
|||
// TODO remove javascript: links
|
|||
} |
|||
} |
|||
|
|||
if (/^on/.test(info.diff.name)) { |
|||
console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name); |
|||
return true; |
|||
} |
|||
} |
|||
/* |
|||
Also reject any elements which would insert any one of |
|||
our forbidden tag types: script, iframe, object, |
|||
applet, video, or audio |
|||
*/ |
|||
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) { |
|||
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) { |
|||
console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName); |
|||
return true; |
|||
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) { |
|||
console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (info.node && info.node.tagName === 'BODY') { |
|||
if (info.diff.action === 'removeAttribute' && |
|||
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
/* DiffDOM will filter out magicline plugin elements |
|||
in practice this will make it impossible to use it |
|||
while someone else is typing, which could be annoying. |
|||
|
|||
we should check when such an element is going to be |
|||
removed, and prevent that from happening. */ |
|||
if (info.node && info.node.tagName === 'SPAN' && |
|||
info.node.getAttribute('contentEditable') === "false") { |
|||
// it seems to be a magicline plugin element...
|
|||
if (info.diff.action === 'removeElement') { |
|||
// and you're about to remove it...
|
|||
// this probably isn't what you want
|
|||
|
|||
/* |
|||
I have never seen this in the console, but the |
|||
magic line is still getting removed on remote |
|||
edits. This suggests that it's getting removed |
|||
by something other than diffDom. |
|||
*/ |
|||
console.log("preventing removal of the magic line!"); |
|||
|
|||
// return true to prevent diff application
|
|||
return true; |
|||
} |
|||
} |
|||
|
|||
// Do not change the contenteditable value in view mode
|
|||
if (readOnly && info.node && info.node.tagName === 'BODY' && |
|||
info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') { |
|||
return true; |
|||
} |
|||
|
|||
// no use trying to recover the cursor if it doesn't exist
|
|||
if (!cursor.exists()) { return; } |
|||
|
|||
/* frame is either 0, 1, 2, or 3, depending on which |
|||
cursor frames were affected: none, first, last, or both |
|||
*/ |
|||
var frame = info.frame = cursor.inNode(info.node); |
|||
|
|||
if (!frame) { return; } |
|||
|
|||
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { |
|||
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); |
|||
|
|||
if (frame & 1) { |
|||
// push cursor start if necessary
|
|||
if (pushes.commonStart < cursor.Range.start.offset) { |
|||
cursor.Range.start.offset += pushes.delta; |
|||
} |
|||
} |
|||
if (frame & 2) { |
|||
// push cursor end if necessary
|
|||
if (pushes.commonStart < cursor.Range.end.offset) { |
|||
cursor.Range.end.offset += pushes.delta; |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
postDiffApply: function (info) { |
|||
if (info.frame) { |
|||
if (info.node) { |
|||
if (info.frame & 1) { cursor.fixStart(info.node); } |
|||
if (info.frame & 2) { cursor.fixEnd(info.node); } |
|||
} else { console.error("info.node did not exist"); } |
|||
|
|||
var sel = cursor.makeSelection(); |
|||
var range = cursor.makeRange(); |
|||
|
|||
cursor.fixSelection(sel, range); |
|||
} |
|||
} |
|||
}; |
|||
}; |
|||
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
|
|||
var andThen = function (editor, Ckeditor, common) { |
|||
//var $iframe = $('#pad-iframe').contents();
|
|||
//var secret = Cryptpad.getSecrets();
|
|||
//var readOnly = secret.keys && !secret.keys.editKeyStr;
|
|||
//if (!secret.keys) {
|
|||
// secret.keys = secret.key;
|
|||
//}
|
|||
var readOnly = false; // TODO
|
|||
var cpNfInner; |
|||
var metadataMgr; |
|||
var onLocal; |
|||
|
|||
var $bar = $('#cke_1_toolbox'); |
|||
|
|||
var $html = $bar.closest('html'); |
|||
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); |
|||
if ($faLink.length) { |
|||
$html.find('iframe').contents().find('head').append($faLink.clone()); |
|||
} |
|||
var isHistoryMode = false; |
|||
|
|||
if (readOnly) { |
|||
$('#cke_1_toolbox > .cke_toolbox_main').hide(); |
|||
} |
|||
|
|||
/* add a class to the magicline plugin so we can pick it out more easily */ |
|||
|
|||
var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$; |
|||
[ml, ml.parentElement].forEach(function (el) { |
|||
el.setAttribute('class', 'non-realtime'); |
|||
}); |
|||
|
|||
var ifrWindow = $html.find('iframe')[0].contentWindow; |
|||
|
|||
var documentBody = ifrWindow.document.body; |
|||
|
|||
var inner = window.inner = documentBody; |
|||
|
|||
var cursor = module.cursor = Cursor(inner); |
|||
|
|||
var openLink = function (e) { |
|||
var el = e.currentTarget; |
|||
if (!el || el.nodeName !== 'A') { return; } |
|||
var href = el.getAttribute('href'); |
|||
var bounceHref = window.location.origin + '/bounce/#' + encodeURIComponent(href); |
|||
if (href) { ifrWindow.open(bounceHref, '_blank'); } |
|||
}; |
|||
|
|||
var setEditable = module.setEditable = function (bool) { |
|||
if (bool) { |
|||
$(inner).css({ |
|||
color: '#333', |
|||
}); |
|||
} |
|||
if (!readOnly || !bool) { |
|||
inner.setAttribute('contenteditable', bool); |
|||
} |
|||
}; |
|||
|
|||
CommonRealtime.onInfiniteSpinner(function () { setEditable(false); }); |
|||
|
|||
// don't let the user edit until the pad is ready
|
|||
setEditable(false); |
|||
|
|||
var initializing = true; |
|||
|
|||
var Title; |
|||
//var UserList;
|
|||
//var Metadata;
|
|||
|
|||
var getHeadingText = function () { |
|||
var text; |
|||
if (['h1', 'h2', 'h3'].some(function (t) { |
|||
var $header = $(inner).find(t + ':first-of-type'); |
|||
if ($header.length && $header.text()) { |
|||
text = $header.text(); |
|||
return true; |
|||
} |
|||
})) { return text; } |
|||
}; |
|||
|
|||
var DD = new DiffDom(mkDiffOptions(cursor, readOnly)); |
|||
|
|||
// apply patches, and try not to lose the cursor in the process!
|
|||
var applyHjson = function (shjson) { |
|||
var userDocStateDom = hjsonToDom(JSON.parse(shjson)); |
|||
|
|||
if (!readOnly && !initializing) { |
|||
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
|
|||
} |
|||
var patch = (DD).diff(inner, userDocStateDom); |
|||
(DD).apply(inner, patch); |
|||
if (readOnly) { |
|||
var $links = $(inner).find('a'); |
|||
// off so that we don't end up with multiple identical handlers
|
|||
$links.off('click', openLink).on('click', openLink); |
|||
} |
|||
}; |
|||
|
|||
var stringifyDOM = module.stringifyDOM = function (dom) { |
|||
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); |
|||
hjson[3] = { |
|||
metadata: metadataMgr.getMetadataLazy() |
|||
}; |
|||
/*hjson[3] = { TODO |
|||
users: UserList.userData, |
|||
defaultTitle: Title.defaultTitle, |
|||
type: 'pad' |
|||
} |
|||
}; |
|||
if (!initializing) { |
|||
hjson[3].metadata.title = Title.title; |
|||
} else if (Cryptpad.initialName && !hjson[3].metadata.title) { |
|||
hjson[3].metadata.title = Cryptpad.initialName; |
|||
}*/ |
|||
return stringify(hjson); |
|||
}; |
|||
|
|||
var realtimeOptions = { |
|||
readOnly: readOnly, |
|||
// really basic operational transform
|
|||
transformFunction : JsonOT.validate, |
|||
// cryptpad debug logging (default is 1)
|
|||
// logLevel: 0,
|
|||
validateContent: function (content) { |
|||
try { |
|||
JSON.parse(content); |
|||
return true; |
|||
} catch (e) { |
|||
console.log("Failed to parse, rejecting patch"); |
|||
return false; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
var setHistory = function (bool, update) { |
|||
isHistoryMode = bool; |
|||
setEditable(!bool); |
|||
if (!bool && update) { |
|||
realtimeOptions.onRemote(); |
|||
} |
|||
}; |
|||
|
|||
realtimeOptions.onRemote = function () { |
|||
if (initializing) { return; } |
|||
if (isHistoryMode) { return; } |
|||
|
|||
var oldShjson = stringifyDOM(inner); |
|||
|
|||
var shjson = module.realtime.getUserDoc(); |
|||
|
|||
// remember where the cursor is
|
|||
cursor.update(); |
|||
|
|||
// Update the user list (metadata) from the hyperjson
|
|||
// TODO Metadata.update(shjson);
|
|||
|
|||
var newInner = JSON.parse(shjson); |
|||
var newSInner; |
|||
if (newInner.length > 2) { |
|||
newSInner = stringify(newInner[2]); |
|||
} |
|||
|
|||
if (newInner[3]) { |
|||
metadataMgr.updateMetadata(newInner[3].metadata); |
|||
} |
|||
|
|||
// build a dom from HJSON, diff, and patch the editor
|
|||
applyHjson(shjson); |
|||
|
|||
if (!readOnly) { |
|||
var shjson2 = stringifyDOM(inner); |
|||
|
|||
// TODO
|
|||
//shjson = JSON.stringify(JSON.parse(shjson).slice(0,3));
|
|||
|
|||
if (shjson2 !== shjson) { |
|||
console.error("shjson2 !== shjson"); |
|||
module.patchText(shjson2); |
|||
|
|||
/* pushing back over the wire is necessary, but it can |
|||
result in a feedback loop, which we call a browser |
|||
fight */ |
|||
if (module.logFights) { |
|||
// what changed?
|
|||
var op = TextPatcher.diff(shjson, shjson2); |
|||
// log the changes
|
|||
TextPatcher.log(shjson, op); |
|||
var sop = JSON.stringify(TextPatcher.format(shjson, op)); |
|||
|
|||
var index = module.fights.indexOf(sop); |
|||
if (index === -1) { |
|||
module.fights.push(sop); |
|||
console.log("Found a new type of browser disagreement"); |
|||
console.log("You can inspect the list in your " + |
|||
"console at `REALTIME_MODULE.fights`"); |
|||
console.log(module.fights); |
|||
} else { |
|||
console.log("Encountered a known browser disagreement: " + |
|||
"available at `REALTIME_MODULE.fights[%s]`", index); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Notify only when the content has changed, not when someone has joined/left
|
|||
var oldSInner = stringify(JSON.parse(oldShjson)[2]); |
|||
if (newSInner && newSInner !== oldSInner) { |
|||
Cryptpad.notify(); |
|||
} |
|||
}; |
|||
|
|||
var exportFile = function () { |
|||
var html = getHTML(inner); |
|||
var suggestion = Title.suggestTitle('cryptpad-document'); |
|||
Cryptpad.prompt(Messages.exportPrompt, |
|||
Cryptpad.fixFileName(suggestion) + '.html', function (filename) { |
|||
if (!(typeof(filename) === 'string' && filename)) { return; } |
|||
var blob = new Blob([html], {type: "text/html;charset=utf-8"}); |
|||
saveAs(blob, filename); |
|||
}); |
|||
}; |
|||
var importFile = function (content) { |
|||
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body)); |
|||
applyHjson(shjson); |
|||
realtimeOptions.onLocal(); |
|||
}; |
|||
|
|||
realtimeOptions.onInit = function (info) { |
|||
readOnly = metadataMgr.getPrivateData().readOnly; |
|||
console.log('onInit'); |
|||
var titleCfg = { getHeadingText: getHeadingText }; |
|||
Title = common.createTitle(titleCfg, realtimeOptions.onLocal, common, metadataMgr); |
|||
var configTb = { |
|||
displayed: ['userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit'], |
|||
title: Title.getTitleConfig(), |
|||
metadataMgr: metadataMgr, |
|||
readOnly: readOnly, |
|||
ifrw: window, |
|||
realtime: info.realtime, |
|||
common: Cryptpad, |
|||
sfCommon: common, |
|||
$container: $bar, |
|||
$contentContainer: $('#cke_1_contents'), |
|||
}; |
|||
toolbar = info.realtime.toolbar = Toolbar.create(configTb); |
|||
Title.setToolbar(toolbar); |
|||
|
|||
var $rightside = toolbar.$rightside; |
|||
var $drawer = toolbar.$drawer; |
|||
|
|||
var src = 'less!/customize/src/less/toolbar.less'; |
|||
require([ |
|||
src |
|||
], function () { |
|||
var $html = $bar.closest('html'); |
|||
$html |
|||
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]') |
|||
.appendTo($html.find('head')); |
|||
}); |
|||
|
|||
$bar.find('#cke_1_toolbar_collapser').hide(); |
|||
if (!readOnly) { |
|||
// Expand / collapse the toolbar
|
|||
var $collapse = Cryptpad.createButton(null, true); |
|||
$collapse.removeClass('fa-question'); |
|||
var updateIcon = function () { |
|||
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up'); |
|||
var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible'); |
|||
if (isCollapsed) { |
|||
if (!initializing) { common.feedback('HIDETOOLBAR_PAD'); } |
|||
$collapse.addClass('fa-caret-down'); |
|||
} |
|||
else { |
|||
if (!initializing) { common.feedback('SHOWTOOLBAR_PAD'); } |
|||
$collapse.addClass('fa-caret-up'); |
|||
} |
|||
}; |
|||
updateIcon(); |
|||
$collapse.click(function () { |
|||
$(window).trigger('resize'); |
|||
$('.cke_toolbox_main').toggle(); |
|||
$(window).trigger('cryptpad-ck-toolbar'); |
|||
updateIcon(); |
|||
}); |
|||
$rightside.append($collapse); |
|||
} else { |
|||
$('.cke_toolbox_main').hide(); |
|||
} |
|||
|
|||
/* add a history button */ |
|||
var histConfig = { |
|||
onLocal: realtimeOptions.onLocal, |
|||
onRemote: realtimeOptions.onRemote, |
|||
setHistory: setHistory, |
|||
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); }, |
|||
$toolbar: $bar |
|||
}; |
|||
var $hist = common.createButton('history', true, {histConfig: histConfig}); |
|||
$drawer.append($hist); |
|||
|
|||
if (!metadataMgr.getPrivateData().isTemplate) { |
|||
var templateObj = { |
|||
rt: info.realtime, |
|||
getTitle: function () { return metadataMgr.getMetadata().title; } |
|||
}; |
|||
var $templateButton = common.createButton('template', true, templateObj); |
|||
$rightside.append($templateButton); |
|||
} |
|||
|
|||
/* add an export button */ |
|||
var $export = Cryptpad.createButton('export', true, {}, exportFile); |
|||
$drawer.append($export); |
|||
|
|||
if (!readOnly) { |
|||
/* add an import button */ |
|||
var $import = Cryptpad.createButton('import', true, { |
|||
accept: 'text/html' |
|||
}, importFile); |
|||
$drawer.append($import); |
|||
} |
|||
|
|||
/* add a forget button */ |
|||
var forgetCb = function (err) { |
|||
if (err) { return; } |
|||
setEditable(false); |
|||
}; |
|||
var $forgetPad = common.createButton('forget', true, {}, forgetCb); |
|||
$rightside.append($forgetPad); |
|||
}; |
|||
|
|||
// this should only ever get called once, when the chain syncs
|
|||
realtimeOptions.onReady = function (info) { |
|||
console.log('onReady'); |
|||
if (!module.isMaximized) { |
|||
module.isMaximized = true; |
|||
$('iframe.cke_wysiwyg_frame').css('width', ''); |
|||
$('iframe.cke_wysiwyg_frame').css('height', ''); |
|||
} |
|||
$('body').addClass('app-pad'); |
|||
|
|||
if (module.realtime !== info.realtime) { |
|||
module.patchText = TextPatcher.create({ |
|||
realtime: info.realtime, |
|||
//logging: true,
|
|||
}); |
|||
} |
|||
|
|||
module.realtime = info.realtime; |
|||
|
|||
var shjson = module.realtime.getUserDoc(); |
|||
|
|||
var newPad = false; |
|||
if (shjson === '') { newPad = true; } |
|||
|
|||
if (!newPad) { |
|||
applyHjson(shjson); |
|||
|
|||
// Update the user list (metadata) from the hyperjson
|
|||
// XXX Metadata.update(shjson);
|
|||
var parsed = JSON.parse(shjson); |
|||
if (parsed[3] && parsed[3].metadata) { |
|||
metadataMgr.updateMetadata(parsed[3].metadata); |
|||
} |
|||
|
|||
if (!readOnly) { |
|||
var shjson2 = stringifyDOM(inner); |
|||
var hjson2 = JSON.parse(shjson2).slice(0,3); |
|||
var hjson = JSON.parse(shjson).slice(0,3); |
|||
if (stringify(hjson2) !== stringify(hjson)) { |
|||
console.log('err'); |
|||
console.error("shjson2 !== shjson"); |
|||
console.log(stringify(hjson2)); |
|||
console.log(stringify(hjson)); |
|||
Cryptpad.errorLoadingScreen(Messages.wrongApp); |
|||
throw new Error(); |
|||
} |
|||
} |
|||
} else { |
|||
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); |
|||
documentBody.innerHTML = Messages.initialState; |
|||
} |
|||
|
|||
Cryptpad.removeLoadingScreen(emitResize); |
|||
setEditable(!readOnly); |
|||
initializing = false; |
|||
|
|||
if (readOnly) { return; } |
|||
//TODO UserList.getLastName(toolbar.$userNameButton, newPad);
|
|||
onLocal(); |
|||
editor.focus(); |
|||
if (newPad) { |
|||
cursor.setToEnd(); |
|||
} else { |
|||
cursor.setToStart(); |
|||
} |
|||
}; |
|||
|
|||
realtimeOptions.onConnectionChange = function (info) { |
|||
setEditable(info.state); |
|||
//toolbar.failed(); TODO
|
|||
if (info.state) { |
|||
initializing = true; |
|||
//toolbar.reconnecting(info.myId); // TODO
|
|||
Cryptpad.findOKButton().click(); |
|||
} else { |
|||
Cryptpad.alert(Messages.common_connectionLost, undefined, true); |
|||
} |
|||
}; |
|||
|
|||
realtimeOptions.onError = onConnectError; |
|||
|
|||
onLocal = realtimeOptions.onLocal = function () { |
|||
console.log('onlocal'); |
|||
if (initializing) { return; } |
|||
if (isHistoryMode) { return; } |
|||
if (readOnly) { return; } |
|||
|
|||
// stringify the json and send it into chainpad
|
|||
var shjson = stringifyDOM(inner); |
|||
|
|||
module.patchText(shjson); |
|||
if (module.realtime.getUserDoc() !== shjson) { |
|||
console.error("realtime.getUserDoc() !== shjson"); |
|||
} |
|||
}; |
|||
|
|||
cpNfInner = common.startRealtime(realtimeOptions); |
|||
metadataMgr = cpNfInner.metadataMgr; |
|||
|
|||
Cryptpad.onLogout(function () { setEditable(false); }); |
|||
|
|||
/* hitting enter makes a new line, but places the cursor inside |
|||
of the <br> instead of the <p>. This makes it such that you |
|||
cannot type until you click, which is rather unnacceptable. |
|||
If the cursor is ever inside such a <br>, you probably want |
|||
to push it out to the parent element, which ought to be a |
|||
paragraph tag. This needs to be done on keydown, otherwise |
|||
the first such keypress will not be inserted into the P. */ |
|||
inner.addEventListener('keydown', cursor.brFix); |
|||
|
|||
editor.on('change', onLocal); |
|||
|
|||
// export the typing tests to the window.
|
|||
// call like `test = easyTest()`
|
|||
// terminate the test like `test.cancel()`
|
|||
window.easyTest = function () { |
|||
cursor.update(); |
|||
var start = cursor.Range.start; |
|||
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); |
|||
onLocal(); |
|||
return test; |
|||
}; |
|||
|
|||
$bar.find('.cke_button').click(function () { |
|||
var e = this; |
|||
var classString = e.getAttribute('class'); |
|||
var classes = classString.split(' ').filter(function (c) { |
|||
return /cke_button__/.test(c); |
|||
}); |
|||
|
|||
var id = classes[0]; |
|||
if (typeof(id) === 'string') { |
|||
common.feedback(id.toUpperCase()); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
var main = function () { |
|||
var Ckeditor; |
|||
var editor; |
|||
var common; |
|||
|
|||
nThen(function (waitFor) { |
|||
ckEditorAvailable(waitFor(function (ck) { |
|||
Ckeditor = ck; |
|||
require(['/pad/wysiwygarea-plugin.js'], waitFor()); |
|||
})); |
|||
$(waitFor(function () { |
|||
Cryptpad.addLoadingScreen(); |
|||
})); |
|||
SFCommon.create(waitFor(function (c) { module.common = common = c; })); |
|||
}).nThen(function (waitFor) { |
|||
Ckeditor.config.toolbarCanCollapse = true; |
|||
if (screen.height < 800) { |
|||
Ckeditor.config.toolbarStartupExpanded = false; |
|||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no'); |
|||
} else { |
|||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes'); |
|||
} |
|||
// Used in ckeditor-config.js
|
|||
Ckeditor.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs; |
|||
editor = Ckeditor.replace('editor1', { |
|||
customConfig: '/customize/ckeditor-config.js', |
|||
}); |
|||
editor.on('instanceReady', waitFor()); |
|||
}).nThen(function (/*waitFor*/) { |
|||
Links.addSupportForOpeningLinksInNewTab(Ckeditor); |
|||
Cryptpad.onError(function (info) { |
|||
if (info && info.type === "store") { |
|||
onConnectError(); |
|||
} |
|||
}); |
|||
andThen(editor, Ckeditor, common); |
|||
}); |
|||
}; |
|||
main(); |
|||
}); |
|||
@ -1,768 +1,223 @@ |
|||
// Load #1, load as little as possible because we are in a race to get the loading screen up.
|
|||
define([ |
|||
'/bower_components/nthen/index.js', |
|||
'/api/config', |
|||
'jquery', |
|||
'/bower_components/chainpad-crypto/crypto.js', |
|||
'/bower_components/chainpad-netflux/chainpad-netflux.js', |
|||
'/bower_components/hyperjson/hyperjson.js', |
|||
'/common/toolbar2.js', |
|||
'/common/cursor.js', |
|||
'/bower_components/chainpad-json-validator/json-ot.js', |
|||
'/common/TypingTests.js', |
|||
'json.sortify', |
|||
'/bower_components/textpatcher/TextPatcher.js', |
|||
'/common/cryptpad-common.js', |
|||
'/common/cryptget.js', |
|||
'/pad/links.js', |
|||
'/bower_components/file-saver/FileSaver.min.js', |
|||
'/bower_components/diff-dom/diffDOM.js', |
|||
|
|||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css', |
|||
'less!/customize/src/less/cryptpad.less', |
|||
], function ($, Crypto, realtimeInput, Hyperjson, |
|||
Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, Cryptget, Links) { |
|||
var saveAs = window.saveAs; |
|||
var Messages = Cryptpad.Messages; |
|||
|
|||
$(function () { |
|||
|
|||
var ifrw = $('#pad-iframe')[0].contentWindow; |
|||
var Ckeditor; // to be initialized later...
|
|||
var DiffDom = window.diffDOM; |
|||
|
|||
Cryptpad.addLoadingScreen(); |
|||
|
|||
var stringify = function (obj) { |
|||
return JSONSortify(obj); |
|||
}; |
|||
|
|||
window.Toolbar = Toolbar; |
|||
window.Hyperjson = Hyperjson; |
|||
|
|||
var slice = function (coll) { |
|||
return Array.prototype.slice.call(coll); |
|||
}; |
|||
|
|||
var removeListeners = function (root) { |
|||
slice(root.attributes).map(function (attr) { |
|||
if (/^on/i.test(attr.name)) { |
|||
root.attributes.removeNamedItem(attr.name); |
|||
} |
|||
}); |
|||
slice(root.children).forEach(removeListeners); |
|||
}; |
|||
|
|||
var hjsonToDom = function (H) { |
|||
var dom = Hyperjson.toDOM(H); |
|||
removeListeners(dom); |
|||
return dom; |
|||
}; |
|||
|
|||
var module = window.REALTIME_MODULE = window.APP = { |
|||
Hyperjson: Hyperjson, |
|||
TextPatcher: TextPatcher, |
|||
logFights: true, |
|||
fights: [], |
|||
Cryptpad: Cryptpad, |
|||
Cursor: Cursor, |
|||
}; |
|||
|
|||
var emitResize = module.emitResize = function () { |
|||
var cw = $('#pad-iframe')[0].contentWindow; |
|||
|
|||
var evt = cw.document.createEvent('UIEvents'); |
|||
evt.initUIEvent('resize', true, false, cw, 0); |
|||
cw.dispatchEvent(evt); |
|||
}; |
|||
|
|||
var toolbar; |
|||
|
|||
var isNotMagicLine = function (el) { |
|||
return !(el && typeof(el.getAttribute) === 'function' && |
|||
el.getAttribute('class') && |
|||
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1); |
|||
}; |
|||
|
|||
/* catch `type="_moz"` before it goes over the wire */ |
|||
var brFilter = function (hj) { |
|||
if (hj[1].type === '_moz') { hj[1].type = undefined; } |
|||
return hj; |
|||
}; |
|||
|
|||
var onConnectError = function () { |
|||
Cryptpad.errorLoadingScreen(Messages.websocketError); |
|||
}; |
|||
|
|||
var andThen = function (Ckeditor) { |
|||
var $iframe = $('#pad-iframe').contents(); |
|||
var secret = Cryptpad.getSecrets(); |
|||
var readOnly = secret.keys && !secret.keys.editKeyStr; |
|||
if (!secret.keys) { |
|||
secret.keys = secret.key; |
|||
'/common/requireconfig.js' |
|||
], function (nThen, ApiConfig, $, RequireConfig) { |
|||
var requireConfig = RequireConfig(); |
|||
|
|||
// Loaded in load #2
|
|||
var CpNfOuter; |
|||
var Cryptpad; |
|||
var Crypto; |
|||
var Cryptget; |
|||
|
|||
var sframeChan; |
|||
var secret; |
|||
var hashes; |
|||
|
|||
nThen(function (waitFor) { |
|||
$(waitFor()); |
|||
}).nThen(function (waitFor) { |
|||
var req = { |
|||
cfg: requireConfig, |
|||
req: [ '/common/loading.js' ], |
|||
pfx: window.location.origin |
|||
}; |
|||
window.rc = requireConfig; |
|||
window.apiconf = ApiConfig; |
|||
$('#sbox-iframe').attr('src', |
|||
ApiConfig.httpSafeOrigin + '/pad/inner.html?' + requireConfig.urlArgs + |
|||
'#' + encodeURIComponent(JSON.stringify(req))); |
|||
|
|||
// This is a cheap trick to avoid loading sframe-channel in parallel with the
|
|||
// loading screen setup.
|
|||
var done = waitFor(); |
|||
var onMsg = function (msg) { |
|||
var data = JSON.parse(msg.data); |
|||
if (data.q !== 'READY') { return; } |
|||
window.removeEventListener('message', onMsg); |
|||
var _done = done; |
|||
done = function () { }; |
|||
_done(); |
|||
}; |
|||
window.addEventListener('message', onMsg); |
|||
|
|||
}).nThen(function (waitFor) { |
|||
// Load #2, the loading screen is up so grab whatever you need...
|
|||
require([ |
|||
'/common/sframe-chainpad-netflux-outer.js', |
|||
'/common/cryptpad-common.js', |
|||
'/bower_components/chainpad-crypto/crypto.js', |
|||
'/common/cryptget.js', |
|||
'/common/sframe-channel.js', |
|||
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, SFrameChannel) { |
|||
CpNfOuter = _CpNfOuter; |
|||
Cryptpad = _Cryptpad; |
|||
Crypto = _Crypto; |
|||
Cryptget = _Cryptget; |
|||
SFrameChannel.create($('#sbox-iframe')[0].contentWindow, waitFor(function (sfc) { |
|||
sframeChan = sfc; |
|||
})); |
|||
Cryptpad.ready(waitFor()); |
|||
})); |
|||
}).nThen(function (waitFor) { |
|||
secret = Cryptpad.getSecrets(); |
|||
if (!secret.channel) { |
|||
// New pad: create a new random channel id
|
|||
secret.channel = Cryptpad.createChannelId(); |
|||
} |
|||
Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; })); |
|||
}).nThen(function (/*waitFor*/) { |
|||
var readOnly = secret.keys && !secret.keys.editKeyStr; |
|||
if (!secret.keys) { secret.keys = secret.key; } |
|||
var parsed = Cryptpad.parsePadUrl(window.location.href); |
|||
if (!parsed.type) { throw new Error(); } |
|||
var defaultTitle = Cryptpad.getDefaultName(parsed); |
|||
var updateMeta = function () { |
|||
//console.log('EV_METADATA_UPDATE');
|
|||
var name; |
|||
nThen(function (waitFor) { |
|||
Cryptpad.getLastName(waitFor(function (err, n) { |
|||
if (err) { console.log(err); } |
|||
name = n; |
|||
})); |
|||
}).nThen(function (/*waitFor*/) { |
|||
sframeChan.event('EV_METADATA_UPDATE', { |
|||
doc: { |
|||
defaultTitle: defaultTitle, |
|||
type: parsed.type |
|||
}, |
|||
user: { |
|||
name: name, |
|||
uid: Cryptpad.getUid(), |
|||
avatar: Cryptpad.getAvatarUrl(), |
|||
profile: Cryptpad.getProfileUrl(), |
|||
curvePublic: Cryptpad.getProxy().curvePublic, |
|||
netfluxId: Cryptpad.getNetwork().webChannels[0].myID, |
|||
}, |
|||
priv: { |
|||
accountName: Cryptpad.getAccountName(), |
|||
origin: window.location.origin, |
|||
pathname: window.location.pathname, |
|||
readOnly: readOnly, |
|||
availableHashes: hashes, |
|||
isTemplate: Cryptpad.isTemplate(window.location.href), |
|||
feedbackAllowed: Cryptpad.isFeedbackAllowed() |
|||
} |
|||
}); |
|||
}); |
|||
}; |
|||
Cryptpad.onDisplayNameChanged(updateMeta); |
|||
sframeChan.onReg('EV_METADATA_UPDATE', updateMeta); |
|||
|
|||
var editor = window.editor = Ckeditor.replace('editor1', { |
|||
customConfig: '/customize/ckeditor-config.js', |
|||
}); |
|||
|
|||
editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor)); |
|||
editor.on('instanceReady', function () { |
|||
var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox'); |
|||
|
|||
var $html = $bar.closest('html'); |
|||
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); |
|||
if ($faLink.length) { |
|||
$html.find('iframe').contents().find('head').append($faLink.clone()); |
|||
} |
|||
var isHistoryMode = false; |
|||
|
|||
if (readOnly) { |
|||
$('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox > .cke_toolbox_main').hide(); |
|||
Cryptpad.onError(function (info) { |
|||
console.log('error'); |
|||
console.log(info); |
|||
if (info && info.type === "store") { |
|||
//onConnectError();
|
|||
} |
|||
}); |
|||
|
|||
/* add a class to the magicline plugin so we can pick it out more easily */ |
|||
|
|||
var ml = $('iframe')[0].contentWindow.CKEDITOR.instances.editor1.plugins.magicline |
|||
.backdoor.that.line.$; |
|||
|
|||
[ml, ml.parentElement].forEach(function (el) { |
|||
el.setAttribute('class', 'non-realtime'); |
|||
sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) { |
|||
Cryptpad.anonRpcMsg(data.msg, data.content, function (err, response) { |
|||
cb({error: err, response: response}); |
|||
}); |
|||
}); |
|||
|
|||
var documentBody = ifrw.$('iframe')[0].contentDocument.body; |
|||
|
|||
var inner = window.inner = documentBody; |
|||
|
|||
// hide all content until the realtime doc is ready
|
|||
$(inner).css({ |
|||
color: '#fff', |
|||
sframeChan.on('Q_SET_PAD_TITLE_IN_DRIVE', function (newTitle, cb) { |
|||
Cryptpad.renamePad(newTitle, undefined, function (err) { |
|||
if (err) { cb('ERROR'); } else { cb(); } |
|||
}); |
|||
}); |
|||
|
|||
var cursor = module.cursor = Cursor(inner); |
|||
|
|||
var setEditable = module.setEditable = function (bool) { |
|||
if (bool) { |
|||
$(inner).css({ |
|||
color: '#333', |
|||
}); |
|||
} |
|||
if (!readOnly || !bool) { |
|||
inner.setAttribute('contenteditable', bool); |
|||
} |
|||
}; |
|||
|
|||
// don't let the user edit until the pad is ready
|
|||
setEditable(false); |
|||
|
|||
var forbiddenTags = [ |
|||
'SCRIPT', |
|||
'IFRAME', |
|||
'OBJECT', |
|||
'APPLET', |
|||
'VIDEO', |
|||
'AUDIO' |
|||
]; |
|||
|
|||
var diffOptions = { |
|||
preDiffApply: function (info) { |
|||
/* |
|||
Don't accept attributes that begin with 'on' |
|||
these are probably listeners, and we don't want to |
|||
send scripts over the wire. |
|||
*/ |
|||
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { |
|||
if (info.diff.name === 'href') { |
|||
// console.log(info.diff);
|
|||
//var href = info.diff.newValue;
|
|||
|
|||
// TODO normalize HTML entities
|
|||
if (/javascript *: */.test(info.diff.newValue)) { |
|||
// TODO remove javascript: links
|
|||
} |
|||
} |
|||
|
|||
if (/^on/.test(info.diff.name)) { |
|||
console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name); |
|||
return true; |
|||
} |
|||
} |
|||
/* |
|||
Also reject any elements which would insert any one of |
|||
our forbidden tag types: script, iframe, object, |
|||
applet, video, or audio |
|||
*/ |
|||
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) { |
|||
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) { |
|||
console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName); |
|||
return true; |
|||
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) { |
|||
console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (info.node && info.node.tagName === 'BODY') { |
|||
if (info.diff.action === 'removeAttribute' && |
|||
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
/* DiffDOM will filter out magicline plugin elements |
|||
in practice this will make it impossible to use it |
|||
while someone else is typing, which could be annoying. |
|||
|
|||
we should check when such an element is going to be |
|||
removed, and prevent that from happening. */ |
|||
if (info.node && info.node.tagName === 'SPAN' && |
|||
info.node.getAttribute('contentEditable') === "false") { |
|||
// it seems to be a magicline plugin element...
|
|||
if (info.diff.action === 'removeElement') { |
|||
// and you're about to remove it...
|
|||
// this probably isn't what you want
|
|||
|
|||
/* |
|||
I have never seen this in the console, but the |
|||
magic line is still getting removed on remote |
|||
edits. This suggests that it's getting removed |
|||
by something other than diffDom. |
|||
*/ |
|||
console.log("preventing removal of the magic line!"); |
|||
|
|||
// return true to prevent diff application
|
|||
return true; |
|||
} |
|||
} |
|||
|
|||
// Do not change the contenteditable value in view mode
|
|||
if (readOnly && info.node && info.node.tagName === 'BODY' && |
|||
info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') { |
|||
return true; |
|||
} |
|||
|
|||
// no use trying to recover the cursor if it doesn't exist
|
|||
if (!cursor.exists()) { return; } |
|||
|
|||
/* frame is either 0, 1, 2, or 3, depending on which |
|||
cursor frames were affected: none, first, last, or both |
|||
*/ |
|||
var frame = info.frame = cursor.inNode(info.node); |
|||
|
|||
if (!frame) { return; } |
|||
|
|||
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { |
|||
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); |
|||
|
|||
if (frame & 1) { |
|||
// push cursor start if necessary
|
|||
if (pushes.commonStart < cursor.Range.start.offset) { |
|||
cursor.Range.start.offset += pushes.delta; |
|||
} |
|||
} |
|||
if (frame & 2) { |
|||
// push cursor end if necessary
|
|||
if (pushes.commonStart < cursor.Range.end.offset) { |
|||
cursor.Range.end.offset += pushes.delta; |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
postDiffApply: function (info) { |
|||
if (info.frame) { |
|||
if (info.node) { |
|||
if (info.frame & 1) { cursor.fixStart(info.node); } |
|||
if (info.frame & 2) { cursor.fixEnd(info.node); } |
|||
} else { console.error("info.node did not exist"); } |
|||
|
|||
var sel = cursor.makeSelection(); |
|||
var range = cursor.makeRange(); |
|||
|
|||
cursor.fixSelection(sel, range); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
var initializing = true; |
|||
|
|||
var Title; |
|||
var UserList; |
|||
var Metadata; |
|||
|
|||
var getHeadingText = function () { |
|||
var text; |
|||
if (['h1', 'h2', 'h3'].some(function (t) { |
|||
var $header = $(inner).find(t + ':first-of-type'); |
|||
if ($header.length && $header.text()) { |
|||
text = $header.text(); |
|||
return true; |
|||
} |
|||
})) { return text; } |
|||
}; |
|||
|
|||
var DD = new DiffDom(diffOptions); |
|||
|
|||
var openLink = function (e) { |
|||
var el = e.currentTarget; |
|||
if (!el || el.nodeName !== 'A') { return; } |
|||
var href = el.getAttribute('href'); |
|||
if (href) { window.open(href, '_blank'); } |
|||
}; |
|||
|
|||
// apply patches, and try not to lose the cursor in the process!
|
|||
var applyHjson = function (shjson) { |
|||
var userDocStateDom = hjsonToDom(JSON.parse(shjson)); |
|||
|
|||
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) { |
|||
var $links = $(inner).find('a'); |
|||
// off so that we don't end up with multiple identical handlers
|
|||
$links.off('click', openLink).on('click', openLink); |
|||
} |
|||
}; |
|||
|
|||
var stringifyDOM = module.stringifyDOM = function (dom) { |
|||
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); |
|||
hjson[3] = { |
|||
metadata: { |
|||
users: UserList.userData, |
|||
defaultTitle: Title.defaultTitle, |
|||
type: 'pad' |
|||
} |
|||
}; |
|||
if (!initializing) { |
|||
hjson[3].metadata.title = Title.title; |
|||
} else if (Cryptpad.initialName && !hjson[3].metadata.title) { |
|||
hjson[3].metadata.title = Cryptpad.initialName; |
|||
} |
|||
return stringify(hjson); |
|||
}; |
|||
|
|||
var realtimeOptions = { |
|||
// the websocket URL
|
|||
websocketURL: Cryptpad.getWebsocketURL(), |
|||
|
|||
// the channel we will communicate over
|
|||
channel: secret.channel, |
|||
|
|||
// the nework used for the file store if it exists
|
|||
network: Cryptpad.getNetwork(), |
|||
|
|||
// our public key
|
|||
validateKey: secret.keys.validateKey || undefined, |
|||
readOnly: readOnly, |
|||
|
|||
// Pass in encrypt and decrypt methods
|
|||
crypto: Crypto.createEncryptor(secret.keys), |
|||
|
|||
// really basic operational transform
|
|||
transformFunction : JsonOT.validate, |
|||
|
|||
// cryptpad debug logging (default is 1)
|
|||
// logLevel: 0,
|
|||
|
|||
validateContent: function (content) { |
|||
try { |
|||
JSON.parse(content); |
|||
return true; |
|||
} catch (e) { |
|||
console.log("Failed to parse, rejecting patch"); |
|||
return false; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
var setHistory = function (bool, update) { |
|||
isHistoryMode = bool; |
|||
setEditable(!bool); |
|||
if (!bool && update) { |
|||
realtimeOptions.onRemote(); |
|||
} |
|||
}; |
|||
|
|||
realtimeOptions.onRemote = function () { |
|||
if (initializing) { return; } |
|||
if (isHistoryMode) { return; } |
|||
|
|||
var oldShjson = stringifyDOM(inner); |
|||
|
|||
var shjson = module.realtime.getUserDoc(); |
|||
|
|||
// remember where the cursor is
|
|||
cursor.update(); |
|||
|
|||
// Update the user list (metadata) from the hyperjson
|
|||
Metadata.update(shjson); |
|||
|
|||
var newInner = JSON.parse(shjson); |
|||
var newSInner; |
|||
if (newInner.length > 2) { |
|||
newSInner = stringify(newInner[2]); |
|||
} |
|||
|
|||
// build a dom from HJSON, diff, and patch the editor
|
|||
applyHjson(shjson); |
|||
|
|||
if (!readOnly) { |
|||
var shjson2 = stringifyDOM(inner); |
|||
if (shjson2 !== shjson) { |
|||
console.error("shjson2 !== shjson"); |
|||
module.patchText(shjson2); |
|||
|
|||
/* pushing back over the wire is necessary, but it can |
|||
result in a feedback loop, which we call a browser |
|||
fight */ |
|||
if (module.logFights) { |
|||
// what changed?
|
|||
var op = TextPatcher.diff(shjson, shjson2); |
|||
// log the changes
|
|||
TextPatcher.log(shjson, op); |
|||
var sop = JSON.stringify(TextPatcher.format(shjson, op)); |
|||
|
|||
var index = module.fights.indexOf(sop); |
|||
if (index === -1) { |
|||
module.fights.push(sop); |
|||
console.log("Found a new type of browser disagreement"); |
|||
console.log("You can inspect the list in your " + |
|||
"console at `REALTIME_MODULE.fights`"); |
|||
console.log(module.fights); |
|||
} else { |
|||
console.log("Encountered a known browser disagreement: " + |
|||
"available at `REALTIME_MODULE.fights[%s]`", index); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Notify only when the content has changed, not when someone has joined/left
|
|||
var oldSInner = stringify(JSON.parse(oldShjson)[2]); |
|||
if (newSInner && newSInner !== oldSInner) { |
|||
Cryptpad.notify(); |
|||
} |
|||
}; |
|||
|
|||
var getHTML = function () { |
|||
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML); |
|||
}; |
|||
|
|||
var domFromHTML = function (html) { |
|||
return new DOMParser().parseFromString(html, 'text/html'); |
|||
}; |
|||
|
|||
var exportFile = function () { |
|||
var html = getHTML(); |
|||
var suggestion = Title.suggestTitle('cryptpad-document'); |
|||
Cryptpad.prompt(Messages.exportPrompt, |
|||
Cryptpad.fixFileName(suggestion) + '.html', function (filename) { |
|||
if (!(typeof(filename) === 'string' && filename)) { return; } |
|||
var blob = new Blob([html], {type: "text/html;charset=utf-8"}); |
|||
saveAs(blob, filename); |
|||
}); |
|||
}; |
|||
var importFile = function (content) { |
|||
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body)); |
|||
applyHjson(shjson); |
|||
realtimeOptions.onLocal(); |
|||
}; |
|||
|
|||
realtimeOptions.onInit = function (info) { |
|||
UserList = Cryptpad.createUserList(info, realtimeOptions.onLocal, Cryptget, Cryptpad); |
|||
|
|||
var titleCfg = { getHeadingText: getHeadingText }; |
|||
Title = Cryptpad.createTitle(titleCfg, realtimeOptions.onLocal, Cryptpad); |
|||
|
|||
Metadata = Cryptpad.createMetadata(UserList, Title, null, Cryptpad); |
|||
|
|||
var configTb = { |
|||
displayed: ['title', 'useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit', 'upgrade'], |
|||
userList: UserList.getToolbarConfig(), |
|||
share: { |
|||
secret: secret, |
|||
channel: info.channel |
|||
}, |
|||
title: Title.getTitleConfig(), |
|||
common: Cryptpad, |
|||
readOnly: readOnly, |
|||
ifrw: ifrw, |
|||
realtime: info.realtime, |
|||
network: info.network, |
|||
$container: $bar, |
|||
$contentContainer: $iframe.find('#cke_1_contents'), |
|||
}; |
|||
toolbar = info.realtime.toolbar = Toolbar.create(configTb); |
|||
|
|||
var src = 'less!/customize/src/less/toolbar.less'; |
|||
require([ |
|||
src |
|||
], function () { |
|||
var $html = $bar.closest('html'); |
|||
$html |
|||
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]') |
|||
.appendTo($html.find('head')); |
|||
}); |
|||
|
|||
Title.setToolbar(toolbar); |
|||
|
|||
var $rightside = toolbar.$rightside; |
|||
var $drawer = toolbar.$drawer; |
|||
|
|||
var editHash; |
|||
|
|||
if (!readOnly) { |
|||
editHash = Cryptpad.getEditHashFromKeys(info.channel, secret.keys); |
|||
} |
|||
|
|||
$bar.find('#cke_1_toolbar_collapser').hide(); |
|||
if (!readOnly) { |
|||
// Expand / collapse the toolbar
|
|||
var $collapse = Cryptpad.createButton(null, true); |
|||
$collapse.removeClass('fa-question'); |
|||
var updateIcon = function () { |
|||
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up'); |
|||
$collapse.attr('title', ''); |
|||
var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible'); |
|||
if (isCollapsed) { |
|||
if (!initializing) { Cryptpad.feedback('HIDETOOLBAR_PAD'); } |
|||
$collapse.addClass('fa-caret-down'); |
|||
$collapse.attr('title', Messages.pad_showToolbar); |
|||
} |
|||
else { |
|||
if (!initializing) { Cryptpad.feedback('SHOWTOOLBAR_PAD'); } |
|||
$collapse.addClass('fa-caret-up'); |
|||
$collapse.attr('title', Messages.pad_hideToolbar); |
|||
} |
|||
}; |
|||
updateIcon(); |
|||
$collapse.click(function () { |
|||
$(window).trigger('resize'); |
|||
$iframe.find('.cke_toolbox_main').toggle(); |
|||
$(window).trigger('cryptpad-ck-toolbar'); |
|||
updateIcon(); |
|||
}); |
|||
$rightside.append($collapse); |
|||
} |
|||
|
|||
/* add a history button */ |
|||
var histConfig = { |
|||
onLocal: realtimeOptions.onLocal, |
|||
onRemote: realtimeOptions.onRemote, |
|||
setHistory: setHistory, |
|||
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); }, |
|||
$toolbar: $bar |
|||
}; |
|||
var $hist = Cryptpad.createButton('history', true, {histConfig: histConfig}); |
|||
$drawer.append($hist); |
|||
|
|||
/* save as template */ |
|||
if (!Cryptpad.isTemplate(window.location.href)) { |
|||
var templateObj = { |
|||
rt: info.realtime, |
|||
Crypt: Cryptget, |
|||
getTitle: function () { return document.title; } |
|||
}; |
|||
var $templateButton = Cryptpad.createButton('template', true, templateObj); |
|||
$rightside.append($templateButton); |
|||
} |
|||
|
|||
/* add an export button */ |
|||
var $export = Cryptpad.createButton('export', true, {}, exportFile); |
|||
$drawer.append($export); |
|||
|
|||
if (!readOnly) { |
|||
/* add an import button */ |
|||
var $import = Cryptpad.createButton('import', true, { |
|||
accept: 'text/html' |
|||
}, importFile); |
|||
$drawer.append($import); |
|||
} |
|||
|
|||
/* add a forget button */ |
|||
var forgetCb = function (err) { |
|||
if (err) { return; } |
|||
setEditable(false); |
|||
}; |
|||
var $forgetPad = Cryptpad.createButton('forget', true, {}, forgetCb); |
|||
$rightside.append($forgetPad); |
|||
|
|||
// set the hash
|
|||
if (!readOnly) { Cryptpad.replaceHash(editHash); } |
|||
}; |
|||
|
|||
// this should only ever get called once, when the chain syncs
|
|||
realtimeOptions.onReady = function (info) { |
|||
if (!module.isMaximized) { |
|||
module.isMaximized = true; |
|||
$iframe.find('iframe.cke_wysiwyg_frame').css('width', ''); |
|||
$iframe.find('iframe.cke_wysiwyg_frame').css('height', ''); |
|||
} |
|||
$iframe.find('body').addClass('app-pad'); |
|||
|
|||
if (module.realtime !== info.realtime) { |
|||
module.patchText = TextPatcher.create({ |
|||
realtime: info.realtime, |
|||
//logging: true,
|
|||
}); |
|||
} |
|||
|
|||
module.realtime = info.realtime; |
|||
|
|||
var shjson = module.realtime.getUserDoc(); |
|||
|
|||
var newPad = false; |
|||
if (shjson === '') { newPad = true; } |
|||
|
|||
if (!newPad) { |
|||
applyHjson(shjson); |
|||
|
|||
// Update the user list (metadata) from the hyperjson
|
|||
Metadata.update(shjson); |
|||
|
|||
if (!readOnly) { |
|||
var shjson2 = stringifyDOM(inner); |
|||
var hjson2 = JSON.parse(shjson2).slice(0,-1); |
|||
var hjson = JSON.parse(shjson).slice(0,-1); |
|||
if (stringify(hjson2) !== stringify(hjson)) { |
|||
console.log('err'); |
|||
console.error("shjson2 !== shjson"); |
|||
// 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 { |
|||
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); |
|||
documentBody.innerHTML = Messages.initialState; |
|||
} |
|||
|
|||
Cryptpad.removeLoadingScreen(emitResize); |
|||
setEditable(!readOnly); |
|||
initializing = false; |
|||
|
|||
if (readOnly) { return; } |
|||
UserList.getLastName(toolbar.$userNameButton, newPad); |
|||
editor.focus(); |
|||
if (newPad) { |
|||
cursor.setToEnd(); |
|||
} else { |
|||
cursor.setToStart(); |
|||
} |
|||
}; |
|||
|
|||
realtimeOptions.onAbort = function () { |
|||
console.log("Aborting the session!"); |
|||
// stop the user from continuing to edit
|
|||
setEditable(false); |
|||
toolbar.failed(); |
|||
Cryptpad.alert(Messages.common_connectionLost, undefined, true); |
|||
}; |
|||
|
|||
realtimeOptions.onConnectionChange = function (info) { |
|||
setEditable(info.state); |
|||
toolbar.failed(); |
|||
if (info.state) { |
|||
initializing = true; |
|||
toolbar.reconnecting(info.myId); |
|||
Cryptpad.findOKButton().click(); |
|||
} else { |
|||
Cryptpad.alert(Messages.common_connectionLost, undefined, true); |
|||
} |
|||
}; |
|||
|
|||
realtimeOptions.onError = onConnectError; |
|||
|
|||
var onLocal = realtimeOptions.onLocal = function () { |
|||
if (initializing) { return; } |
|||
if (isHistoryMode) { return; } |
|||
if (readOnly) { return; } |
|||
|
|||
// stringify the json and send it into chainpad
|
|||
var shjson = stringifyDOM(inner); |
|||
|
|||
module.patchText(shjson); |
|||
if (module.realtime.getUserDoc() !== shjson) { |
|||
console.error("realtime.getUserDoc() !== shjson"); |
|||
sframeChan.on('Q_SETTINGS_SET_DISPLAY_NAME', function (newName, cb) { |
|||
Cryptpad.setAttribute('username', newName, function (err) { |
|||
if (err) { |
|||
console.log("Couldn't set username"); |
|||
console.error(err); |
|||
cb('ERROR'); |
|||
return; |
|||
} |
|||
}; |
|||
|
|||
module.realtimeInput = realtimeInput.start(realtimeOptions); |
|||
|
|||
Cryptpad.onLogout(function () { setEditable(false); }); |
|||
|
|||
/* hitting enter makes a new line, but places the cursor inside |
|||
of the <br> instead of the <p>. This makes it such that you |
|||
cannot type until you click, which is rather unnacceptable. |
|||
If the cursor is ever inside such a <br>, you probably want |
|||
to push it out to the parent element, which ought to be a |
|||
paragraph tag. This needs to be done on keydown, otherwise |
|||
the first such keypress will not be inserted into the P. */ |
|||
inner.addEventListener('keydown', cursor.brFix); |
|||
Cryptpad.changeDisplayName(newName, true); |
|||
cb(); |
|||
}); |
|||
}); |
|||
|
|||
editor.on('change', onLocal); |
|||
sframeChan.on('Q_LOGOUT', function (data, cb) { |
|||
Cryptpad.logout(cb); |
|||
}); |
|||
|
|||
// export the typing tests to the window.
|
|||
// call like `test = easyTest()`
|
|||
// terminate the test like `test.cancel()`
|
|||
window.easyTest = function () { |
|||
cursor.update(); |
|||
var start = cursor.Range.start; |
|||
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); |
|||
onLocal(); |
|||
return test; |
|||
}; |
|||
sframeChan.on('Q_SET_LOGIN_REDIRECT', function (data, cb) { |
|||
sessionStorage.redirectTo = window.location.href; |
|||
cb(); |
|||
}); |
|||
|
|||
$bar.find('.cke_button').click(function () { |
|||
var e = this; |
|||
var classString = e.getAttribute('class'); |
|||
var classes = classString.split(' ').filter(function (c) { |
|||
return /cke_button__/.test(c); |
|||
sframeChan.on('Q_GET_PIN_LIMIT_STATUS', function (data, cb) { |
|||
Cryptpad.isOverPinLimit(function (e, overLimit, limits) { |
|||
cb({ |
|||
error: e, |
|||
overLimit: overLimit, |
|||
limits: limits |
|||
}); |
|||
|
|||
var id = classes[0]; |
|||
if (typeof(id) === 'string') { |
|||
Cryptpad.feedback(id.toUpperCase()); |
|||
} |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
var interval = 100; |
|||
var second = function (Ckeditor) { |
|||
Cryptpad.ready(function () { |
|||
andThen(Ckeditor); |
|||
Cryptpad.reportAppUsage(); |
|||
}); |
|||
Cryptpad.onError(function (info) { |
|||
if (info && info.type === "store") { |
|||
onConnectError(); |
|||
} |
|||
sframeChan.on('Q_MOVE_TO_TRASH', function (data, cb) { |
|||
Cryptpad.moveToTrash(cb); |
|||
}); |
|||
}; |
|||
|
|||
var first = function () { |
|||
Ckeditor = ifrw.CKEDITOR; |
|||
if (Ckeditor) { |
|||
// mobile configuration
|
|||
Ckeditor.config.toolbarCanCollapse = true; |
|||
if (screen.height < 800) { |
|||
Ckeditor.config.toolbarStartupExpanded = false; |
|||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no'); |
|||
} else { |
|||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes'); |
|||
} |
|||
second(Ckeditor); |
|||
} else { |
|||
console.log("Ckeditor was not defined. Trying again in %sms",interval); |
|||
setTimeout(first, interval); |
|||
} |
|||
}; |
|||
sframeChan.on('Q_SAVE_AS_TEMPLATE', function (data, cb) { |
|||
Cryptpad.saveAsTemplate(Cryptget.put, data, cb); |
|||
}); |
|||
|
|||
$(first); |
|||
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) { |
|||
var network = Cryptpad.getNetwork(); |
|||
var hkn = network.historyKeeper; |
|||
var crypto = Crypto.createEncryptor(secret.keys); |
|||
// Get the history messages and send them to the iframe
|
|||
var parse = function (msg) { |
|||
try { |
|||
return JSON.parse(msg); |
|||
} catch (e) { |
|||
return null; |
|||
} |
|||
}; |
|||
var onMsg = function (msg) { |
|||
var parsed = parse(msg); |
|||
if (parsed[0] === 'FULL_HISTORY_END') { |
|||
console.log('END'); |
|||
cb(); |
|||
return; |
|||
} |
|||
if (parsed[0] !== 'FULL_HISTORY') { return; } |
|||
if (parsed[1] && parsed[1].validateKey) { // First message
|
|||
secret.keys.validateKey = parsed[1].validateKey; |
|||
return; |
|||
} |
|||
msg = parsed[1][4]; |
|||
if (msg) { |
|||
msg = msg.replace(/^cp\|/, ''); |
|||
var decryptedMsg = crypto.decrypt(msg, secret.keys.validateKey); |
|||
sframeChan.event('EV_RT_HIST_MESSAGE', decryptedMsg); |
|||
} |
|||
}; |
|||
network.on('message', onMsg); |
|||
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', secret.channel, secret.keys.validateKey])); |
|||
}); |
|||
|
|||
CpNfOuter.start({ |
|||
sframeChan: sframeChan, |
|||
channel: secret.channel, |
|||
network: Cryptpad.getNetwork(), |
|||
validateKey: secret.keys.validateKey || undefined, |
|||
readOnly: readOnly, |
|||
crypto: Crypto.createEncryptor(secret.keys), |
|||
onConnect: function (wc) { |
|||
if (readOnly) { return; } |
|||
Cryptpad.replaceHash(Cryptpad.getEditHashFromKeys(wc.id, secret.keys)); |
|||
} |
|||
}); |
|||
}); |
|||
}); |
|||
@ -1,3 +0,0 @@ |
|||
<!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> |
|||
@ -1,30 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html> |
|||
<head> |
|||
<title>CryptPad</title> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<meta name="referrer" content="no-referrer" /> |
|||
<script async data-bootload="/pad2/outer.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script> |
|||
<style> |
|||
html, body { |
|||
margin: 0px; |
|||
padding: 0px; |
|||
} |
|||
#sbox-iframe { |
|||
position:fixed; |
|||
top:0px; |
|||
left:0px; |
|||
bottom:0px; |
|||
right:0px; |
|||
width:100%; |
|||
height:100%; |
|||
border:none; |
|||
margin:0; |
|||
padding:0; |
|||
overflow:hidden; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<iframe id="sbox-iframe"> |
|||
@ -1,37 +0,0 @@ |
|||
<!DOCTYPE html> |
|||
<html class="cp pad"> |
|||
<head> |
|||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/> |
|||
<script async data-bootload="/pad2/main.js" data-main="/common/sframe-boot.js?ver=1.1" src="/bower_components/requirejs/require.js?ver=2.3.5"></script> |
|||
<style> |
|||
html, body { |
|||
margin: 0px; |
|||
} |
|||
#cke_1_top { |
|||
overflow: visible; |
|||
padding: 0px; |
|||
display: flex; |
|||
} |
|||
#cke_1_toolbox { |
|||
display: inline-block; |
|||
width: 100%; |
|||
background-color: #c1e7ff; |
|||
} |
|||
#cke_1_toolbox .cke_toolbar { |
|||
height: 28px; |
|||
padding: 2px 0; |
|||
} |
|||
#cke_1_top .cryptpad-toolbar { |
|||
padding: 0; |
|||
display: block; |
|||
} |
|||
.cke_wysiwyg_frame { |
|||
min-width: 60%; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body class="app-pad"> |
|||
<textarea style="display:none" id="editor1" name="editor1"></textarea> |
|||
</body> |
|||
</html> |
|||
|
|||
@ -1,60 +0,0 @@ |
|||
define(['/common/cryptpad-common.js'], function (Cryptpad) { |
|||
// Adds a context menu entry to open the selected link in a new tab.
|
|||
// See https://github.com/xwiki-contrib/application-ckeditor/commit/755d193497bf23ed874d874b4ae92fbee887fc10
|
|||
var Messages = Cryptpad.Messages; |
|||
return { |
|||
addSupportForOpeningLinksInNewTab : function (Ckeditor) { |
|||
// Returns the DOM element of the active (currently focused) link. It has also support for linked image widgets.
|
|||
// @return {CKEDITOR.dom.element}
|
|||
var getActiveLink = function(editor) { |
|||
var anchor = Ckeditor.plugins.link.getSelectedLink(editor), |
|||
// We need to do some special checking against widgets availability.
|
|||
activeWidget = editor.widgets && editor.widgets.focused; |
|||
// If default way of getting links didn't return anything useful..
|
|||
if (!anchor && activeWidget && activeWidget.name === 'image' && activeWidget.parts.link) { |
|||
// Since CKEditor 4.4.0 image widgets may be linked.
|
|||
anchor = activeWidget.parts.link; |
|||
} |
|||
return anchor; |
|||
}; |
|||
|
|||
return function(event) { |
|||
var editor = event.editor; |
|||
if (!Ckeditor.plugins.link) { |
|||
return; |
|||
} |
|||
editor.addCommand( 'openLink', { |
|||
exec: function(editor) { |
|||
var anchor = getActiveLink(editor); |
|||
if (anchor) { |
|||
var href = anchor.getAttribute('href'); |
|||
if (href) { |
|||
window.open(href); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
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 (startElement) { |
|||
var anchor = getActiveLink(editor); |
|||
if (anchor && anchor.getAttribute('href')) { |
|||
return {openLink: Ckeditor.TRISTATE_OFF}; |
|||
} |
|||
} |
|||
}); |
|||
editor.contextMenu._.panelDefinition.css.push('.cke_button__openLink_icon {' + |
|||
Ckeditor.skin.getIconStyle('link') + '}'); |
|||
} |
|||
}; |
|||
} |
|||
}; |
|||
}); |
|||
@ -1,790 +0,0 @@ |
|||
require(['/api/config'], function (ApiConfig) { |
|||
// see ckeditor_base.js getUrl()
|
|||
window.CKEDITOR_GETURL = function (resource) { |
|||
if (resource.indexOf( '/' ) === 0) { |
|||
resource = window.CKEDITOR.basePath.replace(/\/bower_components\/.*/, '') + resource; |
|||
} else if (resource.indexOf(':/') === -1) { |
|||
resource = window.CKEDITOR.basePath + resource; |
|||
} |
|||
if (resource[resource.length - 1] !== '/' && resource.indexOf('ver=') === -1) { |
|||
var args = ApiConfig.requireConf.urlArgs; |
|||
if (resource.indexOf('/bower_components/') !== -1) { |
|||
args = 'ver=' + window.CKEDITOR.timestamp; |
|||
} |
|||
resource += (resource.indexOf('?') >= 0 ? '&' : '?') + args; |
|||
} |
|||
return resource; |
|||
}; |
|||
require(['/bower_components/ckeditor/ckeditor.js']); |
|||
}); |
|||
define([ |
|||
'jquery', |
|||
'/bower_components/chainpad-crypto/crypto.js', |
|||
'/bower_components/hyperjson/hyperjson.js', |
|||
'/common/toolbar3.js', |
|||
'/common/cursor.js', |
|||
'/bower_components/chainpad-json-validator/json-ot.js', |
|||
'/common/TypingTests.js', |
|||
'json.sortify', |
|||
'/bower_components/textpatcher/TextPatcher.js', |
|||
'/common/cryptpad-common.js', |
|||
'/common/cryptget.js', |
|||
'/pad/links.js', |
|||
'/bower_components/nthen/index.js', |
|||
'/common/sframe-common.js', |
|||
'/api/config', |
|||
'/common/common-realtime.js', |
|||
|
|||
'/bower_components/file-saver/FileSaver.min.js', |
|||
'/bower_components/diff-dom/diffDOM.js', |
|||
|
|||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', |
|||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css', |
|||
'less!/customize/src/less/cryptpad.less', |
|||
'less!/customize/src/less/toolbar.less' |
|||
], function ( |
|||
$, |
|||
Crypto, |
|||
Hyperjson, |
|||
Toolbar, |
|||
Cursor, |
|||
JsonOT, |
|||
TypingTest, |
|||
JSONSortify, |
|||
TextPatcher, |
|||
Cryptpad, |
|||
Cryptget, |
|||
Links, |
|||
nThen, |
|||
SFCommon, |
|||
ApiConfig, |
|||
CommonRealtime) |
|||
{ |
|||
var saveAs = window.saveAs; |
|||
var Messages = Cryptpad.Messages; |
|||
var DiffDom = window.diffDOM; |
|||
|
|||
var stringify = function (obj) { return JSONSortify(obj); }; |
|||
|
|||
window.Toolbar = Toolbar; |
|||
window.Hyperjson = Hyperjson; |
|||
|
|||
var slice = function (coll) { |
|||
return Array.prototype.slice.call(coll); |
|||
}; |
|||
|
|||
var removeListeners = function (root) { |
|||
slice(root.attributes).map(function (attr) { |
|||
if (/^on/.test(attr.name)) { |
|||
root.attributes.removeNamedItem(attr.name); |
|||
} |
|||
}); |
|||
slice(root.children).forEach(removeListeners); |
|||
}; |
|||
|
|||
var hjsonToDom = function (H) { |
|||
var dom = Hyperjson.toDOM(H); |
|||
removeListeners(dom); |
|||
return dom; |
|||
}; |
|||
|
|||
var module = window.REALTIME_MODULE = window.APP = { |
|||
Hyperjson: Hyperjson, |
|||
TextPatcher: TextPatcher, |
|||
logFights: true, |
|||
fights: [], |
|||
Cryptpad: Cryptpad, |
|||
Cursor: Cursor, |
|||
}; |
|||
|
|||
var emitResize = module.emitResize = function () { |
|||
var evt = window.document.createEvent('UIEvents'); |
|||
evt.initUIEvent('resize', true, false, window, 0); |
|||
window.dispatchEvent(evt); |
|||
}; |
|||
|
|||
var toolbar; |
|||
|
|||
var isNotMagicLine = function (el) { |
|||
return !(el && typeof(el.getAttribute) === 'function' && |
|||
el.getAttribute('class') && |
|||
el.getAttribute('class').split(' ').indexOf('non-realtime') !== -1); |
|||
}; |
|||
|
|||
/* catch `type="_moz"` before it goes over the wire */ |
|||
var brFilter = function (hj) { |
|||
if (hj[1].type === '_moz') { hj[1].type = undefined; } |
|||
return hj; |
|||
}; |
|||
|
|||
var onConnectError = function () { |
|||
Cryptpad.errorLoadingScreen(Messages.websocketError); |
|||
}; |
|||
|
|||
var domFromHTML = function (html) { |
|||
return new DOMParser().parseFromString(html, 'text/html'); |
|||
}; |
|||
|
|||
var forbiddenTags = [ |
|||
'SCRIPT', |
|||
'IFRAME', |
|||
'OBJECT', |
|||
'APPLET', |
|||
'VIDEO', |
|||
'AUDIO' |
|||
]; |
|||
|
|||
var getHTML = function (inner) { |
|||
return ('<!DOCTYPE html>\n' + '<html>\n' + inner.innerHTML); |
|||
}; |
|||
|
|||
var CKEDITOR_CHECK_INTERVAL = 100; |
|||
var ckEditorAvailable = function (cb) { |
|||
var intr; |
|||
var check = function () { |
|||
if (window.CKEDITOR) { |
|||
clearTimeout(intr); |
|||
cb(window.CKEDITOR); |
|||
} |
|||
}; |
|||
intr = setInterval(function () { |
|||
console.log("Ckeditor was not defined. Trying again in %sms", CKEDITOR_CHECK_INTERVAL); |
|||
check(); |
|||
}, CKEDITOR_CHECK_INTERVAL); |
|||
check(); |
|||
}; |
|||
|
|||
var mkDiffOptions = function (cursor, readOnly) { |
|||
return { |
|||
preDiffApply: function (info) { |
|||
/* |
|||
Don't accept attributes that begin with 'on' |
|||
these are probably listeners, and we don't want to |
|||
send scripts over the wire. |
|||
*/ |
|||
if (['addAttribute', 'modifyAttribute'].indexOf(info.diff.action) !== -1) { |
|||
if (info.diff.name === 'href') { |
|||
// console.log(info.diff);
|
|||
//var href = info.diff.newValue;
|
|||
|
|||
// TODO normalize HTML entities
|
|||
if (/javascript *: */.test(info.diff.newValue)) { |
|||
// TODO remove javascript: links
|
|||
} |
|||
} |
|||
|
|||
if (/^on/.test(info.diff.name)) { |
|||
console.log("Rejecting forbidden element attribute with name (%s)", info.diff.name); |
|||
return true; |
|||
} |
|||
} |
|||
/* |
|||
Also reject any elements which would insert any one of |
|||
our forbidden tag types: script, iframe, object, |
|||
applet, video, or audio |
|||
*/ |
|||
if (['addElement', 'replaceElement'].indexOf(info.diff.action) !== -1) { |
|||
if (info.diff.element && forbiddenTags.indexOf(info.diff.element.nodeName) !== -1) { |
|||
console.log("Rejecting forbidden tag of type (%s)", info.diff.element.nodeName); |
|||
return true; |
|||
} else if (info.diff.newValue && forbiddenTags.indexOf(info.diff.newValue.nodeType) !== -1) { |
|||
console.log("Rejecting forbidden tag of type (%s)", info.diff.newValue.nodeName); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
if (info.node && info.node.tagName === 'BODY') { |
|||
if (info.diff.action === 'removeAttribute' && |
|||
['class', 'spellcheck'].indexOf(info.diff.name) !== -1) { |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
/* DiffDOM will filter out magicline plugin elements |
|||
in practice this will make it impossible to use it |
|||
while someone else is typing, which could be annoying. |
|||
|
|||
we should check when such an element is going to be |
|||
removed, and prevent that from happening. */ |
|||
if (info.node && info.node.tagName === 'SPAN' && |
|||
info.node.getAttribute('contentEditable') === "false") { |
|||
// it seems to be a magicline plugin element...
|
|||
if (info.diff.action === 'removeElement') { |
|||
// and you're about to remove it...
|
|||
// this probably isn't what you want
|
|||
|
|||
/* |
|||
I have never seen this in the console, but the |
|||
magic line is still getting removed on remote |
|||
edits. This suggests that it's getting removed |
|||
by something other than diffDom. |
|||
*/ |
|||
console.log("preventing removal of the magic line!"); |
|||
|
|||
// return true to prevent diff application
|
|||
return true; |
|||
} |
|||
} |
|||
|
|||
// Do not change the contenteditable value in view mode
|
|||
if (readOnly && info.node && info.node.tagName === 'BODY' && |
|||
info.diff.action === 'modifyAttribute' && info.diff.name === 'contenteditable') { |
|||
return true; |
|||
} |
|||
|
|||
// no use trying to recover the cursor if it doesn't exist
|
|||
if (!cursor.exists()) { return; } |
|||
|
|||
/* frame is either 0, 1, 2, or 3, depending on which |
|||
cursor frames were affected: none, first, last, or both |
|||
*/ |
|||
var frame = info.frame = cursor.inNode(info.node); |
|||
|
|||
if (!frame) { return; } |
|||
|
|||
if (typeof info.diff.oldValue === 'string' && typeof info.diff.newValue === 'string') { |
|||
var pushes = cursor.pushDelta(info.diff.oldValue, info.diff.newValue); |
|||
|
|||
if (frame & 1) { |
|||
// push cursor start if necessary
|
|||
if (pushes.commonStart < cursor.Range.start.offset) { |
|||
cursor.Range.start.offset += pushes.delta; |
|||
} |
|||
} |
|||
if (frame & 2) { |
|||
// push cursor end if necessary
|
|||
if (pushes.commonStart < cursor.Range.end.offset) { |
|||
cursor.Range.end.offset += pushes.delta; |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
postDiffApply: function (info) { |
|||
if (info.frame) { |
|||
if (info.node) { |
|||
if (info.frame & 1) { cursor.fixStart(info.node); } |
|||
if (info.frame & 2) { cursor.fixEnd(info.node); } |
|||
} else { console.error("info.node did not exist"); } |
|||
|
|||
var sel = cursor.makeSelection(); |
|||
var range = cursor.makeRange(); |
|||
|
|||
cursor.fixSelection(sel, range); |
|||
} |
|||
} |
|||
}; |
|||
}; |
|||
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|||
|
|||
var andThen = function (editor, Ckeditor, common) { |
|||
//var $iframe = $('#pad-iframe').contents();
|
|||
//var secret = Cryptpad.getSecrets();
|
|||
//var readOnly = secret.keys && !secret.keys.editKeyStr;
|
|||
//if (!secret.keys) {
|
|||
// secret.keys = secret.key;
|
|||
//}
|
|||
var readOnly = false; // TODO
|
|||
var cpNfInner; |
|||
var metadataMgr; |
|||
var onLocal; |
|||
|
|||
var $bar = $('#cke_1_toolbox'); |
|||
|
|||
var $html = $bar.closest('html'); |
|||
var $faLink = $html.find('head link[href*="/bower_components/components-font-awesome/css/font-awesome.min.css"]'); |
|||
if ($faLink.length) { |
|||
$html.find('iframe').contents().find('head').append($faLink.clone()); |
|||
} |
|||
var isHistoryMode = false; |
|||
|
|||
if (readOnly) { |
|||
$('#cke_1_toolbox > .cke_toolbox_main').hide(); |
|||
} |
|||
|
|||
/* add a class to the magicline plugin so we can pick it out more easily */ |
|||
|
|||
var ml = Ckeditor.instances.editor1.plugins.magicline.backdoor.that.line.$; |
|||
[ml, ml.parentElement].forEach(function (el) { |
|||
el.setAttribute('class', 'non-realtime'); |
|||
}); |
|||
|
|||
var ifrWindow = $html.find('iframe')[0].contentWindow; |
|||
|
|||
var documentBody = ifrWindow.document.body; |
|||
|
|||
var inner = window.inner = documentBody; |
|||
|
|||
var cursor = module.cursor = Cursor(inner); |
|||
|
|||
var openLink = function (e) { |
|||
var el = e.currentTarget; |
|||
if (!el || el.nodeName !== 'A') { return; } |
|||
var href = el.getAttribute('href'); |
|||
if (href) { ifrWindow.open(href, '_blank'); } |
|||
}; |
|||
|
|||
var setEditable = module.setEditable = function (bool) { |
|||
if (bool) { |
|||
$(inner).css({ |
|||
color: '#333', |
|||
}); |
|||
} |
|||
if (!readOnly || !bool) { |
|||
inner.setAttribute('contenteditable', bool); |
|||
} |
|||
}; |
|||
|
|||
CommonRealtime.onInfiniteSpinner(function () { setEditable(false); }); |
|||
|
|||
// don't let the user edit until the pad is ready
|
|||
setEditable(false); |
|||
|
|||
var initializing = true; |
|||
|
|||
var Title; |
|||
//var UserList;
|
|||
//var Metadata;
|
|||
|
|||
var getHeadingText = function () { |
|||
var text; |
|||
if (['h1', 'h2', 'h3'].some(function (t) { |
|||
var $header = $(inner).find(t + ':first-of-type'); |
|||
if ($header.length && $header.text()) { |
|||
text = $header.text(); |
|||
return true; |
|||
} |
|||
})) { return text; } |
|||
}; |
|||
|
|||
var DD = new DiffDom(mkDiffOptions(cursor, readOnly)); |
|||
|
|||
// apply patches, and try not to lose the cursor in the process!
|
|||
var applyHjson = function (shjson) { |
|||
var userDocStateDom = hjsonToDom(JSON.parse(shjson)); |
|||
|
|||
if (!readOnly && !initializing) { |
|||
userDocStateDom.setAttribute("contenteditable", "true"); // lol wtf
|
|||
} |
|||
var patch = (DD).diff(inner, userDocStateDom); |
|||
(DD).apply(inner, patch); |
|||
if (readOnly) { |
|||
var $links = $(inner).find('a'); |
|||
// off so that we don't end up with multiple identical handlers
|
|||
$links.off('click', openLink).on('click', openLink); |
|||
} |
|||
}; |
|||
|
|||
var stringifyDOM = module.stringifyDOM = function (dom) { |
|||
var hjson = Hyperjson.fromDOM(dom, isNotMagicLine, brFilter); |
|||
hjson[3] = { |
|||
metadata: metadataMgr.getMetadataLazy() |
|||
}; |
|||
/*hjson[3] = { TODO |
|||
users: UserList.userData, |
|||
defaultTitle: Title.defaultTitle, |
|||
type: 'pad' |
|||
} |
|||
}; |
|||
if (!initializing) { |
|||
hjson[3].metadata.title = Title.title; |
|||
} else if (Cryptpad.initialName && !hjson[3].metadata.title) { |
|||
hjson[3].metadata.title = Cryptpad.initialName; |
|||
}*/ |
|||
return stringify(hjson); |
|||
}; |
|||
|
|||
var realtimeOptions = { |
|||
readOnly: readOnly, |
|||
// really basic operational transform
|
|||
transformFunction : JsonOT.validate, |
|||
// cryptpad debug logging (default is 1)
|
|||
// logLevel: 0,
|
|||
validateContent: function (content) { |
|||
try { |
|||
JSON.parse(content); |
|||
return true; |
|||
} catch (e) { |
|||
console.log("Failed to parse, rejecting patch"); |
|||
return false; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
var setHistory = function (bool, update) { |
|||
isHistoryMode = bool; |
|||
setEditable(!bool); |
|||
if (!bool && update) { |
|||
realtimeOptions.onRemote(); |
|||
} |
|||
}; |
|||
|
|||
realtimeOptions.onRemote = function () { |
|||
if (initializing) { return; } |
|||
if (isHistoryMode) { return; } |
|||
|
|||
var oldShjson = stringifyDOM(inner); |
|||
|
|||
var shjson = module.realtime.getUserDoc(); |
|||
|
|||
// remember where the cursor is
|
|||
cursor.update(); |
|||
|
|||
// Update the user list (metadata) from the hyperjson
|
|||
// TODO Metadata.update(shjson);
|
|||
|
|||
var newInner = JSON.parse(shjson); |
|||
var newSInner; |
|||
if (newInner.length > 2) { |
|||
newSInner = stringify(newInner[2]); |
|||
} |
|||
|
|||
if (newInner[3]) { |
|||
metadataMgr.updateMetadata(newInner[3].metadata); |
|||
} |
|||
|
|||
// build a dom from HJSON, diff, and patch the editor
|
|||
applyHjson(shjson); |
|||
|
|||
if (!readOnly) { |
|||
var shjson2 = stringifyDOM(inner); |
|||
|
|||
// TODO
|
|||
//shjson = JSON.stringify(JSON.parse(shjson).slice(0,3));
|
|||
|
|||
if (shjson2 !== shjson) { |
|||
console.error("shjson2 !== shjson"); |
|||
module.patchText(shjson2); |
|||
|
|||
/* pushing back over the wire is necessary, but it can |
|||
result in a feedback loop, which we call a browser |
|||
fight */ |
|||
if (module.logFights) { |
|||
// what changed?
|
|||
var op = TextPatcher.diff(shjson, shjson2); |
|||
// log the changes
|
|||
TextPatcher.log(shjson, op); |
|||
var sop = JSON.stringify(TextPatcher.format(shjson, op)); |
|||
|
|||
var index = module.fights.indexOf(sop); |
|||
if (index === -1) { |
|||
module.fights.push(sop); |
|||
console.log("Found a new type of browser disagreement"); |
|||
console.log("You can inspect the list in your " + |
|||
"console at `REALTIME_MODULE.fights`"); |
|||
console.log(module.fights); |
|||
} else { |
|||
console.log("Encountered a known browser disagreement: " + |
|||
"available at `REALTIME_MODULE.fights[%s]`", index); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Notify only when the content has changed, not when someone has joined/left
|
|||
var oldSInner = stringify(JSON.parse(oldShjson)[2]); |
|||
if (newSInner && newSInner !== oldSInner) { |
|||
Cryptpad.notify(); |
|||
} |
|||
}; |
|||
|
|||
var exportFile = function () { |
|||
var html = getHTML(inner); |
|||
var suggestion = Title.suggestTitle('cryptpad-document'); |
|||
Cryptpad.prompt(Messages.exportPrompt, |
|||
Cryptpad.fixFileName(suggestion) + '.html', function (filename) { |
|||
if (!(typeof(filename) === 'string' && filename)) { return; } |
|||
var blob = new Blob([html], {type: "text/html;charset=utf-8"}); |
|||
saveAs(blob, filename); |
|||
}); |
|||
}; |
|||
var importFile = function (content) { |
|||
var shjson = stringify(Hyperjson.fromDOM(domFromHTML(content).body)); |
|||
applyHjson(shjson); |
|||
realtimeOptions.onLocal(); |
|||
}; |
|||
|
|||
realtimeOptions.onInit = function (info) { |
|||
readOnly = metadataMgr.getPrivateData().readOnly; |
|||
console.log('onInit'); |
|||
var titleCfg = { getHeadingText: getHeadingText }; |
|||
Title = common.createTitle(titleCfg, realtimeOptions.onLocal, common, metadataMgr); |
|||
var configTb = { |
|||
displayed: ['userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit'], |
|||
title: Title.getTitleConfig(), |
|||
metadataMgr: metadataMgr, |
|||
readOnly: readOnly, |
|||
ifrw: window, |
|||
realtime: info.realtime, |
|||
common: Cryptpad, |
|||
sfCommon: common, |
|||
$container: $bar, |
|||
$contentContainer: $('#cke_1_contents'), |
|||
}; |
|||
toolbar = info.realtime.toolbar = Toolbar.create(configTb); |
|||
Title.setToolbar(toolbar); |
|||
|
|||
var $rightside = toolbar.$rightside; |
|||
var $drawer = toolbar.$drawer; |
|||
|
|||
var src = 'less!/customize/src/less/toolbar.less'; |
|||
require([ |
|||
src |
|||
], function () { |
|||
var $html = $bar.closest('html'); |
|||
$html |
|||
.find('head style[data-original-src="' + src.replace(/less!/, '') + '"]') |
|||
.appendTo($html.find('head')); |
|||
}); |
|||
|
|||
$bar.find('#cke_1_toolbar_collapser').hide(); |
|||
if (!readOnly) { |
|||
// Expand / collapse the toolbar
|
|||
var $collapse = Cryptpad.createButton(null, true); |
|||
$collapse.removeClass('fa-question'); |
|||
var updateIcon = function () { |
|||
$collapse.removeClass('fa-caret-down').removeClass('fa-caret-up'); |
|||
var isCollapsed = !$bar.find('.cke_toolbox_main').is(':visible'); |
|||
if (isCollapsed) { |
|||
if (!initializing) { common.feedback('HIDETOOLBAR_PAD'); } |
|||
$collapse.addClass('fa-caret-down'); |
|||
} |
|||
else { |
|||
if (!initializing) { common.feedback('SHOWTOOLBAR_PAD'); } |
|||
$collapse.addClass('fa-caret-up'); |
|||
} |
|||
}; |
|||
updateIcon(); |
|||
$collapse.click(function () { |
|||
$(window).trigger('resize'); |
|||
$('.cke_toolbox_main').toggle(); |
|||
$(window).trigger('cryptpad-ck-toolbar'); |
|||
updateIcon(); |
|||
}); |
|||
$rightside.append($collapse); |
|||
} else { |
|||
$('.cke_toolbox_main').hide(); |
|||
} |
|||
|
|||
/* add a history button */ |
|||
var histConfig = { |
|||
onLocal: realtimeOptions.onLocal, |
|||
onRemote: realtimeOptions.onRemote, |
|||
setHistory: setHistory, |
|||
applyVal: function (val) { applyHjson(val || '["BODY",{},[]]'); }, |
|||
$toolbar: $bar |
|||
}; |
|||
var $hist = common.createButton('history', true, {histConfig: histConfig}); |
|||
$drawer.append($hist); |
|||
|
|||
if (!metadataMgr.getPrivateData().isTemplate) { |
|||
var templateObj = { |
|||
rt: info.realtime, |
|||
getTitle: function () { return metadataMgr.getMetadata().title; } |
|||
}; |
|||
var $templateButton = common.createButton('template', true, templateObj); |
|||
$rightside.append($templateButton); |
|||
} |
|||
|
|||
/* add an export button */ |
|||
var $export = Cryptpad.createButton('export', true, {}, exportFile); |
|||
$drawer.append($export); |
|||
|
|||
if (!readOnly) { |
|||
/* add an import button */ |
|||
var $import = Cryptpad.createButton('import', true, { |
|||
accept: 'text/html' |
|||
}, importFile); |
|||
$drawer.append($import); |
|||
} |
|||
|
|||
/* add a forget button */ |
|||
var forgetCb = function (err) { |
|||
if (err) { return; } |
|||
setEditable(false); |
|||
}; |
|||
var $forgetPad = common.createButton('forget', true, {}, forgetCb); |
|||
$rightside.append($forgetPad); |
|||
}; |
|||
|
|||
// this should only ever get called once, when the chain syncs
|
|||
realtimeOptions.onReady = function (info) { |
|||
console.log('onReady'); |
|||
if (!module.isMaximized) { |
|||
module.isMaximized = true; |
|||
$('iframe.cke_wysiwyg_frame').css('width', ''); |
|||
$('iframe.cke_wysiwyg_frame').css('height', ''); |
|||
} |
|||
$('body').addClass('app-pad'); |
|||
|
|||
if (module.realtime !== info.realtime) { |
|||
module.patchText = TextPatcher.create({ |
|||
realtime: info.realtime, |
|||
//logging: true,
|
|||
}); |
|||
} |
|||
|
|||
module.realtime = info.realtime; |
|||
|
|||
var shjson = module.realtime.getUserDoc(); |
|||
|
|||
var newPad = false; |
|||
if (shjson === '') { newPad = true; } |
|||
|
|||
if (!newPad) { |
|||
applyHjson(shjson); |
|||
|
|||
// Update the user list (metadata) from the hyperjson
|
|||
// XXX Metadata.update(shjson);
|
|||
var parsed = JSON.parse(shjson); |
|||
if (parsed[3] && parsed[3].metadata) { |
|||
metadataMgr.updateMetadata(parsed[3].metadata); |
|||
} |
|||
|
|||
if (!readOnly) { |
|||
var shjson2 = stringifyDOM(inner); |
|||
var hjson2 = JSON.parse(shjson2).slice(0,3); |
|||
var hjson = JSON.parse(shjson).slice(0,3); |
|||
if (stringify(hjson2) !== stringify(hjson)) { |
|||
console.log('err'); |
|||
console.error("shjson2 !== shjson"); |
|||
console.log(stringify(hjson2)); |
|||
console.log(stringify(hjson)); |
|||
Cryptpad.errorLoadingScreen(Messages.wrongApp); |
|||
throw new Error(); |
|||
} |
|||
} |
|||
} else { |
|||
Title.updateTitle(Cryptpad.initialName || Title.defaultTitle); |
|||
documentBody.innerHTML = Messages.initialState; |
|||
} |
|||
|
|||
Cryptpad.removeLoadingScreen(emitResize); |
|||
setEditable(!readOnly); |
|||
initializing = false; |
|||
|
|||
if (readOnly) { return; } |
|||
//TODO UserList.getLastName(toolbar.$userNameButton, newPad);
|
|||
onLocal(); |
|||
editor.focus(); |
|||
if (newPad) { |
|||
cursor.setToEnd(); |
|||
} else { |
|||
cursor.setToStart(); |
|||
} |
|||
}; |
|||
|
|||
realtimeOptions.onConnectionChange = function (info) { |
|||
setEditable(info.state); |
|||
//toolbar.failed(); TODO
|
|||
if (info.state) { |
|||
initializing = true; |
|||
//toolbar.reconnecting(info.myId); // TODO
|
|||
Cryptpad.findOKButton().click(); |
|||
} else { |
|||
Cryptpad.alert(Messages.common_connectionLost, undefined, true); |
|||
} |
|||
}; |
|||
|
|||
realtimeOptions.onError = onConnectError; |
|||
|
|||
onLocal = realtimeOptions.onLocal = function () { |
|||
console.log('onlocal'); |
|||
if (initializing) { return; } |
|||
if (isHistoryMode) { return; } |
|||
if (readOnly) { return; } |
|||
|
|||
// stringify the json and send it into chainpad
|
|||
var shjson = stringifyDOM(inner); |
|||
|
|||
module.patchText(shjson); |
|||
if (module.realtime.getUserDoc() !== shjson) { |
|||
console.error("realtime.getUserDoc() !== shjson"); |
|||
} |
|||
}; |
|||
|
|||
cpNfInner = common.startRealtime(realtimeOptions); |
|||
metadataMgr = cpNfInner.metadataMgr; |
|||
|
|||
Cryptpad.onLogout(function () { setEditable(false); }); |
|||
|
|||
/* hitting enter makes a new line, but places the cursor inside |
|||
of the <br> instead of the <p>. This makes it such that you |
|||
cannot type until you click, which is rather unnacceptable. |
|||
If the cursor is ever inside such a <br>, you probably want |
|||
to push it out to the parent element, which ought to be a |
|||
paragraph tag. This needs to be done on keydown, otherwise |
|||
the first such keypress will not be inserted into the P. */ |
|||
inner.addEventListener('keydown', cursor.brFix); |
|||
|
|||
editor.on('change', onLocal); |
|||
|
|||
// export the typing tests to the window.
|
|||
// call like `test = easyTest()`
|
|||
// terminate the test like `test.cancel()`
|
|||
window.easyTest = function () { |
|||
cursor.update(); |
|||
var start = cursor.Range.start; |
|||
var test = TypingTest.testInput(inner, start.el, start.offset, onLocal); |
|||
onLocal(); |
|||
return test; |
|||
}; |
|||
|
|||
$bar.find('.cke_button').click(function () { |
|||
var e = this; |
|||
var classString = e.getAttribute('class'); |
|||
var classes = classString.split(' ').filter(function (c) { |
|||
return /cke_button__/.test(c); |
|||
}); |
|||
|
|||
var id = classes[0]; |
|||
if (typeof(id) === 'string') { |
|||
common.feedback(id.toUpperCase()); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
var main = function () { |
|||
var Ckeditor; |
|||
var editor; |
|||
var common; |
|||
|
|||
nThen(function (waitFor) { |
|||
ckEditorAvailable(waitFor(function (ck) { |
|||
Ckeditor = ck; |
|||
require(['/pad2/wysiwygarea-plugin.js'], waitFor()); |
|||
})); |
|||
$(waitFor(function () { |
|||
Cryptpad.addLoadingScreen(); |
|||
})); |
|||
SFCommon.create(waitFor(function (c) { module.common = common = c; })); |
|||
}).nThen(function (waitFor) { |
|||
Ckeditor.config.toolbarCanCollapse = true; |
|||
if (screen.height < 800) { |
|||
Ckeditor.config.toolbarStartupExpanded = false; |
|||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=no'); |
|||
} else { |
|||
$('meta[name=viewport]').attr('content', 'width=device-width, initial-scale=1.0, user-scalable=yes'); |
|||
} |
|||
// Used in ckeditor-config.js
|
|||
Ckeditor.CRYPTPAD_URLARGS = ApiConfig.requireConf.urlArgs; |
|||
editor = Ckeditor.replace('editor1', { |
|||
customConfig: '/customize/ckeditor-config.js', |
|||
}); |
|||
editor.on('instanceReady', waitFor()); |
|||
}).nThen(function (/*waitFor*/) { |
|||
Links.addSupportForOpeningLinksInNewTab(Ckeditor); |
|||
Cryptpad.onError(function (info) { |
|||
if (info && info.type === "store") { |
|||
onConnectError(); |
|||
} |
|||
}); |
|||
andThen(editor, Ckeditor, common); |
|||
}); |
|||
}; |
|||
main(); |
|||
}); |
|||
@ -1,224 +0,0 @@ |
|||
// Load #1, load as little as possible because we are in a race to get the loading screen up.
|
|||
define([ |
|||
'/bower_components/nthen/index.js', |
|||
'/api/config', |
|||
'jquery', |
|||
'/common/requireconfig.js' |
|||
], function (nThen, ApiConfig, $, RequireConfig) { |
|||
var requireConfig = RequireConfig(); |
|||
|
|||
// Loaded in load #2
|
|||
var CpNfOuter; |
|||
var Cryptpad; |
|||
var Crypto; |
|||
var Cryptget; |
|||
|
|||
var sframeChan; |
|||
var secret; |
|||
var hashes; |
|||
|
|||
nThen(function (waitFor) { |
|||
$(waitFor()); |
|||
}).nThen(function (waitFor) { |
|||
var req = { |
|||
cfg: requireConfig, |
|||
req: [ '/common/loading.js' ], |
|||
pfx: window.location.origin |
|||
}; |
|||
window.rc = requireConfig; |
|||
window.apiconf = ApiConfig; |
|||
$('#sbox-iframe').attr('src', |
|||
ApiConfig.httpSafeOrigin + '/pad2/inner.html?' + requireConfig.urlArgs + |
|||
'#' + encodeURIComponent(JSON.stringify(req))); |
|||
|
|||
// This is a cheap trick to avoid loading sframe-channel in parallel with the
|
|||
// loading screen setup.
|
|||
var done = waitFor(); |
|||
var onMsg = function (msg) { |
|||
var data = JSON.parse(msg.data); |
|||
if (data.q !== 'READY') { return; } |
|||
window.removeEventListener('message', onMsg); |
|||
var _done = done; |
|||
done = function () { }; |
|||
_done(); |
|||
}; |
|||
window.addEventListener('message', onMsg); |
|||
|
|||
}).nThen(function (waitFor) { |
|||
// Load #2, the loading screen is up so grab whatever you need...
|
|||
require([ |
|||
'/common/sframe-chainpad-netflux-outer.js', |
|||
'/common/cryptpad-common.js', |
|||
'/bower_components/chainpad-crypto/crypto.js', |
|||
'/common/cryptget.js', |
|||
'/common/sframe-channel.js', |
|||
], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, SFrameChannel) { |
|||
CpNfOuter = _CpNfOuter; |
|||
Cryptpad = _Cryptpad; |
|||
Crypto = _Crypto; |
|||
Cryptget = _Cryptget; |
|||
SFrameChannel.create($('#sbox-iframe')[0].contentWindow, waitFor(function (sfc) { |
|||
sframeChan = sfc; |
|||
})); |
|||
Cryptpad.ready(waitFor()); |
|||
})); |
|||
}).nThen(function (waitFor) { |
|||
secret = Cryptpad.getSecrets(); |
|||
if (!secret.channel) { |
|||
// New pad: create a new random channel id
|
|||
secret.channel = Cryptpad.createChannelId(); |
|||
} |
|||
Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; })); |
|||
}).nThen(function (/*waitFor*/) { |
|||
var readOnly = secret.keys && !secret.keys.editKeyStr; |
|||
if (!secret.keys) { secret.keys = secret.key; } |
|||
var parsed = Cryptpad.parsePadUrl(window.location.href); |
|||
parsed.type = parsed.type.replace('pad2', 'pad'); |
|||
if (!parsed.type) { throw new Error(); } |
|||
var defaultTitle = Cryptpad.getDefaultName(parsed); |
|||
var updateMeta = function () { |
|||
//console.log('EV_METADATA_UPDATE');
|
|||
var name; |
|||
nThen(function (waitFor) { |
|||
Cryptpad.getLastName(waitFor(function (err, n) { |
|||
if (err) { console.log(err); } |
|||
name = n; |
|||
})); |
|||
}).nThen(function (/*waitFor*/) { |
|||
sframeChan.event('EV_METADATA_UPDATE', { |
|||
doc: { |
|||
defaultTitle: defaultTitle, |
|||
type: parsed.type |
|||
}, |
|||
user: { |
|||
name: name, |
|||
uid: Cryptpad.getUid(), |
|||
avatar: Cryptpad.getAvatarUrl(), |
|||
profile: Cryptpad.getProfileUrl(), |
|||
curvePublic: Cryptpad.getProxy().curvePublic, |
|||
netfluxId: Cryptpad.getNetwork().webChannels[0].myID, |
|||
}, |
|||
priv: { |
|||
accountName: Cryptpad.getAccountName(), |
|||
origin: window.location.origin, |
|||
pathname: window.location.pathname, |
|||
readOnly: readOnly, |
|||
availableHashes: hashes, |
|||
isTemplate: Cryptpad.isTemplate(window.location.href), |
|||
feedbackAllowed: Cryptpad.isFeedbackAllowed() |
|||
} |
|||
}); |
|||
}); |
|||
}; |
|||
Cryptpad.onDisplayNameChanged(updateMeta); |
|||
sframeChan.onReg('EV_METADATA_UPDATE', updateMeta); |
|||
|
|||
Cryptpad.onError(function (info) { |
|||
console.log('error'); |
|||
console.log(info); |
|||
if (info && info.type === "store") { |
|||
//onConnectError();
|
|||
} |
|||
}); |
|||
|
|||
sframeChan.on('Q_ANON_RPC_MESSAGE', function (data, cb) { |
|||
Cryptpad.anonRpcMsg(data.msg, data.content, function (err, response) { |
|||
cb({error: err, response: response}); |
|||
}); |
|||
}); |
|||
|
|||
sframeChan.on('Q_SET_PAD_TITLE_IN_DRIVE', function (newTitle, cb) { |
|||
Cryptpad.renamePad(newTitle, undefined, function (err) { |
|||
if (err) { cb('ERROR'); } else { cb(); } |
|||
}); |
|||
}); |
|||
|
|||
sframeChan.on('Q_SETTINGS_SET_DISPLAY_NAME', function (newName, cb) { |
|||
Cryptpad.setAttribute('username', newName, function (err) { |
|||
if (err) { |
|||
console.log("Couldn't set username"); |
|||
console.error(err); |
|||
cb('ERROR'); |
|||
return; |
|||
} |
|||
Cryptpad.changeDisplayName(newName, true); |
|||
cb(); |
|||
}); |
|||
}); |
|||
|
|||
sframeChan.on('Q_LOGOUT', function (data, cb) { |
|||
Cryptpad.logout(cb); |
|||
}); |
|||
|
|||
sframeChan.on('Q_SET_LOGIN_REDIRECT', function (data, cb) { |
|||
sessionStorage.redirectTo = window.location.href; |
|||
cb(); |
|||
}); |
|||
|
|||
sframeChan.on('Q_GET_PIN_LIMIT_STATUS', function (data, cb) { |
|||
Cryptpad.isOverPinLimit(function (e, overLimit, limits) { |
|||
cb({ |
|||
error: e, |
|||
overLimit: overLimit, |
|||
limits: limits |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
sframeChan.on('Q_MOVE_TO_TRASH', function (data, cb) { |
|||
Cryptpad.moveToTrash(cb); |
|||
}); |
|||
|
|||
sframeChan.on('Q_SAVE_AS_TEMPLATE', function (data, cb) { |
|||
Cryptpad.saveAsTemplate(Cryptget.put, data, cb); |
|||
}); |
|||
|
|||
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) { |
|||
var network = Cryptpad.getNetwork(); |
|||
var hkn = network.historyKeeper; |
|||
var crypto = Crypto.createEncryptor(secret.keys); |
|||
// Get the history messages and send them to the iframe
|
|||
var parse = function (msg) { |
|||
try { |
|||
return JSON.parse(msg); |
|||
} catch (e) { |
|||
return null; |
|||
} |
|||
}; |
|||
var onMsg = function (msg) { |
|||
var parsed = parse(msg); |
|||
if (parsed[0] === 'FULL_HISTORY_END') { |
|||
console.log('END'); |
|||
cb(); |
|||
return; |
|||
} |
|||
if (parsed[0] !== 'FULL_HISTORY') { return; } |
|||
if (parsed[1] && parsed[1].validateKey) { // First message
|
|||
secret.keys.validateKey = parsed[1].validateKey; |
|||
return; |
|||
} |
|||
msg = parsed[1][4]; |
|||
if (msg) { |
|||
msg = msg.replace(/^cp\|/, ''); |
|||
var decryptedMsg = crypto.decrypt(msg, secret.keys.validateKey); |
|||
sframeChan.event('EV_RT_HIST_MESSAGE', decryptedMsg); |
|||
} |
|||
}; |
|||
network.on('message', onMsg); |
|||
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', secret.channel, secret.keys.validateKey])); |
|||
}); |
|||
|
|||
CpNfOuter.start({ |
|||
sframeChan: sframeChan, |
|||
channel: secret.channel, |
|||
network: Cryptpad.getNetwork(), |
|||
validateKey: secret.keys.validateKey || undefined, |
|||
readOnly: readOnly, |
|||
crypto: Crypto.createEncryptor(secret.keys), |
|||
onConnect: function (wc) { |
|||
if (readOnly) { return; } |
|||
Cryptpad.replaceHash(Cryptpad.getEditHashFromKeys(wc.id, secret.keys)); |
|||
} |
|||
}); |
|||
}); |
|||
}); |
|||
@ -1,740 +0,0 @@ |
|||
/** |
|||
* @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. |
|||
*/ |
|||
|
|||
define(['/api/config'], function (ApiConfig) { |
|||
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?' + ApiConfig.requireConf.urlArgs; |
|||
|
|||
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; |
|||
|
|||
// CryptPad
|
|||
// This breaks Edge so lets use async all of the time
|
|||
useOnloadEvent = true; |
|||
|
|||
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. |
|||
*/ |
|||
Write
Preview
Loading…
Cancel
Save