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)
##### A modern, lightweight and powerful wiki app built on NodeJS, Git and Markdown
*Under development*
*Under active development*
### Documentation
- [Installation Guide](https://wiki.requarks.io/install)
- [Official Website](https://wiki.requarks.io/)
- [Installation Guide](https://wiki.requarks.io/get-started.html)
##### Milestones
- [ ] Account Management
- [x] Assets Management
- [x] Images
- [ ] Files/Documents
- [x] Files/Documents
- [x] Authentication
- [x] Strategies
- [x] Local
@ -46,6 +47,10 @@
- [x] Markdown Editor
- [x] Basic Formatting
- [ ] Links
- [x] Image Selection modal
- [x] File Selection modal
- [x] Inline Code
- [x] Code Editor modal
- [ ] Table Editor
- [x] Move Entry
- [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 codeEditor = null;
// ACE - Mode Loader
@ -33,7 +27,8 @@ let vueCodeBlock = new Vue({
el: '#modal-editor-codeblock',
data: {
modes: modelist.modesByName,
modeSelected: 'text'
modeSelected: 'text',
initContent: ''
},
watch: {
modeSelected: (val, oldVal) => {
@ -45,19 +40,28 @@ let vueCodeBlock = new Vue({
},
methods: {
open: (ev) => {
$('#modal-editor-codeblock').addClass('is-active');
_.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.setAutoScrollEditorIntoView(true);
codeEditor.renderer.updateFull();
}, 1000);
}, 300);
},
cancel: (ev) => {
mdeModalOpenState = false;
$('#modal-editor-codeblock').removeClass('is-active');
vueCodeBlock.initContent = '';
},
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.newFolderError = false;
vueImage.newFolderShow = true;
_.delay(() => { $('#txt-editor-newfoldername').focus(); }, 400);
_.delay(() => { $('#txt-editor-image-newfoldername').focus(); }, 400);
},
newFolderDiscard: (ev) => {
vueImage.newFolderShow = false;
@ -115,7 +115,7 @@ let vueImage = new Vue({
fetchFromUrl: (ev) => {
vueImage.fetchFromUrlURL = '';
vueImage.fetchFromUrlShow = true;
_.delay(() => { $('#txt-editor-fetchimgurl').focus(); }, 400);
_.delay(() => { $('#txt-editor-image-fetchurl').focus(); }, 400);
},
fetchFromUrlDiscard: (ev) => {
vueImage.fetchFromUrlShow = false;
@ -149,8 +149,8 @@ let vueImage = new Vue({
vueImage.renameImageFilename = c.basename || '';
vueImage.renameImageShow = true;
_.delay(() => {
$('#txt-editor-renameimage').focus();
_.defer(() => { $('#txt-editor-renameimage').select(); });
$('#txt-editor-image-rename').focus();
_.defer(() => { $('#txt-editor-image-rename').select(); });
}, 400);
},
renameImageDiscard: () => {
@ -301,10 +301,10 @@ let vueImage = new Vue({
};
});
$.contextMenu('destroy', '.editor-modal-imagechoices > figure');
$.contextMenu('destroy', '.editor-modal-image-choices > figure');
$.contextMenu({
selector: '.editor-modal-imagechoices > figure',
appendTo: '.editor-modal-imagechoices',
selector: '.editor-modal-image-choices > figure',
appendTo: '.editor-modal-image-choices',
position: (opt, x, y) => {
$(opt.$trigger).addClass('is-contextopen');
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;

View File

@ -13,6 +13,7 @@ if($('#mk-editor').length === 1) {
});
//=include editor-image.js
//=include editor-file.js
//=include editor-codeblock.js
var mde = new SimpleMDE({
@ -103,7 +104,9 @@ if($('#mk-editor').length === 1) {
{
name: "file",
action: (editor) => {
//todo
if(!mdeModalOpenState) {
vueFile.open();
}
},
className: "fa fa-file-text-o",
title: "Insert File",
@ -133,9 +136,7 @@ if($('#mk-editor').length === 1) {
mdeModalOpenState = true;
if(mde.codemirror.doc.somethingSelected()) {
codeEditor.setValue(mde.codemirror.doc.getSelection());
} else {
codeEditor.setValue('');
vueCodeBlock.initContent = mde.codemirror.doc.getSelection();
}
vueCodeBlock.open();
@ -170,7 +171,21 @@ if($('#mk-editor').length === 1) {
//-> Save
$('.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, {
data: {
markdown: mde.value()
@ -186,7 +201,6 @@ if($('#mk-editor').length === 1) {
}, (rXHR, rStatus, err) => {
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");
scEditor.setTheme("ace/theme/tomorrow_night");
scEditor.getSession().setMode("ace/mode/markdown");
scEditor.setOption('fontSize', '14px');
scEditor.setOption('hScrollBarAlwaysVisible', false);
scEditor.setOption('wrap', true);
scEditor.setReadOnly(true);
scEditor.renderer.updateFull();

View File

@ -67,7 +67,7 @@
}
#btn-editor-uploadimage {
#btn-editor-image-upload, #btn-editor-file-upload {
position: relative;
overflow: hidden;
@ -94,7 +94,7 @@
}
.editor-modal-imagechoices {
.editor-modal-image-choices {
display: flex;
flex-wrap: wrap;
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 {
.control > span {
@ -215,6 +294,8 @@
overflow-x: hidden;
}
// CODE MIRROR
.CodeMirror {
border-left: none;
border-right: none;
@ -245,16 +326,18 @@
font-size: 14px;
}
// ACE EDITOR
.ace-container {
position: relative;
}
.ace_scroller {
/*.ace_scroller {
width: 100%;
}
.ace_content {
height: 100%;
}
}*/
#page-type-source .ace-container {
min-height: 95vh;
@ -267,13 +350,6 @@
position: relative;
width: 100%;
height: 100%;
#codeblock-editor {
width: 100%;
height: 100%;
min-height: 500px;
}
}
#source-display, #codeblock-editor {
@ -283,26 +359,3 @@
bottom: 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
data: ./data
# ---------------------------------------------------------------------
# Upload Limits
# ---------------------------------------------------------------------
# In megabytes (MB)
uploads:
maxImageFileSize: 3
maxOtherFileSize: 100
# ---------------------------------------------------------------------
# Site Language
# ---------------------------------------------------------------------

View File

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

View File

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

View File

@ -43,7 +43,7 @@
"connect-flash": "^0.1.1",
"connect-mongo": "^1.3.2",
"cookie-parser": "^1.4.3",
"cron": "^1.1.1",
"cron": "^1.2.1",
"express": "^4.14.0",
"express-brute": "^1.0.0",
"express-brute-mongoose": "0.0.7",
@ -53,13 +53,13 @@
"filesize.js": "^1.0.2",
"fs-extra": "^1.0.0",
"git-wrapper2-promise": "^0.2.9",
"highlight.js": "^9.8.0",
"highlight.js": "^9.9.0",
"i18next": "^4.1.1",
"i18next-express-middleware": "^1.0.2",
"i18next-node-fs-backend": "^0.1.3",
"js-yaml": "^3.7.0",
"lodash": "^4.17.2",
"markdown-it": "^8.2.1",
"markdown-it": "^8.2.2",
"markdown-it-abbr": "^1.0.4",
"markdown-it-anchor": "^2.6.0",
"markdown-it-attrs": "^0.8.0",
@ -68,10 +68,11 @@
"markdown-it-external-links": "0.0.6",
"markdown-it-footnote": "^3.0.1",
"markdown-it-task-lists": "^1.4.1",
"mime-types": "^2.1.13",
"moment": "^2.17.1",
"moment-timezone": "^0.5.10",
"mongoose": "^4.7.2",
"multer": "^1.2.0",
"mongoose": "^4.7.3",
"multer": "^1.2.1",
"passport": "^0.3.2",
"passport-facebook": "^2.1.1",
"passport-google-oauth20": "^1.0.0",
@ -87,8 +88,7 @@
"serve-favicon": "^2.3.2",
"sharp": "^0.16.1",
"simplemde": "^1.11.2",
"snyk": "^1.19.1",
"socket.io": "^1.6.0",
"socket.io": "^1.7.2",
"sticky-js": "^1.0.7",
"validator": "^6.2.0",
"validator-as-promised": "^1.0.2",
@ -100,17 +100,16 @@
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"codacy-coverage": "^2.0.0",
"filesize.js": "^1.0.1",
"font-awesome": "^4.6.3",
"gulp": "^3.9.1",
"gulp-babel": "^6.1.2",
"gulp-clean-css": "^2.2.1",
"gulp-clean-css": "^2.3.2",
"gulp-concat": "^2.6.1",
"gulp-gzip": "^1.4.0",
"gulp-include": "^2.3.1",
"gulp-nodemon": "^2.2.1",
"gulp-plumber": "^1.1.0",
"gulp-sass": "^2.3.2",
"gulp-sass": "^3.0.0",
"gulp-tar": "^1.9.0",
"gulp-uglify": "^2.0.0",
"gulp-watch": "^4.3.11",
@ -125,10 +124,10 @@
"mocha-lcov-reporter": "^1.2.0",
"nodemon": "^1.11.0",
"run-sequence": "^1.2.2",
"snyk": "^1.21.2",
"snyk": "^1.22.1",
"sticky-js": "^1.1.6",
"twemoji-awesome": "^1.0.4",
"vue": "^2.1.4"
"vue": "^2.1.6"
},
"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")
i.fa.fa-folder
span New Folder
a.button#btn-editor-uploadimage
a.button#btn-editor-image-upload
i.fa.fa-upload
span Upload Image
label
@ -38,7 +38,7 @@
option(value='center') Centered
option(value='right') Right
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")
img(v-bind:src="'/uploads/t/' + img._id + '.png'")
span: strong {{ img.basename }}
@ -58,7 +58,7 @@
section
label.label Enter the new folder name:
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!
footer
a.button.is-grey.is-outlined(v-on:click="newFolderDiscard") Discard
@ -72,7 +72,7 @@
section
label.label Enter full URL path to the image:
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!
footer
a.button.is-grey.is-outlined(v-on:click="fetchFromUrlDiscard") Discard
@ -86,7 +86,7 @@
section
label.label Enter the new filename (without the extension) of the image:
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!
footer
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
.tabs.is-boxed
ul

View File

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