Files Management + Editor Modal + Code Editor fixes

This commit is contained in:
NGPixel 2016-12-21 20:38:12 -05:00
parent eb2e724f37
commit 9caaeee682
22 changed files with 747 additions and 112 deletions

View File

@ -12,17 +12,18 @@
[![Known Vulnerabilities](https://snyk.io/test/github/requarks/wiki/badge.svg)](https://snyk.io/test/github/requarks/wiki) [![Known Vulnerabilities](https://snyk.io/test/github/requarks/wiki/badge.svg)](https://snyk.io/test/github/requarks/wiki)
##### A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown ##### A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown
*Under development* *Under active development*
### Documentation ### Documentation
- [Installation Guide](https://wiki.requarks.io/install) - [Official Website](https://wiki.requarks.io/)
- [Installation Guide](https://wiki.requarks.io/get-started.html)
##### Milestones ##### Milestones
- [ ] Account Management - [ ] Account Management
- [x] Assets Management - [x] Assets Management
- [x] Images - [x] Images
- [ ] Files/Documents - [x] Files/Documents
- [x] Authentication - [x] Authentication
- [x] Strategies - [x] Strategies
- [x] Local - [x] Local
@ -46,6 +47,10 @@
- [x] Markdown Editor - [x] Markdown Editor
- [x] Basic Formatting - [x] Basic Formatting
- [ ] Links - [ ] Links
- [x] Image Selection modal
- [x] File Selection modal
- [x] Inline Code
- [x] Code Editor modal
- [ ] Table Editor - [ ] Table Editor
- [x] Move Entry - [x] Move Entry
- [x] Navigation - [x] Navigation

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,12 +1,6 @@
let codeEditor = ace.edit("codeblock-editor");
codeEditor.setTheme("ace/theme/tomorrow_night");
codeEditor.getSession().setMode("ace/mode/markdown");
codeEditor.setOption('fontSize', '14px');
codeEditor.setOption('hScrollBarAlwaysVisible', false);
codeEditor.setOption('wrap', true);
let modelist = ace.require("ace/ext/modelist"); let modelist = ace.require("ace/ext/modelist");
let codeEditor = null;
// ACE - Mode Loader // ACE - Mode Loader
@ -33,7 +27,8 @@ let vueCodeBlock = new Vue({
el: '#modal-editor-codeblock', el: '#modal-editor-codeblock',
data: { data: {
modes: modelist.modesByName, modes: modelist.modesByName,
modeSelected: 'text' modeSelected: 'text',
initContent: ''
}, },
watch: { watch: {
modeSelected: (val, oldVal) => { modeSelected: (val, oldVal) => {
@ -45,19 +40,28 @@ let vueCodeBlock = new Vue({
}, },
methods: { methods: {
open: (ev) => { open: (ev) => {
$('#modal-editor-codeblock').addClass('is-active'); $('#modal-editor-codeblock').addClass('is-active');
_.delay(() => { _.delay(() => {
codeEditor.resize(); codeEditor = ace.edit("codeblock-editor");
codeEditor.setTheme("ace/theme/tomorrow_night");
codeEditor.getSession().setMode("ace/mode/" + vueCodeBlock.modeSelected);
codeEditor.setOption('fontSize', '14px');
codeEditor.setOption('hScrollBarAlwaysVisible', false);
codeEditor.setOption('wrap', true);
codeEditor.setValue(vueCodeBlock.initContent);
codeEditor.focus(); codeEditor.focus();
codeEditor.setAutoScrollEditorIntoView(true);
codeEditor.renderer.updateFull(); codeEditor.renderer.updateFull();
}, 1000); }, 300);
}, },
cancel: (ev) => { cancel: (ev) => {
mdeModalOpenState = false; mdeModalOpenState = false;
$('#modal-editor-codeblock').removeClass('is-active'); $('#modal-editor-codeblock').removeClass('is-active');
vueCodeBlock.initContent = '';
}, },
insertCode: (ev) => { insertCode: (ev) => {

View File

@ -0,0 +1,365 @@
let vueFile = new Vue({
el: '#modal-editor-file',
data: {
isLoading: false,
isLoadingText: '',
newFolderName: '',
newFolderShow: false,
newFolderError: false,
folders: [],
currentFolder: '',
currentFile: '',
files: [],
uploadSucceeded: false,
postUploadChecks: 0,
renameFileShow: false,
renameFileId: '',
renameFileFilename: '',
deleteFileShow: false,
deleteFileId: '',
deleteFileFilename: ''
},
methods: {
open: () => {
mdeModalOpenState = true;
$('#modal-editor-file').addClass('is-active');
vueFile.refreshFolders();
},
cancel: (ev) => {
mdeModalOpenState = false;
$('#modal-editor-file').removeClass('is-active');
},
// -------------------------------------------
// INSERT LINK TO FILE
// -------------------------------------------
selectFile: (fileId) => {
vueFile.currentFile = fileId;
},
insertFileLink: (ev) => {
if(mde.codemirror.doc.somethingSelected()) {
mde.codemirror.execCommand('singleSelection');
}
let selFile = _.find(vueFile.files, ['_id', vueFile.currentFile]);
selFile.normalizedPath = (selFile.folder === 'f:') ? selFile.filename : selFile.folder.slice(2) + '/' + selFile.filename;
selFile.titleGuess = _.startCase(selFile.basename);
let fileText = '[' + selFile.titleGuess + '](/uploads/' + selFile.normalizedPath + ' "' + selFile.titleGuess + '")';
mde.codemirror.doc.replaceSelection(fileText);
vueFile.cancel();
},
// -------------------------------------------
// NEW FOLDER
// -------------------------------------------
newFolder: (ev) => {
vueFile.newFolderName = '';
vueFile.newFolderError = false;
vueFile.newFolderShow = true;
_.delay(() => { $('#txt-editor-file-newfoldername').focus(); }, 400);
},
newFolderDiscard: (ev) => {
vueFile.newFolderShow = false;
},
newFolderCreate: (ev) => {
let regFolderName = new RegExp("^[a-z0-9][a-z0-9\-]*[a-z0-9]$");
vueFile.newFolderName = _.kebabCase(_.trim(vueFile.newFolderName));
if(_.isEmpty(vueFile.newFolderName) || !regFolderName.test(vueFile.newFolderName)) {
vueFile.newFolderError = true;
return;
}
vueFile.newFolderDiscard();
vueFile.isLoadingText = 'Creating new folder...';
vueFile.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsCreateFolder', { foldername: vueFile.newFolderName }, (data) => {
vueFile.folders = data;
vueFile.currentFolder = vueFile.newFolderName;
vueFile.files = [];
vueFile.isLoading = false;
});
});
},
// -------------------------------------------
// RENAME FILE
// -------------------------------------------
renameFile: () => {
let c = _.find(vueFile.files, ['_id', vueFile.renameFileId ]);
vueFile.renameFileFilename = c.basename || '';
vueFile.renameFileShow = true;
_.delay(() => {
$('#txt-editor-renamefile').focus();
_.defer(() => { $('#txt-editor-file-rename').select(); });
}, 400);
},
renameFileDiscard: () => {
vueFile.renameFileShow = false;
},
renameFileGo: () => {
vueFile.renameFileDiscard();
vueFile.isLoadingText = 'Renaming file...';
vueFile.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsRenameFile', { uid: vueFile.renameFileId, folder: vueFile.currentFolder, filename: vueFile.renameFileFilename }, (data) => {
if(data.ok) {
vueFile.waitChangeComplete(vueFile.files.length, false);
} else {
vueFile.isLoading = false;
alerts.pushError('Rename error', data.msg);
}
});
});
},
// -------------------------------------------
// MOVE FILE
// -------------------------------------------
moveFile: (uid, fld) => {
vueFile.isLoadingText = 'Moving file...';
vueFile.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsMoveFile', { uid, folder: fld }, (data) => {
if(data.ok) {
vueFile.loadFiles();
} else {
vueFile.isLoading = false;
alerts.pushError('Rename error', data.msg);
}
});
});
},
// -------------------------------------------
// DELETE FILE
// -------------------------------------------
deleteFileWarn: (show) => {
if(show) {
let c = _.find(vueFile.files, ['_id', vueFile.deleteFileId ]);
vueFile.deleteFileFilename = c.filename || 'this file';
}
vueFile.deleteFileShow = show;
},
deleteFileGo: () => {
vueFile.deleteFileWarn(false);
vueFile.isLoadingText = 'Deleting file...';
vueFile.isLoading = true;
Vue.nextTick(() => {
socket.emit('uploadsDeleteFile', { uid: vueFile.deleteFileId }, (data) => {
vueFile.loadFiles();
});
});
},
// -------------------------------------------
// LOAD FROM REMOTE
// -------------------------------------------
selectFolder: (fldName) => {
vueFile.currentFolder = fldName;
vueFile.loadFiles();
},
refreshFolders: () => {
vueFile.isLoadingText = 'Fetching folders list...';
vueFile.isLoading = true;
vueFile.currentFolder = '';
vueFile.currentImage = '';
Vue.nextTick(() => {
socket.emit('uploadsGetFolders', { }, (data) => {
vueFile.folders = data;
vueFile.loadFiles();
});
});
},
loadFiles: (silent) => {
if(!silent) {
vueFile.isLoadingText = 'Fetching files...';
vueFile.isLoading = true;
}
return new Promise((resolve, reject) => {
Vue.nextTick(() => {
socket.emit('uploadsGetFiles', { folder: vueFile.currentFolder }, (data) => {
vueFile.files = data;
if(!silent) {
vueFile.isLoading = false;
}
vueFile.attachContextMenus();
resolve(true);
});
});
});
},
waitChangeComplete: (oldAmount, expectChange) => {
expectChange = (_.isBoolean(expectChange)) ? expectChange : true;
vueFile.postUploadChecks++;
vueFile.isLoadingText = 'Processing...';
Vue.nextTick(() => {
vueFile.loadFiles(true).then(() => {
if((vueFile.files.length !== oldAmount) === expectChange) {
vueFile.postUploadChecks = 0;
vueFile.isLoading = false;
} else if(vueFile.postUploadChecks > 5) {
vueFile.postUploadChecks = 0;
vueFile.isLoading = false;
alerts.pushError('Unable to fetch updated listing', 'Try again later');
} else {
_.delay(() => {
vueFile.waitChangeComplete(oldAmount, expectChange);
}, 1500);
}
});
});
},
// -------------------------------------------
// IMAGE CONTEXT MENU
// -------------------------------------------
attachContextMenus: () => {
let moveFolders = _.map(vueFile.folders, (f) => {
return {
name: (f !== '') ? f : '/ (root)',
icon: 'fa-folder',
callback: (key, opt) => {
let moveFileId = _.toString($(opt.$trigger).data('uid'));
let moveFileDestFolder = _.nth(vueFile.folders, key);
vueFile.moveFile(moveFileId, moveFileDestFolder);
}
};
});
$.contextMenu('destroy', '.editor-modal-file-choices > figure');
$.contextMenu({
selector: '.editor-modal-file-choices > figure',
appendTo: '.editor-modal-file-choices',
position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen');
let trigPos = $(opt.$trigger).position();
let trigDim = { w: $(opt.$trigger).width() / 5, h: $(opt.$trigger).height() / 2 };
opt.$menu.css({ top: trigPos.top + trigDim.h, left: trigPos.left + trigDim.w });
},
events: {
hide: (opt) => {
$(opt.$trigger).removeClass('is-contextopen');
}
},
items: {
rename: {
name: "Rename",
icon: "fa-edit",
callback: (key, opt) => {
vueFile.renameFileId = _.toString(opt.$trigger[0].dataset.uid);
vueFile.renameFile();
}
},
move: {
name: "Move to...",
icon: "fa-folder-open-o",
items: moveFolders
},
delete: {
name: "Delete",
icon: "fa-trash",
callback: (key, opt) => {
vueFile.deleteFileId = _.toString(opt.$trigger[0].dataset.uid);
vueFile.deleteFileWarn(true);
}
}
}
});
}
}
});
$('#btn-editor-file-upload input').on('change', (ev) => {
let curFileAmount = vueFile.files.length;
$(ev.currentTarget).simpleUpload("/uploads/file", {
name: 'binfile',
data: {
folder: vueFile.currentFolder
},
limit: 20,
expect: 'json',
maxFileSize: 0,
init: (totalUploads) => {
vueFile.uploadSucceeded = false;
vueFile.isLoadingText = 'Preparing to upload...';
vueFile.isLoading = true;
},
progress: (progress) => {
vueFile.isLoadingText = 'Uploading...' + Math.round(progress) + '%';
},
success: (data) => {
if(data.ok) {
let failedUpls = _.filter(data.results, ['ok', false]);
if(failedUpls.length) {
_.forEach(failedUpls, (u) => {
alerts.pushError('Upload error', u.msg);
});
if(failedUpls.length < data.results.length) {
alerts.push({
title: 'Some uploads succeeded',
message: 'Files that are not mentionned in the errors above were uploaded successfully.'
});
vueFile.uploadSucceeded = true;
}
} else {
vueFile.uploadSucceeded = true;
}
} else {
alerts.pushError('Upload error', data.msg);
}
},
error: (error) => {
alerts.pushError(error.message, this.upload.file.name);
},
finish: () => {
if(vueFile.uploadSucceeded) {
vueFile.waitChangeComplete(curFileAmount, true);
} else {
vueFile.isLoading = false;
}
}
});
});

View File

@ -78,7 +78,7 @@ let vueImage = new Vue({
vueImage.newFolderName = ''; vueImage.newFolderName = '';
vueImage.newFolderError = false; vueImage.newFolderError = false;
vueImage.newFolderShow = true; vueImage.newFolderShow = true;
_.delay(() => { $('#txt-editor-newfoldername').focus(); }, 400); _.delay(() => { $('#txt-editor-image-newfoldername').focus(); }, 400);
}, },
newFolderDiscard: (ev) => { newFolderDiscard: (ev) => {
vueImage.newFolderShow = false; vueImage.newFolderShow = false;
@ -115,7 +115,7 @@ let vueImage = new Vue({
fetchFromUrl: (ev) => { fetchFromUrl: (ev) => {
vueImage.fetchFromUrlURL = ''; vueImage.fetchFromUrlURL = '';
vueImage.fetchFromUrlShow = true; vueImage.fetchFromUrlShow = true;
_.delay(() => { $('#txt-editor-fetchimgurl').focus(); }, 400); _.delay(() => { $('#txt-editor-image-fetchurl').focus(); }, 400);
}, },
fetchFromUrlDiscard: (ev) => { fetchFromUrlDiscard: (ev) => {
vueImage.fetchFromUrlShow = false; vueImage.fetchFromUrlShow = false;
@ -149,8 +149,8 @@ let vueImage = new Vue({
vueImage.renameImageFilename = c.basename || ''; vueImage.renameImageFilename = c.basename || '';
vueImage.renameImageShow = true; vueImage.renameImageShow = true;
_.delay(() => { _.delay(() => {
$('#txt-editor-renameimage').focus(); $('#txt-editor-image-rename').focus();
_.defer(() => { $('#txt-editor-renameimage').select(); }); _.defer(() => { $('#txt-editor-image-rename').select(); });
}, 400); }, 400);
}, },
renameImageDiscard: () => { renameImageDiscard: () => {
@ -301,10 +301,10 @@ let vueImage = new Vue({
}; };
}); });
$.contextMenu('destroy', '.editor-modal-imagechoices > figure'); $.contextMenu('destroy', '.editor-modal-image-choices > figure');
$.contextMenu({ $.contextMenu({
selector: '.editor-modal-imagechoices > figure', selector: '.editor-modal-image-choices > figure',
appendTo: '.editor-modal-imagechoices', appendTo: '.editor-modal-image-choices',
position: (opt, x, y) => { position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen'); $(opt.$trigger).addClass('is-contextopen');
let trigPos = $(opt.$trigger).position(); let trigPos = $(opt.$trigger).position();
@ -345,7 +345,7 @@ let vueImage = new Vue({
} }
}); });
$('#btn-editor-uploadimage input').on('change', (ev) => { $('#btn-editor-image-upload input').on('change', (ev) => {
let curImageAmount = vueImage.images.length; let curImageAmount = vueImage.images.length;

View File

@ -13,6 +13,7 @@ if($('#mk-editor').length === 1) {
}); });
//=include editor-image.js //=include editor-image.js
//=include editor-file.js
//=include editor-codeblock.js //=include editor-codeblock.js
var mde = new SimpleMDE({ var mde = new SimpleMDE({
@ -103,7 +104,9 @@ if($('#mk-editor').length === 1) {
{ {
name: "file", name: "file",
action: (editor) => { action: (editor) => {
//todo if(!mdeModalOpenState) {
vueFile.open();
}
}, },
className: "fa fa-file-text-o", className: "fa fa-file-text-o",
title: "Insert File", title: "Insert File",
@ -133,9 +136,7 @@ if($('#mk-editor').length === 1) {
mdeModalOpenState = true; mdeModalOpenState = true;
if(mde.codemirror.doc.somethingSelected()) { if(mde.codemirror.doc.somethingSelected()) {
codeEditor.setValue(mde.codemirror.doc.getSelection()); vueCodeBlock.initContent = mde.codemirror.doc.getSelection();
} else {
codeEditor.setValue('');
} }
vueCodeBlock.open(); vueCodeBlock.open();
@ -170,7 +171,21 @@ if($('#mk-editor').length === 1) {
//-> Save //-> Save
$('.btn-edit-save, .btn-create-save').on('click', (ev) => { $('.btn-edit-save, .btn-create-save').on('click', (ev) => {
saveCurrentDocument(ev);
});
$(window).bind('keydown', (ev) => {
if (ev.ctrlKey || ev.metaKey) {
switch (String.fromCharCode(ev.which).toLowerCase()) {
case 's':
ev.preventDefault();
saveCurrentDocument(ev);
break;
}
}
});
let saveCurrentDocument = (ev) => {
$.ajax(window.location.href, { $.ajax(window.location.href, {
data: { data: {
markdown: mde.value() markdown: mde.value()
@ -186,7 +201,6 @@ if($('#mk-editor').length === 1) {
}, (rXHR, rStatus, err) => { }, (rXHR, rStatus, err) => {
alerts.pushError('Something went wrong', 'Save operation failed.'); alerts.pushError('Something went wrong', 'Save operation failed.');
}); });
}
});
} }

View File

@ -4,6 +4,9 @@ if($('#page-type-source').length) {
var scEditor = ace.edit("source-display"); var scEditor = ace.edit("source-display");
scEditor.setTheme("ace/theme/tomorrow_night"); scEditor.setTheme("ace/theme/tomorrow_night");
scEditor.getSession().setMode("ace/mode/markdown"); scEditor.getSession().setMode("ace/mode/markdown");
scEditor.setOption('fontSize', '14px');
scEditor.setOption('hScrollBarAlwaysVisible', false);
scEditor.setOption('wrap', true);
scEditor.setReadOnly(true); scEditor.setReadOnly(true);
scEditor.renderer.updateFull(); scEditor.renderer.updateFull();

View File

@ -67,7 +67,7 @@
} }
#btn-editor-uploadimage { #btn-editor-image-upload, #btn-editor-file-upload {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@ -94,7 +94,7 @@
} }
.editor-modal-imagechoices { .editor-modal-image-choices {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: flex-start; align-items: flex-start;
@ -188,6 +188,85 @@
} }
.editor-modal-file-choices {
overflow: auto;
overflow-x: hidden;
> em {
display: flex;
align-items: center;
padding: 25px;
color: mc('grey', '500');
> i {
font-size: 32px;
margin-right: 10px;
color: mc('grey', '300');
}
}
> figure {
display: flex;
background-color: #FAFAFA;
border-radius: 3px;
padding: 5px;
height: 34px;
margin: 0 0 5px 0;
cursor: pointer;
justify-content: flex-start;
align-items: center;
transition: background-color 0.4s ease;
> i {
width: 16px;
}
> span {
font-size: 14px;
flex: 0 1 auto;
padding: 0 15px;
color: mc('grey', '600');
&:first-of-type {
flex: 1 0 auto;
color: mc('grey', '800');
}
&:last-of-type {
width: 100px;
}
}
&:hover {
background-color: #DDD;
}
&.is-active {
background-color: mc('green', '500');
color: #FFF;
> span, strong {
color: #FFF;
}
}
&.is-contextopen {
background-color: mc('blue', '500');
color: #FFF;
> span, strong {
color: #FFF;
}
}
}
}
.editor-modal-imagealign { .editor-modal-imagealign {
.control > span { .control > span {
@ -215,6 +294,8 @@
overflow-x: hidden; overflow-x: hidden;
} }
// CODE MIRROR
.CodeMirror { .CodeMirror {
border-left: none; border-left: none;
border-right: none; border-right: none;
@ -245,16 +326,18 @@
font-size: 14px; font-size: 14px;
} }
// ACE EDITOR
.ace-container { .ace-container {
position: relative; position: relative;
} }
.ace_scroller { /*.ace_scroller {
width: 100%; width: 100%;
} }
.ace_content { .ace_content {
height: 100%; height: 100%;
} }*/
#page-type-source .ace-container { #page-type-source .ace-container {
min-height: 95vh; min-height: 95vh;
@ -267,13 +350,6 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
#codeblock-editor {
width: 100%;
height: 100%;
min-height: 500px;
}
} }
#source-display, #codeblock-editor { #source-display, #codeblock-editor {
@ -282,27 +358,4 @@
left: 0; left: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
}
.modallayer {
position: fixed;
top: 100px;
width: 100%;
background-color: rgba(255,255,255,0.95);
border-bottom: 1px solid mc('grey', '500');
z-index: 6;
padding: 20px;
border-bottom: 1px solid #CCC;
box-shadow: 0 2px 3px rgba(17,17,17,.1);
display: none;
> h3, .column > h3 {
color: mc('grey', '700');
font-size: 24px;
font-weight: 300;
}
}
.modallayer-content {
} }

View File

@ -31,6 +31,15 @@ paths:
repo: ./repo repo: ./repo
data: ./data data: ./data
# ---------------------------------------------------------------------
# Upload Limits
# ---------------------------------------------------------------------
# In megabytes (MB)
uploads:
maxImageFileSize: 3
maxOtherFileSize: 100
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# Site Language # Site Language
# --------------------------------------------------------------------- # ---------------------------------------------------------------------

View File

@ -53,7 +53,7 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
let destFilename = ''; let destFilename = '';
let destFilePath = ''; let destFilePath = '';
return lcdata.validateUploadsFilename(f.originalname, destFolder).then((fname) => { return lcdata.validateUploadsFilename(f.originalname, destFolder, true).then((fname) => {
destFilename = fname; destFilename = fname;
destFilePath = path.resolve(destFolderPath, destFilename); destFilePath = path.resolve(destFolderPath, destFilename);
@ -106,6 +106,61 @@ router.post('/img', lcdata.uploadImgHandler, (req, res, next) => {
}); });
router.post('/file', lcdata.uploadFileHandler, (req, res, next) => {
let destFolder = _.chain(req.body.folder).trim().toLower().value();
upl.validateUploadsFolder(destFolder).then((destFolderPath) => {
if(!destFolderPath) {
res.json({ ok: false, msg: 'Invalid Folder' });
return true;
}
Promise.map(req.files, (f) => {
let destFilename = '';
let destFilePath = '';
return lcdata.validateUploadsFilename(f.originalname, destFolder, false).then((fname) => {
destFilename = fname;
destFilePath = path.resolve(destFolderPath, destFilename);
//-> Move file to final destination
return fs.moveAsync(f.path, destFilePath, { clobber: false });
}).then(() => {
return {
ok: true,
filename: destFilename,
filesize: f.size
};
}).reflect();
}, {concurrency: 3}).then((results) => {
let uplResults = _.map(results, (r) => {
if(r.isFulfilled()) {
return r.value();
} else {
return {
ok: false,
msg: r.reason().message
};
}
});
res.json({ ok: true, results: uplResults });
return true;
}).catch((err) => {
res.json({ ok: false, msg: err.message });
return true;
});
});
});
router.get('/*', (req, res, next) => { router.get('/*', (req, res, next) => {
let fileName = req.params[0]; let fileName = req.params[0];

View File

@ -42,6 +42,13 @@ module.exports = (socket) => {
}); });
}); });
socket.on('uploadsGetFiles', (data, cb) => {
cb = cb || _.noop;
upl.getUploadsFiles('binary', data.folder).then((f) => {
return cb(f) || true;
});
});
socket.on('uploadsDeleteFile', (data, cb) => { socket.on('uploadsDeleteFile', (data, cb) => {
cb = cb || _.noop; cb = cb || _.noop;
upl.deleteUploadsFile(data.uid).then((f) => { upl.deleteUploadsFile(data.uid).then((f) => {

View File

@ -1,4 +0,0 @@
.sidebar {
background-color: #FFF;
}

View File

@ -4,6 +4,7 @@ var path = require('path'),
Promise = require('bluebird'), Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs-extra')), fs = Promise.promisifyAll(require('fs-extra')),
multer = require('multer'), multer = require('multer'),
os = require('os'),
_ = require('lodash'); _ = require('lodash');
/** /**
@ -44,6 +45,13 @@ module.exports = {
*/ */
initMulter(appconfig) { initMulter(appconfig) {
let maxFileSizes = {
img: appconfig.uploads.maxImageFileSize * 1024 * 1024,
file: appconfig.uploads.maxOtherFileSize * 1024 * 1024
};
//-> IMAGES
this.uploadImgHandler = multer({ this.uploadImgHandler = multer({
storage: multer.diskStorage({ storage: multer.diskStorage({
destination: (req, f, cb) => { destination: (req, f, cb) => {
@ -52,9 +60,9 @@ module.exports = {
}), }),
fileFilter: (req, f, cb) => { fileFilter: (req, f, cb) => {
//-> Check filesize (3 MB max) //-> Check filesize
if(f.size > 3145728) { if(f.size > maxFileSizes.img) {
return cb(null, false); return cb(null, false);
} }
@ -68,6 +76,26 @@ module.exports = {
} }
}).array('imgfile', 20); }).array('imgfile', 20);
//-> FILES
this.uploadFileHandler = multer({
storage: multer.diskStorage({
destination: (req, f, cb) => {
cb(null, path.resolve(ROOTPATH, appconfig.paths.data, 'temp-upload'));
}
}),
fileFilter: (req, f, cb) => {
//-> Check filesize
if(f.size > maxFileSizes.file) {
return cb(null, false);
}
cb(null, true);
}
}).array('binfile', 20);
return true; return true;
}, },
@ -88,8 +116,17 @@ module.exports = {
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs')); fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './thumbs'));
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload')); fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'));
if(os.type() !== 'Windows_NT') {
fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.data, './temp-upload'), '644');
}
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo)); fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo));
fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads')); fs.ensureDirSync(path.resolve(ROOTPATH, appconfig.paths.repo, './uploads'));
if(os.type() !== 'Windows_NT') {
fs.chmodSync(path.resolve(ROOTPATH, appconfig.paths.repo, './upload'), '644');
}
} catch (err) { } catch (err) {
winston.error(err); winston.error(err);
} }
@ -125,13 +162,13 @@ module.exports = {
* @param {String} fld The containing folder * @param {String} fld The containing folder
* @return {Promise<String>} Promise of the accepted filename * @return {Promise<String>} Promise of the accepted filename
*/ */
validateUploadsFilename(f, fld) { validateUploadsFilename(f, fld, isImage) {
let fObj = path.parse(f); let fObj = path.parse(f);
let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(/[^a-z0-9\-]+/g, ''); let fname = _.chain(fObj.name).trim().toLower().kebabCase().value().replace(/[^a-z0-9\-]+/g, '');
let fext = _.toLower(fObj.ext); let fext = _.toLower(fObj.ext);
if(!_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) { if(isImage && !_.includes(['.jpg', '.jpeg', '.png', '.gif', '.webp'], fext)) {
fext = '.png'; fext = '.png';
} }

View File

@ -5,6 +5,7 @@ var path = require('path'),
fs = Promise.promisifyAll(require('fs-extra')), fs = Promise.promisifyAll(require('fs-extra')),
readChunk = require('read-chunk'), readChunk = require('read-chunk'),
fileType = require('file-type'), fileType = require('file-type'),
mime = require('mime-types'),
farmhash = require('farmhash'), farmhash = require('farmhash'),
moment = require('moment'), moment = require('moment'),
chokidar = require('chokidar'), chokidar = require('chokidar'),
@ -199,6 +200,11 @@ module.exports = {
// Get MIME info // Get MIME info
let mimeInfo = fileType(readChunk.sync(fPath, 0, 262)); let mimeInfo = fileType(readChunk.sync(fPath, 0, 262));
if(_.isNil(mimeInfo)) {
mimeInfo = {
mime: mime.lookup(fPathObj.ext) || 'application/octet-stream'
};
}
// Images // Images
@ -244,7 +250,7 @@ module.exports = {
_id: fUid, _id: fUid,
category: 'binary', category: 'binary',
mime: mimeInfo.mime, mime: mimeInfo.mime,
folder: fldName, folder: 'f:' + fldName,
filename: f, filename: f,
basename: fPathObj.name, basename: fPathObj.name,
filesize: s.size filesize: s.size

View File

@ -43,7 +43,7 @@
"connect-flash": "^0.1.1", "connect-flash": "^0.1.1",
"connect-mongo": "^1.3.2", "connect-mongo": "^1.3.2",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"cron": "^1.1.1", "cron": "^1.2.1",
"express": "^4.14.0", "express": "^4.14.0",
"express-brute": "^1.0.0", "express-brute": "^1.0.0",
"express-brute-mongoose": "0.0.7", "express-brute-mongoose": "0.0.7",
@ -53,13 +53,13 @@
"filesize.js": "^1.0.2", "filesize.js": "^1.0.2",
"fs-extra": "^1.0.0", "fs-extra": "^1.0.0",
"git-wrapper2-promise": "^0.2.9", "git-wrapper2-promise": "^0.2.9",
"highlight.js": "^9.8.0", "highlight.js": "^9.9.0",
"i18next": "^4.1.1", "i18next": "^4.1.1",
"i18next-express-middleware": "^1.0.2", "i18next-express-middleware": "^1.0.2",
"i18next-node-fs-backend": "^0.1.3", "i18next-node-fs-backend": "^0.1.3",
"js-yaml": "^3.7.0", "js-yaml": "^3.7.0",
"lodash": "^4.17.2", "lodash": "^4.17.2",
"markdown-it": "^8.2.1", "markdown-it": "^8.2.2",
"markdown-it-abbr": "^1.0.4", "markdown-it-abbr": "^1.0.4",
"markdown-it-anchor": "^2.6.0", "markdown-it-anchor": "^2.6.0",
"markdown-it-attrs": "^0.8.0", "markdown-it-attrs": "^0.8.0",
@ -68,10 +68,11 @@
"markdown-it-external-links": "0.0.6", "markdown-it-external-links": "0.0.6",
"markdown-it-footnote": "^3.0.1", "markdown-it-footnote": "^3.0.1",
"markdown-it-task-lists": "^1.4.1", "markdown-it-task-lists": "^1.4.1",
"mime-types": "^2.1.13",
"moment": "^2.17.1", "moment": "^2.17.1",
"moment-timezone": "^0.5.10", "moment-timezone": "^0.5.10",
"mongoose": "^4.7.2", "mongoose": "^4.7.3",
"multer": "^1.2.0", "multer": "^1.2.1",
"passport": "^0.3.2", "passport": "^0.3.2",
"passport-facebook": "^2.1.1", "passport-facebook": "^2.1.1",
"passport-google-oauth20": "^1.0.0", "passport-google-oauth20": "^1.0.0",
@ -87,8 +88,7 @@
"serve-favicon": "^2.3.2", "serve-favicon": "^2.3.2",
"sharp": "^0.16.1", "sharp": "^0.16.1",
"simplemde": "^1.11.2", "simplemde": "^1.11.2",
"snyk": "^1.19.1", "socket.io": "^1.7.2",
"socket.io": "^1.6.0",
"sticky-js": "^1.0.7", "sticky-js": "^1.0.7",
"validator": "^6.2.0", "validator": "^6.2.0",
"validator-as-promised": "^1.0.2", "validator-as-promised": "^1.0.2",
@ -100,17 +100,16 @@
"chai": "^3.5.0", "chai": "^3.5.0",
"chai-as-promised": "^6.0.0", "chai-as-promised": "^6.0.0",
"codacy-coverage": "^2.0.0", "codacy-coverage": "^2.0.0",
"filesize.js": "^1.0.1",
"font-awesome": "^4.6.3", "font-awesome": "^4.6.3",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-babel": "^6.1.2", "gulp-babel": "^6.1.2",
"gulp-clean-css": "^2.2.1", "gulp-clean-css": "^2.3.2",
"gulp-concat": "^2.6.1", "gulp-concat": "^2.6.1",
"gulp-gzip": "^1.4.0", "gulp-gzip": "^1.4.0",
"gulp-include": "^2.3.1", "gulp-include": "^2.3.1",
"gulp-nodemon": "^2.2.1", "gulp-nodemon": "^2.2.1",
"gulp-plumber": "^1.1.0", "gulp-plumber": "^1.1.0",
"gulp-sass": "^2.3.2", "gulp-sass": "^3.0.0",
"gulp-tar": "^1.9.0", "gulp-tar": "^1.9.0",
"gulp-uglify": "^2.0.0", "gulp-uglify": "^2.0.0",
"gulp-watch": "^4.3.11", "gulp-watch": "^4.3.11",
@ -125,10 +124,10 @@
"mocha-lcov-reporter": "^1.2.0", "mocha-lcov-reporter": "^1.2.0",
"nodemon": "^1.11.0", "nodemon": "^1.11.0",
"run-sequence": "^1.2.2", "run-sequence": "^1.2.2",
"snyk": "^1.21.2", "snyk": "^1.22.1",
"sticky-js": "^1.1.6", "sticky-js": "^1.1.6",
"twemoji-awesome": "^1.0.4", "twemoji-awesome": "^1.0.4",
"vue": "^2.1.4" "vue": "^2.1.6"
}, },
"snyk": true "snyk": true
} }

View File

@ -0,0 +1,80 @@
.modal#modal-editor-file
.modal-background
.modal-container
.modal-content.is-expanded
header.is-green
span Insert File
p.modal-notify(v-bind:class="{ 'is-active': isLoading }")
span {{ isLoadingText }}
i
.modal-toolbar.is-green
a.button(v-on:click="newFolder")
i.fa.fa-folder
span New Folder
a.button#btn-editor-file-upload
i.fa.fa-upload
span Upload File
label
input(type="file", multiple)
section.is-gapless
.columns.is-stretched
.column.is-one-quarter.modal-sidebar.is-green(style={'max-width':'350px'})
.model-sidebar-header Folders
ul.model-sidebar-list
li(v-for="fld in folders")
a(v-on:click="selectFolder(fld)", v-bind:class="{ 'is-active': currentFolder === fld }")
i.icon-folder2
span / {{ fld }}
.column.editor-modal-file-choices
figure(v-for="fl in files", v-bind:class="{ 'is-active': currentFile === fl._id }", v-on:click="selectFile(fl._id)", v-bind:data-uid="fl._id")
i(class='icon-file')
span: strong {{ fl.filename }}
span {{ fl.mime }}
span {{ fl.filesize | filesize }}
em(v-show="files.length < 1")
i.icon-marquee-minus
| This folder is empty.
footer
a.button.is-grey.is-outlined(v-on:click="cancel") Discard
a.button.is-green(v-on:click="insertFileLink") Insert Link to File
.modal.is-superimposed(v-bind:class="{ 'is-active': newFolderShow }")
.modal-background
.modal-container
.modal-content
header.is-light-blue New Folder
section
label.label Enter the new folder name:
p.control.is-fullwidth
input.input#txt-editor-file-newfoldername(type='text', placeholder='folder name', v-model='newFolderName', v-on:keyup.enter="newFolderCreate", v-on:keyup.esc="newFolderDiscard")
span.help.is-danger(v-show="newFolderError") This folder name is invalid!
footer
a.button.is-grey.is-outlined(v-on:click="newFolderDiscard") Discard
a.button.is-light-blue(v-on:click="newFolderCreate") Create
.modal.is-superimposed(v-bind:class="{ 'is-active': renameFileShow }")
.modal-background
.modal-container
.modal-content
header.is-indigo Rename File
section
label.label Enter the new filename (without the extension) of the file:
p.control.is-fullwidth
input.input#txt-editor-file-rename(type='text', placeholder='filename', v-model='renameFileFilename')
span.help.is-danger.is-hidden This filename is invalid!
footer
a.button.is-grey.is-outlined(v-on:click="renameFileDiscard") Discard
a.button.is-light-blue(v-on:click="renameFileGo") Rename
.modal.is-superimposed(v-bind:class="{ 'is-active': deleteFileShow }")
.modal-background
.modal-container
.modal-content
header.is-red Delete file?
section
span Are you sure you want to delete #[strong {{deleteFileFilename}}]?
footer
a.button.is-grey.is-outlined(v-on:click="deleteFileWarn(false)") Discard
a.button.is-red(v-on:click="deleteFileGo") Delete

View File

@ -13,7 +13,7 @@
a.button(v-on:click="newFolder") a.button(v-on:click="newFolder")
i.fa.fa-folder i.fa.fa-folder
span New Folder span New Folder
a.button#btn-editor-uploadimage a.button#btn-editor-image-upload
i.fa.fa-upload i.fa.fa-upload
span Upload Image span Upload Image
label label
@ -38,7 +38,7 @@
option(value='center') Centered option(value='center') Centered
option(value='right') Right option(value='right') Right
option(value='logo') Page Logo option(value='logo') Page Logo
.column.editor-modal-imagechoices .column.editor-modal-image-choices
figure(v-for="img in images", v-bind:class="{ 'is-active': currentImage === img._id }", v-on:click="selectImage(img._id)", v-bind:data-uid="img._id") figure(v-for="img in images", v-bind:class="{ 'is-active': currentImage === img._id }", v-on:click="selectImage(img._id)", v-bind:data-uid="img._id")
img(v-bind:src="'/uploads/t/' + img._id + '.png'") img(v-bind:src="'/uploads/t/' + img._id + '.png'")
span: strong {{ img.basename }} span: strong {{ img.basename }}
@ -58,7 +58,7 @@
section section
label.label Enter the new folder name: label.label Enter the new folder name:
p.control.is-fullwidth p.control.is-fullwidth
input.input#txt-editor-newfoldername(type='text', placeholder='folder name', v-model='newFolderName', v-on:keyup.enter="newFolderCreate", v-on:keyup.esc="newFolderDiscard") input.input#txt-editor-image-newfoldername(type='text', placeholder='folder name', v-model='newFolderName', v-on:keyup.enter="newFolderCreate", v-on:keyup.esc="newFolderDiscard")
span.help.is-danger(v-show="newFolderError") This folder name is invalid! span.help.is-danger(v-show="newFolderError") This folder name is invalid!
footer footer
a.button.is-grey.is-outlined(v-on:click="newFolderDiscard") Discard a.button.is-grey.is-outlined(v-on:click="newFolderDiscard") Discard
@ -72,7 +72,7 @@
section section
label.label Enter full URL path to the image: label.label Enter full URL path to the image:
p.control.is-fullwidth p.control.is-fullwidth
input.input#txt-editor-fetchimgurl(type='text', placeholder='http://www.example.com/some-image.png', v-model='fetchFromUrlURL') input.input#txt-editor-image-fetchurl(type='text', placeholder='http://www.example.com/some-image.png', v-model='fetchFromUrlURL')
span.help.is-danger.is-hidden This URL path is invalid! span.help.is-danger.is-hidden This URL path is invalid!
footer footer
a.button.is-grey.is-outlined(v-on:click="fetchFromUrlDiscard") Discard a.button.is-grey.is-outlined(v-on:click="fetchFromUrlDiscard") Discard
@ -86,7 +86,7 @@
section section
label.label Enter the new filename (without the extension) of the image: label.label Enter the new filename (without the extension) of the image:
p.control.is-fullwidth p.control.is-fullwidth
input.input#txt-editor-renameimage(type='text', placeholder='filename', v-model='renameImageFilename') input.input#txt-editor-image-rename(type='text', placeholder='filename', v-model='renameImageFilename')
span.help.is-danger.is-hidden This filename is invalid! span.help.is-danger.is-hidden This filename is invalid!
footer footer
a.button.is-grey.is-outlined(v-on:click="renameImageDiscard") Discard a.button.is-grey.is-outlined(v-on:click="renameImageDiscard") Discard

View File

@ -1,5 +1,5 @@
.modallayer#modal-editor-link //.modallayer#modal-editor-link
.modallayer-content .modallayer-content
.tabs.is-boxed .tabs.is-boxed
ul ul

View File

@ -22,4 +22,5 @@ block content
include ../modals/edit-discard.pug include ../modals/edit-discard.pug
include ../modals/editor-link.pug include ../modals/editor-link.pug
include ../modals/editor-image.pug include ../modals/editor-image.pug
include ../modals/editor-file.pug
include ../modals/editor-codeblock.pug include ../modals/editor-codeblock.pug