diff --git a/customize.dist/loading.js b/customize.dist/loading.js index e8f8a9453..765ec56f9 100644 --- a/customize.dist/loading.js +++ b/customize.dist/loading.js @@ -169,6 +169,28 @@ define([], function () { height: 100%; background: #5cb85c; } + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(1800deg); + } +} + +.cp-spinner { + display: inline-block; + box-sizing: border-box; + width: 80px; + height: 80px; + border: 11px solid lightgrey; + border-radius: 50%; + border-top-color: transparent; + animation: spin infinite 3s; + animation-timing-function: cubic-bezier(.6,0.15,0.4,0.85); +} + */}).toString().slice(14, -3); var urlArgs = window.location.href.replace(/^.*\?([^\?]*)$/, function (all, x) { return x; }); var elem = document.createElement('div'); @@ -182,7 +204,7 @@ define([], function () { '', '
)?\[[xX]\](<\/p>)?\s*/; @@ -138,6 +142,13 @@ define([ var cls = (isCheckedTaskItem || isUncheckedTaskItem || hasBogusInput) ? ' class="todo-list-item"' : ''; return '
' + p + '
\n'; + }; renderer.paragraph = function (p) { if (p === '[TOC]') { return ''; } - return /' + p + '
\n'; + return renderParagraph(p); + }; + restrictedRenderer.paragraph = function (p) { + return renderParagraph(p); }; var MutationObserver = window.MutationObserver; diff --git a/www/common/media-tag.js b/www/common/media-tag.js index c820d9201..186fe4632 100644 --- a/www/common/media-tag.js +++ b/www/common/media-tag.js @@ -30,9 +30,22 @@ }; + var isplainTextFile = function (metadata) { + // does its type begins with "text/" + if (metadata.type.indexOf("text/") === 0) { return true; } + // no type and no file extension -> let's guess it's plain text + var parsedName = /^(\.?.+?)(\.[^.]+)?$/.exec(metadata.name) || []; + if (!metadata.type && !parsedName[2]) { return true; } + // other exceptions + if (metadata.type === 'application/x-javascript') { return true; } + if (metadata.type === 'application/xml') { return true; } + return false; + }; + // Default config, can be overriden per media-tag call var config = { allowed: [ + 'text/plain', 'image/png', 'image/jpeg', 'image/jpg', @@ -53,6 +66,23 @@ text: "Download" }, Plugins: { + /** + * @param {object} metadataObject {name, metadatatype, owners} containing metadata of the file + * @param {strint} url Url of the blob object + * @param {Blob} content Blob object containing the data of the file + * @param {object} cfg Object {Plugins, allowed, download, pdf} containing infos about plugins + * @param {function} cb Callback function: (err, pluginElement) => {} + */ + text: function (metadata, url, content, cfg, cb) { + var plainText = document.createElement('div'); + plainText.className = "plain-text-reader"; + var reader = new FileReader(); + reader.addEventListener('loadend', function (e) { + plainText.innerText = e.srcElement.result; + cb(void 0, plainText); + }); + reader.readAsText(content); + }, image: function (metadata, url, content, cfg, cb) { var img = document.createElement('img'); img.setAttribute('src', url); @@ -271,6 +301,9 @@ var blob = decrypted.content; var mediaType = getType(mediaObject, metadata, cfg); + if (isplainTextFile(metadata)) { + mediaType = "text"; + } if (mediaType === 'application') { mediaType = mediaObject.extension; diff --git a/www/common/messenger-ui.js b/www/common/messenger-ui.js index 682535d98..223f67bb3 100644 --- a/www/common/messenger-ui.js +++ b/www/common/messenger-ui.js @@ -151,7 +151,7 @@ define([ }); try { var $d = $(d); - DiffMd.apply(DiffMd.render(md || '', true), $d, common); + DiffMd.apply(DiffMd.render(md || '', true, true), $d, common); $d.addClass("cp-app-contacts-content"); // override link clicking, because we're in an iframe @@ -197,7 +197,7 @@ define([ var getChat = function (id) { return $messages.find(dataQuery(id)); }; - + var scrollChatToBottom = function () { var $messagebox = $('.cp-app-contacts-messages'); $messagebox.scrollTop($messagebox[0].scrollHeight); diff --git a/www/common/metadata-manager.js b/www/common/metadata-manager.js index 2b504d39a..02d516ff7 100644 --- a/www/common/metadata-manager.js +++ b/www/common/metadata-manager.js @@ -99,6 +99,7 @@ define(['json.sortify'], function (Sortify) { var addAuthor = function () { if (!meta.user || !meta.user.netfluxId || !priv || !priv.edPublic) { return; } var authors = metadataObj.authors || {}; + var old = Sortify(authors); if (!authors[priv.edPublic]) { authors[priv.edPublic] = { nId: [meta.user.netfluxId], @@ -110,9 +111,11 @@ define(['json.sortify'], function (Sortify) { authors[priv.edPublic].nId.push(meta.user.netfluxId); } } - metadataObj.authors = authors; - metadataLazyObj.authors = JSON.parse(JSON.stringify(authors)); - change(); + if (Sortify(authors) !== old) { + metadataObj.authors = authors; + metadataLazyObj.authors = JSON.parse(JSON.stringify(authors)); + change(); + } }; var netfluxId; diff --git a/www/common/modes.js b/www/common/modes.js index ffed08b17..6d005da30 100644 --- a/www/common/modes.js +++ b/www/common/modes.js @@ -6,141 +6,141 @@ define([ // mode language (extension) var list = Modes.list = [ "APL apl .apl", - "ASCII-Armor asciiarmor", - "ASN.1 asn.1", + "ASCII-Armor asciiarmor .asc", + "ASN.1 asn.1 .asn1", "Asterisk asterisk", "Brainfuck brainfuck .b", "C text/x-csrc .c", "C text/x-c++src .cpp", - "C-like clike", - "Clojure clojure", - "CMake cmake", - "COBOL cobol", - "CoffeeScript coffeescript", - "Common_Lisp commonlisp", - "Crystal crystal", + "C-like clike .c", + "Clojure clojure .clj", + "CMake cmake _", /* no extension */ + "COBOL cobol .cbl", + "CoffeeScript coffeescript .coffee", + "Common_Lisp commonlisp .lisp", + "Crystal crystal .cr", "CSS css .css", - "Cypher cypher", - "D d", - "Dart dart", - "Diff diff", - "Django django", - "Dockerfile dockerfile", - "DTD dtd", - "Dylan dylan", - "EBNF ebnf", - "ECL ecl", - "Eiffel eiffel", + "Cypher cypher .cypher", + "D d .d", + "Dart dart .dart", + "Diff diff .diff", + "Django django .py", + "Dockerfile dockerfile _", /* no extension */ + "DTD dtd .dtd", + "Dylan dylan .dylan", + "EBNF ebnf .ebnf", + "ECL ecl .ecl", + "Eiffel eiffel .e", "Elm elm .elm", - "Erlang erlang", - "Factor factor", - "FCL fcl", - "Forth forth", - "Fortran fortran", - "GAS gas", - "Gherkin gherkin", - "Go go", - "Groovy groovy", - "Haml haml", - "Handlebars handlebars", + "Erlang erlang .erl", + "Factor factor .factor", + "FCL fcl .fcl", + "Forth forth .fs", + "Fortran fortran .f90", + "GAS gas .gas", + "Gherkin gherkin .feature", + "Go go .go", + "Groovy groovy .groovy", + "Haml haml .haml", + "Handlebars handlebars .hbs", "Haskell haskell .hs", - "Haskell-Literate haskell-literate", - "Haxe haxe", + "Haskell-Literate haskell-literate .lhs", + "Haxe haxe .hx", "HTML htmlmixed .html", - "HTTP http", - "IDL idl", - "JADE jade", + "HTTP http _", /* no extension */ + "IDL idl .idl", + "JADE jade .jade", "Java text/x-java .java", "JavaScript javascript .js", - "Jinja2 jinja2", + "Jinja2 jinja2 .j2", "JSX jsx .jsx", - "Julia julia", - "LiveScript livescript", - "Lua lua", + "Julia julia .jl", + "LiveScript livescript .ls", + "Lua lua .lua", "Markdown gfm .md", //"markdown markdown .md", - "Mathematica mathematica", - "mIRC mirc", - "ML mllike", - "Modelica modelica", - "MscGen mscgen", - "MUMPS mumps", - "Nginx nginx", - "NSIS nsis", - "N-Triples ntriples", + "Mathematica mathematica .nb", + "mIRC mirc .irc", + "ML mllike _", /* no extension */ + "Modelica modelica .mo", + "MscGen mscgen .mscgen", + "MUMPS mumps .m", + "Nginx nginx .conf", + "NSIS nsis .nsi", + "N-Triples ntriples .nq", "Objective-C text/x-objectivec .m", - "Octave octave", + "Octave octave .m", "Org-mode orgmode .org", - "Oz oz", - "Pascal pascal", - "PEG.js pegjs", - "Perl perl", - "PHP php", - "Pig pig", - "PowerShell powershell", - "Properties properties", - "Protocol_Buffers protobuf", - "Puppet puppet", + "Oz oz .oz", + "Pascal pascal .pas", + "PEG.js pegjs .pegjs", + "Perl perl .pl", + "PHP php .php", + "Pig pig .pig", + "PowerShell powershell .ps1", + "Properties properties .properties", + "Protocol_Buffers protobuf .proto", + "Puppet puppet .pp", "Python python .py", - "Q q", - "R r", - "RPM rpm", - "RST rst", - "Ruby ruby", - "Rust rust", - "Sass sass", + "Q q .q", + "R r .r", + "RPM rpm .rpm", + "RST rst .rst", + "Ruby ruby .rb", + "Rust rust .rs", + "Sass sass .sass", "Scheme scheme .scm", "Shell shell .sh", - "Sieve sieve", - "Slim slim", - "Smalltalk smalltalk", - "Smarty smarty", - "Solr solr", - "Soy soy", - "SPARQL sparql", - "Spreadsheet spreadsheet", - "SQL sql", - "sTeX stex", - "Stylus stylus", - "Swift swift", - "Tcl tcl", + "Sieve sieve .sieve", + "Slim slim .slim", + "Smalltalk smalltalk _", /* no extension */ + "Smarty smarty _", /* no extension */ + "Solr solr _", /* no extension */ + "Soy soy .soy", + "SPARQL sparql .rq", + "Spreadsheet spreadsheet .xls", + "SQL sql .sql", + "sTeX stex .stex", + "Stylus stylus .styl", + "Swift swift .swift", + "Tcl tcl .tcl", "Text text .txt", - "Textile textile", - "TiddlyWiki tiddlywiki", - "Tiki tiki", - "TOML toml", - "Tornado tornado", - "troff troff", + "Textile textile .textile", + "TiddlyWiki tiddlywiki .tw", + "Tiki tiki _", /* no extension */ + "TOML toml .toml", + "Tornado tornado .tornado", + "troff troff .troff", "TTCN ttcn", "TTCN-cfg ttcn-cfg", - "Turtle turtle", - "Twig twig", - "Visual_Basic vb", - "VBScript vbscript", - "Velocity velocity", - "Verilog verilog", - "VHDL vhdl", - "Vue vue", - "XML xml", + "Turtle turtle .ttl", + "Twig twig .twig", + "Visual_Basic vb .vb", + "VBScript vbscript .vbs", + "Velocity velocity .vm", + "Verilog verilog .v", + "VHDL vhdl .vhdl", + "Vue vue .vue", + "XML xml .xml", //"xwiki xwiki21", - "XQuery xquery", + "XQuery xquery .xquery", "YAML yaml .yaml", - "YAML_Frontmatter yaml-frontmatter", - "Z80 z80" + "YAML_Frontmatter yaml-frontmatter _", /* no extension */ + "Z80 z80 .z80" ].map(function (line) { var kv = line.split(/\s/); return { language: kv[0].replace(/_/g, ' '), mode: kv[1], - ext: kv[2], + ext: kv[2] === '_' ? '' : kv[2], }; }); Modes.extensionOf = function (mode) { - var ext = ''; + var ext; list.some(function (o) { if (o.mode !== mode) { return; } - ext = o.ext || ''; + ext = o.ext; return true; }); return ext; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index ca7e4cd7c..a2e28f63b 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -34,7 +34,7 @@ define([ var sendDriveEvent = function () {}; var registerProxyEvents = function () {}; - var storeHash; + var storeHash, storeChannel; var store = window.CryptPad_AsyncStore = { modules: {} @@ -239,6 +239,20 @@ define([ Store.removeOwnedChannel = function (clientId, data, cb) { if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } + + // "data" used to be a string (channelID), now it can also be an object + // data.force tells us we can safely remove the drive ID + var channel = data; + var force = false; + if (data && typeof(data) === "object") { + channel = data.channel; + force = data.force; + } + + if (channel === storeChannel && !force) { + return void cb({error: 'User drive removal blocked!'}); + } + store.rpc.removeOwnedChannel(data, function (err) { cb({error:err}); }); @@ -573,7 +587,10 @@ define([ })); }).nThen(function (waitFor) { // Delete Drive - Store.removeOwnedChannel(clientId, secret.channel, waitFor()); + Store.removeOwnedChannel(clientId, { + channel: secret.channel + force: true + }, waitFor()); }).nThen(function () { store.network.disconnect(); cb({ @@ -786,6 +803,7 @@ define([ var h = p.hashData; if (AppConfig.disableAnonymousStore && !store.loggedIn) { return void cb(); } + if (p.type === "debug") { return void cb(); } var channelData = Store.channels && Store.channels[channel]; @@ -1915,6 +1933,7 @@ define([ } // No password for drive var secret = Hash.getSecrets('drive', hash); + storeChannel = secret.channel; var listmapConfig = { data: {}, websocketURL: NetConfig.getWebsocketURL(), diff --git a/www/common/sframe-app-framework.js b/www/common/sframe-app-framework.js index af856d1d7..e2a0bd0a2 100644 --- a/www/common/sframe-app-framework.js +++ b/www/common/sframe-app-framework.js @@ -401,7 +401,7 @@ define([ var ext = (typeof(extension) === 'function') ? extension() : extension; var suggestion = title.suggestTitle('cryptpad-document'); UI.prompt(Messages.exportPrompt, - Util.fixFileName(suggestion) + '.' + ext, function (filename) + Util.fixFileName(suggestion) + ext, function (filename) { if (!(typeof(filename) === 'string' && filename)) { return; } if (async) { @@ -454,7 +454,7 @@ define([ return; } if (!mediaTagEmbedder) { console.log('mediaTagEmbedder missing'); return; } - if (data.type !== 'file') { console.log('unhandled embed type ' + data.type); return; } + if (data.type !== 'file') { console.log('unhandled embed type ' + data.type); return; } var privateDat = cpNfInner.metadataMgr.getPrivateData(); var origin = privateDat.fileHost || privateDat.origin; var src = data.src = origin + data.src; diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index 8a0e2de63..f910f12b6 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -39,7 +39,8 @@ define([ }; module.getContentExtension = function (mode) { - return (Modes.extensionOf(mode) || '.txt').slice(1); + var ext = Modes.extensionOf(mode); + return ext !== undefined ? ext : '.txt'; }; module.fileExporter = function (content) { return new Blob([ content ], { type: 'text/plain;charset=utf-8' }); @@ -98,9 +99,17 @@ define([ // lines beginning with a hash are potentially valuable // works for markdown, python, bash, etc. var hash = /^#+(.*?)$/; + var hashAndLink = /^#+\s*\[(.*?)\]\(.*\)\s*$/; if (hash.test(line)) { + // test for link inside the title, and set text just to the name of the link + if (hashAndLink.test(line)) { + line.replace(hashAndLink, function (a, one) { + text = Util.stripTags(one); + }); + return true; + } line.replace(hash, function (a, one) { - text = one; + text = Util.stripTags(one); }); return true; } @@ -386,21 +395,32 @@ define([ exp.mkIndentSettings = function (metadataMgr) { var setIndentation = function (units, useTabs, fontSize, spellcheck) { if (typeof(units) !== 'number') { return; } + var doc = editor.getDoc(); editor.setOption('indentUnit', units); editor.setOption('tabSize', units); editor.setOption('indentWithTabs', useTabs); editor.setOption('spellcheck', spellcheck); - if (!useTabs) { - editor.setOption("extraKeys", { - Tab: function() { - editor.replaceSelection(Array(units + 1).join(" ")); + editor.setOption("extraKeys", { + Tab: function() { + if (doc.somethingSelected()) { + editor.execCommand("indentMore"); } - }); - } else { - editor.setOption("extraKeys", { - Tab: undefined, - }); - } + else { + if (!useTabs) { editor.execCommand("insertSoftTab"); } + else { editor.execCommand("insertTab"); } + } + }, + "Shift-Tab": function () { + editor.execCommand("indentLess"); + }, + "Backspace": function () { + var cursor = doc.getCursor(); + var line = doc.getLine(cursor.line); + if (line.substring(0, cursor.ch).trim() === "") { editor.execCommand("indentLess"); } + else { editor.execCommand("delCharBefore"); } + + }, + }); $('.CodeMirror').css('font-size', fontSize+'px'); }; diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 13229969d..0103cdbaf 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -838,13 +838,6 @@ define([ Cryptpad.setLanguage(data, cb); }); - sframeChan.on('Q_CLEAR_OWNED_CHANNEL', function (channel, cb) { - Cryptpad.clearOwnedChannel(channel, cb); - }); - sframeChan.on('Q_REMOVE_OWNED_CHANNEL', function (channel, cb) { - Cryptpad.removeOwnedChannel(channel, cb); - }); - sframeChan.on('Q_GET_ALL_TAGS', function (data, cb) { Cryptpad.listAllTags(function (err, tags) { cb({ diff --git a/www/drive/app-drive.less b/www/drive/app-drive.less index c85cd3c88..d04abbd08 100644 --- a/www/drive/app-drive.less +++ b/www/drive/app-drive.less @@ -30,6 +30,7 @@ @drive_content-bg-ro: darken(@drive_content-bg, 10%); @drive_selected-bg: #888; + @drive_droppable-bg: #FE9A2E; /* PAGE */ @@ -107,7 +108,7 @@ .cp-app-drive-container { flex: 1; - overflow: auto; + overflow-x: auto; width: 100%; display: flex; flex-flow: row; @@ -121,6 +122,7 @@ #cp-app-drive-tree { resize: none; width: 100% !important; + min-width: unset; max-width: unset; max-height: unset; border-bottom: 1px solid @drive_mobile-tree-border-col; @@ -156,7 +158,7 @@ } .cp-app-drive-element-droppable { - background-color: #FE9A2E; + background-color: @drive_droppable-bg; color: #222; } @@ -239,7 +241,6 @@ max-height: 100%; .cp-app-drive-tree-categories-container { flex: 1; - max-width: 500px; overflow: auto; } img.cp-app-drive-icon { @@ -438,13 +439,13 @@ flex: 1; // Needed to avoid the folder's path to overflows // https://stackoverflow.com/questions/38223879/white-space-nowrap-breaks-flexbox-layout - min-width: 0; + // min-width: 0; } #cp-app-drive-content { box-sizing: border-box; background: @drive_content-bg; color: @drive_content-fg; - overflow: auto; + overflow-y: auto; flex: 1; display: flex; flex-flow: column; @@ -939,6 +940,7 @@ overflow: hidden; text-overflow: ellipsis; transition: all 0.15s; + cursor: pointer; &:first-child { flex-shrink: 1; @@ -946,17 +948,20 @@ &.cp-app-drive-path-separator { color: #ccc; + cursor: default; } &.cp-app-drive-path-collapse { position: relative; } - &:hover { + &.cp-app-drive-element-droppable { + background-color: @drive_droppable-bg; + } + &:not(.cp-app-drive-element-droppable):hover { &:not(.cp-app-drive-path-separator) { background-color: darken(@colortheme_drive-bg, 15%); text-decoration: underline; - cursor: pointer; } & ~ .cp-app-drive-path-element { background-color: darken(@colortheme_drive-bg, 15%); diff --git a/www/drive/inner.js b/www/drive/inner.js index fd7723bca..c7c48668a 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -17,8 +17,6 @@ define([ '/bower_components/chainpad-listmap/chainpad-listmap.js', '/customize/messages.js', - '/common/jscolor.js', - 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/drive/app-drive.less', @@ -43,7 +41,10 @@ define([ { var APP = window.APP = { editable: false, - mobile: function () { return $('body').width() <= 600; }, // Menu and content area are not inline-block anymore for mobiles + mobile: function () { + if (window.matchMedia) { return !window.matchMedia('(any-pointer:fine)').matches; } + else { return $('body').width() <= 600; } + }, isMac: navigator.platform === "MacIntel", }; @@ -299,6 +300,33 @@ define([ }); }; + + APP.selectedFiles = []; + + var isElementSelected = function ($element) { + var elementId = $element.data("path").slice(-1)[0]; + return APP.selectedFiles.indexOf(elementId) !== -1; + }; + var selectElement = function ($element) { + var elementId = $element.data("path").slice(-1)[0]; + if (APP.selectedFiles.indexOf(elementId) === -1) { + APP.selectedFiles.push(elementId); + } + $element.addClass("cp-app-drive-element-selected"); + }; + var unselectElement = function ($element) { + var elementId = $element.data("path").slice(-1)[0]; + var index = APP.selectedFiles.indexOf(elementId); + if (index !== -1) { + APP.selectedFiles.splice(index, 1); + } + $element.removeClass("cp-app-drive-element-selected"); + }; + var findSelectedElements = function () { + return $(".cp-app-drive-element-selected"); + }; + + var createContextMenu = function () { var menu = h('div.cp-contextmenu.dropdown.cp-unselectable', [ h('ul.dropdown-menu', { @@ -369,16 +397,34 @@ define([ 'data-icon': AppConfig.applicationsIcon.slide, 'data-type': 'slide' }, Messages.button_newslide)), - h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { - 'tabindex': '-1', - 'data-icon': AppConfig.applicationsIcon.poll, - 'data-type': 'poll' - }, Messages.button_newpoll)), - h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { - 'tabindex': '-1', - 'data-icon': AppConfig.applicationsIcon.whiteboard, - 'data-type': 'whiteboard' - }, Messages.button_newwhiteboard)), + h('li.dropdown-submenu', [ + h('a.cp-app-drive-context-newdocmenu.dropdown-item', { + 'tabindex': '-1', + 'data-icon': "fa-plus", + }, Messages.fm_morePads || "More pads"), //XXX + h("ul.dropdown-menu", [ + h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { + 'tabindex': '-1', + 'data-icon': AppConfig.applicationsIcon.sheet, + 'data-type': 'sheet' + }, Messages.button_newsheet)), + h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { + 'tabindex': '-1', + 'data-icon': AppConfig.applicationsIcon.whiteboard, + 'data-type': 'whiteboard' + }, Messages.button_newwhiteboard)), + h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { + 'tabindex': '-1', + 'data-icon': AppConfig.applicationsIcon.kanban, + 'data-type': 'kanban' + }, Messages.button_newkanban)), + h('li', h('a.cp-app-drive-context-newdoc.dropdown-item.cp-app-drive-context-editable', { + 'tabindex': '-1', + 'data-icon': AppConfig.applicationsIcon.poll, + 'data-type': 'poll' + }, Messages.button_newpoll)), + ]), + ]), $separator.clone()[0], h('li', h('a.cp-app-drive-context-empty.dropdown-item.cp-app-drive-context-editable', { 'tabindex': '-1', @@ -415,6 +461,7 @@ define([ }, Messages.fc_prop)), ]) ]); + // add icons to the contextmenu options $(menu).find("li a.dropdown-item").each(function (i, el) { var $icon = $(""); if ($(el).attr('data-icon')) { @@ -425,21 +472,46 @@ define([ } $(el).prepend($icon); }); + // add events handlers for the contextmenu submenus $(menu).find(".dropdown-submenu").each(function (i, el) { var $el = $(el); var $a = $el.children().filter("a"); var $sub = $el.find(".dropdown-menu").first(); + var timeoutId; + var showSubmenu = function () { + clearTimeout(timeoutId); + $sub.toggleClass("left", $el.offset().left + $el.outerWidth() + $sub.outerWidth() > $(window).width()); + $el.siblings().find(".dropdown-menu").hide(); + $sub.show(); + }; + var hideSubmenu = function () { + $sub.hide(); + $sub.removeClass("left"); + }; + var mouseOutSubmenu = function () { + // don't hide immediately the submenu + timeoutId = setTimeout(hideSubmenu, 100); + }; // Add submenu expand icon $a.append(h("span.dropdown-toggle")); // Show / hide submenu $el.hover(function () { - setTimeout(function () { // wait for dom to update - $sub.toggleClass("left", $el.offset().left + $el.outerWidth() + $sub.outerWidth() > $(window).width()); - $sub.show(); - }); + showSubmenu(); }, function () { - $sub.hide(); - $sub.removeClass("left"); + mouseOutSubmenu(); + }); + // handle click event + $el.click(function (e) { + var targetItem = $(e.target).closest(".dropdown-item")[0]; // don't close contextmenu if open submenu + var elTarget = $el.children(".dropdown-item")[0]; + if (targetItem === elTarget) { e.stopPropagation(); } + if ($el.children().filter(".dropdown-menu:visible").length !== 0) { + $el.find(".dropdown-menu").hide(); + hideSubmenu(); + } + else { + showSubmenu(); + } }); }); return $(menu); @@ -565,7 +637,8 @@ define([ var sel = {}; var removeSelected = function (keepObj) { - $('.cp-app-drive-element-selected').removeClass("cp-app-drive-element-selected"); + APP.selectedFiles = []; + findSelectedElements().removeClass("cp-app-drive-element-selected"); var $container = $driveToolbar.find('#cp-app-drive-toolbar-contextbuttons'); if (!$container.length) { return; } $container.html(''); @@ -675,7 +748,9 @@ define([ delete sel.move; $content.find('.cp-app-drive-element-selected-tmp') .removeClass('cp-app-drive-element-selected-tmp') - .addClass('cp-app-drive-element-selected'); + .each(function (idx, element) { + selectElement($(element)); + }); e.stopPropagation(); }); @@ -710,7 +785,9 @@ define([ // Ctrl+A select all if (e.which === 65 && (e.ctrlKey || (e.metaKey && APP.isMac))) { $content.find('.cp-app-drive-element:not(.cp-app-drive-element-selected)') - .addClass('cp-app-drive-element-selected'); + .each(function (idx, element) { + selectElement($(element)); + }); return; } @@ -723,7 +800,7 @@ define([ APP.onElementClick(ev, $(el)); }; - var $selection = $content.find('.cp-app-drive-element.cp-app-drive-element-selected'); + var $selection = findSelectedElements(); if ($selection.length === 0) { return void click($elements.first()[0]); } var lastIndex = typeof sel.endSelected === "number" ? sel.endSelected : @@ -842,12 +919,12 @@ define([ return; } removeInput(); - removeSelected(); var $name = $element.find('.cp-app-drive-element-name'); if (!$name.length) { $name = $element.find('> .cp-app-drive-element'); } $name.hide(); + var isFolder = $element.is(".cp-app-drive-element-folder:not(.cp-app-drive-element-sharedf)"); var el = manager.find(path); var name = manager.isFile(el) ? manager.getTitle(el) : path[path.length - 1]; if (manager.isSharedFolder(el)) { @@ -869,14 +946,21 @@ define([ var newName = $input.val(); if (JSON.stringify(path) === JSON.stringify(currentPath)) { manager.rename(path, $input.val(), function () { - renameFoldersOpened(path, newName); - path[path.length - 1] = newName; + if (isFolder) { + renameFoldersOpened(path, newName); + path[path.length - 1] = newName; + } APP.displayDirectory(path); }); } else { manager.rename(path, $input.val(), function () { - renameFoldersOpened(path, newName); + if (isFolder) { + renameFoldersOpened(path, newName); + unselectElement($element); + $element.data("path", $element.data("path").slice(0, -1).concat(newName)); + selectElement($element); + } refresh(); }); } @@ -897,7 +981,6 @@ define([ // 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 @@ -1117,8 +1200,9 @@ define([ var getSelectedPaths = function ($element) { var paths = []; - if ($('.cp-app-drive-element-selected').length > 1) { - var $selected = $('.cp-app-drive-element-selected'); + if (!$element || $element.length === 0) { return paths; } + if (findSelectedElements().length > 1) { + var $selected = findSelectedElements(); $selected.each(function (idx, elmt) { var ePath = $(elmt).data('path'); if (ePath) { @@ -1147,7 +1231,7 @@ define([ } else { $driveToolbar.find('cp-app-drive-toolbar-emptytrash').hide(); } - var $li = $content.find('.cp-app-drive-element-selected'); + var $li = findSelectedElements(); if ($li.length === 0) { $li = findDataHolder($tree.find('.cp-app-drive-element-active')); } @@ -1214,6 +1298,7 @@ define([ if (pos+eh <= h && pos >= 0) { return; } $content.scrollTop(v); }; + // Add the "selected" class to the "li" corresponding to the clicked element var onElementClick = APP.onElementClick = function (e, $element) { // If "Ctrl" is pressed, do not remove the current selection @@ -1250,34 +1335,33 @@ define([ var $el; removeSelected(true); sel.oldSelection.forEach(function (el) { - if (!$(el).hasClass("cp-app-drive-element-selected")) { - $(el).addClass("cp-app-drive-element-selected"); + if (!isElementSelected($(el))) { + selectElement($(el)); } }); for (var i = Math.min(sel.startSelected, sel.endSelected); i <= Math.max(sel.startSelected, sel.endSelected); i++) { $el = $($elements.get(i)); - if (!$el.hasClass("cp-app-drive-element-selected")) { - $el.addClass("cp-app-drive-element-selected"); + if (!isElementSelected($el)) { + selectElement($el); } } } else { - if (!$element.hasClass("cp-app-drive-element-selected")) { - $element.addClass("cp-app-drive-element-selected"); + if (!isElementSelected($element)) { + selectElement($element); } else { - $element.removeClass("cp-app-drive-element-selected"); + unselectElement($element); } } updateContextButton(); }; - var displayMenu = function (e) { - var $menu = $contextMenu; + // show / hide dropdown separators + var hideSeparators = function ($menu) { var showSep = false; var $lastVisibleSep = null; - // show / hide drop-down divider - $menu.find(".dropdown-menu").children().each(function (i, el) { + $menu.children().each(function (i, el) { var $el = $(el); if ($el.is(".dropdown-divider")) { $el.css("display", showSep ? "list-item" : "none"); @@ -1289,18 +1373,36 @@ define([ } }); if (!showSep && $lastVisibleSep) { $lastVisibleSep.css("display", "none"); } // remove last divider if no options after + }; + + // prepare and display contextmenu + var displayMenu = function (e) { + var $menu = $contextMenu; // show / hide submenus $menu.find(".dropdown-submenu").each(function (i, el) { var $el = $(el); + $el.children(".dropdown-menu").css("display", "none"); $el.find("li").each(function (i, li) { if ($(li).css("display") !== "none") { - $(el).css("display", "block"); + $el.css("display", "block"); return; } }); }); + // show / hide separators + $menu.find(".dropdown-menu").each(function (i, menu) { + hideSeparators($(menu)); + }); + // show contextmenu at cursor position $menu.css({ display: "block" }); - if (APP.mobile()) { return; } + if (APP.mobile()) { + $menu.css({ + top: ($("#cp-app-drive-toolbar-context-mobile").offset().top + 32) + 'px', + right: '0px', + left: '' + }); + return; + } var h = $menu.outerHeight(); var w = $menu.outerWidth(); var wH = window.innerHeight; @@ -1353,6 +1455,17 @@ define([ } else { var $element = findDataHolder($(e.target)); + // if clicked from tree + var fromTree = $element.closest("#cp-app-drive-tree").length; + if (fromTree) { + removeSelected(); + } + + // if clicked on non selected element + if (!isElementSelected($element)) { + removeSelected(); + } + if (type === 'trash' && !$element.data('path')) { return; } if (!$element.length) { @@ -1361,8 +1474,8 @@ define([ return false; } - if (!$element.hasClass('cp-app-drive-element-selected')) { - onElementClick(undefined, $element); + if (!isElementSelected($element)) { + selectElement($element); } paths = getSelectedPaths($element); @@ -1432,6 +1545,7 @@ define([ if (!res) { return; } manager.delete(pathsList, function () { pathsList.forEach(removeFoldersOpened); + removeSelected(); refresh(); }); }, null, true); @@ -1442,7 +1556,7 @@ define([ var paths = []; var $element = findDataHolder($(ev.target)); if ($element.hasClass('cp-app-drive-element-selected')) { - var $selected = $('.cp-app-drive-element-selected'); + var $selected = findSelectedElements(); $selected.each(function (idx, elmt) { var ePath = $(elmt).data('path'); if (ePath) { @@ -1459,7 +1573,7 @@ define([ }); } else { removeSelected(); - $element.addClass('cp-app-drive-element-selected'); + selectElement($element); var val = manager.find(path); if (!val) { return; } // The element is not in the object paths = [{ @@ -1478,7 +1592,13 @@ define([ var findDropPath = function (target) { var $target = $(target); - var $el = findDataHolder($target); + var $el; + if ($target.is(".cp-app-drive-path-element")) { + $el = $target; + } + else { + $el = findDataHolder($target); + } var newPath = $el.data('path'); var dropEl = newPath && manager.find(newPath); if (newPath && manager.isSharedFolder(dropEl)) { @@ -1606,7 +1726,8 @@ define([ $owner.attr('title', Messages.fm_padIsOwnedOther); } }; - var addFileData = function (element, $span) { + var thumbsUrls = {}; + var addFileData = function (element, $element) { if (!manager.isFile(element)) { return; } var data = manager.getFileData(element); @@ -1615,7 +1736,7 @@ define([ var hrefData = Hash.parsePadUrl(href); if (hrefData.type) { - $span.addClass('cp-border-color-'+hrefData.type); + $element.addClass('cp-border-color-'+hrefData.type); } var $state = $('', {'class': 'cp-app-drive-element-state'}); @@ -1635,25 +1756,38 @@ define([ var $expire = $expirableIcon.clone().appendTo($state); $expire.attr('title', Messages._getKey('fm_expirablePad', [new Date(data.expire).toLocaleString()])); } - _addOwnership($span, $state, data); + _addOwnership($element, $state, data); var name = manager.getTitle(element); // The element with the class '.name' is underlined when the 'li' is hovered var $name = $('', {'class': 'cp-app-drive-element-name'}).text(name); - $span.append($name); - $span.append($state); - $span.attr('title', name); + $element.append($name); + $element.append($state); + $element.attr('title', name); + + // display the thumbnail + // if the thumbnail has already been displayed once, do not reload it, keep the same url + if (thumbsUrls[element]) { + var img = new Image(); + img.src = thumbsUrls[element]; + $element.find('.cp-icon').addClass('cp-app-drive-element-list'); + $element.prepend(img); + $(img).addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail'); + $(img).attr("draggable", false); + } + else { + common.displayThumbnail(href || data.roHref, data.channel, data.password, $element, function ($thumb) { + // Called only if the thumbnail exists + // Remove the .hide() added by displayThumnail() because it hides the icon in list mode too + $element.find('.cp-icon').removeAttr('style').addClass('cp-app-drive-element-list'); + $thumb.addClass('cp-app-drive-element-grid cp-app-drive-element-thumbnail'); + $thumb.attr("draggable", false); + thumbsUrls[element] = $thumb[0].src; + }); + } var type = Messages.type[hrefData.type] || hrefData.type; - common.displayThumbnail(href || data.roHref, data.channel, data.password, $span, function ($thumb) { - // Called only if the thumbnail exists - // Remove the .hide() added by displayThumnail() because it hides the icon in - // list mode too - $span.find('.cp-icon').removeAttr('style').addClass('cp-app-drive-element-list'); - $thumb.addClass('cp-app-drive-element-grid') - .addClass('cp-app-drive-element-thumbnail'); - }); var $type = $('', { 'class': 'cp-app-drive-element-type cp-app-drive-element-list' }).text(type); @@ -1663,7 +1797,7 @@ define([ var $cdate = $('', { 'class': 'cp-app-drive-element-ctime cp-app-drive-element-list' }).text(getDate(data.ctime)); - $span.append($type).append($adate).append($cdate); + $element.append($type).append($adate).append($cdate); }; var addFolderData = function (element, key, $span) { @@ -1739,12 +1873,9 @@ define([ draggable: true, 'class': 'cp-app-drive-element-row' }); - if (!isFolder && Array.isArray(APP.selectedFiles)) { - var idx = APP.selectedFiles.indexOf(element); - if (idx !== -1) { - $element.addClass('cp-app-drive-element-selected'); - APP.selectedFiles.splice(idx, 1); - } + $element.data('path', newPath); + if (isElementSelected($element)) { + selectElement($element); } $element.prepend($icon).dblclick(function () { if (isFolder) { @@ -1760,11 +1891,10 @@ define([ addFileData(element, $element); } $element.addClass(liClass); - $element.data('path', newPath); addDragAndDropHandlers($element, newPath, isFolder, !isTrash); $element.click(function(e) { e.stopPropagation(); - onElementClick(e, $element, newPath); + onElementClick(e, $element); }); if (!isTrash) { $element.contextmenu(openContextMenu('tree')); @@ -1915,6 +2045,8 @@ define([ } else if (idx > 0 && manager.isFile(el)) { name = getElementName(path); } + $span.data("path", path.slice(0, idx + 1)); + addDragAndDropHandlers($span, path.slice(0, idx), true, true); if (idx === 0) { name = p === SHARED_FOLDER ? name : getPrettyName(p); } else { @@ -2559,22 +2691,19 @@ define([ 'class': 'cp-app-drive-element cp-app-drive-element-file cp-app-drive-element-row' + roClass, draggable: draggable }); - if (Array.isArray(APP.selectedFiles)) { - var sidx = APP.selectedFiles.indexOf(id); - if (sidx !== -1) { - $element.addClass('cp-app-drive-element-selected'); - APP.selectedFiles.splice(sidx, 1); - } + + var path = [rootName, idx]; + $element.data('path', path); + if (isElementSelected($element)) { + selectElement($element); } $element.prepend($icon).dblclick(function () { openFile(id); }); addFileData(id, $element); - var path = [rootName, idx]; - $element.data('path', path); $element.click(function(e) { e.stopPropagation(); - onElementClick(e, $element, path); + onElementClick(e, $element); }); $element.contextmenu(openContextMenu('default')); $element.data('context', 'default'); @@ -2701,8 +2830,8 @@ define([ e.preventDefault(); if (manager.isInTrashRoot(parentPath)) { parentPath = [TRASH]; } else { parentPath.pop(); } - APP.selectedFiles = [r.id]; APP.displayDirectory(parentPath); + APP.selectedFiles = path.slice(-1); }).appendTo($openDir); } $('').text(Messages.fc_prop).click(function () { @@ -2793,7 +2922,7 @@ define([ $element.data('path', path); $element.click(function(e) { e.stopPropagation(); - onElementClick(e, $element, path); + onElementClick(e, $element); }); $element.contextmenu(openContextMenu('default')); $element.data('context', 'default'); @@ -3017,7 +3146,7 @@ define([ $context.click(function (e) { e.preventDefault(); e.stopPropagation(); - var $li = $content.find('.cp-app-drive-element-selected'); + var $li = findSelectedElements(); if ($li.length !== 1) { $li = findDataHolder($tree.find('.cp-app-drive-element-active')); } @@ -3027,11 +3156,6 @@ define([ return; } // Open the menu - $('.cp-contextmenu').css({ - top: ($context.offset().top + 32) + 'px', - right: '0px', - left: '' - }); $li.contextmenu(); }); } else { @@ -3099,7 +3223,7 @@ define([ } });*/ - var $sel = $content.find('.cp-app-drive-element-selected'); + var $sel = findSelectedElements(); if ($sel.length) { $sel[0].scrollIntoView(); } elseĀ { @@ -3111,6 +3235,9 @@ define([ if (history.isHistoryMode) { return void _displayDirectory(path, force); } + if (!manager.comparePath(currentPath, path)) { + removeSelected(); + } updateObject(sframeChan, proxy, function () { copyObjectValue(files, proxy.drive); updateSharedFolders(sframeChan, manager, files, folders, function () { @@ -3461,6 +3588,7 @@ define([ if (!res) { return; } manager.delete(pathsList, function () { pathsList.forEach(removeFoldersOpened); + removeSelected(); refresh(); }); }); @@ -3482,7 +3610,7 @@ define([ if (paths.length !== 1) { return; } displayRenameInput(paths[0].element, paths[0].path); } - if ($(this).hasClass("cp-app-drive-context-color")) { + else if ($(this).hasClass("cp-app-drive-context-color")) { var currentColor = getFolderColor(paths[0].path); pickFolderColor(paths[0].element, currentColor, function (color) { paths.forEach(function (p) { @@ -3707,16 +3835,15 @@ define([ var parentPath = paths[0].path.slice(); if (manager.isInTrashRoot(parentPath)) { parentPath = [TRASH]; } else { parentPath.pop(); } - el = manager.find(paths[0].path); - APP.selectedFiles = [el]; APP.displayDirectory(parentPath); + APP.selectedFiles = paths[0].path.slice(-1); } APP.hideMenu(); }); - $content.on("keydown", function (e) { - if (e.which === 113) { - var paths = $contextMenu.data('paths'); + $(window).on("keydown", function (e) { + if (e.which === 113) { // if F2 key pressed + var paths = getSelectedPaths(findSelectedElements().first()); if (paths.length !== 1) { return; } displayRenameInput(paths[0].element, paths[0].path); } @@ -3728,10 +3855,9 @@ define([ e.preventDefault(); }); $appContainer.on('mouseup', function (e) { - //if (sel.down) { return; } if (e.which !== 1) { return ; } + if ($(e.target).is(".dropdown-submenu a, .dropdown-submenu a span")) { return; } // if we click on dropdown-submenu, don't close menu APP.hideMenu(e); - //removeSelected(e); }); $appContainer.on('click', function (e) { if (e.which !== 1) { return ; } @@ -3750,7 +3876,7 @@ define([ if (manager.isPathIn(currentPath, [FILES_DATA]) && APP.loggedIn) { return; // We can't remove elements directly from filesData } - var $selected = $('.cp-app-drive-element-selected'); + var $selected = findSelectedElements(); if (!$selected.length) { return; } var paths = []; var isTrash = manager.isPathIn(currentPath, [TRASH]); diff --git a/www/file/app-file.less b/www/file/app-file.less index 20428b382..4ac94ca33 100644 --- a/www/file/app-file.less +++ b/www/file/app-file.less @@ -52,6 +52,16 @@ max-width: 100%; max-height: ~"calc(100vh - 96px)"; } + .plain-text-reader { + align-self: flex-start; + width: 90vw; + height: 100%; + padding: 2em; + background-color: white; + overflow-y: auto; + word-wrap: break-word; + white-space: pre-wrap; + } } #cp-app-file-upload-form, #cp-app-file-download-form { diff --git a/www/kanban/export.js b/www/kanban/export.js index 2240031fe..9ee770ac7 100644 --- a/www/kanban/export.js +++ b/www/kanban/export.js @@ -2,7 +2,9 @@ // Pads from the code app will be exported using this format instead of plain text. define([ ], function () { - var module = {}; + var module = { + ext: '.json' + }; module.main = function (userDoc, cb) { var content = userDoc.content; diff --git a/www/kanban/inner.js b/www/kanban/inner.js index 29a4d9c88..609e5ac31 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -367,7 +367,7 @@ define([ }); } - framework.setFileExporter('json', function () { + framework.setFileExporter('.json', function () { return new Blob([JSON.stringify(kanban.getBoardsJSON(), 0, 2)], { type: 'application/json', }); diff --git a/www/pad/export.js b/www/pad/export.js index 263593a54..d1deef146 100644 --- a/www/pad/export.js +++ b/www/pad/export.js @@ -5,7 +5,7 @@ define([ '/bower_components/nthen/index.js', ], function ($, Util, Hyperjson, nThen) { var module = { - type: 'html' + ext: '.html' }; var exportMediaTags = function (inner, cb) { diff --git a/www/pad/inner.js b/www/pad/inner.js index 9b56e56bb..f201b8720 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -786,7 +786,7 @@ define([ }); }, true); - framework.setFileExporter(Exporter.type, function (cb) { + framework.setFileExporter(Exporter.ext, function (cb) { Exporter.main(inner, cb); }, true); diff --git a/www/poll/export.js b/www/poll/export.js index 2b915cced..e080ec9b4 100644 --- a/www/poll/export.js +++ b/www/poll/export.js @@ -3,7 +3,9 @@ define([ '/customize/messages.js', ], function (Messages) { - var module = {}; + var module = { + ext: '.csv' + }; var copyObject = function (obj) { return JSON.parse(JSON.stringify(obj)); diff --git a/www/settings/app-settings.less b/www/settings/app-settings.less index 0c21bfed0..185b22741 100644 --- a/www/settings/app-settings.less +++ b/www/settings/app-settings.less @@ -111,8 +111,14 @@ vertical-align: middle; margin-right: 5px; } - input[type="color"] { - width: 100px; + .cp-settings-cursor-color-picker { + display: inline-block; + vertical-align: middle; + height: 25px; + width: 70px; + margin-right: 10px; + cursor: pointer; + border: 1px solid black; } .cp-settings-language-selector { button.btn { diff --git a/www/settings/inner.js b/www/settings/inner.js index d785391d0..e8e22a027 100644 --- a/www/settings/inner.js +++ b/www/settings/inner.js @@ -15,6 +15,7 @@ define([ '/settings/make-backup.js', '/common/common-feedback.js', + '/common/jscolor.js', '/bower_components/file-saver/FileSaver.min.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'css!/bower_components/components-font-awesome/css/font-awesome.min.css', @@ -1191,13 +1192,13 @@ define([ var $inputBlock = $('