/* jshint browser: true */
( function( window, $ )
{
"use strict";
/*
* Represenets an editor
* @constructor
* @param {DOMNode} element - The TEXTAREA element to add the Wysiwyg to.
* @param {object} userOptions - The default options selected by the user.
*/
function Wysiwyg( element, userOptions ) {
// This calls the $ function, with the element as a parameter and
// returns the jQuery object wrapper for element. It also assigns the
// jQuery object wrapper to the property $editor on `this`.
this.selectedRange = null;
this.editor = $( element );
var editor = $( element );
var defaults = {
hotKeys: {
"Ctrl+b meta+b": "bold",
"Ctrl+i meta+i": "italic",
"Ctrl+u meta+u": "underline",
"Ctrl+z": "undo",
"Ctrl+y meta+y meta+shift+z": "redo",
"Ctrl+l meta+l": "justifyleft",
"Ctrl+r meta+r": "justifyright",
"Ctrl+e meta+e": "justifycenter",
"Ctrl+j meta+j": "justifyfull",
"Shift+tab": "outdent",
"tab": "indent"
},
toolbarSelector: "[data-role=editor-toolbar]",
commandRole: "edit",
activeToolbarClass: "btn-info",
selectionMarker: "edit-focus-marker",
selectionColor: "darkgrey",
dragAndDropImages: true,
keypressTimeout: 200,
fileUploadError: function( reason, detail ) { console.log( "File upload error", reason, detail ); }
};
var options = $.extend( true, {}, defaults, userOptions );
var toolbarBtnSelector = "a[data-" + options.commandRole + "],button[data-" + options.commandRole + "],input[type=button][data-" + options.commandRole + "]";
this.bindHotkeys( editor, options, toolbarBtnSelector );
if ( options.dragAndDropImages ) {
this.initFileDrops( editor, options, toolbarBtnSelector );
}
this.bindToolbar( editor, $( options.toolbarSelector ), options, toolbarBtnSelector );
editor.attr( "contenteditable", true )
.on( "mouseup keyup mouseout", function() {
this.saveSelection();
this.updateToolbar( editor, toolbarBtnSelector, options );
}.bind( this ) );
$( window ).bind( "touchend", function( e ) {
var isInside = ( editor.is( e.target ) || editor.has( e.target ).length > 0 ),
currentRange = this.getCurrentRange(),
clear = currentRange && ( currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset );
if ( !clear || isInside ) {
this.saveSelection();
this.updateToolbar( editor, toolbarBtnSelector, options );
}
} );
}
Wysiwyg.prototype.readFileIntoDataUrl = function( fileInfo ) {
var loader = $.Deferred(),
fReader = new FileReader();
fReader.onload = function( e ) {
loader.resolve( e.target.result );
};
fReader.onerror = loader.reject;
fReader.onprogress = loader.notify;
fReader.readAsDataURL( fileInfo );
return loader.promise();
};
Wysiwyg.prototype.cleanHtml = function( o ) {
var self = this;
if ( $( self ).data( "wysiwyg-html-mode" ) === true ) {
$( self ).html( $( self ).text() );
$( self ).attr( "contenteditable", true );
$( self ).data( "wysiwyg-html-mode", false );
}
// Strip the images with src="data:image/.." out;
if ( o === true && $( self ).parent().is( "form" ) ) {
var gGal = $( self ).html;
if ( $( gGal ).has( "img" ).length ) {
var gImages = $( "img", $( gGal ) );
var gResults = [];
var gEditor = $( self ).parent();
$.each( gImages, function( i, v ) {
if ( $( v ).attr( "src" ).match( /^data:image\/.*$/ ) ) {
gResults.push( gImages[ i ] );
$( gEditor ).prepend( "" );
$( v ).attr( "src", "postedimage/" + i );
}
} );
}
}
var html = $( self ).html();
return html && html.replace( /(
|\s|
<\/div>| )*$/, "" );
};
Wysiwyg.prototype.updateToolbar = function( editor, toolbarBtnSelector, options ) {
if ( options.activeToolbarClass ) {
$( options.toolbarSelector ).find( toolbarBtnSelector ).each( function() {
var self = $( this );
var commandArr = self.data( options.commandRole ).split( " " );
var command = commandArr[ 0 ];
// If the command has an argument and its value matches this button. == used for string/number comparison
if ( commandArr.length > 1 && document.queryCommandEnabled( command ) && document.queryCommandValue( command ) === commandArr[ 1 ] ) {
self.addClass( options.activeToolbarClass );
}
// Else if the command has no arguments and it is active
else if ( commandArr.length === 1 && document.queryCommandEnabled( command ) && document.queryCommandState( command ) ) {
self.addClass( options.activeToolbarClass );
}
// Else the command is not active
else {
self.removeClass( options.activeToolbarClass );
}
} );
}
};
Wysiwyg.prototype.execCommand = function( commandWithArgs, valueArg, editor, options, toolbarBtnSelector ) {
var commandArr = commandWithArgs.split( " " ),
command = commandArr.shift(),
args = commandArr.join( " " ) + ( valueArg || "" );
var parts = commandWithArgs.split( "-" );
if ( parts.length === 1 ) {
document.execCommand( command, false, args );
} else if ( parts[ 0 ] === "format" && parts.length === 2 ) {
document.execCommand( "formatBlock", false, parts[ 1 ] );
}
( editor ).trigger( "change" );
this.updateToolbar( editor, toolbarBtnSelector, options );
};
Wysiwyg.prototype.bindHotkeys = function( editor, options, toolbarBtnSelector ) {
var self = this;
$.each( options.hotKeys, function( hotkey, command ) {
if(!command) return;
$( editor ).keydown( hotkey, function( e ) {
if ( editor.attr( "contenteditable" ) && $( editor ).is( ":visible" ) ) {
e.preventDefault();
e.stopPropagation();
self.execCommand( command, null, editor, options, toolbarBtnSelector );
}
} ).keyup( hotkey, function( e ) {
if ( editor.attr( "contenteditable" ) && $( editor ).is( ":visible" ) ) {
e.preventDefault();
e.stopPropagation();
}
} );
} );
editor.keyup( function() { editor.trigger( "change" ); } );
};
Wysiwyg.prototype.getCurrentRange = function() {
var sel, range;
if ( window.getSelection ) {
sel = window.getSelection();
if ( sel.getRangeAt && sel.rangeCount ) {
range = sel.getRangeAt( 0 );
}
} else if ( document.selection ) {
range = document.selection.createRange();
}
return range;
};
Wysiwyg.prototype.saveSelection = function() {
this.selectedRange = this.getCurrentRange();
};
Wysiwyg.prototype.restoreSelection = function() {
var selection;
if ( window.getSelection || document.createRange ) {
selection = window.getSelection();
if ( this.selectedRange ) {
try {
selection.removeAllRanges();
}
catch ( ex ) {
document.body.createTextRange().select();
document.selection.empty();
}
selection.addRange( this.selectedRange );
}
} else if ( document.selection && this.selectedRange ) {
this.selectedRange.select();
}
};
// Adding Toggle HTML based on the work by @jd0000, but cleaned up a little to work in this context.
Wysiwyg.prototype.toggleHtmlEdit = function( editor ) {
if ( editor.data( "wysiwyg-html-mode" ) !== true ) {
var oContent = editor.html();
var editorPre = $( "
" );
$( editorPre ).append( document.createTextNode( oContent ) );
$( editorPre ).attr( "contenteditable", true );
$( editor ).html( " " );
$( editor ).append( $( editorPre ) );
$( editor ).attr( "contenteditable", false );
$( editor ).data( "wysiwyg-html-mode", true );
$( editorPre ).focus();
} else {
$( editor ).html( $( editor ).text() );
$( editor ).attr( "contenteditable", true );
$( editor ).data( "wysiwyg-html-mode", false );
$( editor ).focus();
}
};
Wysiwyg.prototype.insertFiles = function( files, options, editor, toolbarBtnSelector ) {
var self = this;
editor.focus();
$.each( files, function( idx, fileInfo ) {
if ( /^image\//.test( fileInfo.type ) ) {
$.when( self.readFileIntoDataUrl( fileInfo ) ).done( function( dataUrl ) {
self.execCommand( "insertimage", dataUrl, editor, options, toolbarBtnSelector );
editor.trigger( "image-inserted" );
} ).fail( function( e ) {
options.fileUploadError( "file-reader", e );
} );
} else {
options.fileUploadError( "unsupported-file-type", fileInfo.type );
}
} );
};
Wysiwyg.prototype.markSelection = function( input, color, options ) {
this.restoreSelection( );
if ( document.queryCommandSupported( "hiliteColor" ) ) {
document.execCommand( "hiliteColor", false, color || "transparent" );
}
this.saveSelection( );
input.data( options.selectionMarker, color );
};
Wysiwyg.prototype.bindToolbar = function( editor, toolbar, options, toolbarBtnSelector ) {
var self = this;
toolbar.find( toolbarBtnSelector ).click( function() {
self.restoreSelection( );
editor.focus();
if ( editor.data( options.commandRole ) === "html" ) {
self.toggleHtmlEdit( editor );
} else {
self.execCommand( $( this ).data( options.commandRole ), null, editor, options, toolbarBtnSelector );
}
self.saveSelection( );
} );
toolbar.find( "[data-toggle=dropdown]" ).click( this.restoreSelection( ) );
toolbar.find( "input[type=text][data-" + options.commandRole + "]" ).on( "webkitspeechchange change", function() {
var newValue = this.value; // Ugly but prevents fake double-calls due to selection restoration
this.value = "";
self.restoreSelection( );
if ( newValue ) {
editor.focus();
self.execCommand( $( this ).data( options.commandRole ), newValue, editor, options, toolbarBtnSelector );
}
self.saveSelection( );
} ).on( "focus", function() {
var input = $( this );
if ( !input.data( options.selectionMarker ) ) {
self.markSelection( input, options.selectionColor, options );
input.focus();
}
} ).on( "blur", function() {
var input = $( this );
if ( input.data( options.selectionMarker ) ) {
self.markSelection( input, false, options );
}
} );
toolbar.find( "input[type=file][data-" + options.commandRole + "]" ).change( function() {
self.restoreSelection( );
if ( this.type === "file" && this.files && this.files.length > 0 ) {
self.insertFiles( this.files, options, editor, toolbarBtnSelector );
}
self.saveSelection( );
this.value = "";
} );
};
Wysiwyg.prototype.initFileDrops = function( editor, options, toolbarBtnSelector ) {
var self = this;
editor.on( "dragenter dragover", false ).on( "drop", function( e ) {
var dataTransfer = e.originalEvent.dataTransfer;
e.stopPropagation();
e.preventDefault();
if ( dataTransfer && dataTransfer.files && dataTransfer.files.length > 0 ) {
self.insertFiles( dataTransfer.files, options, editor, toolbarBtnSelector );
}
} );
};
/*
* Represenets an editor
* @constructor
* @param {object} userOptions - The default options selected by the user.
*/
$.fn.wysiwyg = function( userOptions ) {
var wysiwyg = new Wysiwyg( this, userOptions );
};
} )( window, window.jQuery );