Merge branch 'staging' into design_dialogs_props

pull/1/head
yflory 5 years ago
commit 5de5bb603d

@ -19,6 +19,7 @@ www/pad/wysiwygarea-plugin.js
www/pad/mediatag-plugin.js
www/pad/mediatag-plugin-dialog.js
www/pad/disable-base64.js
www/pad/wordcount/
www/kanban/jkanban.js
www/common/jscolor.js

@ -1,4 +1,43 @@
# Elasmotherium release notes
# FalklandWolf release (3.5.0)
## Goals
This release features work that we've been planning for a long time centered around sharing collections of documents in a more granular way.
This is our first release since David Benqué joined our team, so in addition to these team-centric updates we also worked on integrating some UI/UX improvements.
## Update notes
Updating to 3.5.0 from 3.4.0 is simple.
1. stop your server
2. pull the latest code via git
3. run `bower update`
4. restart your server
## Features
* We restyled some elements throughout the platform:
* our tooltips have a sleeker flat design
* the quota bar which appears in the drive, teams, and settings pages has also been improved
* we've begun improving the look and feel of various popup dialogs
* We've added support for password-change for owned uploaded files and owned shared folders:
* changing passwords for encrypted files means that the original file will be removed from the server and a new file will be encrypted with a new key and uploaded to a new location on the server. References to the original file will be broken. This includes links, media-tags embedded within pads, and items in other users' drives or shared folders to which you do not have access.
* the process is very similar for shared folders stored in users' CryptDrives, except that users will have the opportunity to enter the new password when they visit the platform.
* We're very happy to finally introduce the notion of _read-only shared folders_. While we've had the capacity to make shared folders read-only for some time, it was only in the same sense as pads were read-only.
* This is to say that while a viewer cannot modify the document, any links to encrypted documents within that document would confer their natural editing rights to viewers, making it possible to accidentally leak access when a single pad was shared.
* Our new read-only shared folders encrypt the editing keys for the documents they contain, such that only those with the ability to change the folder structure itself have the inherent capacity to edit the documents contained within. We think this is more intuitive than the alternative, but it took a lot of work to make it happen!
* Unfortunately, older shared folders created before this release will already contain the cryptographic keys which confer editing rights. Pads which are added to shared folders from this release onward will have the keys for their editing rights encrypted. We'll offer the ability for owners to migrate these shared folders in an upcoming release once we've added the ability to selectively trim document history.
* Similarly, we've introduced the notion of _viewers_ in teams. Viewers are listed in the team roster and have the ability to view the contents of the team's drive, but not to edit them or add new documents.
* Unfortunately, the notion of viewers is also complicated by the fact that documents added to team drives or shared folders in team drives did not have their editing keys encrypted. The first team member to open the team drive since we've deployed this release will run a migration that will encrypt the keys saved within the team drive, however, the encryption keys will remain in the drive's history until we develop a means of selectively trimming history.
## Bug fixes
* We discovered and fixed some bugs in the serverside code responsible for handling some aspects of file upload related to starting a new upload after having cancelled a previous session.
* We also identified a regression in Our _slides_ app related to the rendering of `<br>` tags, such as you might create with a `****` sequence in the corresponding markdown. This was introduced with some overly broad CSS that was intended to style our notifications page. We've since made the notifications styles more specific such that they can't interfere with other applications.
* We've become aware of some mysterious behaviour in Firefox that seems to cause some tabs or functionality to reconnect to the server after going offline while other aspects of the platform did not. Until now we've always assumed that users were connected or not, and this partial connection has revealed some bugs in our implementation. Consequently, we've begun adding some measures to detect odd behaviour if it occurs. We expect to have determined the cause of this behaviour and to have proposed a solution by our next release.
# Elasmotherium release (3.4.0)
## Goals

@ -10,7 +10,7 @@ CKEDITOR.editorConfig = function( config ) {
// document itself and causes problems when it's sent across the wire and reflected back
config.removePlugins= 'resize,elementspath';
config.resize_enabled= false; //bottom-bar
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax';
config.extraPlugins= 'autolink,colorbutton,colordialog,font,indentblock,justify,mediatag,print,blockbase64,mathjax,wordcount';
config.toolbarGroups= [
// {"name":"clipboard","groups":["clipboard","undo"]},
//{"name":"editing","groups":["find","selection"]},

@ -282,12 +282,16 @@
border-radius: 0;
color: @alertify-btn-fg;
border: 1px solid @colortheme_alertify-cancel-border;
border: 1px solid @alertify-btn-fg;
&.no-margin {
margin: 0;
}
&:hover, &:active {
background-color: @alertify-light-bg;
}
&.safe, &.danger {
color: @colortheme_old-base;
white-space: normal;
@ -321,10 +325,16 @@
}
}
&:hover, &:active {
background-color: contrast(@colortheme_modal-bg, darken(@colortheme_alertify-cancel, 10%), lighten(@colortheme_alertify-cancel, 10%));
&.cancel {
border-color: @colortheme_alertify-cancel-border;
color: @colortheme_alertify-cancel-border;
&:hover, &:hover {
background-color: fade(@colortheme_alertify-cancel-border, 25%);
}
}
&:focus {
//border: 1px dotted @alertify-base;
box-shadow: 0px 0px 5px @colortheme_alertify-primary;

@ -56,7 +56,7 @@
@colortheme_alertify-disabled-text: #ffffff;
@colortheme_alertify-disabled-border: #6c757d;
@colortheme_alertify-cancel: @colortheme_modal-bg;
@colortheme_alertify-cancel-border: #ccc;
@colortheme_alertify-cancel-border: #949494;
@colortheme_notification-log: fade(@colortheme_logo-1, 90%);
@colortheme_notification-color: #fff;;

@ -9,8 +9,27 @@
flex: 1;
min-width: 0;
}
label, .fa {
margin-left: 10px;
.fa {
width: 30px;
height: 30px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background-color: rgba(0,0,0,0.1);
}
}
}
.cp-password-change-container {
display: flex;
align-items: center;
.cp-password-container {
margin-bottom: 0 !important;
flex: 1;
}
button {
margin: 0 !important;
}
}
}

@ -507,7 +507,8 @@
}
}
.cp-toolbar-top {
@media screen and (max-width: @browser_media-medium-screen) {
@media screen and (max-width: @browser_media-medium-screen),
screen and (max-height: 500px) {
flex-wrap: wrap;
height: @toolbar_line-height;
.cp-pad-not-pinned {

@ -2,7 +2,7 @@
Cryptpad includes support for building a Docker image and running it to provide a Cryptpad instance. You can manage the container manually, or let Docker Compose manage it for you.
A full tutorial is available [on the Cryptpad Github wiki](https://github.com/xwiki-labs/cryptpad/wiki/Docker-(with-Nginx-and-Traefik)). This document provides a brief overview.
A full tutorial is available [on the Cryptpad Github wiki](https://github.com/xwiki-labs/cryptpad/wiki/Docker). This document provides a brief overview.
## Features

@ -18,8 +18,6 @@ define(['/customize/application_config.js'], function (AppConfig) {
deprecatedKey: 'deprecated',
MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 3,
MAX_TEAMS_OWNED: AppConfig.maxOwnedTeams || 1,
// Sub
plan: 'CryptPad_plan',
// Apps
criticalApps: ['profile', 'settings', 'debug', 'admin', 'support', 'notifications']
};

@ -595,9 +595,10 @@ define([
}, opts);
var input = h('input.cp-password-input', attributes);
var reveal = UI.createCheckbox('cp-password-reveal', Messages.password_show);
//var reveal = UI.createCheckbox('cp-password-reveal', Messages.password_show);
var eye = h('span.fa.fa-eye.cp-password-reveal');
/*
$(reveal).find('input').on('change', function () {
if($(this).is(':checked')) {
$(input).prop('type', 'text');
@ -607,26 +608,41 @@ define([
$(input).prop('type', 'password');
$(input).focus();
});
*/
$(eye).mousedown(function () {
$(input).prop('type', 'text');
$(input).focus();
}).mouseup(function(){
$(input).prop('type', 'password');
$(input).focus();
}).mouseout(function(){
$(input).prop('type', 'password');
$(input).focus();
});
if (displayEye) {
$(eye).mousedown(function () {
$(input).prop('type', 'text');
$(input).focus();
}).mouseup(function(){
$(input).prop('type', 'password');
$(input).focus();
}).mouseout(function(){
$(input).prop('type', 'password');
$(input).focus();
});
} else {
$(eye).click(function () {
if ($(this).hasClass('fa-eye')) {
$(input).prop('type', 'text');
$(input).focus();
$(this).removeClass('fa-eye').addClass('fa-eye-slash');
return;
}
$(input).prop('type', 'password');
$(input).focus();
$(this).removeClass('fa-eye-slash').addClass('fa-eye');
});
}
/*if (displayEye) {
$(reveal).hide();
} else {
$(eye).hide();
}
}*/
return h('span.cp-password-container', [
input,
reveal,
//reveal,
eye
]);
};
@ -996,6 +1012,7 @@ define([
if (e.which === 32) {
e.stopPropagation();
e.preventDefault();
if ($(input).is(':checked')) { return; }
$(input).prop('checked', !$(input).is(':checked'));
$(input).change();
}

@ -570,7 +570,7 @@ define([
style: 'flex: 1;'
});
var passwordOk = h('button', Messages.properties_changePasswordButton);
var changePass = h('span.cp-password-container', [
var changePass = h('span.cp-password-change-container', [
newPassword,
passwordOk
]);
@ -3524,19 +3524,24 @@ define([
(cb || function () {})();
};
UIElements.displayPasswordPrompt = function (common, isError) {
UIElements.displayPasswordPrompt = function (common, cfg, isError) {
var error;
if (isError) { error = setHTML(h('p.cp-password-error'), Messages.password_error); }
var info = h('p.cp-password-info', Messages.password_info);
var password = UI.passwordInput({placeholder: Messages.password_placeholder});
var button = h('button', Messages.password_submit);
cfg = cfg || {};
if (cfg.value && !isError) {
$(password).find('.cp-password-input').val(cfg.value);
}
var submit = function () {
var value = $(password).find('.cp-password-input').val();
UI.addLoadingScreen();
common.getSframeChannel().query('Q_PAD_PASSWORD_VALUE', value, function (err, data) {
if (!data) {
UIElements.displayPasswordPrompt(common, true);
UIElements.displayPasswordPrompt(common, cfg, true);
}
});
};
@ -4091,6 +4096,7 @@ define([
common.mailbox.sendTo("INVITE_TO_TEAM_ANSWER", {
answer: yes,
teamChannel: msg.content.team.channel,
teamName: teamName,
user: {
displayName: user.name,
avatar: user.avatar,

@ -294,7 +294,8 @@ define([
// Display the notification
var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous;
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || '');
var teamName = Util.fixHTML(Util.find(msg, ['content', 'team', 'metadata', 'name']) || '') ||
Util.fixHTML(Util.find(msg, ['content', 'teamName']));
var key = 'team_' + (msg.content.answer ? 'accept' : 'decline') + 'Invitation';
content.getFormatText = function () {
return Messages._getKey(key, [name, teamName]);

@ -575,7 +575,8 @@ define([
support: Util.find(store.proxy, ['mailboxes', 'support', 'channel']),
pendingFriends: store.proxy.friends_pending || {},
supportPrivateKey: Util.find(store.proxy, ['mailboxes', 'supportadmin', 'keys', 'curvePrivate']),
teams: teams
teams: teams,
plan: account.plan
}
};
cb(JSON.parse(JSON.stringify(metadata)));
@ -1835,8 +1836,9 @@ define([
//var data = cmdData.data;
var s = getStore(cmdData.teamId);
if (s.offline) {
broadcast([], 'NETWORK_DISCONNECT');
return void cb({ error: 'OFFLINE' });
var send = s.id ? s.sendEvent : sendDriveEvent;
send('NETWORK_DISCONNECT');
return void cb({ error: 'OFFLINE' });
}
var cb2 = function (data2) {
// Send the CHANGE event to all the stores because the command may have
@ -2291,17 +2293,18 @@ define([
if (path[0] === 'drive' && path[1] === "migrate" && value === 1) {
rt.network.disconnect();
rt.realtime.abort();
broadcast([], 'NETWORK_DISCONNECT');
sendDriveEvent('NETWORK_DISCONNECT');
}
});
// Proxy handlers (reconnect only called when the proxy is ready)
rt.proxy.on('disconnect', function () {
store.offline = true;
broadcast([], 'NETWORK_DISCONNECT');
sendDriveEvent('NETWORK_DISCONNECT');
});
rt.proxy.on('reconnect', function (info) {
rt.proxy.on('reconnect', function () {
store.offline = false;
broadcast([], 'NETWORK_RECONNECT', {myId: info.myId});
sendDriveEvent('NETWORK_RECONNECT');
});
// Ping clients regularly to make sure one tab was not closed without sending a removeClient()

@ -20,11 +20,11 @@ define([
}
profile.edit = Hash.getEditHashFromKeys(secret);
profile.view = Hash.getViewHashFromKeys(secret);
cb();
setTimeout(cb);
});
return;
}
cb();
setTimeout(cb);
};
var openChannel = function (ctx) {

@ -21,7 +21,8 @@ define([
// No version: visible edit
// Version 2: encrypted edit links
SF.checkMigration = function (secondaryKey, proxy, uo, cb) {
SF.checkMigration = function (secondaryKey, proxy, uo, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var drive = proxy.drive || proxy;
// View access: can't migrate
if (!secondaryKey) { return void cb(); }
@ -82,7 +83,7 @@ define([
};
SF.load = function (config, id, data, _cb) {
var cb = Util.once(_cb);
var cb = Util.once(Util.mkAsync(_cb));
var network = config.network;
var store = config.store;
var isNew = config.isNew;
@ -188,7 +189,7 @@ define([
return;
}
sf.teams.forEach(function (obj) {
var leave = function () { SF.leave(secret.channel, teamId); };
var leave = function () { SF.leave(secret.channel, obj.store.id); };
/*
var uo = obj.store.manager.addProxy(obj.id, rt, leave, obj.secondaryKey);
// NOTE: Shared folder migration, disable for now
@ -300,6 +301,7 @@ define([
*/
SF.loadSharedFolders = function (Store, network, store, userObject, waitFor) {
var shared = Util.find(store.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {};
var w = waitFor();
nThen(function (waitFor) {
Object.keys(shared).forEach(function (id) {
var sf = shared[id];
@ -309,7 +311,9 @@ define([
isNewChannel: Store.isNewChannel
}, id, sf, waitFor());
});
}).nThen(waitFor());
}).nThen(function () {
setTimeout(w);
});
};
return SF;

@ -25,10 +25,6 @@ define([
var Nacl = window.nacl;
var initializeTeams = function (ctx, cb) {
cb();
};
var registerChangeEvents = function (ctx, team, proxy, fId) {
if (!team) { return; }
if (!fId) {
@ -57,6 +53,14 @@ define([
return false;
}
});
proxy.on('disconnect', function () {
team.offline = true;
team.sendEvent('NETWORK_DISCONNECT');
});
proxy.on('reconnect', function () {
team.offline = false;
team.sendEvent('NETWORK_RECONNECT');
});
}
proxy.on('change', [], function (o, n, p) {
if (fId) {
@ -101,12 +105,6 @@ define([
path: p
});
});
proxy.on('disconnect', function () {
team.offline = true;
});
proxy.on('reconnect', function (/* info */) {
team.offline = false;
});
};
var closeTeam = function (ctx, teamId) {
@ -338,7 +336,7 @@ define([
};
var openChannel = function (ctx, teamData, id, _cb) {
var cb = Util.once(_cb);
var cb = Util.once(Util.mkAsync(_cb));
var hash = teamData.hash || teamData.roHash;
var secret = Hash.getSecrets('team', hash, teamData.password);
@ -1277,10 +1275,6 @@ define([
var teams = store.proxy.teams = store.proxy.teams || {};
initializeTeams(ctx, waitFor(function (err) {
if (err) { return; }
}));
// Listen for changes in our access rights (if another worker receives edit access)
ctx.store.proxy.on('change', ['teams'], function (o, n, p) {
if (p[2] !== 'hash') { return; }
@ -1363,6 +1357,10 @@ define([
removeClient(ctx, clientId);
};
team.execCommand = function (clientId, obj, cb) {
if (ctx.store.offline) {
return void cb({ error: 'OFFLINE' });
}
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SUBSCRIBE') {

@ -45,6 +45,8 @@ define([
var ids = id ? [id] : exp.findChannels([channel]);
ids.forEach(function (i) {
var data = exp.getFileData(i, true);
var oldHref = exp.getHref(data);
if (oldHref === href) { return; }
data.href = exp.cryptor.encrypt(href);
});
};

@ -206,7 +206,7 @@ define([
// 2c: 'view' pad and '/p/' and a wrong password stored --> the seed is incorrect
// 2d: 'view' pad and '/p/' and password never stored (security feature) --> password-prompt
var askPassword = function (wrongPasswordStored) {
var askPassword = function (wrongPasswordStored, cfg) {
// Ask for the password and check if the pad exists
// If the pad doesn't exist, it means the password isn't correct
// or the pad has been deleted
@ -223,7 +223,6 @@ define([
if (wrongPasswordStored) {
// Store the correct password
nThen(function (w) {
// XXX noPasswordStored: return; ?
Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl());
Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl());
if (parsed.hashData.mode === 'edit') {
@ -250,11 +249,14 @@ define([
// Not a file, so we can use `isNewChannel`
Cryptpad.isNewChannel(window.location.href, password, next);
});
sframeChan.event("EV_PAD_PASSWORD");
sframeChan.event("EV_PAD_PASSWORD", cfg);
};
var done = waitFor();
var stored = false;
var passwordCfg = {
value: ''
};
nThen(function (w) {
Cryptpad.getPadAttribute('title', w(function (err, data) {
stored = (!err && typeof (data) === "string");
@ -264,7 +266,7 @@ define([
}), parsed.getUrl());
}).nThen(function (w) {
if (!password && !stored && sessionStorage.newPadPassword) {
password = sessionStorage.newPadPassword;
passwordCfg.value = sessionStorage.newPadPassword;
delete sessionStorage.newPadPassword;
}
@ -274,7 +276,7 @@ define([
Cryptpad.getFileSize(window.location.href, password, w(function (e, size) {
if (size !== 0) { return void todo(); }
// Wrong password or deleted file?
askPassword(true);
askPassword(true, passwordCfg);
}));
return;
}
@ -293,7 +295,7 @@ define([
return void todo();
}
// Wrong password or deleted file?
askPassword(true);
askPassword(true, passwordCfg);
}));
}).nThen(done);
}
@ -364,7 +366,6 @@ define([
donateURL: Cryptpad.donateURL,
upgradeURL: Cryptpad.upgradeURL
},
plan: localStorage[Utils.Constants.plan],
isNewFile: isNewFile,
isDeleted: isNewFile && window.location.hash.length > 0,
forceCreationScreen: forceCreationScreen,

@ -595,8 +595,8 @@ define([
UI.addTooltips();
ctx.sframeChan.on("EV_PAD_PASSWORD", function () {
UIElements.displayPasswordPrompt(funcs);
ctx.sframeChan.on("EV_PAD_PASSWORD", function (cfg) {
UIElements.displayPasswordPrompt(funcs, cfg);
});
ctx.sframeChan.on("EV_PAD_PASSWORD_ERROR", function () {

@ -170,7 +170,6 @@
"viewOpenTitle": "Obre aquest document en mode només de lectura en una pestanya nova",
"fileShare": "Copia l'enllaç",
"getEmbedCode": "Obté el codi d'incrustat",
"viewEmbedTitle": "Inscrusta el document en una pàgina externa",
"viewEmbedTag": "Per incrustar aquest document, poseu aquest iframe allà on vulgueu de la vostra pàgina. Podeu donar-li l'estil desitjat utilitzant els atributs CSS o HTML.",
"fileEmbedTitle": "Incrusta el fitxer en una pàgina externa",
"fileEmbedScript": "Per incrustar aquest fitxer, poseu aquest script un cop a la vostra pàgina per carregar l'Etiqueta Multimèdia:",

@ -168,7 +168,6 @@
"viewOpenTitle": "Pad schreibgeschützt in neuem Tab öffnen",
"fileShare": "Link kopieren",
"getEmbedCode": "Einbettungscode anzeigen",
"viewEmbedTitle": "Das Pad in eine externe Webseite einbetten",
"viewEmbedTag": "Um dieses Pad einzubetten, platziere diesen iframe an der gewünschten Stelle deiner HTML-Seite. Du kannst es mit CSS oder HTML-Attributen gestalten.",
"fileEmbedTitle": "Die Datei in einer externen Seite einbetten",
"fileEmbedScript": "Um diese Datei einzubetten, füge dieses Skript einmal in deiner Webseite ein, damit das Media-Tag geladen wird:",
@ -955,15 +954,14 @@
"properties_passwordWarning": "Das Passwort wurde erfolgreich geändert, aber dein CryptDrive konnte nicht aktualisiert werden. Du musst möglicherweise die alte Version des Pads manuell entfernen.<br>Klicke auf OK, um die Seite neu zu laden und die Zugriffsrechte zu aktualisieren.",
"properties_passwordSuccess": "Das Passwort wurde erfolgreich geändert.<br>Klicke auf OK, um die Seite neu zu laden und die Zugriffsrechte zu aktualisieren.",
"properties_changePasswordButton": "Absenden",
"share_linkCategory": "Link teilen",
"share_linkCategory": "Link",
"share_linkAccess": "Zugriffsrechte",
"share_linkEdit": "Bearbeiten",
"share_linkView": "Ansehen",
"share_linkOptions": "Linkoptionen",
"share_linkEmbed": "Einbettungsmodus (Werkzeugleiste und Benutzerliste sind verborgen)",
"share_linkPresent": "Anzeigemodus (Bearbeitbare Abschnitte sind verborgen)",
"share_linkOpen": "In einem neuen Tab öffnen",
"share_linkCopy": "In die Zwischenablage kopieren",
"share_linkEmbed": "Einbettungsmodus (Werkzeugleiste und Benutzerliste verbergen)",
"share_linkPresent": "Anzeigemodus",
"share_linkOpen": "Vorschau",
"share_linkCopy": "Kopieren",
"share_embedCategory": "Einbetten",
"share_mediatagCopy": "Media-Tag in die Zwischenablage kopieren",
"loading_pad_1": "Initialisiere Pad",
@ -977,7 +975,7 @@
"sharedFolders_create_name": "Neuer Ordner",
"sharedFolders_create_owned": "Eigener Ordner",
"sharedFolders_create_password": "Ordnerpasswort",
"sharedFolders_share": "Teile diese URL mit anderen registrierten Benutzern, um ihnen Zugriff auf den geteilten Ordner zu geben. Sobald sie diese URL öffnen, wird der geteilte Ordner zu ihrem CryptDrive hinzugefügt.",
"sharedFolders_share": "Teile diesen Link mit anderen registrierten Benutzern, um ihnen Zugriff auf den geteilten Ordner zu geben. Sobald sie diesen Link öffnen, wird der geteilte Ordner zu ihrem CryptDrive hinzugefügt.",
"chrome68": "Anscheinend benutzt du Chrome oder Chromium in Version 68. Ein darin enthaltener Fehler führt dazu, dass nach ein paar Sekunden die Seite komplett weiß wird oder nicht mehr auf Klicks reagiert. Um das Problem zu beheben, wechsle den Tab und kehre zu CryptPad zurück, oder versuche zu scrollen. Dieser Fehler sollte in der nächsten Version deines Browsers behoben sein.",
"autostore_file": "Diese Datei",
"autostore_sf": "Dieser Ordner",
@ -1233,5 +1231,16 @@
"driveOfflineError": "Die Verbindung zu CryptPad ist verloren gegangen. Änderungen an diesem Pad werden nicht in deinem CryptDrive gespeichert. Bitte schließe alle CryptPad-Tabs und versuche es in einem neuen Fenster erneut. ",
"storageStatus": "Speicher:<br /><b>{0}</b> von <b>{1}</b> belegt",
"teams_table": "Rollen",
"teams_table_generic": "Rollen und Berechtigungen"
"teams_table_generic": "Rollen und Berechtigungen",
"teams_table_generic_view": "Ansehen: Zugriff auf Ordner und Pads (nur Lesen)",
"teams_table_generic_edit": "Bearbeiten: Erstellen, Ändern und Löschen von Ordnern und Pads",
"teams_table_generic_admin": "Mitglieder verwalten: Einladen und Entfernen von Mitgliedern, Ändern von Benutzerrollen bis maximal Admin",
"teams_table_generic_own": "Team verwalten: Ändern von Namen und Avatar des Teams, Hinzufügen oder Entfernen von Eigentümerschaften des Teams, Löschung des Teams",
"teams_table_specific": "Ausnahmen",
"teams_table_specificHint": "Dies sind ältere geteilte Ordner, wo Benutzer noch Bearbeitungsrechte haben. Für hier erstellte oder hierhin kopierte Pads gelten Standard-Berechtigungen.",
"teams_table_admins": "Mitglieder verwalten",
"teams_table_owners": "Team verwalten",
"teams_table_role": "Rolle",
"share_contactCategory": "Kontakte",
"pad_wordCount": "Wörter: {0}"
}

@ -141,7 +141,6 @@
"viewOpenTitle": "Άνοιγμα αυτού του pad μόνο για ανάγνωση σε νέα καρτέλα",
"fileShare": "Αντιγραφή συνδέσμου",
"getEmbedCode": "Κώδικας ενσωμάτωσης",
"viewEmbedTitle": "Ενσωματώστε αυτό το pad σε μία εξωτερική σελίδα",
"viewEmbedTag": "Για να ενσωματώσετε αυτό το pad, συμπεριλάβετε αυτό το iframe στη σελίδα σας, στο σημείο που θέλετε. Μπορείτε να το διαμορφώσετε χρησιμοποιώντας CSS η HTML παραμέτρους.",
"fileEmbedTitle": "Ενσωματώστε το αρχείο σε μια εξωτερική σελίδα",
"fileEmbedScript": "Για να ενσωματώσετε αυτό το αρχείο, συμπεριλάβετε αυτό το script στη σελίδα σας για να φορτωθεί το Media Tag:",

@ -496,7 +496,6 @@
"kanban_removeItemConfirm": "Estás seguro que quieres eliminar este ítem?",
"printBackgroundNoValue": "<em>No se muestra fondo de pantalla</em>",
"getEmbedCode": "Obtener el código insertado",
"viewEmbedTitle": "Insertar la nota en una página externa",
"viewEmbedTag": "Para insertar esta nota, incluya este iframe en su página donde usted quiera. Puede darle estilo usando CSS o atributos HTML.",
"fileEmbedTitle": "Insertar el archivo en una pagina externa",
"fileEmbedScript": "Para insertar este archivo, incluya este código una vez en su página para cargar el Etiqueta de Media:",

@ -170,7 +170,6 @@
"viewOpenTitle": "Ouvrir le lien en lecture seule dans un nouvel onglet",
"fileShare": "Copier le lien",
"getEmbedCode": "Obtenir le code d'intégration",
"viewEmbedTitle": "Intégrer le pad dans une page web",
"viewEmbedTag": "Pour intégrer ce pad, veuillez inclure l'iframe suivant dans votre page là où vous souhaitez l'afficher. Vous pouvez changer sa taille en utilisant du code CSS ou des attributs HTML.",
"fileEmbedTitle": "Intégrer le fichier dans une page web",
"fileEmbedScript": "Pour intégrer un fichier, veuillez inclure le script suivant une fois dans votre page afin de pouvoir charger le Media Tag :",
@ -962,15 +961,14 @@
"properties_passwordWarning": "Le mot de passe a été modifié avec succès mais nous n'avons pas réussi à mettre à jour votre CryptDrive avec les nouvelles informations. Vous devrez peut-être supprimer manuellement l'ancienne version de ce pad.<br>Appuyez sur OK pour recharger le pad et mettre à jour vos droits d'accès.",
"properties_passwordSuccess": "Le mot de passe a été modifié avec succès.<br>Appuyez sur OK pour mettre à jour vos droits d'accès.",
"properties_changePasswordButton": "Valider",
"share_linkCategory": "Partage",
"share_linkCategory": "Lien",
"share_linkAccess": "Droits d'accès",
"share_linkEdit": "Édition",
"share_linkView": "Lecture-seule",
"share_linkOptions": "Options du lien",
"share_linkEmbed": "Mode intégration (barre d'outils cachée)",
"share_linkPresent": "Mode présentation (sections d'édition cachées)",
"share_linkOpen": "Ouvrir le lien",
"share_linkCopy": "Copier le lien",
"share_linkEmbed": "Mode intégration (cache la barre d'outils)",
"share_linkPresent": "Présenter",
"share_linkOpen": "Apperçu",
"share_linkCopy": "Copier",
"share_embedCategory": "Intégration",
"share_mediatagCopy": "Copier le mediatag",
"loading_pad_1": "Initialisation du pad",
@ -984,7 +982,7 @@
"sharedFolders_create_name": "Nom du dossier",
"sharedFolders_create_owned": "Être propriétaire du dossier",
"sharedFolders_create_password": "Mot de passe du dossier",
"sharedFolders_share": "Partager cette URL avec d'autres utilisateurs enregistrés leur donne accès au dossier partagé. Une fois l'URL ouverte, le dossier partagé sera ajouté au répertoire racine de leur CryptDrive.",
"sharedFolders_share": "Partager ce lien avec d'autres utilisateurs enregistrés leur donne accès au dossier partagé. Une fois le lien ouvert, le dossier partagé sera ajouté à leur CryptDrive.",
"chrome68": "Il semblerait que vous utilisiez le navigateur Chrome version 68. Ce navigateur contient un bug rendant certaines pages entièrement blanches après quelques secondes ou bloquant les clics. Pour corriger ce problème, vous pouvez vous déplacer vers un nouvel onglet et revenir ou vous pouvez essayer de faire défiler la page. Ce bug devrait être corrigé dans la prochaine version du navigateur.",
"autostore_file": "fichier",
"autostore_sf": "dossier",
@ -1242,5 +1240,7 @@
"teams_table_specificHint": "Dans ces dossiers partagés qui sont d'une version antérieure, les lecteurs peuvent modifier les pads existants. Les pads créés ou copiés dans ces dossiers auront les permissions standard.",
"teams_table_admins": "Gérer les membres",
"teams_table_owners": "Gérer l'équipe",
"teams_table_role": "Rôle"
"teams_table_role": "Rôle",
"share_contactCategory": "Contacts",
"pad_wordCount": "Mots : {0}"
}

@ -169,7 +169,6 @@
"viewOpenTitle": "Apri questo pad in modalità solo lettura in una nuova finestra",
"fileShare": "Copia il link",
"getEmbedCode": "Mostra il codice per embedding",
"viewEmbedTitle": "Fai l'embed di questo pad in una pagina esterna",
"viewEmbedTag": "Per fare l'embed di questo pad, includi questo iframe nella tua pagina dovunque tu voglia. Puoi modificarne lo stile con gli attributi HTML o CSS.",
"fileEmbedTitle": "Fai l'embed di questo file in una pagina esterna",
"fileEmbedScript": "Per fare l'embed di questo file, includi questo script una volta nella tua pagina per caricare il Media Tag:",

@ -1241,5 +1241,6 @@
"teams_table_specificHint": "These are older shared folders where viewers still have permission to edit existing pads. Pads created or copied into these folders will have standard permissions.",
"teams_table_admins": "Manage members",
"teams_table_owners": "Manage team",
"teams_table_role": "Role"
"teams_table_role": "Role",
"pad_wordCount": "Words: {0}"
}

@ -161,7 +161,6 @@
"viewOpenTitle": "Åpne denne paden i lesemodus i en ny fane",
"fileShare": "Kopier linken",
"getEmbedCode": "Hent innlagt kode",
"viewEmbedTitle": "Legg inn paden i en ekstern side",
"canvas_width": "Bredde",
"canvas_opacity": "Gjennomsiktighet",
"canvas_opacityLabel": "Gjennomsiktighet:{0}",

@ -404,7 +404,6 @@
"viewOpen": "",
"viewOpenTitle": "",
"getEmbedCode": "",
"viewEmbedTitle": "",
"viewEmbedTag": "",
"fileEmbedTitle": "",
"fileEmbedScript": "",

@ -392,7 +392,6 @@
"themeButton": "Temă",
"themeButtonTitle": "Alege tema de culori de folosit pentru cod si editorul slide-urilor",
"fileShare": "Copiază linkul",
"viewEmbedTitle": "Include pad-ul într-o pagină externă",
"fileEmbedTitle": "Include fișierul într-o pagină externă",
"fileEmbedTag": "După care plasează această etichetă Media oriunde pe pagina unde vrei sa o plasezi",
"ok": "Ok",

@ -168,7 +168,6 @@
"viewOpenTitle": "Открыть данный документ для чтения в новой вкладке",
"fileShare": "Скопировать ссылку",
"getEmbedCode": "Получить код для встраивания",
"viewEmbedTitle": "Встроить документ во внешнюю страницу",
"notifyJoined": "{0} присоединился к совместной сессии",
"notifyRenamed": "{0} теперь известен как {1}",
"notifyLeft": "{0} покинул совместную сессию",

@ -439,7 +439,12 @@ define([
console.log(doc);
};
config.onLocal = function () { };
var toRestore;
config.onLocal = function (a, restore) {
if (!toRestore || !restore) { return; }
cpNfInner.chainpad.contentUpdate(toRestore);
};
config.onInit = function (info) {
Title = common.createTitle({});
@ -459,10 +464,13 @@ define([
/* add a history button */
var histConfig = {
onLocal: config.onLocal,
onLocal: function () {
config.onLocal(null, true);
},
onRemote: config.onRemote,
setHistory: setHistory,
applyVal: function (val) {
toRestore = val;
displayDoc(JSON.parse(val) || {});
},
$toolbar: $bar,

@ -274,10 +274,10 @@ define([
APP.toolbar.failed();
if (!noAlert) { UI.alert(Messages.common_connectionLost, undefined, true); }
};
var onReconnect = function (info) {
var onReconnect = function () {
setEditable(true);
if (drive.refresh) { drive.refresh(); }
APP.toolbar.reconnecting(info.myId);
APP.toolbar.reconnecting();
UI.findOKButton().click();
};
@ -287,9 +287,8 @@ define([
sframeChan.on('EV_NETWORK_DISCONNECT', function () {
onDisconnect();
});
sframeChan.on('EV_NETWORK_RECONNECT', function (data) {
// data.myId;
onReconnect(data);
sframeChan.on('EV_NETWORK_RECONNECT', function () {
onReconnect();
});
common.onLogout(function () { setEditable(false); });
});

@ -94,8 +94,8 @@ define([
Cryptpad.onNetworkDisconnect.reg(function () {
sframeChan.event('EV_NETWORK_DISCONNECT');
});
Cryptpad.onNetworkReconnect.reg(function (data) {
sframeChan.event('EV_NETWORK_RECONNECT', data);
Cryptpad.onNetworkReconnect.reg(function () {
sframeChan.event('EV_NETWORK_RECONNECT');
});
Cryptpad.drive.onLog.reg(function (msg) {
sframeChan.event('EV_DRIVE_LOG', msg);

@ -152,6 +152,9 @@ define([
e.preventDefault();
e.stopPropagation();
save();
if (!$input.val()) { return; }
if (!$(el).closest('.kanban-item').is(':last-child')) { return; }
$(el).closest('.kanban-board').find('.kanban-title-button.fa-plus').click();
return;
}
if (e.which === 27) {
@ -301,6 +304,8 @@ define([
e.preventDefault();
e.stopPropagation();
save();
if (!$input.val()) { return; }
$(el).closest('.kanban-board').find('.kanban-title-button.fa-plus').click();
return;
}
if (e.which === 27) {
@ -385,6 +390,97 @@ define([
$container.addClass('cp-app-readonly');
});
var getSelectedElement = function () {
var node = document.getSelection().anchorNode;
return (node.nodeType === 3 ? node.parentNode : node);
};
var getCursor = function () {
if (!kanban || !kanban.inEditMode) { return; }
try {
var el = getSelectedElement();
var input = $(el).is('input') ? el : $(el).find('input')[0];
if (!input) { return; }
var $input = $(input);
var pos;
var $item = $(el).closest('.kanban-item');
if ($item.length) {
pos = kanban.findElementPosition($item[0]);
}
var board = $input.closest('.kanban-board').attr('data-id');
var val = ($input.val && $input.val()) || '';
var start = input.selectionStart;
var end = input.selectionEnd;
var boardEl = kanban.options.boards.find(function (b) {
return b.id === board;
});
var oldVal = ((pos ? boardEl.item[pos] : boardEl) || {}).title;
return {
board: board,
pos: pos,
value: val,
start: start,
end: end,
oldValue: oldVal
};
} catch (e) {
return {};
}
};
var restoreCursor = function (data) {
try {
var boardEl = kanban.options.boards.find(function (b) {
return b.id === data.board;
});
if (!boardEl) { return; }
var $board = $('.kanban-board[data-id="'+data.board+'"');
// Editing a board title...
if (!data.pos && $board.length) {
if (boardEl.title !== data.oldValue) { return; }
$board.find('.kanban-title-board').click();
var $boardInput = $board.find('.kanban-title-board input');
$boardInput.val(data.value);
$boardInput[0].selectionStart = data.start;
$boardInput[0].selectionEnd = data.end;
return;
}
// Editing a deleted board title: abort
if (!data.pos) {
return;
}
// An item was added: add a new item
if (!data.oldValue) {
$board.find('.kanban-title-button.fa-plus').click();
var $newInput = $board.find('.kanban-item:last-child input');
$newInput.val(data.value);
$newInput[0].selectionStart = data.start;
$newInput[0].selectionEnd = data.end;
return;
}
// An item was edited: click on the correct item
var newVal = boardEl.item[data.pos];
if (!newVal || newVal.title !== data.oldValue) { return; }
var $el = $('.kanban-board[data-id="' + data.board + '"]')
.find('.kanban-item:nth-child('+(data.pos + 1)+')');
$el.find('.kanban-item-text').click();
var $input = $el.find('input');
if ($input.length) {
$input.val(data.value);
$input[0].selectionStart = data.start;
$input[0].selectionEnd = data.end;
}
} catch (e) {
return;
}
};
framework.onContentUpdate(function (newContent) {
// Init if needed
if (!kanban) {
@ -399,11 +495,12 @@ define([
var remoteContent = newContent.content;
if (Sortify(currentContent) !== Sortify(remoteContent)) {
// reinit kanban (TODO: optimize to diff only)
var cursor = getCursor();
verbose("Content is different.. Applying content");
kanban.setBoards(remoteContent);
kanban.inEditMode = false;
addRemoveItemButton(framework, kanban);
restoreCursor(cursor);
}
});

@ -93,6 +93,10 @@ define([
return ud.content.hash === data.content.hash;
});
notifsData.push(data);
if (data.content.msg.type === 'REQUEST_PAD_ACCESS') { return; } // FIXME find a way to display this notifications wihtout knowing the title
if (data.content.msg.type === 'INVITE_TO_TEAM_ANSWER') { console.log(data); }
if (data.content.msg.type === 'INVITE_TO_TEAM_ANSWER'
&& !data.content.msg.content.teamName) { return; } // FIXME find a way to display this notifications wihtout knowing the team name
var el = common.mailbox.createElement(data);
var time = new Date(data.content.time);
$(el).find(".cp-notification-content").append(h("span.notification-time", time.toLocaleString()));

@ -17,6 +17,13 @@ body.cp-app-pad {
height: 28px;
padding: 2px 0;
}
.cp-app-pad-wordCount {
float: right;
display: inline-flex;
height: 24px;
align-items: center;
padding: 4px;
}
}
.cke_wysiwyg_frame {
width: 100%;

@ -616,6 +616,8 @@ define([
var patch = (DD).diff(inner, userDocStateDom);
(DD).apply(inner, patch);
editor.fire('cp-wc'); // Update word count
// Restore cursor position
var newText = inner.outerHTML;
var ops = ChainPad.Diff.diff(oldText, newText);
@ -835,9 +837,20 @@ define([
inner.addEventListener('input', function () {
framework.localChange();
updateCursor();
editor.fire('cp-wc'); // Update word count
});
editor.on('change', framework.localChange);
var wordCount = h('span.cp-app-pad-wordCount');
$('.cke_toolbox_main').append(wordCount);
editor.on('cp-wc-update', function () {
if (!editor.wordCount || typeof (editor.wordCount.wordCount) === "undefined") {
wordCount.innerText = '';
return;
}
wordCount.innerText = Messages._getKey('pad_wordCount', [editor.wordCount.wordCount]);
});
// export the typing tests to the window.
// call like `test = easyTest()`
// terminate the test like `test.cancel()`
@ -949,6 +962,7 @@ define([
};
Ckeditor.plugins.addExternal('mediatag','/pad/', 'mediatag-plugin.js');
Ckeditor.plugins.addExternal('blockbase64','/pad/', 'disable-base64.js');
Ckeditor.plugins.addExternal('wordcount','/pad/wordcount/', 'plugin.js');
module.ckeditor = editor = Ckeditor.replace('editor1', {
customConfig: '/customize/ckeditor-config.js',
});

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Ingo Herbote
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,106 @@
CKEditor-WordCount-Plugin
=========================
WordCount Plugin for CKEditor v4 (or above) that counts the words/characters an shows the word count and/or char count in the footer of the editor.
![Screenshot](http://www.watchersnet.de/Portals/0/screenshots/dnn/CKEditorWordCountPlugin.png)
#### Demo
http://w8tcha.github.com/CKEditor-WordCount-Plugin/
DISCLAIMER: This is a forked Version, i can not find the original Author anymore if anyone knows the original Author please contact me and i can include the Author in the Copyright Notices.
#### License
Licensed under the terms of the MIT License.
#### Installation
If building a new editor using the CKBuilder from http://ckeditor.com/, there is no need to follow numbers steps below. If adding the Word Count & Char Count plugin to an already established CKEditor, follow the numbered steps below.
1. Download the Word Count & Char Count plugin from http://ckeditor.com/addon/wordcount or https://github.com/w8tcha/CKEditor-WordCount-Plugin. This will download a folder named **wordcount_*version*.zip** or **CKEditor-WordCount-Plugin-master.zip** to your Downloads folder.
2. Download the Notification plugin from http://ckeditor.com/addon/notification. This will download a folder named **notification_*version*.zip** to your Downloads folder.
3. Extract the .zip folders for both the Word Count & Char Count and Notification plugin. After extraction, you should have a folder named **wordcount** and a folder named **notification**.
4. Move the wordcount folder to /web/server/root/ckeditor/plugins/. Move the notification folder to /web/server/root/ckeditor/plugins/.
5. Add the following line of text to the config.js file, which is located at /web/server/root/ckeditor/.
```javascript
config.extraPlugins = 'wordcount,notification';
```
Below is an example of what your config.js file might look like after adding config.extraPlugins = 'wordcount,notification';
```javascript
CKEDITOR.editorConfig = function( config ) {
config.extraPlugins = 'wordcount,notification';
config.toolbar [
et cetera . . .
];
};
```
There now should be text in the bottom right-hand corner of your CKEditor which counts the number of Paragraphs and number of Words in your CKEditor.
To modify the behavior of the Word Count & Char Count text at the bottom right-hand corner of your CKEditor, add the following text to your config.js file located at /web/server/root/ckeditor/config.js.
````js
config.wordcount = {
// Whether or not you want to show the Paragraphs Count
showParagraphs: true,
// Whether or not you want to show the Word Count
showWordCount: true,
// Whether or not you want to show the Char Count
showCharCount: false,
// Whether or not you want to count Spaces as Chars
countSpacesAsChars: false,
// Whether or not to include Html chars in the Char Count
countHTML: false,
// Whether or not to include Line Breaks in the Char Count
countLineBreaks: false,
// Maximum allowed Word Count, -1 is default for unlimited
maxWordCount: -1,
// Maximum allowed Char Count, -1 is default for unlimited
maxCharCount: -1,
// Maximum allowed Paragraphs Count, -1 is default for unlimited
maxParagraphs: -1,
// How long to show the 'paste' warning, 0 is default for not auto-closing the notification
pasteWarningDuration: 0,
// Add filter to add or remove element before counting (see CKEDITOR.htmlParser.filter), Default value : null (no filter)
filter: new CKEDITOR.htmlParser.filter({
elements: {
div: function( element ) {
if(element.attributes.class == 'mediaembed') {
return false;
}
}
}
})
};
````
**Note:** If you plan to change some of the JavaScript, you probably will not want to use the CKBuilder, because this will place the JavaScript of the Word Count & Char Count plugin in the ckeditor.js file located at /web/server/root/ckeditor/ckeditor.js. The JavaScript for the Word Count & Char Count plugin in the ckeditor.js file is different than the JavaScript used when manually adding the Word Count & Char Count plugin. When manually adding the Word Count & Char Count plugin, the JavaScript will be in the plugin.js file located at
---
If you want to query the current wordcount you can do it via
```javascript
// get the word count
CKEDITOR.instances.editor1.wordCount.wordCount
// get the char count
CKEDITOR.instances.editor1.wordCount.charCount
```

@ -0,0 +1,3 @@
.cke_wordcount {display:block;float:right;margin-top:-2px;margin-right:3px;color:black;}
.cke_wordcountLimitReached {color:red! important}

@ -0,0 +1,12 @@
// Arabic Translation by Amine BENHAMIDA
CKEDITOR.plugins.setLang('wordcount', 'ar', {
WordCount: 'كلمات:',
CharCount: 'حروف:',
CharCountWithHTML: 'حروف مع إتش تي إم إل',
Paragraphs: 'فقرات',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'لا يمكن اضافة هذا المحتوى لانه تجاوز الحد الاقصى',
Selected: 'محدد: ',
title: 'احصائيات'
});

@ -0,0 +1,17 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'bg', {
WordCount: 'Думи:',
WordCountRemaining: 'Оставащи думи',
CharCount: 'Знаци:',
CharCountRemaining: 'Знаци',
CharCountWithHTML: 'Знаци (с HTML):',
CharCountWithHTMLRemaining: 'Оставащи знаци (с HTML)',
Paragraphs: 'Параграфи:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Съдържанието не може да бъде поставено, защото е над разрешения лимит',
Selected: 'Избрани: ',
title: 'Статистика'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'ca', {
WordCount: 'Paraules:',
CharCount: 'Caràcters:',
CharCountWithHTML: 'Caràcters (including HTML):',
Paragraphs: 'Paragraphs:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Content can not be pasted because it is above the allowed limit',
Selected: 'Selected: ',
title: 'Estadístiques'
});

@ -0,0 +1,15 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'cs',
{
WordCount: 'Slov: ',
CharCount: 'Znaků: ',
CharCountWithHTML: 'Znaků (s HTML): ',
Paragraphs: 'Odstavců: ',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Obsah nelze vložit',
Selected: 'Výběr: ',
title: 'Statistika'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'da', {
WordCount: 'Ord:',
CharCount: 'Karakterer:',
CharCountWithHTML: 'Karakterer (med HTML):',
Paragraphs: 'Afsnit:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Indholdet kan ikke indsættes da det er længere end den tilladte grænse.',
Selected: 'Markeret: ',
title: 'Statistik'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'de', {
WordCount: 'Wörter:',
CharCount: 'Zeichen:',
CharCountWithHTML: 'Zeichen (inkl. HTML):',
Paragraphs: 'Absätze:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Content can not be pasted because it is above the allowed limit',
Selected: 'Selected: ',
title: 'Statistik'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'el', {
WordCount: 'Λέξεις:',
CharCount: 'Χαρακτήρες:',
CharCountWithHTML: 'Χαρακτήρες (μαζί με HTML):',
Paragraphs: 'Paragraphs:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Content can not be pasted because it is above the allowed limit',
Selected: 'Selected: ',
title: 'Στατιστικά'
});

@ -0,0 +1,17 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'en', {
WordCount: 'Words:',
WordCountRemaining: 'Words remaining',
CharCount: 'Characters:',
CharCountRemaining: 'Characters remaining',
CharCountWithHTML: 'Characters (with HTML):',
CharCountWithHTMLRemaining: 'Characters (with HTML) remaining',
Paragraphs: 'Paragraphs:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Content cannot be pasted because it is above the allowed limit',
Selected: 'Selected: ',
title: 'Statistics'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'es', {
WordCount: 'Palabras:',
CharCount: 'Carácteres:',
CharCountWithHTML: 'Carácteres (con HTML):',
Paragraphs: 'Párrafos:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'El contenido no se puede pegar, ya que se encuentra fuera del límite permitido',
Selected: 'Seleccionado: ',
title: 'Estadísticas'
});

@ -0,0 +1,17 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'eu', {
WordCount: 'Hitzak:',
WordCountRemaining: 'Gelditzen diren hitzak',
CharCount: 'Karaktereak:',
CharCountRemaining: 'Gelditzen diren karaktereak',
CharCountWithHTML: 'Karaktereak (HTMLarekin):',
CharCountWithHTMLRemaining: 'Gelditzen diren karaktereak (HTMLarekin)',
Paragraphs: 'Paragrafoak:',
ParagraphsRemaining: 'Gelditzen diren paragrafoak',
pasteWarning: 'Ezin da edukia itsatsi, onartutako muga gainditu duelako',
Selected: 'Hautatuta: ',
title: 'Estatistikak'
});

@ -0,0 +1,13 @@
/*
Its The Persian (Farsi) Language Translate For Iranian By "Mohsen Esmaili"
*/
CKEDITOR.plugins.setLang('wordcount', 'fa', {
WordCount: 'لغت:',
CharCount: 'کاراکتر:',
CharCountWithHTML: 'کاراکترها (با HTML):',
Paragraphs: 'پاراگراف:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'محتوای مورد نظر را نمی توان چسباند. زیرا این بیشتر از حد مجاز است.',
Selected: 'انتخاب شده: ',
title: 'آمار'
});

@ -0,0 +1,15 @@
/**
* Finnish localisation.
*
* @author Joel Posti / Response200.pro
*/
CKEDITOR.plugins.setLang('wordcount', 'fi', {
WordCount: 'Sanoja:',
CharCount: 'Merkkejä:',
CharCountWithHTML: 'Merkkejä (ml. HTML):',
Paragraphs: 'Kappaleita:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Sisältöä ei voida liittää, koska se ylittää sallitun rajan.',
Selected: 'Valittuna: ',
title: 'Statistiikkaa'
});

@ -0,0 +1,12 @@
// French Translation by Nicolas M. et Pierre-Luc Auclair
CKEDITOR.plugins.setLang('wordcount', 'fr', {
WordCount: 'Mots :',
CharCount: 'Caractères :',
CharCountWithHTML: 'Caractères (incluant HTML) :',
Paragraphs: 'Paragraphes :',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Le contenu ne peut pas être collé car il dépasse la limite autorisée',
Selected: 'Sélectionné :',
title: 'Statistiques'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'he', {
WordCount: 'מילים:',
CharCount: 'תווים:',
CharCountWithHTML: 'תווים (כולל HTML):',
Paragraphs: 'פסקאות:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'לא ניתן להדביק תוכן בשל עודף תווים',
Selected: 'נבחר: ',
title: 'סטטיסטיקות'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'hr', {
WordCount: 'Riječi:',
CharCount: 'Znakova:',
CharCountWithHTML: 'Znakova (uključujući HTML):',
Paragraphs: 'Paragraphs:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Content can not be pasted because it is above the allowed limit',
Selected: 'Selected: ',
title: 'Statistika'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'hu', {
WordCount: 'Szavak:',
CharCount: 'Karakaterek:',
CharCountWithHTML: 'Karakterek (HTML tagekkel):',
Paragraphs: 'Bekezdések:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'A szöveget nem lehet beilleszteni, mert a megadott limit felett van',
Selected: 'Kiválasztva: ',
title: 'Statisztika'
});

@ -0,0 +1,15 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
@author translation: Davide Montorio
*/
CKEDITOR.plugins.setLang('wordcount', 'it', {
WordCount: 'Parole:',
CharCount: 'Caratteri:',
CharCountWithHTML: 'Caratteri (HTML incluso):',
Paragraphs: 'Paragrafi:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Il contenuto non può essere incollato poiché supera il limite massimo di caratteri disponibili',
Selected: 'Selezionato: ',
title: 'Statistiche'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'ja', {
WordCount: '単語数:',
CharCount: '文字数:',
CharCountWithHTML: '文字数 (HTMLタグを含む):',
Paragraphs: '段落数:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: '文字数/単語数の上限を超えるため、貼り付けできません。',
Selected: '選択中の字数:',
title: 'ワードカウント'
});

@ -0,0 +1,16 @@
/*
Korean translation by Maxime Houdais
*/
CKEDITOR.plugins.setLang('wordcount', 'ko', {
WordCount: '단어:',
WordCountRemaining: '남은 단어',
CharCount: '글자:',
CharCountRemaining: '남은 글자',
CharCountWithHTML: '글자 와 HTML:',
CharCountWithHTMLRemaining: '남은 글자 와 HTML',
Paragraphs: '단락:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: '허용 된 한도를 초과하여 콘텐츠를 붙여 넣을 수 없습니다.',
Selected: '선택:',
title: '통계'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'nl', {
WordCount: 'Woorden:',
CharCount: 'Tekens:',
CharCountWithHTML: 'Tekens (inclusief HTML):',
Paragraphs: 'Paragrafen:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'De tekst kan niet worden geplakt omdat de limiet is overschreden',
Selected: 'Geselecteerd: ',
title: 'Statistieken'
});

@ -0,0 +1,11 @@
// Norwegian translation by Vegard S.
CKEDITOR.plugins.setLang('wordcount', 'no', {
WordCount: 'Ord:',
CharCount: 'Tegn:',
CharCountWithHTML: 'Tegn (including HTML):',
Paragraphs: 'Paragraphs:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Content can not be pasted because it is above the allowed limit',
Selected: 'Selected: ',
title: 'Statistikk'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'pl', {
WordCount: 'Słów:',
CharCount: 'Znaków:',
CharCountWithHTML: 'Znaków (wraz z kodem HTML):',
Paragraphs: 'Paragrafy:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Zawartość nie może zostać wklejona, ponieważ przekracza dozwolony limit',
Selected: 'Zaznaczono: ',
title: 'Statystyka'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'pt-br', {
WordCount: 'Contagem de palavras:',
CharCount: 'Contagem de carateres:',
CharCountWithHTML: 'Carateres (incluindo HTML):',
Paragraphs: 'Parágrafos:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Conteúdo não pode ser colado porque ultrapassa o limite permitido',
Selected: 'Selecionado: ',
title: 'Estatísticas'
});

@ -0,0 +1,10 @@
CKEDITOR.plugins.setLang('wordcount', 'pt', {
WordCount: 'Palavras:',
CharCount: 'Caracteres:',
CharCountWithHTML: 'Carateres (incluindo HTML):',
Paragraphs: 'Parágrafos:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'O conteúdo não pode ser colado porque ultrapassa o limite permitido',
Selected: 'Selecionado: ',
title: 'Estatísticas'
});

@ -0,0 +1,15 @@
// Romanian Translation by Bogdanov Mihail
CKEDITOR.plugins.setLang('wordcount', 'ro', {
WordCount: 'Numar cuvinte',
WordCountRemaining: 'Cuvinte ramase',
CharCount: 'Numar caracter:',
CharCountRemaining: 'Caractere ramase:',
CharCountWithHTML: 'Numar caractere (cu HTML):',
CharCountWithHTMLRemaining: 'Caractere (cu HTML) ramase',
Paragraphs: 'Paragrafe:',
ParagraphsRemaining: 'Paragrafe ramase',
pasteWarning: 'Continutul nu poate fi adaugat deoarece este mai mare decat limita setata',
Selected: 'Selectat:',
title: 'Statistici'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'ru', {
WordCount: 'Слов:',
CharCount: 'Символов:',
CharCountWithHTML: ' (включая HTML-разметку):',
Paragraphs: 'Параграфов:',
ParagraphsRemaining: 'Параграфов осталось',
pasteWarning: 'Контент не может быть вставлен, т.к. привышает допустимый лимит',
Selected: 'Выделено: ',
title: 'Статистика'
});

@ -0,0 +1,15 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'sk', {
WordCount: 'Slov:',
CharCount: 'Znakov:',
CharCountWithHTML: 'Znakov (vrátane HTML):',
Paragraphs: 'Odstavcov:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Obsah sa nedá prilepiť.',
Selected: 'Výber: ',
title: 'Štatistika'
});

@ -0,0 +1,15 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'sv', {
WordCount: 'Ord:',
CharCount: 'Tecken:',
CharCountRemaining: 'tecken återstår',
CharCountWithHTML: 'Tecken (inklusive HTML):',
Paragraphs: 'Paragraf:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Innehåll kan inte klistras in eftersom det överskrider den tillåtna gränsen',
Selected: 'Valt: ',
title: 'Statistik'
});

@ -0,0 +1,14 @@
/*
Mesut ÇAKIR
mesut.cakir@hotmail.com.tr
*/
CKEDITOR.plugins.setLang('wordcount', 'tr', {
WordCount: 'Kelime:',
CharCount: 'Karakter:',
CharCountWithHTML: 'Karakter (HTML dahil):',
Paragraphs: 'Paragraf:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: 'Content can not be pasted because it is above the allowed limit',
Selected: 'Selected: ',
title: 'İstatistik'
});

@ -0,0 +1,17 @@
/*
Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'uk', {
WordCount: 'Слів:',
WordCountRemaining: 'Слів залишилося',
CharCount: 'Символів:',
CharCountRemaining: 'Символів залишилося',
CharCountWithHTML: 'Символів (включаючи HTML-розмітку):',
CharCountWithHTMLRemaining: 'Символів (включаючи HTML-розмітку) залишилося',
Paragraphs: 'Параграфів:',
ParagraphsRemaining: 'Параграфів залишилося',
pasteWarning: 'Контент не може бути вставлено, оскільки перевищує допустимий ліміт',
Selected: 'Виділено: ',
title: 'Статистика'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'zh-cn', {
WordCount: '词数:',
CharCount: '字符:',
CharCountWithHTML: '字符 (含HTML)',
Paragraphs: '段落:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: '由于上限允许,内容不能粘贴',
Selected: '已选择: ',
title: '统计'
});

@ -0,0 +1,14 @@
/*
Copyright (c) 2003-2012, CKSource - Frederico Knabben. All rights reserved.
For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.setLang('wordcount', 'zh', {
WordCount: '詞數:',
CharCount: '字數:',
CharCountWithHTML: '字數 (含HTML)',
Paragraphs: '段落:',
ParagraphsRemaining: 'Paragraphs remaining',
pasteWarning: '由於字數達到上限,內容不能粘貼',
Selected: '已選擇: ',
title: '統計'
});

@ -0,0 +1,330 @@
/**
* @license Copyright (c) CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.html or http://ckeditor.com/license
*/
CKEDITOR.plugins.add("wordcount",
{
lang: "ar,bg,ca,cs,da,de,el,en,es,eu,fa,fi,fr,he,hr,hu,it,ko,ja,nl,no,pl,pt,pt-br,ru,sk,sv,tr,uk,zh-cn,zh,ro", // %REMOVE_LINE_CORE%
version: "1.17.6",
requires: 'htmlwriter,notification,undo',
bbcodePluginLoaded: false,
onLoad: function() {
CKEDITOR.document.appendStyleSheet(this.path + "css/wordcount.css");
},
init: function(editor) {
var defaultFormat = "",
lastWordCount = -1,
lastCharCount = -1,
lastParagraphs = -1,
timeoutId = 0,
notification = null;
// Default Config
var defaultConfig = {
showRemaining: false,
showParagraphs: true,
showWordCount: true,
showCharCount: false,
countBytesAsChars: false,
countSpacesAsChars: false,
countHTML: false,
countLineBreaks: false,
hardLimit: true,
warnOnLimitOnly: false,
//MAXLENGTH Properties
maxWordCount: -1,
maxCharCount: -1,
maxParagraphs: -1,
// Filter
filter: null,
};
// Get Config & Lang
var config = CKEDITOR.tools.extend(defaultConfig, editor.config.wordcount || {}, true);
if (config.showParagraphs) {
if (config.maxParagraphs > -1) {
if (config.showRemaining) {
defaultFormat += "%paragraphsCount% " + editor.lang.wordcount.ParagraphsRemaining;
} else {
defaultFormat += editor.lang.wordcount.Paragraphs + " %paragraphsCount%";
defaultFormat += "/" + config.maxParagraphs;
}
} else {
defaultFormat += editor.lang.wordcount.Paragraphs + " %paragraphsCount%";
}
}
if (config.showParagraphs && (config.showWordCount || config.showCharCount)) {
defaultFormat += ", ";
}
if (config.showWordCount) {
if (config.maxWordCount > -1) {
if (config.showRemaining) {
defaultFormat += "%wordCount% " + editor.lang.wordcount.WordCountRemaining;
} else {
defaultFormat += editor.lang.wordcount.WordCount + " %wordCount%";
defaultFormat += "/" + config.maxWordCount;
}
} else {
defaultFormat += editor.lang.wordcount.WordCount + " %wordCount%";
}
}
if (config.showCharCount && config.showWordCount) {
defaultFormat += ", ";
}
if (config.showCharCount) {
if (config.maxCharCount > -1) {
if (config.showRemaining) {
defaultFormat += "%charCount% " +
editor.lang.wordcount[config.countHTML
? "CharCountWithHTMLRemaining"
: "CharCountRemaining"];
} else {
defaultFormat += editor.lang.wordcount[config.countHTML
? "CharCountWithHTML"
: "CharCount"] +
" %charCount%";
defaultFormat += "/" + config.maxCharCount;
}
} else {
defaultFormat += editor.lang.wordcount[config.countHTML ? "CharCountWithHTML" : "CharCount"] +
" %charCount%";
}
}
var format = defaultFormat;
bbcodePluginLoaded = typeof editor.plugins.bbcode != 'undefined';
function strip(html) {
if (bbcodePluginLoaded) {
// stripping out BBCode tags [...][/...]
return html.replace(/\[.*?\]/gi, '');
}
var tmp = document.createElement("div");
// Add filter before strip
html = filter(html);
tmp.innerHTML = html;
if (tmp.textContent == "" && typeof tmp.innerText == "undefined") {
return "";
}
return tmp.textContent || tmp.innerText;
}
/**
* Implement filter to add or remove before counting
* @param html
* @returns string
*/
function filter(html) {
if (config.filter instanceof CKEDITOR.htmlParser.filter) {
var fragment = CKEDITOR.htmlParser.fragment.fromHtml(html),
writer = new CKEDITOR.htmlParser.basicWriter();
config.filter.applyTo(fragment);
fragment.writeHtml(writer);
return writer.getHtml();
}
return html;
}
function countCharacters(text) {
if (config.countHTML) {
return config.countBytesAsChars ? countBytes(filter(text)) : filter(text).length;
}
var normalizedText;
// strip body tags
if (editor.config.fullPage) {
var i = text.search(new RegExp("<body>", "i"));
if (i != -1) {
var j = text.search(new RegExp("</body>", "i"));
text = text.substring(i + 6, j);
}
}
normalizedText = text;
if (!config.countSpacesAsChars) {
normalizedText = text.replace(/\s/g, "").replace(/&nbsp;/g, "");
}
if (config.countLineBreaks) {
normalizedText = normalizedText.replace(/(\r\n|\n|\r)/gm, " ");
} else {
normalizedText = normalizedText.replace(/(\r\n|\n|\r)/gm, "").replace(/&nbsp;/gi, " ");
}
normalizedText = strip(normalizedText).replace(/^([\t\r\n]*)$/, "");
return config.countBytesAsChars ? countBytes(normalizedText) : normalizedText.length;
}
function countBytes(text) {
var count = 0, stringLength = text.length, i;
text = String(text || "");
for (i = 0; i < stringLength; i++) {
var partCount = encodeURI(text[i]).split("%").length;
count += partCount == 1 ? 1 : partCount - 1;
}
return count;
}
function countParagraphs(text) {
return (text.replace(/&nbsp;/g, " ").replace(/(<([^>]+)>)/ig, "").replace(/^\s*$[\n\r]{1,}/gm, "++")
.split("++").length);
}
function countWords(text) {
var normalizedText = text.replace(/(\r\n|\n|\r)/gm, " ").replace(/^\s+|\s+$/g, "")
.replace("&nbsp;", " ");
normalizedText = strip(normalizedText);
var words = normalizedText.split(/\s+/);
for (var wordIndex = words.length - 1; wordIndex >= 0; wordIndex--) {
if (words[wordIndex].match(/^([\s\t\r\n]*)$/)) {
words.splice(wordIndex, 1);
}
}
return (words.length);
}
function updateCounter(editorInstance) {
var paragraphs = 0,
wordCount = 0,
charCount = 0,
text;
// BeforeGetData and getData events are fired when calling
// getData(). We can prevent this by passing true as an
// argument to getData(). This allows us to fire the events
// manually with additional event data: firedBy. This additional
// data helps differentiate calls to getData() made by
// wordCount plugin from calls made by other plugins/code.
editorInstance.fire("beforeGetData", { firedBy: "wordCount.updateCounter" }, editor);
text = editorInstance.getData(true);
editorInstance.fire("getData", { dataValue: text, firedBy: "wordCount.updateCounter" }, editor);
if (text) {
if (config.showCharCount) {
charCount = countCharacters(text);
}
if (config.showParagraphs) {
paragraphs = countParagraphs(text);
}
if (config.showWordCount) {
wordCount = countWords(text);
}
}
var html = format;
if (config.showRemaining) {
if (config.maxCharCount >= 0) {
html = html.replace("%charCount%", config.maxCharCount - charCount);
} else {
html = html.replace("%charCount%", charCount);
}
if (config.maxWordCount >= 0) {
html = html.replace("%wordCount%", config.maxWordCount - wordCount);
} else {
html = html.replace("%wordCount%", wordCount);
}
if (config.maxParagraphs >= 0) {
html = html.replace("%paragraphsCount%", config.maxParagraphs - paragraphs);
} else {
html = html.replace("%paragraphsCount%", paragraphs);
}
} else {
html = html.replace("%wordCount%", wordCount).replace("%charCount%", charCount).replace("%paragraphsCount%", paragraphs);
}
(editorInstance.config.wordcount || (editorInstance.config.wordcount = {})).wordCount = wordCount;
(editorInstance.config.wordcount || (editorInstance.config.wordcount = {})).charCount = charCount;
if (charCount == lastCharCount && wordCount == lastWordCount && paragraphs == lastParagraphs) {
if (charCount == config.maxCharCount || wordCount == config.maxWordCount || paragraphs > config.maxParagraphs) {
editorInstance.fire('saveSnapshot');
}
return true;
}
//If the limit is already over, allow the deletion of characters/words. Otherwise,
//the user would have to delete at one go the number of offending characters
var deltaWord = wordCount - lastWordCount;
var deltaChar = charCount - lastCharCount;
var deltaParagraphs = paragraphs - lastParagraphs;
lastWordCount = wordCount;
lastCharCount = charCount;
lastParagraphs = paragraphs;
if (lastWordCount == -1) {
lastWordCount = wordCount;
}
if (lastCharCount == -1) {
lastCharCount = charCount;
}
if (lastParagraphs == -1) {
lastParagraphs = paragraphs;
}
// update instance
editorInstance.wordCount =
{
paragraphs: paragraphs,
wordCount: wordCount,
charCount: charCount
};
editor.fire('cp-wc-update');
return true;
}
function isCloseToLimits() {
if (config.maxWordCount > -1 && config.maxWordCount - lastWordCount < 5) {
return true;
}
if (config.maxCharCount > -1 && config.maxCharCount - lastCharCount < 20) {
return true;
}
if (config.maxParagraphs > -1 && config.maxParagraphs - lastParagraphs < 1) {
return true;
}
return false;
}
editor.on('cp-wc', function(event) {
clearTimeout(timeoutId);
timeoutId = setTimeout(
updateCounter.bind(this, event.editor),
250
);
}, editor, null, 250);
}
});

@ -368,6 +368,7 @@ define([
var content = [];
APP.module.execCommand('LIST_TEAMS', null, function (obj) {
if (!obj) { return; }
if (obj.error === "OFFLINE") { return UI.alert(Messages.driveOfflineError); }
if (obj.error) { return void console.error(obj.error); }
var list = [];
var keys = Object.keys(obj).slice(0,3);
@ -453,6 +454,7 @@ define([
name: name
}, function (obj) {
if (obj && obj.error) {
if (obj.error === "OFFLINE") { return UI.alert(Messages.driveOfflineError); }
console.error(obj.error);
$spinner.hide();
return void UI.warn(Messages.error);
@ -754,7 +756,7 @@ define([
});
var pending = Object.keys(roster).filter(function (k) {
if (!roster[k].pending) { return; }
return roster[k].role === "MEMBER" || !roster[k].role;
return roster[k].role === "MEMBER" || roster[k].role === "VIEWER" || !roster[k].role;
}).map(function (k) {
return makeMember(common, roster[k], me);
});
@ -1125,10 +1127,10 @@ define([
toolbar.failed();
if (!noAlert) { UI.alert(Messages.common_connectionLost, undefined, true); }
};
var onReconnect = function (info) {
var onReconnect = function () {
setEditable(true);
if (APP.team && driveAPP.refresh) { driveAPP.refresh(); }
toolbar.reconnecting(info.myId);
toolbar.reconnecting();
UI.findOKButton().click();
};
@ -1138,9 +1140,8 @@ define([
sframeChan.on('EV_NETWORK_DISCONNECT', function () {
onDisconnect();
});
sframeChan.on('EV_NETWORK_RECONNECT', function (data) {
// data.myId;
onReconnect(data);
sframeChan.on('EV_NETWORK_RECONNECT', function () {
onReconnect();
});
common.onLogout(function () { setEditable(false); });
});

@ -69,14 +69,21 @@ define([
Cryptpad.onNetworkDisconnect.reg(function () {
sframeChan.event('EV_NETWORK_DISCONNECT');
});
Cryptpad.onNetworkReconnect.reg(function (data) {
sframeChan.event('EV_NETWORK_RECONNECT', data);
Cryptpad.onNetworkReconnect.reg(function () {
sframeChan.event('EV_NETWORK_RECONNECT');
});
Cryptpad.universal.onEvent.reg(function (obj) {
// Intercept events for the team drive and send them the required way
if (obj.type !== 'team' ||
['DRIVE_CHANGE', 'DRIVE_LOG', 'DRIVE_REMOVE'].indexOf(obj.data.ev) === -1) { return; }
sframeChan.event('EV_'+obj.data.ev, obj.data.data);
if (obj.type !== 'team') { return; }
if (['DRIVE_CHANGE', 'DRIVE_LOG', 'DRIVE_REMOVE'].indexOf(obj.data.ev) !== -1) {
sframeChan.event('EV_'+obj.data.ev, obj.data.data);
}
if (obj.data.ev === 'NETWORK_RECONNECT') {
sframeChan.event('EV_NETWORK_RECONNECT');
}
if (obj.data.ev === 'NETWORK_DISCONNECT') {
sframeChan.event('EV_NETWORK_DISCONNECT');
}
});
};
SFCommonO.start({

Loading…
Cancel
Save