/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. Neither the name of "Scrivo" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* $Id: BaseHTMLEditControl.js 152 2011-12-08 00:30:17Z geert $
*/
"use strict";
SUI.control.BaseHTMLEditControl = SUI.defineClass(
/** @lends SUI.control.BaseHTMLEditControl.prototype */{
/** @ignore */ baseClass: SUI.Box,
/**
* @class
* SUI.control.BaseHTMLEditControl implements a WYSIWIG/HTML edit control. It
* should provide a standardized interface to the different implementations
* of contenteditable in various browsers. Because of code size of the
* class it is splitted in two parts: this is BaseHTMLEditControl and
* implements all of the technical structure, the other part
* HTMLEditControl only implements the actions that can be performed on
* the content.
*
* @augments SUI.Box
*
* @description
* SUI.control.BaseHTMLEditControl constructor. This base class was created
* to limit code size. You don't need this constructor HTMLEditControl is
* the one you need.
*
* @constructs
*
* @param {inherit} arg
* @param {boolean} arg.ieStrictMode Whether strict mode should be used
* in IE
* @param {Function} arg.onCommandExecuted event handler, see
* onCommandExecuted()
* @param {Function} arg.onContextMenu event handler, see onContextMenu()
* @param {Function} arg.onFocus event handler, see onFocus()
* @param {Function} arg.onKeyDown event handler, see onKeyDown()
* @param {Function} arg.onLoad event handler, see onLoad()
* @param {Function} arg.onSelectionChange event handler, see
* onSelectionChange()
*
* @protected
*/
initializer: function(arg) {
SUI.control.BaseHTMLEditControl.initializeBase(this, arg);
var that = this;
// if the editor document is in strict mode, default true
if (arg.ieStrictMode !== undefined) {
this._ieStrictMode = arg.ieStrictMode;
}
// Set the event handlers
if (arg.onLoad) {
this.addListener("onLoad", arg.onLoad);
}
if (arg.onCommandExecuted) {
this.addListener("onCommandExecuted", arg.onCommandExecuted);
}
if (arg.onContextMenu) {
this.addListener("onContextMenu", arg.onContextMenu);
}
if (arg.onFocus) {
this.addListener("onFocus", arg.onFocus);
}
if (arg.onKeyDown) {
this.addListener("onKeyDown", arg.onKeyDown);
}
if (arg.onSelectionChange) {
this.addListener("onSelectionChange", arg.onSelectionChange);
}
// Create the iframe for the contenteditable
this.iframe = SUI.browser.createElement("IFRAME");
this.iframe.frameBorder = 0;
//frameborder="0"
// Set the onload handler on which we further initialize the
// contenteditable
SUI.browser.addEventListener(this.iframe, "load", function(e) {
if (!that._onLoadIframe(new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
});
if (SUI.browser.isIE) {
SUI.browser.addEventListener(this.iframe, "blur", function(e) {
that.bmk = that.tmpBmk;
});
}
// Add a SUI style class
SUI.style.addClass(this.iframe, "sui-scrivo-he");
// Set the initial content for the iframe: strict, UTF-8 and base
var doctype =
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.1 Strict//EN\">\n";
// Don't use strict doctype in IE if requested
if (SUI.browser.isIE && ! this._ieStrictMode) {
doctype = "";
}
// Apparently IE 8 does not support setting the charset through the
// meta tag when using https (navigation cancelled). Do this later.
var charset = "<meta http-equiv=\"content-type\""
+ " content=\"text/html; charset=UTF-8\">";
if (SUI.browser.isIE && SUI.browser.version < 9) {
charset = "";
}
this.iframe.src = "javascript: '" + doctype
+ "<html>"
+ "<head>" + charset
+ "<base href=\""+SCRIVO_BASE_DIR+"/\">"
+ "</head>"
+ "<body spellcheck=\"false\">"
+ "</body>"
+ "</html>'";
// And append the iframe to our SUI.Box container
this.el().appendChild(this.iframe);
// Initialize the object that manages the paste actions
this._pasteData = new SUI.control.PasteData({ editor: this });
},
/**
* Set the focus to the editable region.
*/
focus: function() {
if (this._canFocus && !this._pasteData._intercepting) {
if (SUI.browser.isIE) {
if (this._focussedElement) {
this._focussedElement.focus();
} else {
this._editDoc.body.focus();
}
if (!this.focussed && this.bmk) {
this.focussed = true;
this._ieRestoreBookmark(this.bmk);
this.bmk = null;
}
} else {
this._editDoc.body.focus();
}
}
},
/**
* Get the block format at the current cursor position.
* @return {String} tag name of the block where the cursor is located
*/
getCurrentBlockFormat: function() {
var bFmt = this._editDoc.queryCommandValue("FormatBlock");
var fmt = this._blockFormats[bFmt];
return (fmt === undefined) ? "" : fmt;
},
/**
* Get a reference to the document object of the page in the editor
* @return {HTMLElementNode} document element of the page being edited
*/
getDocument: function() {
return this._editDoc;
},
/**
* Get the element that's currently selected in the editor
* @return {HTMLElementNode} the element node currently selected in the
* editor
*/
getSelectedElement: function() {
return this._getParentElementSelection(this._getSelection());
},
/**
* Get HTML content for the edit control.
* Throws {ReferenceError} if the editor was not fully loaded yet
* @return {String} HTML data fragment from editor (no a top container)
*/
getValue: function() {
if (!this._editDoc) {
throw new ReferenceError(
"getValue failed because the editor was not fully loaded yet");
}
for (var i=0; i<this._editableElements.length; i++) {
this._editableElements[i].contentEditable = false;
}
var data = this._editDoc.body.innerHTML;
for (var i=0; i<this._editableElements.length; i++) {
this._editableElements[i].contentEditable = true;
}
return data;
},
/**
* Is the editor document in strict mode. This value can only be false in
* IE, it returns always true (and also not affects) other browsers.
* @return {boolean} Whether the editor document is in strict mode.
*/
ieStrictMode: function() {
return this._ieStrictMode;
},
/**
* Tell the HTML edit control which block formats you want to use
* @param {string[]} blockFormats array in which each element has a value
* key containing a block tag (P, H1 ... H6)
*/
initBlockFormats: function(blockFormats) {
// if already initialized
if (this._blockFormats) {
return;
}
this._blockFormats = {};
if (SUI.browser.isIE) {
// Find the block format names. IE returns internationalized
// results on the queryCommandValue("FormatBlock") command
// ('Kop 1' instead of 'Header 1' for example), so we need
// these to map them back to HTML tags. Other browsers seem
// to return just the tag name.
this._blockFormats[""] = "";
// create an offscreen tmp div
var tmp = SUI.browser.createElement();
tmp.unselectable = false;
SUI.style.setRect(tmp, 10, -110, 100, 100);
tmp.contentEditable= true;
document.body.appendChild(tmp);
// for all given block elements ...
for (var i=1; i<blockFormats.length; i++) {
// ... try to make a block element of that type ...
try {
var el = document.createElement(blockFormats[i].value);
el.innerHTML = "dummy";
} catch (e) { continue; }
// ... add it to our tmp div ...
tmp.appendChild(el);
// ... and focus an select the block element, ...
try {
tmp.focus();
} catch(e) {}
document.execCommand("SelectAll", false, null);
// ... now get the internationalized name and use that as key
// in _blockFormats
var bFmt = document.queryCommandValue("FormatBlock");
this._blockFormats[bFmt] = blockFormats[i].value;
// remove the dummy node
tmp.removeChild(tmp.firstChild);
}
// we're done with the div
document.body.removeChild(tmp);
} else {
// in other browsers we can use the tag name
for (var i=1; i<blockFormats.length; i++) {
this._blockFormats[blockFormats[i].value.toLowerCase()] =
blockFormats[i].value;
}
}
},
/**
* SUI framework layOut method
*/
layOut: function() {
// Size the host of the iframe (a SUI.Box)
SUI.control.BaseHTMLEditControl.parentMethod(this, "layOut");
// and size the contained iframe to the size of this box
SUI.style.setRect(
this.iframe, 0, 0, this.width(), this.height());
},
/**
* Fires after an editor command is executed (and content is changed), an
* important trigger for updating your user interface.
*/
onCommandExecuted: function() {
},
/**
* Fires when the user selects the wrong mouse button
* @param {int} x x-offset (downwards) of the mouse click
* @param {int} y y-offset of the mouse click
*/
onContextMenu: function(x, y) {
},
/**
* Fires when the document body in the editor window (iframe) receives
* the focus.
* @param {SUI.Event} evnt focus event
*/
onFocus: function(evnt) {
},
/**
* Fires when the user presses (the) any key
* @param {SUI.Event} evnt keydown event
*/
onKeyDown: function(evnt) {
},
/**
* Fires after the content is set into the editor with setValue
*/
onLoad: function() {
},
/**
* Fires when the user selects (moves it cursor) to an other element
* in the DOM tree, an important trigger for updating your user interface.
*/
onSelectionChange: function() {
},
/**
* Get or set the paste method to use wehn pasting.
* @param {string} [p] The desired method for pasting text: 'text',
* 'filtered' or 'html' (or none to use this method as a getter).
* @return {string} The current paste method setting (or null if this
* method was used as a setter).
*/
pasteMethod: function(p) {
return this._pasteData.pasteMethod(p);
},
/**
* Clear the contents of the control
*/
resetIframe: function() {
// older FireFoxes need this very hard reset to empty the
// contenteditable (and to keep working). Editor cursor is still
// visible but editing is not possible. It is likely that this patch
// is not necessary. As reported , bug 504268 occurs when
// contenteditable elements are hidden and redisplayed. At this moment
// the contenteditable elements are always rebuild and not reused.
if (SUI.browser.isGekco) {
this._editDoc = null;
this.el().removeChild(this.iframe);
this.el().appendChild(this.iframe);
} else {
if (this._editDoc) {
this._editDoc.body.innerHTML = "";
}
}
},
/**
* Force an onload event to happen to (Gecko refuses to do an onload if
* the iframe in initially hidden, so you can use this)
*/
geckoForceOnloadEvent: function() {
if (!this._loaded && SUI.browser.isGecko) {
// Correct way:
// var onload = this._editDoc.createEvent("HTMLEvents");
// onload.initEvent("load", true, true);
// this.iframe.dispatchEvent(onload);
// forces an event, but still the error occurs. A very blunt
// approach seems to do the trick well.
this.el().removeChild(this.iframe);
this.el().appendChild(this.iframe);
}
},
/**
* Supply CSS data for the edit control. The current CSS style code and
* linked stylesheets are removed. First the external style sheets are
* linked in the order they are provided, then the CSS code is appended.
* @param {String} styleDef CSS code
* @param {String} classNameBody A CSS class name for the document body.
* @param {string[]} styleSheets array of CSS stylesheet links
*/
setCSS: function(styleDef, classNameBody, styleSheets) {
if (!this._editDoc) {
// If the contenteditable iframe is not fully loaded store
// arguments for later use
this._tmpStoreStyleSheet = [];
for (var i=0; i<arguments.length; i++) {
this._tmpStoreStyleSheet.push(arguments[i]);
}
for (; i<3; i++) {
this._tmpStoreStyleSheet.push(null);
}
} else {
classNameBody = classNameBody || "";
if (classNameBody != "") {
// Set class attribute of editable body
SUI.style.addClass(this._editDoc.body, classNameBody);
}
var head = this._editDoc.getElementsByTagName("head")[0];
// Remove the old style sheets ...
var l = head.getElementsByTagName("LINK");
for (var i=0; i<l.length; i++) {
head.removeChild(l[i]);
}
// ... and append the new ones
if (styleSheets) {
for (i=0; i<styleSheets.length; i++) {
var l = this._editDoc.createElement("LINK");
l.type = "text/css";
l.rel = "stylesheet";
l.href = styleSheets[i];
head.appendChild(l);
}
}
var id_scrivo_styles = "scrivo_styles";
// If we've written CSS code earlier, remove it
var st = this._editDoc.getElementById(id_scrivo_styles);
if (st) {
head.removeChild(st);
}
// Now create the CSS code (add scrivo system styles last) ...
var cssText = (styleDef ? styleDef : "") + "\n"
+ this._createSystemCSS();
// ... create a style sheet ...
var st = this._editDoc.createElement("STYLE");
st.type = "text/css";
st.id = id_scrivo_styles;
if (SUI.browser.isIE) {
st.styleSheet.cssText = cssText;
} else { // w3c
st.appendChild(this._editDoc.createTextNode(cssText));
}
// ... and add it to the editable document
head.appendChild(st);
}
},
/**
* TODO
* @param {int[]} ids The editable ids
*/
setEditableElementIds: function(ids) {
this._editableElementIds = ids;
},
/**
* Set HTML content for the edit control. Calls the onload handler when
* the data is fully loaded.
* @param {String} val HTML data fragment (no need for a top container)
*/
setValue: function(val) {
if (!this._editDoc) {
// If the contenteditable iframe is not fully loaded store
// arguments for later use
this._tmpStore = val;
} else {
// Setting the contenteditable to false before assigning a value
// prevents the action to be pushed onto the undo stack (as
// seen on IE8)
this._setContentEditable(false);
this._editDoc.body.innerHTML = val;
this._setContentEditable(true);
// This patch was needed for FF 3.6: After loading the content
// editor seemed partially locked: all delete functionality
// like delete, backspace, execCommand('delete') but also
// execCommand('insertHTML') did not work. In the later case the
// content was inserted, but the current selection was not
// deleted). Inserting content by for instance typing but also
// after pasting HTML, corrected the situation.
if (SUI.browser.isGecko) {
this._initGecko();
}
// images are not very well selectable in WebKit, so fix that
this._webKitImgFix();
// now we're ready loading the content: call clients onload
this.callListener("onLoad");
}
},
// Holds a list of block formats with keys as
// queryCommandValue("FormatBlock") returns
_blockFormats: null,
// Gecko has some layouting problems after using focus en execCommand in
// init fase
_canFocus: true,
// an array with the id's of the editable elements (none defined -> the
// document body will be editable.
_editableElementIds: [],
// an array containt references to the editable elements.
_editableElements: [],
// reference to the contenteditable iframe's document
_editDoc: null,
// reference to the contenteditable iframe's window
_editWin: null,
// the element that is focussed to set the focus back (IE when using
// multiple editable regions).
_focussedElement: null,
// queryCommandEnabled("paste") trigger onbeforepaste event what we want
// to prevent
_ieDoBeforePaste: true,
// display documents in scrict mode in IE (always true for other browsers),
// you'll encounter several problems if you do so: cursor problems when
// navigating with error keys, collapsing document element and possibly
// more
_ieStrictMode: true,
// We need to save some parameters to detect onselectionchange
_lastSel: {
cllps: null, //< trigger value, normally true/false
elem: null,
cnt: 0 //< problems with -> after bold
},
// To check if the onload has happend (FF has this bug with initially
// hidden content divs)
_loaded: false,
// Utility object that helps us to intercept to user's paste actions
// in order to clean the data before it is inserted in the content
_pasteData: null,
// location of scrivo library
_scrivoDir: "scrivo",
// Temporary storage for content while waiting for onload to finish
_tmpStore: null,
// Temporary storage for css data while while waiting for onload to finish
_tmpStoreStyleSheet: null,
// binary or: canUndo = 1, canRedo = 2
_undoState: 0,
/**
* Construct the system stylesheet. The system stylesheet gives the user
* visual feedback of 'invisible' things in the browser, like language
* marks, anchors, misspelled words etc.
*/
_createSystemCSS: function() {
return (
".sys_language {\n"
+ " border: solid gray 1px;\n"
+ " background-color: #FFFFEE;\n"
+ " padding-left: 18px;\n"
+ " padding-right: 2px;\n"
+ " background-image: url(\""
/* + this._scrivoDir + "/sui/" */ + SUI.imgDir + "/"
+ SUI.resource.ecLang + "\");\n"
+ " background-repeat: no-repeat;\n"
+ "}\n"
+ ".sys_abbr {\n"
+ " border: solid gray 1px;\n"
+ " background-color: #FFFFEE;\n"
+ " padding-left: 18px;\n"
+ " padding-right: 2px;\n"
+ " background-image: url(\""
/* + this._scrivoDir + "/sui/" */ + SUI.imgDir + "/"
+ SUI.resource.ecAbbr + "\");\n"
+ " background-repeat: no-repeat;\n"
+ "}\n"
+ ".sys_anchor {\n"
+ " border: solid gray 1px;\n"
+ " background-color: #FFFFEE;\n"
+ " padding-left: 18px;\n"
+ " padding-right: 2px;\n"
+ " background-image: url(\""
/* + this._scrivoDir + "/sui/" */ + SUI.imgDir + "/"
+ SUI.resource.ecAnchor+ "\");\n"
+ " background-repeat: no-repeat;\n"
+ "}\n"
+ "table tr .sys_table_head {\n"
+ " background-color: #DD0000;\n"
+ " color: #FFFF00;\n"
+ "}\n"
+ "table tr .sys_table_axis {\n"
+ " background-color: #00CC00;\n"
+ " color: #FFFF00;\n"
+ "}\n"
+ "table tr .sys_show_cell {\n"
+ " border-bottom: dashed #C0C0C0 1px;\n"
+ " border-right: dashed #C0C0C0 1px;\n"
+ "}\n"
+ ".sys_spell {"
+ " border-bottom: solid #D00000 2px;\n"
+ "}\n"
+ ".sys_spell_suggestions {\n"
+ " border-bottom: dashed #D00000 2px;\n"
+ "}\n"
+ ".sys_spell_compoundword {\n"
+ " border-bottom: dashed #00D000 2px;\n"
+ "}\n");
},
/**
* Excecute standard contenteditable commands
*/
_execCommand: function(id, opt){
// first set the focus back, it is likely that we lost it
this.focus();
if (SUI.browser.isIE) {
// IE: if there is a selection run execCommand of the
// selection else run execCommand of the document
var sel = this._editDoc.selection.createRange();
if (this._editDoc.selection.type == "None") {
this._editDoc.execCommand(id, false, opt);
} else {
sel.execCommand(id, false, opt);
}
} else {
this._editDoc.execCommand(id, false, opt);
}
},
/**
* execCommand fontsize is not supported very well. Therefore we use
* fontname instead. Non IE browsers will insert a span and we search
* for these spans and set to font size with the value set in the
* font family field.
*/
_spanFix: function () {
// Get all the font tags ...
var l = this._editDoc.getElementsByTagName("SPAN");
// ... and work your way downwards (upwards in the document)
for (var i=l.length-1; i>=0; i--) {
// Try to get the face attribute
var fce = l[i].style.fontFamily;
if (fce) {
// Yes: first check the first two characters of the value
// (allow for a quote)
var of = (fce.charAt(0)=="'" || fce.charAt(0)=='"') ? 1 : 0;
if (fce.substr(of, 2) == "s_") {
l[i].style.fontSize =
fce.replace("s_", "").replace("P", "%");
l[i].style.fontFamily = "";
}
}
}
},
/**
* The IE editor inserts font tags in the content. And since we like spans
* better we replace then. Note that we cripple the face attribute
* (f_Roman and s_100%). So we correct that too.
*/
_fontFix: function () {
// Get all the font tags ...
var l = this._editDoc.getElementsByTagName("FONT");
// ... and work your way downwards (upwards in the document)
for (var i=l.length-1; i>=0; i--) {
// Try to get the face attribute
var fce = l[i].getAttribute("face");
if (fce) {
// Yes: replace font or size with a span, first check the
// first two characters of the value (allow for a quote)
var of = (fce.charAt(0)=="'" || fce.charAt(0)=='"') ? 1 : 0;
switch (fce.substr(of, 2)) {
case "f_":
var sp = this._editDoc.createElement("SPAN");
sp.innerHTML = l[i].innerHTML;
sp.style.fontFamily = fce.replace("f_", "");
this._replaceNode(l[i], sp);
break;
case "s_":
var sp = this._editDoc.createElement("SPAN");
sp.innerHTML = l[i].innerHTML;
sp.style.fontSize =
fce.replace("s_", "").replace("P", "%");
this._replaceNode(l[i], sp);
break;
}
} else {
// No: try to get the color attribute
var fce = l[i].getAttribute("color");
if (fce) {
// replace with a colored span
var sp = this._editDoc.createElement("SPAN");
sp.innerHTML = l[i].innerHTML;
sp.style.color = fce;
this._replaceNode(l[i], sp);
}
}
}
},
/**
* Get the current range object from the current selection in the editor
* (param sel is null) or a given selection.
*/
_getCurrentRange: function(sel) {
if (!sel) {
sel = this._getSelection();
}
if (sel) {
if (SUI.browser.isIE) { // IE
return sel.createRange();
}
if(sel.rangeCount && sel.getRangeAt) { // W3C
return sel.getRangeAt(0);
}
}
return null;
},
/**
* Get the parent element node (no text node) of a given selection.
* @param sel A selection
*/
_getParentElementSelection: function (sel) {
// In IE when there's no selection in the editor the document needs
// to have the focus to determine the parentElement.
if (SUI.browser.isIE && sel.type=="None") {
this.focus();
}
var r = this._getCurrentRange(sel);
if (r) {
if (SUI.browser.isIE) {
if (sel.type=="Control") {
return SUI.browser.version < 9
? r.commonParentElement() : r.item(0);
}
return r.parentElement();
} else {
var n = r.commonAncestorContainer;
while(n.nodeType != 1 && n.nodeType != 9){
n = n.parentNode;
}
return n;
}
}
return null;
},
/**
* Get the current selection in the editor
*/
_getSelection: function() {
return this._editDoc.selection
? this._editDoc.selection // IE
: this._editWin.getSelection(); // W3C
},
/**
* In IE's case we don't want to get the event from the window object
* but the iframe's window object instead
*/
_ieEvent: function(e) {
return SUI.browser.isIE && SUI.browser.version < 9
? this._editWin.event : e;
},
/**
* Finding text in IE using their range object
*/
_ieFind: function(what, caseSensitive, backward, wholeWord) {
// build the find flag value
var flg = 0;
if (backward) {
flg += 1;
}
if (wholeWord) {
flg += 2;
}
if (caseSensitive) {
flg += 4;
}
// get a range object for searching
var rng = this._editDoc.selection.createRange();
// collapse in the search direction
rng.collapse(backward);
// and search the text
var strFound = rng.findText(what, 10000000, flg);
// select the text when found
if (strFound) {
rng.select();
}
return strFound;
},
_ieRestoreBookmark: function(bmk) {
// ... and restore the selection
var rng = null;
if (bmk) {
if (bmk.type === "Range") {
rng = bmk.value;
} else if (bmk.type === "Bookmark") {
// Use IEs bookmark facility to restore the selection
rng = this._editDoc.body.createTextRange();
rng.moveToBookmark(bmk.value);
} else {
// Get the control range ...
rng = this._editDoc.body.createControlRange();
// ... and clear it ...
while (rng.length) {
rng.remove(0);
}
// ... add the stored value to the range and select it
rng.add(bmk.value);
}
}
if (rng) {
rng.select();
}
return rng;
},
_ieSaveBookmark: function(useBookmark) {
var bmk = null;
var range = this._editDoc.selection.createRange();
if (this._editDoc.selection.type === "Control") {
// An element was selected
bmk = { type: "Control", value: range.item(0) };
} else {
if (useBookmark) {
// Use IEs bookmark facility to store the selection
if (range.getBookmark) {
bmk = { type: "Bookmark", value: range.getBookmark() };
}
} else {
bmk = { type: "Range", value: range };
}
}
return bmk;
},
/**
* Gecko specific initialization for the contenteditable iframe
*/
_initGecko: function() {
// prevent Gecko layOut problems
this._canFocus = false;
this._execCommand("enableInlineTableEditing", false);
this._execCommand("enableObjectResizing", false);
this._execCommand("insertbronreturn", false);
this._execCommand("styleWithCSS", false);
this._canFocus = true;
// remove '_moz_editor_bogus_node' br from editor content ...
if (this._editDoc.body.firstChild
&& this._editDoc.body.firstChild.nodeType == 1) {
// ... however this seems not really necessary in our case (due
// to the method of providing initial document?)
if (this._editDoc.body.firstChild.getAttribute(
"_moz_editor_bogus_node")) {
this._editDoc.body.removeChild(this._editDoc.body.firstChild);
if (this._editDoc.body.innerHTMLlength <= 1) {
this._editDoc.body.innerHTML = "<p> </p>";
}
}
}
},
/**
* IE specific initialization for the contenteditable iframe
*/
_initIE: function() {
var that = this;
// We only want execute _onSelectionChangeIE if the control is focussed
this.focussed = null;
if (SUI.browser.version < 9) {
// IE onselectionchange, fired when we're selecting other DOM nodes in
// the editor. Really usefull for enabling editor buttons for instance.
// We're trying to mimic this behavior also on other browsers.
SUI.browser.addEventListener(this._editDoc, "selectionchange",
function(e) {
if (that.focussed) {
var e = that._ieEvent(e);
if (!that._onSelectionChangeIE(
new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
}
}
);
}
// IE loses its editor selection if another element in the interface
// is selected. So we need to save and restore the selection when we
// lose the selection and restore it later. The blur and focus events
// are the obvious events to use, but that did not work out well. the
// IE specific events onbeforedeactivate and beforeactivate events
// seem to do the job well.
// Place to store the IE bookmark
this.bmk = null;
this.tmpBmk = null;
// Store the current selection
SUI.browser.addEventListener(this._editDoc.body, "beforedeactivate",
function(e) {
var e = that._ieEvent(e);
if (!e.toElement) {
// If there is no toElement we're moving out of this
// document's body (i.e. to the userinterface). Now we
// can set the focussed flag to false so that
// our onselectionchange will not be triggered.
// Note IE 9 uses the onselectionchange handling of real
// browsers.
if (SUI.browser.version < 9) {
that.focussed = false;
}
// We want to save the bookmark when we're moving the
// focus out of the body, but after IE 9 the toElement
// was broken. Now we store it and let iframe.onblur take
// it over.
that.tmpBmk = that._ieSaveBookmark();
}
}
);
// Restore the current selection
SUI.browser.addEventListener(this._editDoc.body, "beforeactivate",
function() {
that.focussed = true;
that._ieRestoreBookmark(that.bmk);
that.bmk = null;
}
);
if (!this._editDoc.body.innerHTML) {
this._editDoc.body.innerHTML = "<p></p>";
}
},
/**
* WebKit specific initialization for the contenteditable iframe
*/
_initWebKit: function() {
if (!this._editDoc.body.innerHTML) {
this.setValue("<p><br></p>");
}
},
/**
* Call host's onContexMenu and prevent the default context menu
*/
_onContextMenu: function(e) {
this.callListener("onContextMenu",
SUI.browser.getX(e.event), SUI.browser.getY(e.event));
if (SUI.browser.isIE) {
e.event.returnValue = false;
} else {
e.event.preventDefault();
}
},
/**
* Call the onfocus listener
*/
_onFocus: function(e) {
if (SUI.browser.isIE) {
this._editDoc.selection.createRange().select();
}
this.callListener("onFocus", e);
},
/**
* Call host's onKeyDown
*/
_onKeyDown: function(e) {
// TODO check paste
// Patch webkit's behaviour for shift-enter (actually a Safari issue)
if (SUI.browser.isWebKit && e.event.keyCode == 13 && e.event.shiftKey) {
this._execCommand("insertLineBreak");
e.event.preventDefault();
}
if (!SUI.browser.isIE && e.event.ctrlKey && e.event.keyCode === 86) {
this._pasteData.interceptPaste();
}
this.callListener("onKeyDown", e);
// IE 8 has it wrong when a control (f.i. image) selection in a
// contenteditable region was made and the backspace key was pressed:
// it does 'browser back' instead of deleting the control.
if (SUI.browser.isIE && e.event.keyCode == 8) {
// if a control was selected ..
if (this._editDoc.selection.type === "Control") {
// ... delete it and prevent IE's default action
this._execCommand("delete");
e.event.returnValue = false;
}
}
},
/**
* Some stuff going on onKeyUp. Here we can fix some stuff that WebKit
* does wrong, and finish a past action. It also triggers
* _onSelectionChange in certain cases.
*/
_onKeyUp: function(e) {
// Replace div to p if WebKit inserts a div on enter
if (SUI.browser.isWebKit && e.event.keyCode == 13) {
this._webKitNoDivOnEnterFix();
}
// TODO check paste
// If we were currently pasting finish that action
// this._pasteData.finishPaste();
var x = (this._editDoc.queryCommandEnabled("Undo") ? 1 : 0)
+ (this._editDoc.queryCommandEnabled("Redo") ? 2 : 0);
if (this._undoState !== x) {
this._undoState = x;
this.callListener("onCommandExecuted");
}
// If some sort of control key was pressed do a _onSelectionChange
if (e.event.ctrlKey == true || e.event.keyCode < 48
|| e.event.keyCode > 90) {
this._onSelectionChange(e);
}
},
/**
* This onload handler is called when the initial generated empty
* document was inserted into the iframe. On this onload handler we
* further complete the initialization process and do some more
* interesting things like setting the contenteditable state,
* event handlers, set (if any) content, etc.
*/
_onLoadIframe: function() {
var that = this;
// reference to the iframe's window
this._editWin = this.iframe.contentWindow;
// reference to the docment element in the iframe, this is important:
// it is referenced all over the place
this._editDoc =
(this.iframe.contentWindow || this.iframe.contentDocument);
if (this._editDoc.document) {
this._editDoc = this._editDoc.document;
}
// Set charset here for IE 8 and less (see construction of iframe).
if (SUI.browser.isIE && SUI.browser.version < 9) {
this._editDoc.charset = "UTF-8";
}
// Yes: we can go editing
this._setContentEditable(true);
// Browser specific initialization
if (SUI.browser.isGecko) {
this._initGecko();
} else if (SUI.browser.isWebKit) {
this._initWebKit();
} else if (SUI.browser.isIE) {
this._initIE();
} else if (!this._editDoc.body.innerHTML) {
this._editDoc.body.innerHTML = "<p> </p>";
}
// onMouseUp event of the editable document
SUI.browser.addEventListener(this._editDoc, "mouseup", function(e) {
e = that._ieEvent(e);
if (!that._onMouseUp(new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
});
// onFocus event of the editable document
SUI.browser.addEventListener(this._editWin, "focus", function(e) {
e = that._ieEvent(e);
if (!that._onFocus(new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
});
// onKeyDown event of the editable document
SUI.browser.addEventListener(this._editDoc, "keydown", function(e) {
e = that._ieEvent(e);
if (!that._onKeyDown(new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
});
// onKeyUp event of the editable document
SUI.browser.addEventListener(this._editDoc, "keyup", function(e) {
e = that._ieEvent(e);
if (!that._onKeyUp(new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
});
// onContextMenu event of the editable document
SUI.browser.addEventListener(
this._editDoc, "contextmenu", function(e) {
e = that._ieEvent(e);
if (!that._onContextMenu(new SUI.Event(this, e))) {
SUI.browser.noPropagation(e);
}
});
// onPaste event of the editable body
/* _onPaste: two different strategies here. IE lets you access the
* clipboard so it is easy to get the data and do your own processing.
* Other browser are not that convenient, here we intercept the paste
* action by pasting into a hidden div. At a later moment the contents
* of that div is read and inserted into the editor (finishPaste)
*/
if (SUI.browser.isIE) {
SUI.browser.addEventListener(
this._editDoc.body, "beforepaste",
function(e) {
e = that._ieEvent(e);
if (that._ieDoBeforePaste) {
if (!that._pasteData.interceptPaste()) {
SUI.browser.noPropagation(e);
}
}
}
);
} else if (SUI.browser.isWebKit) {
// TODO check paste
SUI.browser.addEventListener(this._editDoc.body, "paste",
function(e) {
if (that._pasteData._div) {
e = that._ieEvent(e);
// check if the clipboard has HTML data
if (e.clipboardData.items.length > 0) {
var hasHtml = false;
for (var i=0; i<e.clipboardData.types.length; i++) {
if (e.clipboardData.types[i] === "text/html") {
hasHtml = true;
}
}
that._pasteData._div.innerHTML =
e.clipboardData.getData(
hasHtml?"text/html":"text/plain");
}
SUI.browser.noPropagation(e);
e.preventDefault();
that._pasteData.finishPaste();
}
}
);
}
// Now the contenteditable is loaded an with everything in place we now
// can load the stylesheets ...
if (this._tmpStoreStyleSheet) {
this.setCSS(
this._tmpStoreStyleSheet[0], this._tmpStoreStyleSheet[1],
this._tmpStoreStyleSheet[2]);
}
// ... and content into the control
if (this._tmpStore) {
this.setValue(this._tmpStore);
} else {
this.callListener("onLoad");
}
this._loaded = true;
},
/**
* On mouse-up: good chance that we've changed the selection
*/
_onMouseUp: function(e) {
this._onSelectionChange(e);
},
/**
* Try of we can detect if the cursor or selection moved to another
* node in the DOM tree
*/
_onSelectionChange: function(e) {
if (SUI.browser.isIE && SUI.browser.version < 9) {
// IE has its own onselectionchange
return;
} else {
var sel = this._getSelection();
var elem = this._getParentElementSelection(sel);
if (this._lastSel.cllps !== null
&& sel.isCollapsed == this._lastSel.cllps) {
// if not first iteration (!== null) and collapsed state
// is not changed then check if current node selection is
// changed or if we're in the next iteration
if (this._lastSel.cnt == 1 || elem != this._lastSel.elem) {
if (elem != this._lastSel.elem) {
this._lastSel.cnt = 0;
}
this._lastSel.elem = elem;
this.callListener("onSelectionChange");
}
this._lastSel.cnt++;
} else {
// Collapsed state changed or first iteration
this._lastSel.cnt = 0;
this._lastSel.elem = elem;
this.callListener("onSelectionChange");
}
this._lastSel.cllps = sel.isCollapsed;
}
},
/**
* Don't try of we can detect if the cursor or selection moved to another
* node in the DOM tree: IE does it for us. This is now only for IE 8 and
* below.
*/
_onSelectionChangeIE: function(e) {
this.callListener("onSelectionChange");
},
/**
* A browser independent implementation of outerHTML
*/
_outerHTML: function(n) {
if (SUI.browser.isIE) {
return n.outerHTML;
} else {
var n2 = document.createElement("DIV");
n2.appendChild(n.cloneNode(true));
var r = n2.innerHTML;
return r;
}
},
/**
* Select the word a the current range, but only if the range is
* collapsed. This is a creates a range and is not yet added to
* a selection.
*/
_rangeSelectWord: function(r) {
if (SUI.browser.isIE) {
// only try to select a word if the range is collapsed
if (r.text != "") {
return;
}
// extend the range
if (r.expand("word")) {
if (r.text == "") {
return;
}
// remove a trailing space from the selection, a common
// nuisance
if (r.text.indexOf(" ") != -1) {
r.moveEnd("character", -1);
}
}
} else {
// only try to select a word if the range is collapsed
if (!r.collapsed) {
return;
}
var so = r.startOffset;
// extend the range to the whitespace before the marker
try {
for (var i=so; i>=0; i--) {
r.setStart(r.startContainer, i);
var c = r.toString();
if (c.match(/\s/)) {
r.setStart(r.startContainer, i+1);
break;
}
}
} catch(e){}
// extend the range to the whitespace after the marker
try {
for (var i=so; i<100; i++) {
r.setEnd(r.startContainer, i);
var c = r.toString();
c = ""+c[c.length-1];
if (c.match(/\s/)) {
r.setEnd(r.startContainer, i-1);
break;
}
}
} catch(e){}
// remove a trailing space from the selection, a common nuisance
var rs = r.toString();
try {
if (rs.length > 0 && rs[rs.length-1].match(/\s/)) {
r.setEnd(r.startContainer, r.startOffset+rs.length-1);
}
} catch(e){}
}
},
/**
* Replace a node, use execCommand inserthtml if possible
*/
_replaceNode: function(oldn, newn) {
if (SUI.browser.isIE) {
// does not work in IE, so use DOM and break the undo stack
oldn.parentNode.replaceChild(newn, oldn);
} else {
// select the node ...
var r = this._getCurrentRange();
r.selectNode(oldn);
var s = this._getSelection();
s.removeAllRanges();
s.addRange(r);
// ... and replace it
this._execCommand("delete");
this._execCommand("inserthtml", this._outerHTML(newn));
}
},
/**
* Select the word a the current cursor position, but only if the range is
* collapsed. It is a non-IE method but it is the way IE usually behaves,
* so we don't need it for that.
*/
_selectWord: function() {
if (SUI.browser.isIE) {
return;
}
var s = this._getSelection();
var r = this._getCurrentRange(s);
if (r.collapsed) {
this._rangeSelectWord(r);
if (r.collapsed) {
return;
}
}
s.removeAllRanges();
s.addRange(r);
},
/**
* Set contentEditable property of the body or otherwise sepecified
* elements
*/
_setContentEditable: function(editable) {
var that = this;
// Clear all contenteditables.
for (var i=0; i<this._editableElements.length; i++) {
this._editableElements[i].contentEditable = false;
}
this._editableElements = [];
this._focussedElement = null;
// If we need to set it.
if (editable) {
// Loop through all the ids of the elements that need to be
// contentedtiable ...
for (var i=0; i<this._editableElementIds.length; i++) {
// ... get that element and ...
var e = this._editDoc.getElementById(
this._editableElementIds[i]);
// ... if found set it and add the element to the internal list.
if (e) {
this._editableElements.push(e);
e.contentEditable = true;
if (SUI.browser.isIE) {
SUI.browser.addEventListener(e, "focus", function(e) {
that._focussedElement = this;
});
}
}
}
// If there are no elements marked for contenteditable ...
if (this._editableElements.length == 0) {
// ... create our own marker.
this._editableElements.push(this._editDoc.body);
this._editDoc.body.contentEditable = true;
}
}
},
/**
* Fix images in WebKit: let the user select images by clicking on them
*/
_webKitImgFix: function(elem) {
if (SUI.browser.isWebKit) {
// if it is WebKit get all the images and add onclick handlers
var imgs = this._editDoc.getElementsByTagName("IMG");
for (var i=0; i<imgs.length; i++) {
// it might be added before, so try to remove handler first
SUI.browser.removeEventListener(imgs[i], "click",
this._webKitImgFixOnClick);
// add the onclick handler for images in WebKit
SUI.browser.addEventListener(imgs[i], "click",
this._webKitImgFixOnClick);
}
}
},
/**
* Onclick handler for WebKit images. WebKit fails to select image when
* user clicks it
*/
_webKitImgFixOnClick: function(e) {
var r = this.ownerDocument.createRange();
r.selectNode(this);
var s = this.ownerDocument.getSelection();
s.removeAllRanges();
s.addRange(r);
},
/**
* Fix undesired behavior in WebKit: it inserts a DIV on an Enter in
* certain contexts (f.i. after a header or if there is no initial
* paragraph)
*/
_webKitNoDivOnEnterFix: function() {
// what's the selections parent?
var n = this.getSelectedElement();
if (n.tagName == "DIV") {
// it is a div: replace it with a p
var p = this._editDoc.createElement("P");
p.innerHTML = n.innerHTML;
n.parentNode.insertBefore(p, n);
n.parentNode.removeChild(n);
// set the cursor at the beginning of the new p
var range = this._editDoc.createRange();
range.selectNodeContents(p);
range.collapse(false);
var s = this._getSelection();
s.removeAllRanges();
s.addRange(range);
}
}
});