🏠 

Greasy Fork is available in English.

wysiwyg.js

'wysiwyg.js' is a (uglified) 12k contenteditable-editor with no dependencies.

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.org/scripts/11012/62427/wysiwygjs.js

/**
* wysiwyg.js
*/
(function(window, document, navigator, undefined){
'use strict';
// http://stackoverflow.com/questions/97962/debounce-clicks-when-submitting-a-web-form
var debounce = function( callback, wait, cancelprevious )
{
var timeout;
return function()
{
if( timeout )
{
if( ! cancelprevious )
return ;
clearTimeout( timeout );
}
var context = this,
args = arguments;
timeout = setTimeout(
function()
{
timeout = null;
callback.apply( context, args );
}, wait );
};
};
// http://stackoverflow.com/questions/12949590/how-to-detach-event-in-ie-6-7-8-9-using-javascript
var addEvent = function( element, type, handler, useCapture )
{
if( element.addEventListener ) {
element.addEventListener( type, handler, useCapture ? true : false );
}
else if( element.attachEvent ) {
element.attachEvent( 'on' + type, handler );
}
else if( element != window )
element['on' + type] = handler;
};
var removeEvent = function( element, type, handler, useCapture )
{
if( element.removeEventListener ) {
element.removeEventListener( type, handler, useCapture ? true : false );
}
else if( element.detachEvent) {
element.detachEvent( 'on' + type, handler );
}
else if( element != window )
element['on' + type] = null;
};
// http://www.cristinawithout.com/content/function-trigger-events-javascript
var fireEvent = function( element, type, bubbles, cancelable )
{
if( document.createEvent ) {
var event = document.createEvent('Event');
event.initEvent( type, bubbles !== undefined ? bubbles : true, cancelable !== undefined ? cancelable : false );
element.dispatchEvent(event);
}
else if( document.createEventObject ) { //IE
var event = document.createEventObject();
element.fireEvent( 'on' + type, event );
}
else if( typeof(element['on' + type]) == 'function' )
element['on' + type]();
};
// prevent default
var cancelEvent = function( e )
{
if( e.preventDefault )
e.preventDefault();
else
e.returnValue = false;
if( e.stopPropagation )
e.stopPropagation();
else
e.cancelBubble = true;
return false;
};
// http://stackoverflow.com/questions/13377887/javascript-node-undefined-in-ie8-and-under
var Node_ELEMENT_NODE = typeof(Node) != 'undefined' ? Node.ELEMENT_NODE : 1;
var Node_TEXT_NODE = typeof(Node) != 'undefined' ? Node.TEXT_NODE : 3;
// http://stackoverflow.com/questions/2234979/how-to-check-in-javascript-if-one-element-is-a-child-of-another
var isOrContainsNode = function( ancestor, descendant )
{
var node = descendant;
while( node )
{
if( node === ancestor )
return true;
node = node.parentNode;
}
return false;
};
// http://stackoverflow.com/questions/667951/how-to-get-nodes-lying-inside-a-range-with-javascript
var nextNode = function( node, container )
{
if( node.firstChild )
return node.firstChild;
while( node )
{
if( node == container ) // do not walk out of the container
return null;
if( node.nextSibling )
return node.nextSibling;
node = node.parentNode;
}
return null;
};
// save/restore selection
// http://stackoverflow.com/questions/13949059/persisting-the-changes-of-range-objects-after-selection-in-html/13950376#13950376
var saveSelection = function( containerNode )
{
if( window.getSelection )
{
var sel = window.getSelection();
if( sel.rangeCount > 0 )
return sel.getRangeAt(0);
}
else if( document.selection )
{
var sel = document.selection;
return sel.createRange();
}
return null;
};
var restoreSelection = function( containerNode, savedSel )
{
if( ! savedSel )
return;
if( window.getSelection )
{
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedSel);
}
else if( document.selection )
{
savedSel.select();
}
};
// http://stackoverflow.com/questions/12603397/calculate-width-height-of-the-selected-text-javascript
// http://stackoverflow.com/questions/6846230/coordinates-of-selected-text-in-browser-page
var getSelectionRect = function()
{
if( window.getSelection )
{
var sel = window.getSelection();
if( ! sel.rangeCount )
return false;
var range = sel.getRangeAt(0).cloneRange();
if( range.getBoundingClientRect ) // Missing for Firefox 3.5+3.6
{
var rect = range.getBoundingClientRect();
// Safari 5.1 returns null, IE9 returns 0/0/0/0 if image selected
if( ! rect || (rect.left == 0 && rect.top == 0 && rect.right == 0 && rect.bottom == 0) )
return false;
return {
// Firefox returns floating-point numbers
left: parseInt(rect.left),
top: parseInt(rect.top),
width: parseInt(rect.right - rect.left),
height: parseInt(rect.bottom - rect.top)
};
}
/*
// Fall back to inserting a temporary element (only for Firefox 3.5 and 3.6)
var span = document.createElement('span');
if( span.getBoundingClientRect )
{
// Ensure span has dimensions and position by
// adding a zero-width space character
span.appendChild( document.createTextNode('\u200b') );
range.insertNode( span );
var rect = span.getBoundingClientRect();
var spanParent = span.parentNode;
spanParent.removeChild( span );
// Glue any broken text nodes back together
spanParent.normalize();
return {
left: parseInt(rect.left),
top: parseInt(rect.top),
width: parseInt(rect.right - rect.left),
height: parseInt(rect.bottom - rect.top)
};
}
*/
}
else if( document.selection )
{
var sel = document.selection;
if( sel.type != 'Control' )
{
var range = sel.createRange();
// http://javascript.info/tutorial/coordinates
// http://www.softcomplex.com/docs/get_window_size_and_scrollbar_position.html
// http://www.howtocreate.co.uk/tutorials/javascript/browserwindow
return {
left: range.boundingLeft,
top: range.boundingTop,
width: range.boundingWidth,
height: range.boundingHeight
};
}
}
return false;
};
var getSelectionCollapsed = function( containerNode )
{
if( window.getSelection )
{
var sel = window.getSelection();
if( sel.isCollapsed )
return true;
return false;
}
else if( document.selection )
{
var sel = document.selection;
if( sel.type == 'Text' )
{
var range = document.selection.createRange();
var textrange = document.body.createTextRange();
textrange.moveToElementText(containerNode);
textrange.setEndPoint('EndToStart', range);
return range.htmlText.length == 0;
}
if( sel.type == 'Control' ) // e.g. an image selected
return false;
// sel.type == 'None' -> collapsed selection
}
return true;
};
// http://stackoverflow.com/questions/7781963/js-get-array-of-all-selected-nodes-in-contenteditable-div
var getSelectedNodes = function( containerNode )
{
if( window.getSelection )
{
var sel = window.getSelection();
if( ! sel.rangeCount )
return [];
var nodes = [];
for( var i=0; i < sel.rangeCount; ++i )
{
var range = sel.getRangeAt(i),
node = range.startContainer,
endNode = range.endContainer;
while( node )
{
// add this node?
if( node != containerNode )
{
var node_inside_selection = false;
if( sel.containsNode )
node_inside_selection = sel.containsNode( node, true );
else // IE11
{
// http://stackoverflow.com/questions/5884210/how-to-find-if-a-htmlelement-is-enclosed-in-selected-text
var noderange = document.createRange();
noderange.selectNodeContents( node );
for( var i=0; i < sel.rangeCount; ++i )
{
var range = sel.getRangeAt(i);
// start after or end before -> skip node
if( range.compareBoundaryPoints(range.END_TO_START,noderange) >= 0 &&
range.compareBoundaryPoints(range.START_TO_END,noderange) <= 0 )
{
node_inside_selection = true;
break;
}
}
}
if( node_inside_selection )
nodes.push( node );
}
node = nextNode( node, node == endNode ? endNode : containerNode );
}
}
// Fallback
if( nodes.length == 0 && isOrContainsNode(containerNode,sel.focusNode) && sel.focusNode != containerNode )
nodes.push( sel.focusNode );
return nodes;
}
else if( document.selection )
{
var sel = document.selection;
if( sel.type == 'Text' )
{
var nodes = [];
var ranges = sel.createRangeCollection();
for( var i=0; i < ranges.length; ++i )
{
var range = ranges[i],
parentNode = range.parentElement(),
node = parentNode;
while( node )
{
// No clue how to detect whether a TextNode is within the selection...
// ElementNode is easy: http://stackoverflow.com/questions/5884210/how-to-find-if-a-htmlelement-is-enclosed-in-selected-text
var noderange = range.duplicate();
noderange.moveToElementText( node.nodeType != Node_ELEMENT_NODE ? node.parentNode : node );
// start after or end before -> skip node
if( noderange.compareEndPoints('EndToStart',range) >= 0 &&
noderange.compareEndPoints('StartToEnd',range) <= 0 )
{
// no "Array.indexOf()" in IE8
var in_array = false;
for( var j=0; j < nodes.length; ++j )
{
if( nodes[j] !== node )
continue;
in_array = true;
break;
}
if( ! in_array )
nodes.push( node );
}
node = nextNode( node, parentNode );
}
}
// Fallback
if( nodes.length == 0 && isOrContainsNode(containerNode,document.activeElement) && document.activeElement != containerNode )
nodes.push( document.activeElement );
return nodes;
}
if( sel.type == 'Control' ) // e.g. an image selected
{
var nodes = [];
// http://msdn.microsoft.com/en-us/library/ie/hh826021%28v=vs.85%29.aspx
var range = sel.createRange();
for( var i=0; i < range.length; ++i )
nodes.push( range(i) );
return nodes;
}
}
return [];
};
// http://stackoverflow.com/questions/8513368/collapse-selection-to-start-of-selection-not-div
var collapseSelectionEnd = function()
{
if( window.getSelection )
{
var sel = window.getSelection();
if( ! sel.isCollapsed )
{
// Form-submits via Enter throw 'NS_ERROR_FAILURE' on Firefox 34
try {
sel.collapseToEnd();
}
catch( e ) {
}
}
}
else if( document.selection )
{
var sel = document.selection;
if( sel.type != 'Control' )
{
var range = sel.createRange();
range.collapse(false);
range.select();
}
}
};
// http://stackoverflow.com/questions/4652734/return-html-from-a-user-selected-text/4652824#4652824
var getSelectionHtml = function( containerNode )
{
if( getSelectionCollapsed( containerNode ) )
return null;
if( window.getSelection )
{
var sel = window.getSelection();
if( sel.rangeCount )
{
var container = document.createElement('div'),
len = sel.rangeCount;
for( var i=0; i < len; ++i )
{
var contents = sel.getRangeAt(i).cloneContents();
container.appendChild(contents);
}
return container.innerHTML;
}
}
else if( document.selection )
{
var sel = document.selection;
if( sel.type == 'Text' )
{
var range = sel.createRange();
return range.htmlText;
}
}
return null;
};
var selectionInside = function( containerNode, force )
{
// selection inside editor?
if( window.getSelection )
{
var sel = window.getSelection();
if( isOrContainsNode(containerNode,sel.anchorNode) && isOrContainsNode(containerNode,sel.focusNode) )
return true;
// selection at least partly outside editor
if( ! force )
return false;
// force selection to editor
var range = document.createRange();
range.selectNodeContents( containerNode );
range.collapse( false );
sel.removeAllRanges();
sel.addRange(range);
}
else if( document.selection )
{
var sel = document.selection;
if( sel.type == 'Control' ) // e.g. an image selected
{
// http://msdn.microsoft.com/en-us/library/ie/hh826021%28v=vs.85%29.aspx
var range = sel.createRange();
if( range.length != 0 && isOrContainsNode(containerNode,range(0)) ) // test only the first element
return true;
}
else //if( sel.type == 'Text' || sel.type == 'None' )
{
// Range of container
// http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div
var rangeContainer = document.body.createTextRange();
rangeContainer.moveToElementText(containerNode);
// Compare with selection range
var range = sel.createRange();
if( rangeContainer.inRange(range) )
return true;
}
// selection at least partly outside editor
if( ! force )
return false;
// force selection to editor
// http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div
var range = document.body.createTextRange();
range.moveToElementText(containerNode);
range.setEndPoint('StartToEnd',range); // collapse
range.select();
}
return true;
};
/*
var clipSelectionTo = function( containerNode )
{
if( window.getSelection && containerNode.compareDocumentPosition )
{
var sel = window.getSelection();
var left_node = sel.anchorNode,
left_offset = sel.anchorOffset,
right_node = sel.focusNode,
right_offset = sel.focusOffset;
// http://stackoverflow.com/questions/10710733/dom-determine-if-the-anchornode-or-focusnode-is-on-the-left-side
if( (left_node == right_node && left_offset > right_offset) ||
(left_node.compareDocumentPosition(right_node) & Node.DOCUMENT_POSITION_PRECEDING) )
{
// Right-to-left selection
left_node = sel.focusNode;
left_offset = sel.focusOffset;
right_node = sel.anchorNode,
right_offset = sel.anchorOffset;
}
// Speed up: selection inside editor
var left_inside = isOrContainsNode(containerNode,left_node),
right_inside = isOrContainsNode(containerNode,right_node);
if( left_inside && right_inside )
return true;
// Selection before/after container?
if( ! left_inside && containerNode.compareDocumentPosition(left_node) & Node.DOCUMENT_POSITION_FOLLOWING )
return false; // selection after
if( ! right_inside && containerNode.compareDocumentPosition(right_node) & Node.DOCUMENT_POSITION_PRECEDING )
return false; // selection before
// Selection partly before/after container
// http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div
var range = document.createRange();
range.selectNodeContents( containerNode );
if( left_inside )
range.setStart( left_node, left_offset );
if( right_inside )
range.setEnd( right_node, right_offset );
sel.removeAllRanges();
sel.addRange(range);
return true;
}
else if( document.selection )
{
var sel = document.selection;
if( sel.type == 'Text' )
{
// Range of container
// http://stackoverflow.com/questions/12243898/how-to-select-all-text-in-contenteditable-div
var rangeContainer = document.body.createTextRange();
rangeContainer.moveToElementText(containerNode);
// Compare with selection range
var range = sel.createRange();
if( rangeContainer.inRange(range) )
return true;
// Selection before/after container?
if( rangeContainer.compareEndPoints('StartToEnd',range) > 0 )
return false;
if( rangeContainer.compareEndPoints('EndToStart',range) < 0 )
return false;
// Selection partly before/after container
if( rangeContainer.compareEndPoints('StartToStart',range) > 0 )
range.setEndPoint('StartToStart',rangeContainer);
if( rangeContainer.compareEndPoints('EndToEnd',range) < 0 )
range.setEndPoint('EndToEnd',rangeContainer);
// select range
range.select();
return true;
}
}
return true;
};
*/
// http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div/6691294#6691294
// http://stackoverflow.com/questions/4823691/insert-an-html-element-in-a-contenteditable-element
// http://stackoverflow.com/questions/6139107/programatically-select-text-in-a-contenteditable-html-element
var pasteHtmlAtCaret = function( containerNode, html )
{
if( window.getSelection )
{
// IE9 and non-IE
var sel = window.getSelection();
if( sel.getRangeAt && sel.rangeCount )
{
var range = sel.getRangeAt(0);
// Range.createContextualFragment() would be useful here but is
// only relatively recently standardized and is not supported in
// some browsers (IE9, for one)
var el = document.createElement('div');
el.innerHTML = html;
var frag = document.createDocumentFragment(), node, lastNode;
while ( (node = el.firstChild) ) {
lastNode = frag.appendChild(node);
}
if( isOrContainsNode(containerNode, range.commonAncestorContainer) )
{
range.deleteContents();
range.insertNode(frag);
}
else {
containerNode.appendChild(frag);
}
// Preserve the selection
if( lastNode )
{
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
}
}
else if( document.selection )
{
// IE <= 8
var sel = document.selection;
if( sel.type != 'Control' )
{
var originalRange = sel.createRange();
originalRange.collapse(true);
var range = sel.createRange();
if( isOrContainsNode(containerNode, range.parentElement()) )
range.pasteHTML( html );
else // simply append to Editor
{
var textRange = document.body.createTextRange();
textRange.moveToElementText(containerNode);
textRange.collapse(false);
textRange.select();
textRange.pasteHTML( html );
}
// Preserve the selection
range = sel.createRange();
range.setEndPoint('StartToEnd', originalRange);
range.select();
}
}
};
// Interface: Create wysiwyg
window.wysiwyg = function( option )
{
// Options
option = option || {};
var option_element = option.element || null;
if( typeof(option_element) == 'string' )
option_element = document.getElementById( option_element );
var option_onkeypress = option.onkeypress || null;
var option_onselection = option.onselection || null;
var option_onplaceholder = option.onplaceholder || null;
var option_hijackcontextmenu = option.hijackcontextmenu || false;
// Keep textarea if browser can't handle content-editable
var is_textarea = option_element.nodeName == 'TEXTAREA' || option_element.nodeName == 'INPUT';
if( is_textarea )
{
// http://stackoverflow.com/questions/1882205/how-do-i-detect-support-for-contenteditable-via-javascript
var canContentEditable = 'contentEditable' in document.body;
if( canContentEditable )
{
// Sniffer useragent...
var webkit = navigator.userAgent.match(/(?:iPad|iPhone|Android).* AppleWebKit\/([^ ]+)/);
if( webkit && 420 <= parseInt(webkit[1]) && parseInt(webkit[1]) < 534 ) // iPhone 1 was Webkit/420
canContentEditable = false;
}
if( ! canContentEditable )
{
// Keep textarea
var node_textarea = option_element;
// Add a 'newline' after each '<br>'
var newlineAfterBR = function( html ) {
return html.replace(/<br[ \/]*>\n?/gi,'<br>\n');
};
node_textarea.value = newlineAfterBR( node_textarea.value );
// Command structure
var dummy_this = function() {
return this;
};
var dummy_null = function() {
return null;
};
return {
legacy: true,
// properties
getElement: function()
{
return node_textarea;
},
getHTML: function()
{
return node_textarea.value;
},
setHTML: function( html )
{
node_textarea.value = newlineAfterBR( html );
return this;
},
getSelectedHTML: dummy_null,
sync: dummy_this,
// selection and popup
collapseSelection: dummy_this,
openPopup: dummy_null,
closePopup: dummy_this,
// exec commands
removeFormat: dummy_this,
bold: dummy_this,
italic: dummy_this,
underline: dummy_this,
strikethrough: dummy_this,
forecolor: dummy_this,
highlight: dummy_this,
fontName: dummy_this,
fontSize: dummy_this,
subscript: dummy_this,
superscript: dummy_this,
align: dummy_this,
format: dummy_this,
indent: dummy_this,
insertLink: dummy_this,
insertImage: dummy_this,
insertHTML: dummy_this,
insertList: dummy_this
};
}
}
// create content-editable
var node_textarea = null,
node_wysiwyg = null;
if( is_textarea )
{
// Textarea
node_textarea = option_element;
node_textarea.style.display = 'none';
// Contenteditable
node_wysiwyg = document.createElement( 'DIV' );
node_wysiwyg.innerHTML = node_textarea.value;
var parent = node_textarea.parentNode,
next = node_textarea.nextSibling;
if( next )
parent.insertBefore( node_wysiwyg, next );
else
parent.appendChild( node_wysiwyg );
}
else
node_wysiwyg = option_element;
node_wysiwyg.setAttribute( 'contentEditable', 'true' ); // IE7 is case sensitive
// IE8 uses 'document' instead of 'window'
// http://tanalin.com/en/articles/ie-version-js/
var window_ie8 = (document.all && !document.addEventListener) ? document : window;
// Sync Editor with Textarea
var syncTextarea = null;
if( is_textarea )
{
var previous_html = node_wysiwyg.innerHTML;
syncTextarea = function()
{
var new_html = node_wysiwyg.innerHTML;
if( new_html == previous_html )
return ;
// HTML changed
node_textarea.value = new_html;
previous_html = new_html;
// Event Handler
fireEvent( node_textarea, 'change', false );
};
}
// Show placeholder
var showPlaceholder;
if( option_onplaceholder )
{
var placeholder_visible = false;
showPlaceholder = function()
{
// Test if wysiwyg has content
var wysiwyg_empty = true;
var node = node_wysiwyg;
while( node )
{
node = nextNode( node, node_wysiwyg );
// Test if node contains something visible
if( ! node )
;
else if( node.nodeType == Node_ELEMENT_NODE )
{
if( node.nodeName == 'IMG' )
{
wysiwyg_empty = false;
break;
}
}
else if( node.nodeType == Node_TEXT_NODE )
{
var text = node.nodeValue;
if( text && text.search(/[^\s]/) != -1 )
{
wysiwyg_empty = false;
break;
}
}
}
if( placeholder_visible != wysiwyg_empty )
{
option_onplaceholder( wysiwyg_empty );
placeholder_visible = wysiwyg_empty;
}
};
showPlaceholder();
}
// Handle selection
var popup_saved_selection = null, // preserve selection during popup
handleSelection = null,
debounced_handleSelection = null;
if( option_onselection )
{
handleSelection = function( clientX, clientY, rightclick )
{
// Detect collapsed selection
var collapsed = getSelectionCollapsed( node_wysiwyg );
// List of all selected nodes
var nodes = getSelectedNodes( node_wysiwyg );
// Rectangle of the selection
var rect = (clientX === null || clientY === null) ? null :
{
left: clientX,
top: clientY,
width: 0,
height: 0
};
var selectionRect = getSelectionRect();
if( selectionRect )
rect = selectionRect;
if( rect )
{
// So far 'rect' is relative to viewport
if( node_wysiwyg.getBoundingClientRect )
{
// Make it relative to the editor via 'getBoundingClientRect()'
var boundingrect = node_wysiwyg.getBoundingClientRect();
rect.left -= parseInt(boundingrect.left);
rect.top -= parseInt(boundingrect.top);
}
else
{
var node = node_wysiwyg,
offsetLeft = 0,
offsetTop = 0,
fixed = false;
do {
offsetLeft += node.offsetLeft ? parseInt(node.offsetLeft) : 0;
offsetTop += node.offsetTop ? parseInt(node.offsetTop) : 0;
if( node.style.position == 'fixed' )
fixed = true;
}
while( node = node.offsetParent );
rect.left -= offsetLeft - (fixed ? 0 : window.pageXOffset);
rect.top -= offsetTop - (fixed ? 0 : window.pageYOffset);
}
// Trim rectangle to the editor
if( rect.left < 0 )
rect.left = 0;
if( rect.top < 0 )
rect.top = 0;
if( rect.width > node_wysiwyg.offsetWidth )
rect.width = node_wysiwyg.offsetWidth;
if( rect.height > node_wysiwyg.offsetHeight )
rect.height = node_wysiwyg.offsetHeight;
}
else if( nodes.length )
{
// What else could we do? Offset of first element...
for( var i=0; i < nodes.length; ++i )
{
var node = nodes[i];
if( node.nodeType != Node_ELEMENT_NODE )
continue;
rect = {
left: node.offsetLeft,
top: node.offsetTop,
width: node.offsetWidth,
height: node.offsetHeight
};
break;
}
}
// Callback
option_onselection( collapsed, rect, nodes, rightclick );
};
debounced_handleSelection = debounce( handleSelection, 1 );
}
// Open popup
var node_popup = null;
var popupClickClose = function( e )
{
// http://www.quirksmode.org/js/events_properties.html
if( !e )
var e = window.event;
var target = e.target || e.srcElement;
if( target.nodeType == Node_TEXT_NODE ) // defeat Safari bug
target = target.parentNode;
// Click within popup?
if( isOrContainsNode(node_popup,target) )
return ;
// close popup
popupClose();
};
var popupOpen = function()
{
// Already open?
if( node_popup )
return node_popup;
// Global click closes popup
addEvent( window_ie8, 'mousedown', popupClickClose, true );
// Create popup element
node_popup = document.createElement( 'DIV' );
var parent = node_wysiwyg.parentNode,
next = node_wysiwyg.nextSibling;
if( next )
parent.insertBefore( node_popup, next );
else
parent.appendChild( node_popup );
return node_popup;
};
var popupClose = function()
{
if( ! node_popup )
return ;
node_popup.parentNode.removeChild( node_popup );
node_popup = null;
removeEvent( window_ie8, 'mousedown', popupClickClose, true );
};
// Focus/Blur events
addEvent( node_wysiwyg, 'focus', function()
{
// forward focus/blur to the textarea
if( node_textarea )
fireEvent( node_textarea, 'focus', false );
});
addEvent( node_wysiwyg, 'blur', function()
{
// sync textarea immediately
if( syncTextarea )
syncTextarea();
// forward focus/blur to the textarea
if( node_textarea )
fireEvent( node_textarea, 'blur', false );
});
// Change events
var debounced_changeHandler = null;
if( showPlaceholder || syncTextarea )
{
// debounce 'syncTextarea' a second time, because 'innerHTML' is quite burdensome
var debounced_syncTextarea = syncTextarea ? debounce( syncTextarea, 250, true ) : null; // high timeout is save, because of "onblur" fires immediately
var changeHandler = function( e )
{
if( showPlaceholder )
showPlaceholder();
if( debounced_syncTextarea )
debounced_syncTextarea();
};
debounced_changeHandler = debounce( changeHandler, 1 );
// Catch change events
// http://stackoverflow.com/questions/1391278/contenteditable-change-events/1411296#1411296
// http://stackoverflow.com/questions/8694054/onchange-event-with-contenteditable/8694125#8694125
// https://github.com/mindmup/bootstrap-wysiwyg/pull/50/files
// http://codebits.glennjones.net/editing/events-contenteditable.htm
addEvent( node_wysiwyg, 'input', debounced_changeHandler );
addEvent( node_wysiwyg, 'DOMNodeInserted', debounced_changeHandler );
addEvent( node_wysiwyg, 'DOMNodeRemoved', debounced_changeHandler );
addEvent( node_wysiwyg, 'DOMSubtreeModified', debounced_changeHandler );
addEvent( node_wysiwyg, 'DOMCharacterDataModified', debounced_changeHandler ); // polyfill input in IE 9-10
addEvent( node_wysiwyg, 'propertychange', debounced_changeHandler );
addEvent( node_wysiwyg, 'textInput', debounced_changeHandler );
addEvent( node_wysiwyg, 'paste', debounced_changeHandler );
addEvent( node_wysiwyg, 'cut', debounced_changeHandler );
addEvent( node_wysiwyg, 'drop', debounced_changeHandler );
}
// Key events
// http://sandbox.thewikies.com/html5-experiments/key-events.html
var keyHandler = function( e, phase )
{
// http://www.quirksmode.org/js/events_properties.html
if( !e )
var e = window.event;
var code = 0;
if( e.keyCode )
code = e.keyCode;
else if( e.which )
code = e.which;
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
var character = e.charCode;
// Callback
if( phase == 1 && option_onkeypress )
{
var rv = option_onkeypress( code, character?String(String):String.fromCharCode(code), e.shiftKey||false, e.altKey||false, e.ctrlKey||false, e.metaKey||false );
if( rv === false ) // dismiss key
return cancelEvent( e );
}
// Keys can change the selection
if( phase == 2 || phase == 3 )
{
popup_saved_selection = null;
if( debounced_handleSelection )
debounced_handleSelection( null, null, false );
}
// Most keys can cause changes
if( phase == 2 && debounced_changeHandler )
{
switch( code )
{
case 33: // pageUp
case 34: // pageDown
case 35: // end
case 36: // home
case 37: // left
case 38: // up
case 39: // right
case 40: // down
// cursors do not
break;
default:
// call change handler
debounced_changeHandler();
break;
}
}
};
addEvent( node_wysiwyg, 'keydown', function( e )
{
return keyHandler( e, 1 );
});
addEvent( node_wysiwyg, 'keypress', function( e )
{
return keyHandler( e, 2 );
});
addEvent( node_wysiwyg, 'keyup', function( e )
{
return keyHandler( e, 3 );
});
// Mouse events
var mouseHandler = function( e, rightclick )
{
// http://www.quirksmode.org/js/events_properties.html
if( !e )
var e = window.event;
// mouse position
var clientX = null,
clientY = null;
if( e.clientX && e.clientY )
{
clientX = e.clientX;
clientY = e.clientY;
}
else if( e.pageX && e.pageY )
{
clientX = e.pageX - window.pageXOffset;
clientY = e.pageY - window.pageYOffset;
}
// mouse button
if( e.which && e.which == 3 )
rightclick = true;
else if( e.button && e.button == 2 )
rightclick = true;
// remove event handler
removeEvent( window_ie8, 'mouseup', mouseHandler );
// Callback selection
popup_saved_selection = null;
if( ! option_hijackcontextmenu && rightclick )
return ;
if( debounced_handleSelection )
debounced_handleSelection( clientX, clientY, rightclick );
};
addEvent( node_wysiwyg, 'mousedown', function( e )
{
// catch event if 'mouseup' outside 'node_wysiwyg'
removeEvent( window_ie8, 'mouseup', mouseHandler );
addEvent( window_ie8, 'mouseup', mouseHandler );
});
addEvent( node_wysiwyg, 'mouseup', function( e )
{
mouseHandler( e );
// Trigger change
if( debounced_changeHandler )
debounced_changeHandler();
});
addEvent( node_wysiwyg, 'dblclick', function( e )
{
mouseHandler( e );
});
addEvent( node_wysiwyg, 'selectionchange',  function( e )
{
mouseHandler( e );
});
if( option_hijackcontextmenu )
{
addEvent( node_wysiwyg, 'contextmenu', function( e )
{
mouseHandler( e, true );
return cancelEvent( e );
});
}
// exec command
// https://developer.mozilla.org/en-US/docs/Web/API/document.execCommand
// http://www.quirksmode.org/dom/execCommand.html
var execCommand = function( command, param, force_selection )
{
// give selection to contenteditable element
restoreSelection( node_wysiwyg, popup_saved_selection );
if( ! selectionInside(node_wysiwyg, force_selection) ) // returns 'selection inside editor'
return false;
// for webkit, mozilla, opera
if( window.getSelection )
{
// Buggy, call within 'try/catch'
try {
if( document.queryCommandSupported && ! document.queryCommandSupported(command) )
return false;
return document.execCommand( command, false, param );
}
catch( e ) {
}
}
// for IE
else if( document.selection )
{
var sel = document.selection;
if( sel.type != 'None' )
{
var range = sel.createRange();
// Buggy, call within 'try/catch'
try {
if( ! range.queryCommandEnabled(command) )
return false;
return range.execCommand( command, false, param );
}
catch( e ) {
}
}
}
return false;
};
// Command structure
var trailingDiv = null;
var IEtrailingDIV = function()
{
// Detect IE - http://stackoverflow.com/questions/17907445/how-to-detect-ie11
if( document.all || !!window.MSInputMethodContext )
{
// Workaround IE11 - https://github.com/wysiwygjs/wysiwyg.js/issues/14
trailingDiv = document.createElement( 'DIV' );
node_wysiwyg.appendChild( trailingDiv );
}
};
var callUpdates = function( selection_destroyed )
{
// Remove IE11 workaround
if( trailingDiv )
{
node_wysiwyg.removeChild( trailingDiv );
trailingDiv = null;
}
// change-handler
if( debounced_changeHandler )
debounced_changeHandler();
// handle saved selection
if( selection_destroyed )
{
collapseSelectionEnd();
popup_saved_selection = null; // selection destroyed
}
else if( popup_saved_selection )
popup_saved_selection = saveSelection( node_wysiwyg );
};
return {
// properties
getElement: function()
{
return node_wysiwyg;
},
getHTML: function()
{
return node_wysiwyg.innerHTML;
},
setHTML: function( html )
{
node_wysiwyg.innerHTML = html;
callUpdates( true ); // selection destroyed
return this;
},
getSelectedHTML: function()
{
restoreSelection( node_wysiwyg, popup_saved_selection );
if( ! selectionInside(node_wysiwyg) )
return null;
return getSelectionHtml( node_wysiwyg );
},
sync: function()
{
if( syncTextarea )
syncTextarea();
return this;
},
// selection and popup
collapseSelection: function()
{
collapseSelectionEnd();
popup_saved_selection = null; // selection destroyed
return this;
},
openPopup: function()
{
if( ! popup_saved_selection )
popup_saved_selection = saveSelection( node_wysiwyg ); // save current selection
return popupOpen();
},
closePopup: function()
{
popupClose();
return this;
},
removeFormat: function()
{
execCommand( 'removeFormat' );
execCommand( 'unlink' );
callUpdates();
return this;
},
bold: function()
{
execCommand( 'bold' );
callUpdates();
return this;
},
italic: function()
{
execCommand( 'italic' );
callUpdates();
return this;
},
underline: function()
{
execCommand( 'underline' );
callUpdates();
return this;
},
strikethrough: function()
{
execCommand( 'strikeThrough' );
callUpdates();
return this;
},
forecolor: function( color )
{
execCommand( 'foreColor', color );
callUpdates();
return this;
},
highlight: function( color )
{
// http://stackoverflow.com/questions/2756931/highlight-the-text-of-the-dom-range-element
if( ! execCommand('hiliteColor',color) ) // some browsers apply 'backColor' to the whole block
execCommand( 'backColor', color );
callUpdates();
return this;
},
fontName: function( name )
{
execCommand( 'fontName', name );
callUpdates();
return this;
},
fontSize: function( size )
{
execCommand( 'fontSize', size );
callUpdates();
return this;
},
subscript: function()
{
execCommand( 'subscript' );
callUpdates();
return this;
},
superscript: function()
{
execCommand( 'superscript' );
callUpdates();
return this;
},
align: function( align )
{
IEtrailingDIV();
if( align == 'left' )
execCommand( 'justifyLeft' );
else if( align == 'center' )
execCommand( 'justifyCenter' );
else if( align == 'right' )
execCommand( 'justifyRight' );
else if( align == 'justify' )
execCommand( 'justifyFull' );
callUpdates();
return this;
},
format: function( tagname )
{
IEtrailingDIV();
execCommand( 'formatBlock', tagname );
callUpdates();
return this;
},
indent: function( outdent )
{
IEtrailingDIV();
execCommand( outdent ? 'outdent' : 'indent' );
callUpdates();
return this;
},
insertLink: function( url )
{
execCommand( 'createLink', url );
callUpdates( true ); // selection destroyed
return this;
},
insertImage: function( url )
{
execCommand( 'insertImage', url, true );
callUpdates( true ); // selection destroyed
return this;
},
insertHTML: function( html )
{
if( ! execCommand('insertHTML', html, true) )
{
// IE 11 still does not support 'insertHTML'
restoreSelection( node_wysiwyg, popup_saved_selection );
selectionInside( node_wysiwyg, true );
pasteHtmlAtCaret( node_wysiwyg, html );
}
callUpdates( true ); // selection destroyed
return this;
},
insertList: function( ordered )
{
IEtrailingDIV();
execCommand( ordered ? 'insertOrderedList' : 'insertUnorderedList' );
callUpdates();
return this;
}
};
};
})(window, document, navigator);
/**
* wysiwyg-editor.js
*/
(function(window, document, $, undefined){
'use strict';
// http://stackoverflow.com/questions/17242144/javascript-convert-hsb-hsv-color-to-rgb-accurately
var HSVtoRGB = function( h, s, v )
{
var r, g, b, i, f, p, q, t;
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6)
{
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
var hr = Math.floor(r * 255).toString(16);
var hg = Math.floor(g * 255).toString(16);
var hb = Math.floor(b * 255).toString(16);
return '#' + (hr.length < 2 ? '0' : '') + hr +
(hg.length < 2 ? '0' : '') + hg +
(hb.length < 2 ? '0' : '') + hb;
};
// Encode htmlentities() - http://stackoverflow.com/questions/5499078/fastest-method-to-escape-html-tags-as-html-entities
var html_encode = function( string )
{
return string.replace(/[&<>"]/g, function(tag)
{
var charsToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;'
};
return charsToReplace[tag] || tag;
});
};
// Create the Editor
var create_editor = function( $textarea, classes, placeholder, toolbar_position, toolbar_buttons, toolbar_submit, label_selectImage,
placeholder_url, placeholder_embed, max_imagesize, on_imageupload, force_imageupload, video_from_url, on_keypress )
{
// Content: Insert link
var wysiwygeditor_insertLink = function( wysiwygeditor, url )
{
if( ! url )
;
else if( wysiwygeditor.getSelectedHTML() )
wysiwygeditor.insertLink( url );
else
wysiwygeditor.insertHTML( '<a href="' + html_encode(url) + '">' + html_encode(url) + '</a>' );
wysiwygeditor.closePopup().collapseSelection();
};
var content_insertlink = function(wysiwygeditor, $modify_link)
{
var $button = toolbar_button( toolbar_submit );
var $inputurl = $('<input type="text" value="' + ($modify_link ? $modify_link.attr('href') : '') + '" />').addClass('wysiwyg-input')
.keypress(function(event){
if( event.which != 10 && event.which != 13 )
return ;
if( $modify_link )
{
$modify_link.attr( 'href', $inputurl.val() );
wysiwygeditor.closePopup().collapseSelection();
}
else
wysiwygeditor_insertLink( wysiwygeditor,$inputurl.val() );
});
if( placeholder_url )
$inputurl.attr( 'placeholder', placeholder_url );
var $okaybutton = $button.click(function(event){
if( $modify_link )
{
$modify_link.attr( 'href', $inputurl.val() );
wysiwygeditor.closePopup().collapseSelection();
}
else
wysiwygeditor_insertLink( wysiwygeditor, $inputurl.val() );
event.stopPropagation();
event.preventDefault();
return false;
});
var $content = $('<div/>').addClass('wysiwyg-toolbar-form')
.attr('unselectable','on');
$content.append($inputurl).append($okaybutton);
return $content;
};
// Content: Insert image
var content_insertimage = function(wysiwygeditor)
{
// Add image to editor
var insert_image_wysiwyg = function( url, filename )
{
var html = '<img id="wysiwyg-insert-image" src="" alt=""' + (filename ? ' title="'+html_encode(filename)+'"' : '') + ' />';
wysiwygeditor.insertHTML( html ).closePopup().collapseSelection();
var $image = $('#wysiwyg-insert-image').removeAttr('id');
if( max_imagesize )
{
$image.css({maxWidth: max_imagesize[0]+'px',
maxHeight: max_imagesize[1]+'px'})
.load( function() {
$image.css({maxWidth: '',
maxHeight: ''});
// Resize $image to fit "clip-image"
var image_width = $image.width(),
image_height = $image.height();
if( image_width > max_imagesize[0] || image_height > max_imagesize[1] )
{
if( (image_width/image_height) > (max_imagesize[0]/max_imagesize[1]) )
{
image_height = parseInt(image_height / image_width * max_imagesize[0]);
image_width = max_imagesize[0];
}
else
{
image_width = parseInt(image_width / image_height * max_imagesize[1]);
image_height = max_imagesize[1];
}
$image.attr('width',image_width)
.attr('height',image_height);
}
});
}
$image.attr('src', url);
};
// Create popup
var $content = $('<div/>').addClass('wysiwyg-toolbar-form')
.attr('unselectable','on');
// Add image via 'Browse...'
var $fileuploader = null,
$fileuploader_input = $('<input type="file" />')
.css({position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
opacity: 0,
cursor: 'pointer'});
if( ! force_imageupload && window.File && window.FileReader && window.FileList )
{
// File-API
var loadImageFromFile = function( file )
{
// Only process image files
if( ! file.type.match('image.*') )
return;
var reader = new FileReader();
reader.onload = function(event) {
var dataurl = event.target.result;
insert_image_wysiwyg( dataurl, file.name );
};
// Read in the image file as a data URL
reader.readAsDataURL( file );
};
$fileuploader = $fileuploader_input
.attr('draggable','true')
.change(function(event){
var files = event.target.files; // FileList object
for(var i=0; i < files.length; ++i)
loadImageFromFile( files[i] );
})
.on('dragover',function(event){
event.originalEvent.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
event.stopPropagation();
event.preventDefault();
return false;
})
.on('drop', function(event){
var files = event.originalEvent.dataTransfer.files; // FileList object.
for(var i=0; i < files.length; ++i)
loadImageFromFile( files[i] );
event.stopPropagation();
event.preventDefault();
return false;
});
}
else if( on_imageupload )
{
// Upload image to a server
var $input = $fileuploader_input
.change(function(event){
on_imageupload.call( this, insert_image_wysiwyg );
});
$fileuploader = $('<form/>').append($input);
}
if( $fileuploader )
$('<div/>').addClass( 'wysiwyg-browse' )
.html( label_selectImage )
.append( $fileuploader )
.appendTo( $content );
// Add image via 'URL'
var $button = toolbar_button( toolbar_submit );
var $inputurl = $('<input type="text" value="" />').addClass('wysiwyg-input')
.keypress(function(event){
if( event.which == 10 || event.which == 13 )
insert_image_wysiwyg( $inputurl.val() );
});
if( placeholder_url )
$inputurl.attr( 'placeholder', placeholder_url );
var $okaybutton = $button.click(function(event){
insert_image_wysiwyg( $inputurl.val() );
event.stopPropagation();
event.preventDefault();
return false;
});
$content.append( $('<div/>').append($inputurl).append($okaybutton) );
return $content;
};
// Content: Insert video
var content_insertvideo = function(wysiwygeditor)
{
// Add video to editor
var insert_video_wysiwyg = function( url, html )
{
url = $.trim(url||'');
html = $.trim(html||'');
var website_url = false;
if( url.length && ! html.length )
website_url = url;
else if( html.indexOf('<') == -1 && html.indexOf('>') == -1 &&
html.match(/^(?:https?:\/)?\/?(?:[^:\/\s]+)(?:(?:\/\w+)*\/)(?:[\w\-\.]+[^#?\s]+)(?:.*)?(?:#[\w\-]+)?$/) )
website_url = html;
if( website_url && video_from_url )
html = video_from_url( website_url ) || '';
if( ! html.length && website_url )
html = '<video src="' + html_encode(website_url) + '" />';
wysiwygeditor.insertHTML( html ).closePopup().collapseSelection();
};
// Create popup
var $content = $('<div/>').addClass('wysiwyg-toolbar-form')
.attr('unselectable','on');
// Add video via '<embed/>'
var $textareaembed = $('<textarea>').addClass('wysiwyg-input wysiwyg-inputtextarea');
if( placeholder_embed )
$textareaembed.attr( 'placeholder', placeholder_embed );
$('<div/>').addClass( 'wysiwyg-embedcode' )
.append( $textareaembed )
.appendTo( $content );
// Add video via 'URL'
var $button = toolbar_button( toolbar_submit );
var $inputurl = $('<input type="text" value="" />').addClass('wysiwyg-input')
.keypress(function(event){
if( event.which == 10 || event.which == 13 )
insert_video_wysiwyg( $inputurl.val() );
});
if( placeholder_url )
$inputurl.attr( 'placeholder', placeholder_url );
var $okaybutton = $button.click(function(event){
insert_video_wysiwyg( $inputurl.val(), $textareaembed.val() );
event.stopPropagation();
event.preventDefault();
return false;
});
$content.append( $('<div/>').append($inputurl).append($okaybutton) );
return $content;
};
// Content: Color palette
var content_colorpalette = function( wysiwygeditor, forecolor )
{
var $content = $('<table/>')
.attr('cellpadding','0')
.attr('cellspacing','0')
.attr('unselectable','on');
for( var row=1; row < 15; ++row ) // should be '16' - but last line looks so dark
{
var $rows = $('<tr/>');
for( var col=0; col < 25; ++col ) // last column is grayscale
{
var color;
if( col == 24 )
{
var gray = Math.floor(255 / 13 * (14 - row)).toString(16);
var hexg = (gray.length < 2 ? '0' : '') + gray;
color = '#' + hexg + hexg + hexg;
}
else
{
var hue        = col / 24;
var saturation = row <= 8 ? row     /8 : 1;
var value      = row  > 8 ? (16-row)/8 : 1;
color = HSVtoRGB( hue, saturation, value );
}
$('<td/>').addClass('wysiwyg-toolbar-color')
.attr('title', color)
.attr('unselectable','on')
.css({backgroundColor: color})
.click(function(){
var color = this.title;
if( forecolor )
wysiwygeditor.forecolor( color ).closePopup().collapseSelection();
else
wysiwygeditor.highlight( color ).closePopup().collapseSelection();
return false;
})
.appendTo( $rows );
}
$content.append( $rows );
}
return $content;
};
// Handlers
var get_toolbar_handler = function( name, popup_callback )
{
switch( name )
{
case 'insertimage':
if( ! popup_callback )
return null;
return function( target ) {
popup_callback( content_insertimage(wysiwygeditor), target );
};
case 'insertvideo':
if( ! popup_callback )
return null;
return function( target ) {
popup_callback( content_insertvideo(wysiwygeditor), target );
};
case 'insertlink':
if( ! popup_callback )
return null;
return function( target ) {
popup_callback( content_insertlink(wysiwygeditor), target );
};
case 'bold':
return function() {
wysiwygeditor.bold(); // .closePopup().collapseSelection()
};
case 'italic':
return function() {
wysiwygeditor.italic(); // .closePopup().collapseSelection()
};
case 'underline':
return function() {
wysiwygeditor.underline(); // .closePopup().collapseSelection()
};
case 'strikethrough':
return function() {
wysiwygeditor.strikethrough(); // .closePopup().collapseSelection()
};
case 'forecolor':
if( ! popup_callback )
return null;
return function( target ) {
popup_callback( content_colorpalette(wysiwygeditor,true), target );
};
case 'highlight':
if( ! popup_callback )
return null;
return function( target ) {
popup_callback( content_colorpalette(wysiwygeditor,false), target );
};
case 'alignleft':
return function() {
wysiwygeditor.align('left'); // .closePopup().collapseSelection()
};
case 'aligncenter':
return function() {
wysiwygeditor.align('center'); // .closePopup().collapseSelection()
};
case 'alignright':
return function() {
wysiwygeditor.align('right'); // .closePopup().collapseSelection()
};
case 'alignjustify':
return function() {
wysiwygeditor.align('justify'); // .closePopup().collapseSelection()
};
case 'subscript':
return function() {
wysiwygeditor.subscript(); // .closePopup().collapseSelection()
};
case 'superscript':
return function() {
wysiwygeditor.superscript(); // .closePopup().collapseSelection()
};
case 'indent':
return function() {
wysiwygeditor.indent(); // .closePopup().collapseSelection()
};
case 'outdent':
return function() {
wysiwygeditor.indent(true); // .closePopup().collapseSelection()
};
case 'orderedList':
return function() {
wysiwygeditor.insertList(true); // .closePopup().collapseSelection()
};
case 'unorderedList':
return function() {
wysiwygeditor.insertList(); // .closePopup().collapseSelection()
};
case 'removeformat':
return function() {
wysiwygeditor.removeFormat().closePopup().collapseSelection();
};
}
return null;
}
// Create the toolbar
var toolbar_button = function( button ) {
return $('<a/>').addClass( 'wysiwyg-toolbar-icon' )
.attr('href','#')
.attr('title', button.title)
.attr('unselectable','on')
.append(button.image);
};
var add_buttons_to_toolbar = function( $toolbar, selection, popup_open_callback, popup_position_callback )
{
$.each( toolbar_buttons, function(key, value) {
if( ! value )
return ;
// Skip buttons on the toolbar
if( selection === false && 'showstatic' in value && ! value.showstatic )
return ;
// Skip buttons on selection
if( selection === true && 'showselection' in value && ! value.showselection )
return ;
// Click handler
var toolbar_handler;
if( 'click' in value )
toolbar_handler = function( target ) {
value.click( $(target) );
};
else if( 'popup' in value )
toolbar_handler = function( target ) {
var $popup = popup_open_callback();
var overwrite_offset = value.popup( $popup, $(target) );
popup_position_callback( $popup, target, overwrite_offset );
};
else
toolbar_handler = get_toolbar_handler( key, function( $content, target ) {
var $popup = popup_open_callback();
$popup.append( $content );
popup_position_callback( $popup, target );
$popup.find('input[type=text]:first').focus();
});
// Create the toolbar button
var $button;
if( toolbar_handler )
$button = toolbar_button( value ).click( function(event) {
toolbar_handler( event.currentTarget );
// Give the focus back to the editor. Technically not necessary
if( get_toolbar_handler(key) ) // only if not a popup-handler
wysiwygeditor.getElement().focus();
event.stopPropagation();
event.preventDefault();
return false;
});
else if( value.html )
$button = $(value.html);
if( $button )
$toolbar.append( $button );
});
};
var popup_position = function( $popup, $container, left, top )  // left+top relative to $container
{
// Test parents
var offsetparent = $container.get(0).offsetParent,
offsetparent_offset = { left: 0, top: 0 },  //$.offset() does not work with Safari 3 and 'position:fixed'
offsetparent_fixed = false,
offsetparent_overflow = false,
popup_width = $popup.width(),
node = offsetparent;
while( node )
{
offsetparent_offset.left += node.offsetLeft;
offsetparent_offset.top += node.offsetTop;
var $node = $(node);
if( $node.css('position') == 'fixed' )
offsetparent_fixed = true;
if( $node.css('overflow') != 'visible' )
offsetparent_overflow = true;
node = node.offsetParent;
}
// Move $popup as high as possible in the DOM tree: offsetParent of $container
var $offsetparent = $(offsetparent || document.body);
$offsetparent.append( $popup );
var offset = $container.position();
left += offset.left;
top += offset.top;
// Trim to offset-parent
if( offsetparent_fixed || offsetparent_overflow )
{
if( left + popup_width > $offsetparent.width() - 1 )
left = $offsetparent.width() - popup_width - 1;
if( left < 1 )
left = 1;
}
// Trim to viewport
var viewport_width = $(window).width();
if( offsetparent_offset.left + left + popup_width > viewport_width - 1 )
left = viewport_width - offsetparent_offset.left - popup_width - 1;
var scroll_left = offsetparent_fixed ? 0 : $(window).scrollLeft();
if( offsetparent_offset.left + left < scroll_left + 1 )
left = scroll_left - offsetparent_offset.left + 1;
// Set offset
$popup.css({ left: parseInt(left) + 'px',
top: parseInt(top) + 'px' });
};
// Transform the textarea to contenteditable
var hotkeys = {};
var create_wysiwyg = function( $textarea, $container, placeholder )
{
var option = {
element: $textarea.get(0),
onkeypress: function( code, character, shiftKey, altKey, ctrlKey, metaKey )
{
// Ask master
if( on_keypress && on_keypress(code, character, shiftKey, altKey, ctrlKey, metaKey) === false )
return false; // swallow key
// Exec hotkey
if( character && !shiftKey && !altKey && ctrlKey && !metaKey )
{
var hotkey = character.toLowerCase();
if( ! hotkeys[hotkey] )
return ;
hotkeys[hotkey]();
return false; // prevent default
}
},
onselection: function( collapsed, rect, nodes, rightclick )
{
var show_popup = true,
$special_popup = null;
// Click on a link opens the link-popup
if( collapsed )
$.each( nodes, function(index, node) {
if( $(node).parents('a').length != 0 ) { // only clicks on text-nodes
$special_popup = content_insertlink( wysiwygeditor, $(node).parents('a:first') )
return false; // break
}
});
// Fix type error - https://github.com/wysiwygjs/wysiwyg.js/issues/4
if( ! rect )
show_popup = false;
// Force a special popup?
else if( $special_popup )
;
// A right-click always opens the popup
else if( rightclick )
;
// No selection-popup wanted?
else if( toolbar_position != 'selection' && toolbar_position != 'top-selection' && toolbar_position != 'bottom-selection' )
show_popup = false;
// Selected popup wanted, but nothing selected (=selection collapsed)
else if( collapsed )
show_popup = false;
// Only one image? Better: Display a special image-popup
else if( nodes.length == 1 && nodes[0].nodeName == 'IMG' ) // nodes is not a sparse array
show_popup = false;
if( ! show_popup )
{
wysiwygeditor.closePopup();
return ;
}
// Popup position
var $popup;
var apply_popup_position = function()
{
var popup_width = $popup.outerWidth();
// Point is the center of the selection - relative to $container not the element
var container_offset = $container.offset(),
editor_offset = $(wysiwygeditor.getElement()).offset();
var left = rect.left + parseInt(rect.width / 2) - parseInt(popup_width / 2) + editor_offset.left - container_offset.left;
var top = rect.top + rect.height + editor_offset.top - container_offset.top;
popup_position( $popup, $container, left, top );
};
// Open popup
$popup = $(wysiwygeditor.openPopup());
// if wrong popup -> create a new one
if( $popup.hasClass('wysiwyg-popup') && ! $popup.hasClass('wysiwyg-popuphover') || $popup.data('special') != (!!$special_popup) )
$popup = $(wysiwygeditor.closePopup().openPopup());
if( ! $popup.hasClass('wysiwyg-popup') )
{
// add classes + buttons
$popup.addClass( 'wysiwyg-popup wysiwyg-popuphover' );
if( $special_popup )
$popup.empty().append( $special_popup ).data('special',true);
else
add_buttons_to_toolbar( $popup, true,
function() {
return $popup.empty();
},
apply_popup_position );
}
// Apply position
apply_popup_position();
},
hijackcontextmenu: (toolbar_position == 'selection')
};
if( placeholder )
{
var $placeholder = $('<div/>').addClass( 'wysiwyg-placeholder' )
.html( placeholder )
.hide();
$container.prepend( $placeholder );
option.onplaceholder = function( visible ) {
if( visible )
$placeholder.show();
else
$placeholder.hide();
};
}
var wysiwygeditor = wysiwyg( option );
return wysiwygeditor;
}
// Create a container
var $container = $('<div/>').addClass('wysiwyg-container');
if( classes )
$container.addClass( classes );
$textarea.wrap( $container );
$container = $textarea.parent( '.wysiwyg-container' );
// Create the editor-wrapper if placeholder
var $wrapper = false;
if( placeholder )
{
$wrapper = $('<div/>').addClass('wysiwyg-wrapper')
.click(function(){     // Clicking the placeholder focus editor - fixes IE6-IE8
wysiwygeditor.getElement().focus();
});
$textarea.wrap( $wrapper );
$wrapper = $textarea.parent( '.wysiwyg-wrapper' );
}
// Create the WYSIWYG Editor
var wysiwygeditor = create_wysiwyg( $textarea, placeholder ? $wrapper : $container, placeholder );
if( wysiwygeditor.legacy )
{
var $textarea = $(wysiwygeditor.getElement());
$textarea.addClass( 'wysiwyg-textarea' );
if( $textarea.is(':visible') ) // inside the DOM
$textarea.width( $container.width() - ($textarea.outerWidth() - $textarea.width()) );
}
else
$(wysiwygeditor.getElement()).addClass( 'wysiwyg-editor' );
// Hotkey+Commands-List
var commands = {};
$.each( toolbar_buttons, function(key, value) {
if( ! value || ! value.hotkey )
return ;
var toolbar_handler = get_toolbar_handler( key );
if( ! toolbar_handler )
return ;
hotkeys[value.hotkey.toLowerCase()] = toolbar_handler;
commands[key] = toolbar_handler;
});
// Toolbar on top or bottom
if( toolbar_position != 'selection' )
{
var toolbar_top = toolbar_position == 'top' || toolbar_position == 'top-selection';
var $toolbar = $('<div/>').addClass( 'wysiwyg-toolbar' ).addClass( toolbar_top ? 'wysiwyg-toolbar-top' : 'wysiwyg-toolbar-bottom' );
add_buttons_to_toolbar( $toolbar, false,
function() {
// Open a popup from the toolbar
var $popup = $(wysiwygeditor.openPopup());
// if wrong popup -> create a new one
if( $popup.hasClass('wysiwyg-popup') && $popup.hasClass('wysiwyg-popuphover') )
$popup = $(wysiwygeditor.closePopup().openPopup());
if( ! $popup.hasClass('wysiwyg-popup') )
// add classes + content
$popup.addClass( 'wysiwyg-popup' );
return $popup;
},
function( $popup, target, overwrite_offset ) {
// Popup position
var $button = $(target);
var popup_width = $popup.outerWidth();
// Point is the top/bottom-center of the button
var left = $button.offset().left - $container.offset().left + parseInt($button.width() / 2) - parseInt(popup_width / 2);
var top = $button.offset().top - $container.offset().top;
if( toolbar_top )
top += $button.outerHeight();
else
top -= $popup.outerHeight();
if( overwrite_offset )
{
left = overwrite_offset.left;
top = overwrite_offset.top;
}
popup_position( $popup, $container, left, top );
});
if( toolbar_top )
$container.prepend( $toolbar );
else
$container.append( $toolbar );
}
// Export userdata
return {
wysiwygeditor: wysiwygeditor,
$container: $container
};
};
// jQuery Interface
$.fn.wysiwyg = function( option, param )
{
if( ! option || typeof(option) === 'object' )
{
option = $.extend( {}, option );
return this.each(function() {
var $that = $(this);
// Already an editor
if( $that.data( 'wysiwyg') )
return ;
// Two modes: toolbar on top and on bottom
var classes = option.classes,
placeholder = option.placeholder || $that.attr('placeholder'),
toolbar_position = (option.toolbar && (option.toolbar == 'top' || option.toolbar == 'top-selection' || option.toolbar == 'bottom' || option.toolbar == 'bottom-selection' || option.toolbar == 'selection')) ? option.toolbar : 'top-selection',
toolbar_buttons = option.buttons,
toolbar_submit = option.submit,
label_selectImage = option.selectImage,
placeholder_url = option.placeholderUrl || null,
placeholder_embed = option.placeholderEmbed || null,
max_imagesize = option.maxImageSize || null,
on_imageupload = option.onImageUpload || null,
force_imageupload = option.forceImageUpload && on_imageupload,
video_from_url = option.videoFromUrl || null,
on_keypress = option.onKeyPress;
// Create the WYSIWYG Editor
var data = create_editor( $that, classes, placeholder, toolbar_position, toolbar_buttons, toolbar_submit, label_selectImage,
placeholder_url, placeholder_embed, max_imagesize, on_imageupload, force_imageupload, video_from_url, on_keypress );
$that.data( 'wysiwyg', data );
});
}
else if( this.length == 1 )
{
var data = this.data('wysiwyg');
if( ! data )
return this;
if( option == 'container' )
return data.$container;
if( option == 'shell' )
return data.wysiwygeditor;
}
return this;
};
})(window, document, jQuery);