From 2d30393243740de3459806f6b20a65b9e84ae2db Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 23 Feb 2017 11:45:00 +0100 Subject: [PATCH 1/6] Add support for updated translation key --- customize.dist/messages.js | 23 +++++++++++++++++----- customize.dist/translations/messages.es.js | 3 ++- customize.dist/translations/messages.fr.js | 3 ++- customize.dist/translations/messages.js | 4 ++-- www/assert/main.js | 16 --------------- www/assert/translations/main.js | 8 +++++--- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/customize.dist/messages.js b/customize.dist/messages.js index fb613d591..629a87054 100644 --- a/customize.dist/messages.js +++ b/customize.dist/messages.js @@ -50,9 +50,26 @@ define(req, function(Default, Language) { var langs = arguments; Object.keys(externalMap).forEach(function (code, i) { var translation = langs[i]; + var updated = {}; + Object.keys(Default).forEach(function (k) { + if (/^updated_[0-9]+_/.test(k) && !translation[k]) { + var key = k.split('_').slice(2).join('_'); + // Make sure we don't already have an update for that key. It should not happen + // but if it does, keep the latest version + if (updated[key]) { + var ek = updated[key]; + if (parseInt(ek.split('_')[1]) > parseInt(k.split('_')[1])) { return; } + } + updated[key] = k; + } + }); Object.keys(Default).forEach(function (k) { if (/^_/.test(k)) { return; } - if (!translation[k]) { + if (!translation[k] || updated[k]) { + if (updated[k]) { + missing.push([code, k, 2, 'out.' + updated[k]]); + return; + } missing.push([code, k, 1]); } }); @@ -62,10 +79,6 @@ define(req, function(Default, Language) { missing.push([code, k, 0]); } }); - /*if (typeof(translation._languageName) !== 'string') { - var warning = 'key [_languageName] is missing from translation [' + code + ']'; - missing.push(warning); - }*/ }); cb(missing); }); diff --git a/customize.dist/translations/messages.es.js b/customize.dist/translations/messages.es.js index d9f6a8544..c24e1df7c 100644 --- a/customize.dist/translations/messages.es.js +++ b/customize.dist/translations/messages.es.js @@ -12,7 +12,8 @@ define(function () { out.type.poll = 'Encuesta'; out.type.slide = 'Presentación'; - out.common_connectionLost = "Connexión perdida
El documento está ahora en modo solo lectura hasta que la conexión vuelva."; + out.updated_0_common_connectionLost = "Connexión perdida
El documento está ahora en modo solo lectura hasta que la conexión vuelva."; + out.common_connectionLost = out.updated_0_common_connectionLost; out.disconnected = "Desconectado"; out.synchronizing = "Sincronización"; diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 1ad5e907d..b96d497d3 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -16,7 +16,8 @@ define(function () { out.button_newpoll = 'Nouveau sondage'; out.button_newslide = 'Nouvelle présentation'; - out.common_connectionLost = "Connexion au serveur perdue
Vous êtes désormais en mode lecture seule jusqu'au retour de la connexion."; + out.updated_0_common_connectionLost = "Connexion au serveur perdue
Vous êtes désormais en mode lecture seule jusqu'au retour de la connexion."; + out.common_connectionLost = out.updated_0_common_connectionLost; out.websocketError = 'Impossible de se connecter au serveur WebSocket...'; out.typeError = "Ce document temps-réel n'est pas compatible avec l'application sélectionnée"; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 30b051634..abfec9239 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -18,8 +18,8 @@ define(function () { // NOTE: We want to update the 'common_connectionLost' key. // Please do not add a new 'updated_common_connectionLostAndInfo' but change directly the value of 'common_connectionLost' - out.updated_common_connectionLostAndInfo = "Server Connection Lost
You're now in read-only mode until the connection is back."; - out.common_connectionLost = out.updated_common_connectionLostAndInfo; + out.updated_0_common_connectionLost = "Server Connection Lost
You're now in read-only mode until the connection is back."; + out.common_connectionLost = out.updated_0_common_connectionLost; out.websocketError = 'Unable to connect to the websocket server...'; out.typeError = "That realtime document is not compatible with the selected application"; diff --git a/www/assert/main.js b/www/assert/main.js index 51bdf00a8..b6a1fbd45 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -141,22 +141,6 @@ define([ strungJSON(orig); }); - assert(function () { - var todo = function (missing) { - if (missing.length !== 0) { - missing.forEach(function (msg) { - console.log('* ' + msg); - }); - - // No, this is crappy, it's going to cause tests to fail basically all of the time. - //return false; - } - }; - Cryptpad.Messages._checkTranslationState(todo); - - return true; - }, "expected all translation keys in default language to be present in all translations. See console for details."); - var swap = function (str, dict) { return str.replace(/\{\{(.*?)\}\}/g, function (all, key) { return typeof dict[key] !== 'undefined'? dict[key] : all; diff --git a/www/assert/translations/main.js b/www/assert/translations/main.js index 1b1683afe..e8825d422 100644 --- a/www/assert/translations/main.js +++ b/www/assert/translations/main.js @@ -21,6 +21,7 @@ define([ var code = msg[0]; var key = msg[1]; var needed = msg[2]; + var value = msg[3] || '""'; if (str !== code) { if (str !== "") @@ -38,10 +39,11 @@ define([ } } - res += (need ? '' : '// ') + 'out.' + key + ' = "";'; - if (need) - { + res += (need ? '' : '// ') + 'out.' + key + ' = ' + value + ';'; + if (need === 1) { res += ' // ' + JSON.stringify(English[key]); + } else if (need === 2) { + res += ' // TODO: Key updated --> make sure the updated key "'+ value +'" exists and is translated before that one.'; } return res; }).join('\n'))); From a0340f141917ff382b7a5af73c0edc7b96509163 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 23 Feb 2017 14:27:18 +0100 Subject: [PATCH 2/6] Open link in a new tab in /pad --- www/pad/links.js | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ www/pad/main.js | 4 +++- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 www/pad/links.js diff --git a/www/pad/links.js b/www/pad/links.js new file mode 100644 index 000000000..ed369c506 --- /dev/null +++ b/www/pad/links.js @@ -0,0 +1,58 @@ +define(function () { + // Adds a context menu entry to open the selected link in a new tab. + // See https://github.com/xwiki-contrib/application-ckeditor/commit/755d193497bf23ed874d874b4ae92fbee887fc10 + return { + addSupportForOpeningLinksInNewTab : function (Ckeditor) { + 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: 'Open Link in New Tab', + command: 'openLink', + group: 'link', + order: -1 + }); + } + if (editor.contextMenu) { + editor.contextMenu.addListener(function(startElement, selection, path) { + 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') + '}'); + } + // 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; + }; + }; + } + }; +}); \ No newline at end of file diff --git a/www/pad/main.js b/www/pad/main.js index 7b3fbf4b8..f105e7582 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -12,12 +12,13 @@ define([ '/common/cryptpad-common.js', '/common/visible.js', '/common/notify.js', + '/pad/links.js', '/bower_components/file-saver/FileSaver.min.js', '/bower_components/diff-dom/diffDOM.js', '/bower_components/jquery/dist/jquery.min.js', ], function (Crypto, realtimeInput, Hyperjson, Toolbar, Cursor, JsonOT, TypingTest, JSONSortify, TextPatcher, Cryptpad, - Visible, Notify) { + Visible, Notify, Links) { var $ = window.jQuery; var saveAs = window.saveAs; var Messages = Cryptpad.Messages; @@ -102,6 +103,7 @@ define([ customConfig: '/customize/ckeditor-config.js', }); + editor.on('instanceReady', Links.addSupportForOpeningLinksInNewTab(Ckeditor)); editor.on('instanceReady', function (Ckeditor) { var $bar = $('#pad-iframe')[0].contentWindow.$('#cke_1_toolbox'); var parsedHash = Cryptpad.parsePadUrl(window.location.href); From d8cc2903cc01b1e767ae2a2dbff49db07a8ffa19 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 23 Feb 2017 17:25:25 +0100 Subject: [PATCH 3/6] Display context menu actions in the toolbar when an element is selected --- www/drive/file.css | 7 ++ www/drive/file.less | 7 ++ www/drive/inner.html | 34 +++---- www/drive/main.js | 231 ++++++++++++++++++++++++++----------------- 4 files changed, 171 insertions(+), 108 deletions(-) diff --git a/www/drive/file.css b/www/drive/file.css index bc9755009..51739a13c 100644 --- a/www/drive/file.css +++ b/www/drive/file.css @@ -406,3 +406,10 @@ span.fa-folder-open { background: #fff; border: 1px solid #888; } +#driveToolbar #contextButtonsContainer { + float: right; + margin: 5px; +} +#driveToolbar #contextButtonsContainer button { + vertical-align: top; +} diff --git a/www/drive/file.less b/www/drive/file.less index f2f3a5ea6..29dbca40c 100644 --- a/www/drive/file.less +++ b/www/drive/file.less @@ -465,6 +465,13 @@ span { } } } + #contextButtonsContainer { + float: right; + margin: 5px; + button { + vertical-align: top; + } + } } diff --git a/www/drive/inner.html b/www/drive/inner.html index 9f337f5ff..93b06e211 100644 --- a/www/drive/inner.html +++ b/www/drive/inner.html @@ -19,39 +19,39 @@ diff --git a/www/drive/main.js b/www/drive/main.js index 8bb9d1852..4c5e667bc 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -281,56 +281,81 @@ define([ // Replace a file/folder name by an input to change its value var displayRenameInput = function ($element, path) { - if (!APP.editable) { return; } - if (!path || path.length < 2) { - logError("Renaming a top level element (root, trash or filesData) is forbidden."); - return; - } - removeInput(); - removeSelected(); - var $name = $element.find('.name'); - if (!$name.length) { - $name = $element.find('.element'); - } - $name.hide(); - var name = path[path.length - 1]; - var $input = $('', { - placeholder: name, - value: name - }); - $input.on('keyup', function (e) { - if (e.which === 13) { - removeInput(); - filesOp.renameElement(path, $input.val(), function () { - refresh(); - }); + // NOTE: setTimeout(f, 0) otherwise the "rename" button in the toolbar is not working + window.setTimeout(function () { + if (!APP.editable) { return; } + if (!path || path.length < 2) { + logError("Renaming a top level element (root, trash or filesData) is forbidden."); + return; } - }); - //$element.parent().append($input); - $name.after($input); - $input.focus(); - $input.select(); - // We don't want to open the file/folder when clicking on the input - $input.on('click dblclick', function (e) { + removeInput(); removeSelected(); - e.stopPropagation(); - }); - // Remove the browser ability to drag text from the input to avoid - // triggering our drag/drop event handlers - $input.on('dragstart dragleave drag drop', function (e) { - e.preventDefault(); - e.stopPropagation(); - }); - // Make the parent element non-draggable when selecting text in the field - // since it would remove the input - $input.on('mousedown', function (e) { - e.stopPropagation(); - $input.parents('li').attr("draggable", false); - }); - $input.on('mouseup', function (e) { - e.stopPropagation(); - $input.parents('li').attr("draggable", true); - }); + var $name = $element.find('.name'); + if (!$name.length) { + $name = $element.find('.element'); + } + $name.hide(); + var name = path[path.length - 1]; + var $input = $('', { + placeholder: name, + value: name + }); + $input.on('keyup', function (e) { + if (e.which === 13) { + removeInput(); + filesOp.renameElement(path, $input.val(), function () { + refresh(); + }); + } + }); + //$element.parent().append($input); + $name.after($input); + $input.focus(); + $input.select(); + // We don't want to open the file/folder when clicking on the input + $input.on('click dblclick', function (e) { + removeSelected(); + e.stopPropagation(); + }); + // Remove the browser ability to drag text from the input to avoid + // triggering our drag/drop event handlers + $input.on('dragstart dragleave drag drop', function (e) { + e.preventDefault(); + e.stopPropagation(); + }); + // Make the parent element non-draggable when selecting text in the field + // since it would remove the input + $input.on('mousedown', function (e) { + e.stopPropagation(); + $input.parents('li').attr("draggable", false); + }); + $input.on('mouseup', function (e) { + e.stopPropagation(); + $input.parents('li').attr("draggable", true); + }); + },0); + }; + + var filterContextMenu = function ($menu, $element) { + var path = $element.data('path'); + + var hide = []; + if (!APP.editable) { + hide.push($menu.find('a.editable')); + } + if (!isOwnDrive()) { + hide.push($menu.find('a.own')); + } + if ($element.is('.file-element')) { + hide.push($menu.find('a.newfolder')); + } else { + hide.push($menu.find('a.open_ro')); + } + if (path && path.length > 4) { + hide.push($menu.find('a.restore')); + hide.push($menu.find('a.properties')); + } + return hide; }; var updateContextButton = function () { @@ -339,21 +364,54 @@ define([ $li = $tree.find('.element.active').closest('li'); } var $button = $driveToolbar.find('#contextButton'); - if ($li.length !== 1 - || !$._data($li[0], 'events').contextmenu - || $._data($li[0], 'events').contextmenu.length === 0) { - $button.hide(); + if ($button.length) { // mobile + if ($li.length !== 1 + || !$._data($li[0], 'events').contextmenu + || $._data($li[0], 'events').contextmenu.length === 0) { + $button.hide(); + return; + } + $button.show(); + $button.css({ + background: '#000' + }); + window.setTimeout(function () { + $button.css({ + background: '' + }); + }, 500); return; } - $button.show(); - $button.css({ - background: '#000' + // Non mobile + var $container = $driveToolbar.find('#contextButtonsContainer'); + if (!$container.length) { return; } + $container.html(''); + var $element = $li; + var $menu = $element.data('context'); + var path = $element.data('path'); + if (!$menu || !path) { return; } + var actions = []; + var $actions = $menu.find('a'); + var toHide = filterContextMenu($menu, $element); + $actions = $actions.filter(function (i, el) { + for (var j = 0; j < toHide.length; j++) { + if ($(el).is(toHide[j])) { return false; }; + } + return true; + }); + $actions.each(function (i, el) { + var $a = $('