-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Using custom editor to edit files within elfinder
edit
command supports any (I hope) WYSIWYG.
commandsOptions: {
edit: {
editors : [
{
// CodeMirror
// `mimes` is not set for support everything kind of text file
load : function(textarea) {
var cmUrl = '//cdnjs.cloudflare.com/ajax/libs/codemirror/5.26.0/',
dfrd = $.Deferred(),
self = this,
init = function() {
var ta = $(textarea),
base = ta.parent(),
editorBase;
// set base height
base.height(base.height());
// CodeMirror configure
editor = CodeMirror.fromTextArea(textarea, {
lineNumbers: true,
lineWrapping: true
});
// return editor instance
dfrd.resolve(editor);
// Auto mode set
var info, m, mode, spec;
if (! info) {
info = CodeMirror.findModeByMIME(self.file.mime);
}
if (! info && (m = self.file.name.match(/.+\.([^.]+)$/))) {
info = CodeMirror.findModeByExtension(m[1]);
}
if (info) {
CodeMirror.modeURL = cmUrl + 'mode/%N/%N.js';
mode = info.mode;
spec = info.mime;
editor.setOption('mode', spec);
CodeMirror.autoLoadMode(editor, mode);
// show MIME:mode in title bar
base.prev().children('.elfinder-dialog-title').append(' (' + spec + ' : ' + mode + ')');
}
// editor base node
editorBase = $(editor.getWrapperElement());
ta.data('cm', true);
// fit height to base
editorBase.height('100%');
// TextArea button and Setting button
$('<div class="ui-dialog-buttonset"/>').css('float', 'left')
.append(
$('<button>TextArea</button>')
.button()
.on('click', function(){
if (ta.data('cm')) {
ta.removeData('cm');
editorBase.hide();
ta.val(editor.getValue()).show().focus();
$(this).text('CodeMirror');
} else {
ta.data('cm', true);
editorBase.show();
editor.setValue(ta.hide().val());
editor.refresh();
editor.focus();
$(this).text('TextArea');
}
})
)
.prependTo(base.next());
};
// load script then init
if (typeof CodeMirror === 'undefined') {
this.fm.loadScript([
cmUrl + 'codemirror.min.js',
cmUrl + 'addon/mode/loadmode.js',
cmUrl + 'mode/meta.js'
], init);
this.fm.loadCss(cmUrl + 'codemirror.css');
} else {
init();
}
return dfrd;
},
close : function(textarea, instance) {
instance && instance.toTextArea();
},
save : function(textarea, instance) {
instance && $(textarea).data('cm') && (textarea.value = instance.getValue());
},
focus : function(textarea, instance) {
instance && $(textarea).data('cm') && instance.focus();
},
resize : function(textarea, instance, e, data) {
instance && instance.refresh();
}
}
]
}
}
commandsOptions: {
edit: {
editors : [
{
// ACE Editor
// `mimes` is not set for support everything kind of text file
load : function(textarea) {
var self = this,
dfrd = $.Deferred(),
cdn = '//cdnjs.cloudflare.com/ajax/libs/ace/1.2.5',
init = function() {
if (typeof ace === 'undefined') {
self.fm.loadScript([
cdn+'/ace.js',
cdn+'/ext-modelist.js',
cdn+'/ext-settings_menu.js',
cdn+'/ext-language_tools.js'
], start);
} else {
start();
}
},
start = function() {
var editor, editorBase, mode,
ta = $(textarea),
taBase = ta.parent(),
dialog = taBase.parent(),
id = textarea.id + '_ace',
ext = self.file.name.replace(/^.+\.([^.]+)|(.+)$/, '$1$2').toLowerCase(),
// MIME/mode map
mimeMode = {
'text/x-php' : 'php',
'application/x-php' : 'php',
'text/html' : 'html',
'application/xhtml+xml' : 'html',
'text/javascript' : 'javascript',
'application/javascript' : 'javascript',
'text/css' : 'css',
'text/x-c' : 'c_cpp',
'text/x-csrc' : 'c_cpp',
'text/x-chdr' : 'c_cpp',
'text/x-c++' : 'c_cpp',
'text/x-c++src' : 'c_cpp',
'text/x-c++hdr' : 'c_cpp',
'text/x-shellscript' : 'sh',
'application/x-csh' : 'sh',
'text/x-python' : 'python',
'text/x-java' : 'java',
'text/x-java-source' : 'java',
'text/x-ruby' : 'ruby',
'text/x-perl' : 'perl',
'application/x-perl' : 'perl',
'text/x-sql' : 'sql',
'text/xml' : 'xml',
'application/docbook+xml' : 'xml',
'application/xml' : 'xml'
};
// set basePath of ace
ace.config.set('basePath', cdn);
// set base height
taBase.height(taBase.height());
// detect mode
mode = ace.require('ace/ext/modelist').getModeForPath('/' + self.file.name).name;
if (mode === 'text') {
if (mimeMode[self.file.mime]) {
mode = mimeMode[self.file.mime];
}
}
// show MIME:mode in title bar
taBase.prev().children('.elfinder-dialog-title').append(' (' + self.file.mime + ' : ' + mode.split(/[\/\\]/).pop() + ')');
// TextArea button and Setting button
$('<div class="ui-dialog-buttonset"/>').css('float', 'left')
.append(
$('<button>TextArea</button>')
.button()
.on('click', function(){
if (ta.data('ace')) {
ta.removeData('ace');
editorBase.hide();
ta.val(editor.session.getValue()).show().focus();
$(this).text('AceEditor');
} else {
ta.data('ace', true);
editorBase.show();
editor.setValue(ta.hide().val(), -1);
editor.focus();
$(this).text('TextArea');
}
})
)
.append(
$('<button>Ace editor setting</button>')
.button({
icons: {
primary: 'ui-icon-gear',
secondary: 'ui-icon-triangle-1-e'
},
text: false
})
.on('click', function(){
editor.showSettingsMenu();
})
)
.prependTo(taBase.next());
// Base node of Ace editor
editorBase = $('<div id="'+id+'" style="width:100%; height:100%;"/>').text(ta.val()).insertBefore(ta.hide());
// Ace editor configure
ta.data('ace', true);
editor = ace.edit(id);
ace.require('ace/ext/language_tools');
ace.require('ace/ext/settings_menu').init(editor);
editor.$blockScrolling = Infinity;
editor.setOptions({
theme: 'ace/theme/monokai',
mode: 'ace/mode/' + mode,
fontSize: '14px',
wrap: true,
enableBasicAutocompletion: true,
enableSnippets: true,
enableLiveAutocompletion: false
});
editor.commands.addCommand({
name : "saveFile",
bindKey: {
win : 'Ctrl-s',
mac : 'Command-s'
},
exec: function(editor) {
self.doSave();
}
});
editor.commands.addCommand({
name : "closeEditor",
bindKey: {
win : 'Ctrl-w|Ctrl-q',
mac : 'Command-w|Command-q'
},
exec: function(editor) {
self.doCancel();
}
});
editor.resize();
dfrd.resolve(editor);
};
// init & start
init();
return dfrd;
},
close : function(textarea, instance) {
if (instance) {
instance.destroy();
$(textarea).show();
}
},
save : function(textarea, instance) {
instance && $(textarea).data('ace') && (textarea.value = instance.session.getValue());
},
focus : function(textarea, instance) {
instance && $(textarea).data('ace') && instance.focus();
},
resize : function(textarea, instance, e, data) {
instance && instance.resize();
}
}
]
}
}
commandsOptions: {
edit: {
editors : [
{
// TinyMCE for html file
mimes : ['text/html'],
exts : ['htm', 'html', 'xhtml'],
load : function(textarea) {
var dfrd = $.Deferred(),
init = function(loaded) {
var base = $(textarea).parent();
h = base.height(),
delta = base.outerHeight(true) - h;
// set base height
base.height(h);
// fit height function
textarea._setHeight = function(h) {
var base = $(this).parent(),
h = h || base.height(),
ctrH = 0,
areaH;
base.find('.mce-container-body:first').children('.mce-toolbar,.mce-toolbar-grp,.mce-statusbar').each(function() {
ctrH += $(this).outerHeight(true);
});
areaH = h - ctrH - delta;
base.find('.mce-edit-area iframe:first').height(areaH);
return areaH;
};
// TinyMCE configure
tinymce.init({
selector: '#' + textarea.id,
plugins: [
'fullpage' // require for getting full HTML
],
init_instance_callback : function(editor) {
// fit height on init
setTimeout(function() {
textarea._setHeight(h);
}, loaded? 0 : 500);
// return editor instance
dfrd.resolve(editor);
}
});
};
if (typeof tinymce === 'undefined') {
$.getScript('//cdnjs.cloudflare.com/ajax/libs/tinymce/4.6.2/tinymce.min.js', init);
} else {
init(true);
}
return dfrd;
},
close : function(textarea, instance) {
instance && tinymce.execCommand('mceRemoveEditor', false, textarea.id);
},
save : function(textarea, instance) {
instance && instance.save();
},
focus : function(textarea, instance) {
instance && instance.focus();
},
resize : function(textarea, instance, e, data) {
var self;
if (instance && data && typeof data.minimize !== 'undefined') {
// for dialog minimize function
if (data.minimize === 'on') {
// destroy on minimized
tinymce.execCommand('mceRemoveEditor', false, textarea.id);
} else {
// rebuild editor
self = this;
this.load(textarea).done(function(editor) {
self.instance = editor;
});
}
return;
}
// fit height to base node on dialog resize
textarea._setHeight();
}
}
]
}
}
commandsOptions: {
edit: {
editors : [
{
// CKEditor for html file
mimes : ['text/html'],
exts : ['htm', 'html', 'xhtml'],
load : function(textarea) {
var dfrd = $.Deferred(),
init = function() {
var base = $(textarea).parent(),
h = base.height();
// set base height
base.height(h);
// CKEditor configure
CKEDITOR.replace(textarea.id, {
startupFocus : true,
fullPage: true,
allowedContent: true,
on: {
'instanceReady' : function(e) {
e.editor.resize('100%', h);
// return editor instance
dfrd.resolve(e.editor);
}
}
});
};
if (typeof CKEDITOR === 'undefined') {
$.getScript('//cdnjs.cloudflare.com/ajax/libs/ckeditor/4.6.0/ckeditor.js', init);
} else {
init();
}
return dfrd;
},
close : function(textarea, instance) {
instance && instance.destroy();
},
save : function(textarea, instance) {
instance && (textarea.value = instance.getData());
},
focus : function(textarea, instance) {
instance && instance.focus();
},
resize : function(textarea, instance, e, data) {
var self;
if (instance) {
if (typeof data.minimize !== 'undefined') {
// for dialog minimize function
if (data.minimize === 'on') {
// destroy on minimized
instance.destroy();
} else {
// rebuild editor
self = this;
this.load(textarea).done(function(editor) {
self.instance = editor;
});
}
return;
}
if (instance.status === 'ready') {
instance.resize('100%', $(textarea).parent().height());
}
}
}
}
]
}
}
There are a couple of possible challenges with the code above.
The portion of the example that covers save has two issues.
First, the buttons shown to the user at the bottom of the edit dialog are "Cancel", "Save & Close" and "Save". The save code above does both a save and a close on the editor. To match the expected behavior, remove the "mceRemoveEditor" line.
Second, the example only grabs the selected area. To get the results you expect, remove ".selection" from the other line in the save function.
The results look like this:
save : function(textarea, editor, trkt ) {
textarea.value = tinymce.get(textarea.id).getContent({format : 'html'});
}
The second challenge is that part of the html cleanup TinyMCE does when loading content is to strip out everything that's not within the body tags.
So this html file:
<html>
<head>
...
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
Becomes this:
<h1>Hello World</h1>
When that's saved, the mime type elFinder gets from the server is no longer text/html so elFinder won't allow you to edit the file as html.
One way to change this behavior is to use the full elfinder js file (e.g. js/elfinder.full.js) and modify it to remember what's changed. Modify the open portion of dialog function in elFinder.prototype.commands.edit to pass the file content back to your elfinder.html file (change marked by comments):
elFinder.prototype.commands.edit = function() {
...
dialog = function(id, file, content) {
var dfrd = $.Deferred(),
...
open : function() {
fm.disable();
ta.focus();
ta[0].setSelectionRange && ta[0].setSelectionRange(0, 0);
if (ta.editor) {
// tim 4til7 wood, 17 Feb 2016
// pass the content back to the editor
// so html outside body tags can be saved later
ta.editor.instance = ta.editor.load(ta[0], content ) || null;
And then remember, as well as restore, the html that tinyMCE will strip out in elfinder.html. Before you initialize elFinder:
var _o = {};
// remember what wraps the content
_o.remember = function( content ) {
var o = _o;
var b1 = content.indexOf( '<body', 0 ) + 1;
var b2 = ( b1 > -1 ) ? content.indexOf( '>', b1 ) + 1 : -1;
var bC = ( b2 > -1 ) ? content.substring( 0, b2 ) : '';
var a1 = content.lastIndexOf('</body');
var aC = ( a1 > -1 ) ? content.substring( a1 ) : '';
o.trkr = {
//content : content,
surroundingContent : {
before : bC,
after : aC
}
};
};
Call the rememember function in your load function in commandOptions:
commandsOptions : {
edit : {
...
editors : [
...
load : function( textarea, content ) {
var o = _o;
o.remember( content );
tinymce.execCommand('mceAddEditor', false, textarea.id);
},
Then add the stripped html back in your save function:
commandsOptions : {
edit : {
...
editors : [
save : function(textarea, editor ) {
var o = _o;
textarea.value = o.trkr.surroundingContent.before
+ tinymce.get(textarea.id).getContent({format : 'html'})
+ o.trkr.surroundingContent.after;
}
Because we're passing the "fixed" html via the textarea, this is an imperfect solution. Some things, depending on browser, will still get stripped out by the browser itself. To avoid this, the rewrapping would need to be done in elFinder.