Greasy Fork is available in English.
'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 = { '&': '&', '<': '<', '>': '>', '"': '"' }; 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);