Skip to content

Using custom editor to edit files within elfinder

Tim Wood edited this page Feb 18, 2016 · 25 revisions

edit command supports any (I hope) WYSIWYG.

Here is simple example for TinyMCE v3.x:

// init 
tinyMCE.init({});

// elfinder options
var opts = {
  commandsOptions : {
    edit : {
      editors : [
        {
          mimes : ['text/html'],  // add here other mimes if required
          load : function(textarea) {
            tinyMCE.execCommand('mceAddControl', true, textarea.id);
          },
          close : function(textarea, instance) {
            tinyMCE.execCommand('mceRemoveControl', false, textarea.id);
          },
          save : function(textarea, editor) {
            textarea.value = tinyMCE.get(textarea.id).selection.getContent({format : 'html'});
            tinyMCE.execCommand('mceRemoveControl', false, textarea.id);
          }
        },
        {...} // probably other editors for other mime types
      ]
    }
  }
}

Options

  • mimes - enable current editor only on next mime types
  • load - calls after edit dialog shown
  • close - before dialog close
  • save - before send textarea content to backend

This is just example to get the idea how it works (I'm not TinyMCE guru so maybe somebody can fix the code).

And here is simple example for TinyMCE v 4.x as custom editor

In file 'elfinder.html':

  1. on head section add:

<script src="http://tinymce.cachefly.net/4.0/tinymce.min.js"></script>

  1. inside the '$().ready(function()' after elfinder options "$('#finder').elfinder({});" add:

tinymce.init({});

  1. inside the 'elfinder options':
  commandsOptions : {
    edit : {
      mimes : ['text/plain', 'text/html', 'text/javascript'], //types to edit
      editors : [
        {
          mimes : ['text/html'],  //types to edit with tinyMCE
          load : function(textarea) {
            tinymce.execCommand('mceAddEditor', false, textarea.id);
          },
          close : function(textarea, instance) {
            tinymce.execCommand('mceRemoveEditor', false, textarea.id);
          },
          save : function(textarea, editor) {
            textarea.value = tinyMCE.get(textarea.id).selection.getContent({format : 'html'});
            tinymce.execCommand('mceRemoveEditor', false, textarea.id);
          }
        },
        {...} // probably other editors for other mime types
      ]
    }
  }

With this method you don't have to download tinyMCE because you get it from CDN site.

Others WYSIWYGs examples are welcome.

And here is example for CKEditor4(HTML file) and Ace editor(Any text file) as custom editor on elFinder 2.1

commandsOptions : {
	edit : {
		editors : [
			{
				// CKEditor for html file
				mimes : ['text/html'],
				exts  : ['htm', 'html', 'xhtml'],
				load : function(textarea) {
					$('head').append($('<script>').attr('src', '[PATH TO]/ckeditor.js'));
					return CKEDITOR.replace( textarea.id, {
						startupFocus : true,
						fullPage: true,
						allowedContent: true
					});
				},
				close : function(textarea, instance) {
					instance.destroy();
				},
				save : function(textarea, instance) {
					textarea.value = instance.getData();
				},
				focus : function(textarea, instance) {
					instance && instance.focus();
				}
			},
			{
				// `mimes` is not set for support everything kind of text file
				load : function(textarea) {
					if (typeof ace !== 'object') {
						$('head').append($('<script>').attr('src', '[PATH TO]/ace.js'));
						$('head').append($('<script>').attr('src', '[PATH TO]/ext-modelist.js'));
						$('head').append($('<script>').attr('src', '[PATH TO]/ext-settings_menu.js'));
						$('head').append($('<script>').attr('src', '[PATH TO]/ext-language_tools.js'));
					}
					var self = this, editor, editorBase, mode,
					ta = $(textarea),
					taBase = ta.parent(),
					dialog = taBase.parent(),
					id = textarea.id + '_ace',
					ext = this.file.name.replace(/^.+\.([^.]+)|(.+)$/, '$1$2').toLowerCase(),
					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'
					},
					resize = function(){
						dialog.height($(window).height() * 0.9).trigger('posinit');
						taBase.height(dialog.height() - taBase.prev().outerHeight(true) - taBase.next().outerHeight(true) - 8);
					};
					
					mode = ace.require('ace/ext/modelist').getModeForPath('/' + self.file.name).name;
					if (mode === 'text') {
						if (mimeMode[self.file.mime]) {
							mode = mimeMode[self.file.mime];
						}
					}
					
					taBase.prev().append(' (' + self.file.mime + ' : ' + mode.split(/[\/\\]/).pop() + ')');
					
					$('<div class="ui-dialog-buttonset"/>').css('float', 'left')
					.append(
						$('<button>TextArea</button>')
						.button()
						.on('click', function(){
							if (ta.data('ace')) {
								ta.data('ace', false);
								editorBase.hide();
								ta.val(editor.session.getValue()).show().focus();
								$(this).find('span').text('AceEditor');
							} else {
								ta.data('ace', true);
								editor.setValue(ta.hide().val(), -1);
								editorBase.show();
								editor.focus();
								$(this).find('span').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());
					
					editorBase = $('<div id="'+id+'" style="width:100%; height:100%;"/>').text(ta.val()).insertBefore(ta.hide());
					
					ta.data('ace', true);
					editor = ace.edit(id);
					ace.require('ace/ext/settings_menu').init(editor);
					editor.$blockScrolling = Infinity;
					editor.setOptions({
						theme: 'ace/theme/monokai',
						mode: 'ace/mode/' + mode,
						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();
						}
					});
					dialog.on('resize', function(){ editor.resize(); });
					$(window).on('resize', function(e){
						if (e.target !== this) return;
						dialog.data('resizeTimer') && clearTimeout(dialog.data('resizeTimer'));
						dialog.data('resizeTimer', setTimeout(function(){ resize(); }, 300));
					});
					resize();
					editor.resize();
					
					return editor;
				},
				close : function(textarea, instance) {
					instance.destroy();
					$(textarea).show();
				},
				save : function(textarea, instance) {
					if ($(textarea).data('ace')) {
						$(textarea).val(instance.session.getValue());
					}
				},
				focus : function(textarea, instance) {
					instance.focus();
				}
			}
		]
	}
}

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>
<head>
   ...
</head>
<body>
 <h1>Hello World</h1>
</body>
</html>

Becomes this:

<h1>Hello World</h1>

At that point, 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. What you'll modify, specifically, is the dialog function in elFinder.prototype.commands.edit:

elFinder.prototype.commands.edit = function() {
  ...
		dialog = function(id, file, content) {
			// tim 4til7 wood, 16 Feb 2016
			// _trkr to remember what wraps the content
			if( self.options.rewrapHtmlOnSave && file.mime == "text/html" ) {
				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 ) : '';
			} else {
				var bC = '';
				var aC = '';
			}
			var _trkr = {
				//content : content,
				surroundingContent : {
					before	: bC,
					after	: aC
				}
			};
			// end remember what wraps the content

			var dfrd = $.Deferred(),

And then handle adding it back. Unfortunately, I wasn't able to track down a proper place to do this in elFinder itself, but it can be done in your file (e.g. elfinder.html) by turning on the change above (with the rewrapHtmlOnSave flag) and then wrapping the edited html with the stuff that was stripped out:

commandsOptions = {
	edit : {
		mimes : ['text/plain', 'text/html', 'text/javascript', 'text/css'], //types to edit
		rewrapHtmlOnSave : true,
		editors : [
			{
				...
				save : function(textarea, editor, _trkr ) {
					textarea.value = _trkr.surroundingContent.before
						+ tinymce.get(textarea.id).getContent({format : 'html'})
						+ _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.

An alternate solution is to handle almost everything in your code by just modifying the open command in the elfinder.full.js file (just after the elFinder.prototype.commands.edit shown above) by adding "content" as a second parameter to ta.editor.load:

		open    : function() { 
			fm.disable();
			ta.focus(); 
			ta[0].setSelectionRange && ta[0].setSelectionRange(0, 0);
			if (ta.editor) {
				ta.editor.instance = ta.editor.load( ta[0], content ) || null;
				ta.editor.focus(ta[0], ta.editor.instance);

Clone this wiki locally