Skip to content

Using custom editor to edit files within elfinder

Kenston Choi edited this page Aug 17, 2022 · 25 revisions

edit command supports any (I hope) WYSIWYG.

CodeMirror 5

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();
                }
            }
        ]
    }
}

ACE Editor

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();
                }
            }
        ]
    }
}

TinyMCE 4

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();
                }
            }
        ]
    }
}

CKEditor 4

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());
                        }
                    }
                }
            }
        ]
    }
}

Notes on using TinyMCE v 4.x as custom editor

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.

Clone this wiki locally