diff --git a/modules/editor/skins/xquared/LICENSE b/modules/editor/skins/xquared/LICENSE
index b47e925fd..7a9ef2b6d 100644
--- a/modules/editor/skins/xquared/LICENSE
+++ b/modules/editor/skins/xquared/LICENSE
@@ -2,13 +2,16 @@ Xquared is copyrighted free software by Alan Kang .
You can redistribute and/or modify it under the terms of the LGPL.
(http://www.gnu.org/licenses/lgpl.html)
-Following is a list of dependencies:
- * prototype javascript framework
- * Homepage: http://prototypejs.org
- * License: http://dev.rubyonrails.org/browser/spinoffs/prototype/trunk/LICENSE?format=raw
+While Xquared itself has no dependencies with external libraries, you need following libraries in order to build Xquared from source code:
* jsspec
* Homepage: http://jania.pe.kr/aw/moin.cgi/JSSpec
* License: http://www.gnu.org/licenses/lgpl.html
* yui-compressor
* Homepage: http://developer.yahoo.com/yui/compressor/
* License: http://developer.yahoo.com/yui/license.html
+ * selenium-core
+ * Homepage: http://selenium-core.openqa.org/
+ * License: http://selenium-core.openqa.org/license.jsp
+ * jsdoc_toolkit
+ * Homepage: http://code.google.com/p/jsdoc-toolkit/
+ * License: http://www.opensource.org/licenses/mit-license.php
\ No newline at end of file
diff --git a/modules/editor/skins/xquared/README b/modules/editor/skins/xquared/README
index dcdd58778..ff702cde2 100644
--- a/modules/editor/skins/xquared/README
+++ b/modules/editor/skins/xquared/README
@@ -4,6 +4,6 @@ editor module aim to support major modern web browsers.
This software is licensed under the terms you may find in the file
named "LICENSE" in this directory.
-For more information, see http://labs.openmaru.com/projects/xquared/
+For more information, see http://xquared.springbook.playmaru.net/
Thanks for using Xquared.
\ No newline at end of file
diff --git a/modules/editor/skins/xquared/editor.html b/modules/editor/skins/xquared/editor.html
index 674d9936d..5aa6e3ef2 100644
--- a/modules/editor/skins/xquared/editor.html
+++ b/modules/editor/skins/xquared/editor.html
@@ -1,14 +1,14 @@
-
-
+
+
-
-
+
+
@@ -40,7 +40,7 @@
-
+
diff --git a/modules/editor/skins/xquared/images/content/placeholder.gif b/modules/editor/skins/xquared/images/content/placeholder.gif
new file mode 100644
index 000000000..af4601238
Binary files /dev/null and b/modules/editor/skins/xquared/images/content/placeholder.gif differ
diff --git a/modules/editor/skins/xquared/images/toolbar/iframe.gif b/modules/editor/skins/xquared/images/toolbar/iframe.gif
new file mode 100644
index 000000000..6f351c37a
Binary files /dev/null and b/modules/editor/skins/xquared/images/toolbar/iframe.gif differ
diff --git a/modules/editor/skins/xquared/images/toolbar/movie.gif b/modules/editor/skins/xquared/images/toolbar/movie.gif
new file mode 100644
index 000000000..43cbafb50
Binary files /dev/null and b/modules/editor/skins/xquared/images/toolbar/movie.gif differ
diff --git a/modules/editor/skins/xquared/images/toolbar/removeLink.gif b/modules/editor/skins/xquared/images/toolbar/removeLink.gif
new file mode 100644
index 000000000..0049162b7
Binary files /dev/null and b/modules/editor/skins/xquared/images/toolbar/removeLink.gif differ
diff --git a/modules/editor/skins/xquared/javascripts/Browser.js b/modules/editor/skins/xquared/javascripts/Browser.js
new file mode 100644
index 000000000..c9035b82e
--- /dev/null
+++ b/modules/editor/skins/xquared/javascripts/Browser.js
@@ -0,0 +1,110 @@
+/**
+ * @namespace Contains browser detection codes
+ *
+ * @requires Xquared.js
+ */
+xq.Browser = new function() {
+ // By Rendering Engines
+
+ /**
+ * True if rendering engine is Trident
+ * @type boolean
+ */
+ this.isTrident = navigator.appName === "Microsoft Internet Explorer",
+
+ /**
+ * True if rendering engine is Webkit
+ * @type boolean
+ */
+ this.isWebkit = navigator.userAgent.indexOf('AppleWebKit/') > -1,
+
+ /**
+ * True if rendering engine is Gecko
+ * @type boolean
+ */
+ this.isGecko = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1,
+
+ /**
+ * True if rendering engine is KHTML
+ * @type boolean
+ */
+ this.isKHTML = navigator.userAgent.indexOf('KHTML') !== -1,
+
+ /**
+ * True if rendering engine is Presto
+ * @type boolean
+ */
+ this.isPresto = navigator.appName === "Opera",
+
+
+
+ // By Platforms
+ /**
+ * True if platform is Mac
+ * @type boolean
+ */
+ this.isMac = navigator.userAgent.indexOf("Macintosh") !== -1,
+
+ /**
+ * True if platform is Ubuntu Linux
+ * @type boolean
+ */
+ this.isUbuntu = navigator.userAgent.indexOf('Ubuntu') !== -1,
+
+ /**
+ * True if platform is Windows
+ * @type boolean
+ */
+ this.isWin = navigator.userAgent.indexOf('Windows') !== -1,
+
+
+
+ // By Browsers
+ /**
+ * True if browser is Internet Explorer
+ * @type boolean
+ */
+ this.isIE = navigator.appName === "Microsoft Internet Explorer",
+
+ /**
+ * True if browser is Internet Explorer 6
+ * @type boolean
+ */
+ this.isIE6 = navigator.userAgent.indexOf('MSIE 6') !== -1,
+
+ /**
+ * True if browser is Internet Explorer 7
+ * @type boolean
+ */
+ this.isIE7 = navigator.userAgent.indexOf('MSIE 7') !== -1,
+
+ /**
+ * True if browser is Internet Explorer 8
+ * @type boolean
+ */
+ this.isIE8 = navigator.userAgent.indexOf('MSIE 8') !== -1,
+
+ /**
+ * True if browser is Firefox
+ * @type boolean
+ */
+ this.isFF = navigator.userAgent.indexOf('Firefox') !== -1,
+
+ /**
+ * True if browser is Firefox 2
+ * @type boolean
+ */
+ this.isFF2 = navigator.userAgent.indexOf('Firefox/2') !== -1,
+
+ /**
+ * True if browser is Firefox 3
+ * @type boolean
+ */
+ this.isFF3 = navigator.userAgent.indexOf('Firefox/3') !== -1,
+
+ /**
+ * True if browser is Safari
+ * @type boolean
+ */
+ this.isSafari = navigator.userAgent.indexOf('Safari') !== -1
+};
\ No newline at end of file
diff --git a/modules/editor/skins/xquared/javascripts/DomTree.js b/modules/editor/skins/xquared/javascripts/DomTree.js
new file mode 100644
index 000000000..488902f74
--- /dev/null
+++ b/modules/editor/skins/xquared/javascripts/DomTree.js
@@ -0,0 +1,329 @@
+/**
+ * @requires Xquared.js
+ */
+xq.DomTree = xq.Class(/** @lends xq.DomTree.prototype */{
+ /**
+ * Provides various tree operations.
+ *
+ * TODO: Add specs
+ *
+ * @constructs
+ */
+ initialize: function() {
+ xq.addToFinalizeQueue(this);
+
+ this._blockTags = ["DIV", "DD", "LI", "ADDRESS", "CAPTION", "DT", "H1", "H2", "H3", "H4", "H5", "H6", "HR", "P", "BODY", "BLOCKQUOTE", "PRE", "PARAM", "DL", "OL", "UL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"];
+ this._blockContainerTags = ["DIV", "DD", "LI", "BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"];
+ this._listContainerTags = ["OL", "UL", "DL"];
+ this._tableCellTags = ["TH", "TD"];
+ this._blockOnlyContainerTags = ["BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR"];
+ this._atomicTags = ["IMG", "OBJECT", "PARAM", "BR", "HR"];
+ },
+
+ getBlockTags: function() {
+ return this._blockTags;
+ },
+
+ /**
+ * Find common ancestor(parent) and his immediate children(left and right).
+ *
+ * A --- B -+- C -+- D -+- E
+ * |
+ * +- F -+- G
+ *
+ * For example:
+ * > findCommonAncestorAndImmediateChildrenOf("E", "G")
+ *
+ * will return
+ *
+ * > {parent:"B", left:"C", right:"F"}
+ */
+ findCommonAncestorAndImmediateChildrenOf: function(left, right) {
+ if(left.parentNode === right.parentNode) {
+ return {
+ left:left,
+ right:right,
+ parent:left.parentNode
+ };
+ } else {
+ var parentsOfLeft = this.collectParentsOf(left, true);
+ var parentsOfRight = this.collectParentsOf(right, true);
+ var ca = this.getCommonAncestor(parentsOfLeft, parentsOfRight);
+
+ var leftAncestor = parentsOfLeft.find(function(node) {return node.parentNode === ca});
+ var rightAncestor = parentsOfRight.find(function(node) {return node.parentNode === ca});
+
+ return {
+ left:leftAncestor,
+ right:rightAncestor,
+ parent:ca
+ };
+ }
+ },
+
+ /**
+ * Find leaves at edge.
+ *
+ * A --- B -+- C -+- D -+- E
+ * |
+ * +- F -+- G
+ *
+ * For example:
+ * > getLeavesAtEdge("A")
+ *
+ * will return
+ *
+ * > ["E", "G"]
+ */
+ getLeavesAtEdge: function(element) {
+ if(!element.hasChildNodes()) return [null, null];
+
+ var findLeft = function(el) {
+ for (var i = 0; i < el.childNodes.length; i++) {
+ if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findLeft(el.childNodes[i]);
+ }
+ return el;
+ }.bind(this);
+
+ var findRight=function(el) {
+ for (var i = el.childNodes.length; i--;) {
+ if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findRight(el.childNodes[i]);
+ }
+ return el;
+ }.bind(this);
+
+ var left = findLeft(element);
+ var right = findRight(element);
+
+ return [left === element ? null : left, right === element ? null : right];
+ },
+
+ getCommonAncestor: function(parents1, parents2) {
+ for(var i = 0; i < parents1.length; i++) {
+ for(var j = 0; j < parents2.length; j++) {
+ if(parents1[i] === parents2[j]) return parents1[i];
+ }
+ }
+ },
+
+ collectParentsOf: function(node, includeSelf, exitCondition) {
+ var parents = [];
+ if(includeSelf) parents.push(node);
+
+ while((node = node.parentNode) && (node.nodeName !== "HTML") && !(typeof exitCondition === "function" && exitCondition(node))) parents.push(node);
+ return parents;
+ },
+
+ isDescendantOf: function(parent, child) {
+ if(parent.length > 0) {
+ for(var i = 0; i < parent.length; i++) {
+ if(this.isDescendantOf(parent[i], child)) return true;
+ }
+ return false;
+ }
+
+ if(parent === child) return false;
+
+ while (child = child.parentNode)
+ if (child === parent) return true;
+ return false;
+ },
+
+ /**
+ * Perform tree walking (foreward)
+ */
+ walkForward: function(node) {
+ var target = node.firstChild;
+ if(target) return target;
+
+ // intentional assignment for micro performance turing
+ if(target = node.nextSibling) return target;
+
+ while(node = node.parentNode) {
+ // intentional assignment for micro performance turing
+ if(target = node.nextSibling) return target;
+ }
+
+ return null;
+ },
+
+ /**
+ * Perform tree walking (backward)
+ */
+ walkBackward: function(node) {
+ if(node.previousSibling) {
+ node = node.previousSibling;
+ while(node.hasChildNodes()) {node = node.lastChild;}
+ return node;
+ }
+
+ return node.parentNode;
+ },
+
+ /**
+ * Perform tree walking (to next siblings)
+ */
+ walkNext: function(node) {return node.nextSibling},
+
+ /**
+ * Perform tree walking (to next siblings)
+ */
+ walkPrev: function(node) {return node.previousSibling},
+
+ /**
+ * Returns true if target is followed by start
+ */
+ checkTargetForward: function(start, target) {
+ return this._check(start, this.walkForward, target);
+ },
+
+ /**
+ * Returns true if start is followed by target
+ */
+ checkTargetBackward: function(start, target) {
+ return this._check(start, this.walkBackward, target);
+ },
+
+ findForward: function(start, condition, exitCondition) {
+ return this._find(start, this.walkForward, condition, exitCondition);
+ },
+
+ findBackward: function(start, condition, exitCondition) {
+ return this._find(start, this.walkBackward, condition, exitCondition);
+ },
+
+ _check: function(start, direction, target) {
+ if(start === target) return false;
+
+ while(start = direction(start)) {
+ if(start === target) return true;
+ }
+ return false;
+ },
+
+ _find: function(start, direction, condition, exitCondition) {
+ while(start = direction(start)) {
+ if(exitCondition && exitCondition(start)) return null;
+ if(condition(start)) return start;
+ }
+ return null;
+ },
+
+ /**
+ * Walks Forward through DOM tree from start to end, and collects all nodes that matches with a filter.
+ * If no filter provided, it just collects all nodes.
+ *
+ * @param {Element} start Starting element.
+ * @param {Element} end Ending element.
+ * @param {Function} filter A filter function.
+ */
+ collectNodesBetween: function(start, end, filter) {
+ if(start === end) return [start, end].findAll(filter || function() {return true});
+
+ var nodes = this.collectForward(start, function(node) {return node === end}, filter);
+ if(
+ start !== end &&
+ typeof filter === "function" &&
+ filter(end)
+ ) nodes.push(end);
+
+ return nodes;
+ },
+
+ collectForward: function(start, exitCondition, filter) {
+ return this.collect(start, this.walkForward, exitCondition, filter);
+ },
+
+ collectBackward: function(start, exitCondition, filter) {
+ return this.collect(start, this.walkBackward, exitCondition, filter);
+ },
+
+ collectNext: function(start, exitCondition, filter) {
+ return this.collect(start, this.walkNext, exitCondition, filter);
+ },
+
+ collectPrev: function(start, exitCondition, filter) {
+ return this.collect(start, this.walkPrev, exitCondition, filter);
+ },
+
+ collect: function(start, next, exitCondition, filter) {
+ var nodes = [start];
+
+ while(true) {
+ start = next(start);
+ if(
+ (start === null) ||
+ (typeof exitCondition === "function" && exitCondition(start))
+ ) break;
+
+ nodes.push(start);
+ }
+
+ return (typeof filter === "function") ? nodes.findAll(filter) : nodes;
+ },
+
+ hasBlocks: function(element) {
+ var nodes = element.childNodes;
+ for(var i = 0; i < nodes.length; i++) {
+ if(this.isBlock(nodes[i])) return true;
+ }
+ return false;
+ },
+
+ hasMixedContents: function(element) {
+ if(!this.isBlock(element)) return false;
+ if(!this.isBlockContainer(element)) return false;
+
+ var hasTextOrInline = false;
+ var hasBlock = false;
+ for(var i = 0; i < element.childNodes.length; i++) {
+ var node = element.childNodes[i];
+ if(!hasTextOrInline && this.isTextOrInlineNode(node)) hasTextOrInline = true;
+ if(!hasBlock && this.isBlock(node)) hasBlock = true;
+
+ if(hasTextOrInline && hasBlock) break;
+ }
+ if(!hasTextOrInline || !hasBlock) return false;
+
+ return true;
+ },
+
+ isBlockOnlyContainer: function(element) {
+ if(!element) return false;
+ return this._blockOnlyContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1;
+ },
+
+ isTableCell: function(element) {
+ if(!element) return false;
+ return this._tableCellTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1;
+ },
+
+ isBlockContainer: function(element) {
+ if(!element) return false;
+ return this._blockContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1;
+ },
+
+ isHeading: function(element) {
+ if(!element) return false;
+ return (typeof element === 'string' ? element : element.nodeName).match(/H\d/);
+ },
+
+ isBlock: function(element) {
+ if(!element) return false;
+ return this._blockTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1;
+ },
+
+ isAtomic: function(element) {
+ if(!element) return false;
+ return this._atomicTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1;
+ },
+
+ isListContainer: function(element) {
+ if(!element) return false;
+ return this._listContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1;
+ },
+
+ isTextOrInlineNode: function(node) {
+ return node && (node.nodeType === 3 || !this.isBlock(node));
+ }
+});
\ No newline at end of file
diff --git a/modules/editor/skins/xquared/javascripts/EditHistory.js b/modules/editor/skins/xquared/javascripts/EditHistory.js
new file mode 100644
index 000000000..c0e79fea0
--- /dev/null
+++ b/modules/editor/skins/xquared/javascripts/EditHistory.js
@@ -0,0 +1,151 @@
+/**
+ * @requires Xquared.js
+ * @requires rdom/Factory.js
+ */
+xq.EditHistory = xq.Class(/** @lends xq.EditHistory.prototype */{
+ /**
+ * Manages editing history and performs UNDO/REDO.
+ *
+ * @constructs
+ * @param {xq.rdom.Base} rdom Base instance
+ * @param {Number} [max=100] maximum UNDO buffer size.
+ */
+ initialize: function(rdom, max) {
+ xq.addToFinalizeQueue(this);
+ if (!rdom) throw "IllegalArgumentException";
+
+ this.disabled = false;
+ this.max = max || 100;
+ this.rdom = rdom;
+ this.index = -1;
+ this.queue = [];
+
+ this.lastModified = Date.get();
+ },
+ getLastModifiedDate: function() {
+ return this.lastModified;
+ },
+ isUndoable: function() {
+ return this.queue.length > 0 && this.index > 0;
+ },
+ isRedoable: function() {
+ return this.queue.length > 0 && this.index < this.queue.length - 1;
+ },
+ disable: function() {
+ this.disabled = true;
+ },
+ enable: function() {
+ this.disabled = false;
+ },
+ undo: function() {
+ this.pushContent();
+
+ if (this.isUndoable()) {
+ this.index--;
+ this.popContent();
+ return true;
+ } else {
+ return false;
+ }
+ },
+ redo: function() {
+ if (this.isRedoable()) {
+ this.index++;
+ this.popContent();
+ return true;
+ } else {
+ return false;
+ }
+ },
+ onCommand: function() {
+ this.lastModified = Date.get();
+ if(this.disabled) return false;
+
+ return this.pushContent();
+ },
+ onEvent: function(event) {
+ this.lastModified = Date.get();
+ if(this.disabled) return false;
+
+ var arrowKeys = [33,34,35,36,37,39];
+ // @WORKAROUND: Mac에서 화살표 up/down 누를 때 pushContent 하면 캐럿이 튄다
+ if(!xq.Browser.isMac) arrowKeys.push(38,40);
+
+ // ignore some event types
+ if(['blur', 'mouseup'].indexOf(event.type) !== -1) return false;
+
+ // ignore normal keys
+ if('keydown' === event.type && !(event.ctrlKey || event.metaKey)) return false;
+ if(['keydown', 'keyup', 'keypress'].indexOf(event.type) !== -1 && !event.ctrlKey && !event.altKey && !event.metaKey && arrowKeys.indexOf(event.keyCode) === -1) return false;
+ if(['keydown', 'keyup', 'keypress'].indexOf(event.type) !== -1 && (event.ctrlKey || event.metaKey) && [89,90].indexOf(event.keyCode) !== -1) return false;
+
+ // ignore ctrl/shift/alt/meta keys
+ if([16,17,18,224].indexOf(event.keyCode) !== -1) return false;
+
+ return this.pushContent();
+ },
+ popContent: function() {
+ this.lastModified = Date.get();
+ var entry = this.queue[this.index];
+ if (entry.caret > 0) {
+ var html=entry.html.substring(0, entry.caret) + '' + entry.html.substring(entry.caret);
+ this.rdom.getRoot().innerHTML = html;
+ } else {
+ this.rdom.getRoot().innerHTML = entry.html;
+ }
+ this.restoreCaret();
+ },
+ pushContent: function(ignoreCaret) {
+ if(xq.Browser.isTrident && !ignoreCaret && !this.rdom.hasFocus()) return false;
+ if(!this.rdom.getCurrentElement()) return false;
+
+ var html = this.rdom.getRoot().innerHTML;
+ if(html === (this.queue[this.index] ? this.queue[this.index].html : null)) return false;
+
+ var caret = ignoreCaret ? -1 : this.saveCaret();
+
+ if(this.queue.length >= this.max) {
+ this.queue.shift();
+ } else {
+ this.index++;
+ }
+
+ this.queue.splice(this.index, this.queue.length - this.index, {html:html, caret:caret});
+ return true;
+ },
+ clear: function() {
+ this.index = -1;
+ this.queue = [];
+ this.pushContent(true);
+ },
+ saveCaret: function() {
+ if(this.rdom.hasSelection()) return null;
+
+ var bookmark = this.rdom.saveSelection();
+ var marker = this.rdom.pushMarker();
+
+ var str = xq.Browser.isTrident ? '
+ * Note that the validation will be performed regardless of this value when you switching edit mode.
+ * @type boolean
+ */
+ this.config.noValidationInSourceEditMode = false;
+
+ /**
+ * Automatically hooks onsubmit event.
+ * @type boolean
+ */
+ this.config.automaticallyHookSubmitEvent = true;
+
+ /**
+ * Set of whitelist(tag name and attributes) for use in validator
+ * @type Object
+ */
+ this.config.whitelist = xq.predefinedWhitelist;
+
+ /**
+ * Specifies a value of ID attribute for WYSIWYG document's body
+ * @type String
+ */
+ this.config.bodyId = "";
+
+ /**
+ * Specifies a value of CLASS attribute for WYSIWYG document's body
+ * @type String
+ */
+ this.config.bodyClass = "xed";
+
+ /**
+ * Plugins
+ * @type Object
+ */
+ this.config.plugins = {};
+
+ /**
+ * Shortcuts
+ * @type Object
+ */
+ this.config.shortcuts = {};
+
+ /**
+ * Autocorrections
+ * @type Object
+ */
+ this.config.autocorrections = {};
+
+ /**
+ * Autocompletions
+ * @type Object
+ */
+ this.config.autocompletions = {};
+
+ /**
+ * Template processors
+ * @type Object
+ */
+ this.config.templateProcessors = {};
+
+ /**
+ * Context menu handlers
+ * @type Object
+ */
+ this.config.contextMenuHandlers = {};
+
+ /**
+ * Original content element
+ * @type Element
+ */
+ this.contentElement = contentElement;
+
+ /**
+ * Owner document of content element
+ * @type Document
+ */
+ this.doc = this.contentElement.ownerDocument;
+
+ /**
+ * Body of content element
+ * @type Element
+ */
+ this.body = this.doc.body;
+
+ /**
+ * False or 'source' means source editing mode, true or 'wysiwyg' means WYSIWYG editing mode.
+ * @type Object
+ */
+ this.currentEditMode = '';
+
+ /**
+ * Timer
+ * @type xq.Timer
+ */
+ this.timer = new xq.Timer(100);
+
+ /**
+ * Base instance
+ * @type xq.rdom.Base
+ */
+ this.rdom = xq.rdom.Base.createInstance();
+
+ /**
+ * Base instance
+ * @type xq.validator.Base
+ */
+ this.validator = null;
+
+ /**
+ * Outmost wrapper div
+ * @type Element
+ */
+ this.outmostWrapper = null;
+
+ /**
+ * Source editor container
+ * @type Element
+ */
+ this.sourceEditorDiv = null;
+
+ /**
+ * Source editor textarea
+ * @type Element
+ */
+ this.sourceEditorTextarea = null;
+
+ /**
+ * WYSIWYG editor container
+ * @type Element
+ */
+ this.wysiwygEditorDiv = null;
+
+ /**
+ * Outer frame
+ * @type IFrame
+ */
+ this.outerFrame = null;
+
+ /**
+ * Design mode iframe
+ * @type IFrame
+ */
+ this.editorFrame = null;
+
+ this.toolbarContainer = toolbarContainer;
+
+ /**
+ * Toolbar container
+ * @type Element
+ */
+ this.toolbar = null;
+
+ /**
+ * Undo/redo manager
+ * @type xq.EditHistory
+ */
+ this.editHistory = null;
+
+ /**
+ * Context menu container
+ * @type Element
+ */
+ this.contextMenuContainer = null;
+
+ /**
+ * Context menu items
+ * @type Array
+ */
+ this.contextMenuItems = null;
+
+ /**
+ * Platform dependent key event type
+ * @type String
+ */
+ this.platformDepedentKeyEventType = (xq.Browser.isMac && xq.Browser.isGecko ? "keypress" : "keydown");
+
+ this.addShortcuts(this.getDefaultShortcuts());
+
+ this.addListener({
+ onEditorCurrentContentChanged: function(xed) {
+ var curFocusElement = xed.rdom.getCurrentElement();
+ if(!curFocusElement || curFocusElement.ownerDocument !== xed.rdom.getDoc()) {
+ return;
+ }
+
+ if(xed.lastFocusElement !== curFocusElement) {
+ if(!xed.rdom.tree.isBlockOnlyContainer(xed.lastFocusElement) && xed.rdom.tree.isBlock(xed.lastFocusElement)) {
+ xed.rdom.removeTrailingWhitespace(xed.lastFocusElement);
+ }
+ xed._fireOnElementChanged(xed, xed.lastFocusElement, curFocusElement);
+ xed.lastFocusElement = curFocusElement;
+ }
+
+ xed.toolbar.triggerUpdate();
+ }
+ });
+ },
+
+ finalize: function() {
+ for(var key in this.config.plugins) this.config.plugins[key].unload();
+ },
+
+
+
+ /////////////////////////////////////////////
+ // Configuration Management
+
+ getDefaultShortcuts: function() {
+ if(xq.Browser.isMac) {
+ // Mac FF & Safari
+ return [
+ {event:"Ctrl+Shift+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
+ {event:"SPACE", handler:"this.handleSpace()"},
+ {event:"ENTER", handler:"this.handleEnter(false, false)"},
+ {event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
+ {event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
+ {event:"TAB", handler:"this.handleTab()"},
+ {event:"Shift+TAB", handler:"this.handleShiftTab()"},
+ {event:"DELETE", handler:"this.handleDelete()"},
+ {event:"BACKSPACE", handler:"this.handleBackspace()"},
+
+ {event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
+ {event:"Meta+B", handler:"this.handleStrongEmphasis()"},
+ {event:"Ctrl+I", handler:"this.handleEmphasis()"},
+ {event:"Meta+I", handler:"this.handleEmphasis()"},
+ {event:"Ctrl+U", handler:"this.handleUnderline()"},
+ {event:"Meta+U", handler:"this.handleUnderline()"},
+ {event:"Ctrl+K", handler:"this.handleStrike()"},
+ {event:"Meta+K", handler:"this.handleStrike()"},
+ {event:"Meta+Z", handler:"this.handleUndo()"},
+ {event:"Meta+Shift+Z", handler:"this.handleRedo()"},
+ {event:"Meta+Y", handler:"this.handleRedo()"}
+ ];
+ } else if(xq.Browser.isUbuntu) {
+ // Ubunto FF
+ return [
+ {event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
+ {event:"SPACE", handler:"this.handleSpace()"},
+ {event:"ENTER", handler:"this.handleEnter(false, false)"},
+ {event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
+ {event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
+ {event:"TAB", handler:"this.handleTab()"},
+ {event:"Shift+TAB", handler:"this.handleShiftTab()"},
+ {event:"DELETE", handler:"this.handleDelete()"},
+ {event:"BACKSPACE", handler:"this.handleBackspace()"},
+
+ {event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
+ {event:"Ctrl+I", handler:"this.handleEmphasis()"},
+ {event:"Ctrl+U", handler:"this.handleUnderline()"},
+ {event:"Ctrl+K", handler:"this.handleStrike()"},
+ {event:"Ctrl+Z", handler:"this.handleUndo()"},
+ {event:"Ctrl+Shift+Z", handler:"this.handleRedo()"},
+ {event:"Ctrl+Y", handler:"this.handleRedo()"}
+ ];
+ } else {
+ // Win IE & FF
+ return [
+ {event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
+ {event:"SPACE", handler:"this.handleSpace()"},
+ {event:"ENTER", handler:"this.handleEnter(false, false)"},
+ {event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
+ {event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
+ {event:"TAB", handler:"this.handleTab()"},
+ {event:"Shift+TAB", handler:"this.handleShiftTab()"},
+ {event:"DELETE", handler:"this.handleDelete()"},
+ {event:"BACKSPACE", handler:"this.handleBackspace()"},
+
+ {event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
+ {event:"Ctrl+I", handler:"this.handleEmphasis()"},
+ {event:"Ctrl+U", handler:"this.handleUnderline()"},
+ {event:"Ctrl+K", handler:"this.handleStrike()"},
+ {event:"Ctrl+Z", handler:"this.handleUndo()"},
+ {event:"Ctrl+Shift+Z", handler:"this.handleRedo()"},
+ {event:"Ctrl+Y", handler:"this.handleRedo()"}
+ ];
+ }
+ },
+
+ /**
+ * Adds or replaces plugin.
+ *
+ * @param {String} id unique identifier
+ */
+ addPlugin: function(id) {
+ // already added?
+ if(this.config.plugins[id]) return;
+
+ // else
+ var clazz = xq.plugin[id + "Plugin"];
+ if(!clazz) throw "Unknown plugin id: [" + id + "]";
+
+ var plugin = new clazz();
+ this.config.plugins[id] = plugin;
+ plugin.load(this);
+ },
+
+ /**
+ * Adds several plugins at once.
+ *
+ * @param {Array} list of plugin ids.
+ */
+ addPlugins: function(list) {
+ for(var i = 0; i < list.length; i++) {
+ this.addPlugin(list[i]);
+ }
+ },
+
+ /**
+ * Returns plugin matches with given identifier.
+ *
+ * @param {String} id unique identifier
+ */
+ getPlugin: function(id) {return this.config.plugins[id];},
+
+ /**
+ * Returns entire plugins
+ */
+ getPlugins: function() {return this.config.plugins;},
+
+ /**
+ * Remove plugin matches with given identifier.
+ *
+ * @param {String} id unique identifier
+ */
+ removePlugin: function(id) {
+ var plugin = this.config.shortcuts[id];
+ if(plugin) {
+ plugin.unload();
+ }
+
+ delete this.config.shortcuts[id];
+ },
+
+
+
+ /**
+ * Adds or replaces keyboard shortcut.
+ *
+ * @param {String} shortcut keymap expression like "CTRL+Space"
+ * @param {Object} handler string or function to be evaluated or called
+ */
+ addShortcut: function(shortcut, handler) {
+ this.config.shortcuts[shortcut] = {"event":new xq.Shortcut(shortcut), "handler":handler};
+ },
+
+ /**
+ * Adds several keyboard shortcuts at once.
+ *
+ * @param {Array} list of shortcuts. each element should have following structure: {event:"keymap expression", handler:handler}
+ */
+ addShortcuts: function(list) {
+ for(var i = 0; i < list.length; i++) {
+ this.addShortcut(list[i].event, list[i].handler);
+ }
+ },
+
+ /**
+ * Returns keyboard shortcut matches with given keymap expression.
+ *
+ * @param {String} shortcut keymap expression like "CTRL+Space"
+ */
+ getShortcut: function(shortcut) {return this.config.shortcuts[shortcut];},
+
+ /**
+ * Returns entire keyboard shortcuts' map
+ */
+ getShortcuts: function() {return this.config.shortcuts;},
+
+ /**
+ * Remove keyboard shortcut matches with given keymap expression.
+ *
+ * @param {String} shortcut keymap expression like "CTRL+Space"
+ */
+ removeShortcut: function(shortcut) {delete this.config.shortcuts[shortcut];},
+
+ /**
+ * Adds or replaces autocorrection handler.
+ *
+ * @param {String} id unique identifier
+ * @param {Object} criteria regex pattern or function to be used as a criterion for match
+ * @param {Object} handler string or function to be evaluated or called when criteria met
+ */
+ addAutocorrection: function(id, criteria, handler) {
+ if(criteria.exec) {
+ var pattern = criteria;
+ criteria = function(text) {return text.match(pattern)};
+ }
+ this.config.autocorrections[id] = {"criteria":criteria, "handler":handler};
+ },
+
+ /**
+ * Adds several autocorrection handlers at once.
+ *
+ * @param {Array} list of autocorrection. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler}
+ */
+ addAutocorrections: function(list) {
+ for(var i = 0; i < list.length; i++) {
+ this.addAutocorrection(list[i].id, list[i].criteria, list[i].handler);
+ }
+ },
+
+ /**
+ * Returns autocorrection handler matches with given id
+ *
+ * @param {String} id unique identifier
+ */
+ getAutocorrection: function(id) {return this.config.autocorrection[id];},
+
+ /**
+ * Returns entire autocorrections' map
+ */
+ getAutocorrections: function() {return this.config.autocorrections;},
+
+ /**
+ * Removes autocorrection handler matches with given id
+ *
+ * @param {String} id unique identifier
+ */
+ removeAutocorrection: function(id) {delete this.config.autocorrections[id];},
+
+ /**
+ * Adds or replaces autocompletion handler.
+ *
+ * @param {String} id unique identifier
+ * @param {Object} criteria regex pattern or function to be used as a criterion for match
+ * @param {Object} handler string or function to be evaluated or called when criteria met
+ */
+ addAutocompletion: function(id, criteria, handler) {
+ if(criteria.exec) {
+ var pattern = criteria;
+ criteria = function(text) {
+ var m = pattern.exec(text);
+ return m ? m.index : -1;
+ };
+ }
+ this.config.autocompletions[id] = {"criteria":criteria, "handler":handler};
+ },
+
+ /**
+ * Adds several autocompletion handlers at once.
+ *
+ * @param {Array} list of autocompletion. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler}
+ */
+ addAutocompletions: function(list) {
+ for(var i = 0; i < list.length; i++) {
+ this.addAutocompletion(list[i].id, list[i].criteria, list[i].handler);
+ }
+ },
+
+ /**
+ * Returns autocompletion handler matches with given id
+ *
+ * @param {String} id unique identifier
+ */
+ getAutocompletion: function(id) {return this.config.autocompletions[id];},
+
+ /**
+ * Returns entire autocompletions' map
+ */
+ getAutocompletions: function() {return this.config.autocompletions;},
+
+ /**
+ * Removes autocompletion handler matches with given id
+ *
+ * @param {String} id unique identifier
+ */
+ removeAutocompletion: function(id) {delete this.config.autocompletions[id];},
+
+ /**
+ * Adds or replaces template processor.
+ *
+ * @param {String} id unique identifier
+ * @param {Object} handler string or function to be evaluated or called when template inserted
+ */
+ addTemplateProcessor: function(id, handler) {
+ this.config.templateProcessors[id] = {"handler":handler};
+ },
+
+ /**
+ * Adds several template processors at once.
+ *
+ * @param {Array} list of template processors. Each element should have following structure: {id:"identifier", handler:handler}
+ */
+ addTemplateProcessors: function(list) {
+ for(var i = 0; i < list.length; i++) {
+ this.addTemplateProcessor(list[i].id, list[i].handler);
+ }
+ },
+
+ /**
+ * Returns template processor matches with given id
+ *
+ * @param {String} id unique identifier
+ */
+ getTemplateProcessor: function(id) {return this.config.templateProcessors[id];},
+
+ /**
+ * Returns entire template processors' map
+ */
+ getTemplateProcessors: function() {return this.config.templateProcessors;},
+
+ /**
+ * Removes template processor matches with given id
+ *
+ * @param {String} id unique identifier
+ */
+ removeTemplateProcessor: function(id) {delete this.config.templateProcessors[id];},
+
+
+
+ /**
+ * Adds or replaces context menu handler.
+ *
+ * @param {String} id unique identifier
+ * @param {Object} handler string or function to be evaluated or called when onContextMenu occured
+ */
+ addContextMenuHandler: function(id, handler) {
+ this.config.contextMenuHandlers[id] = {"handler":handler};
+ },
+
+ /**
+ * Adds several context menu handlers at once.
+ *
+ * @param {Array} list of handlers. Each element should have following structure: {id:"identifier", handler:handler}
+ */
+ addContextMenuHandlers: function(list) {
+ for(var i = 0; i < list.length; i++) {
+ this.addContextMenuHandler(list[i].id, list[i].handler);
+ }
+ },
+
+ /**
+ * Returns context menu handler matches with given id
+ *
+ * @param {String} id unique identifier
+ */
+ getContextMenuHandler: function(id) {return this.config.contextMenuHandlers[id];},
+
+ /**
+ * Returns entire context menu handlers' map
+ */
+ getContextMenuHandlers: function() {return this.config.contextMenuHandlers;},
+
+ /**
+ * Removes context menu handler matches with given id
+ *
+ * @param {String} id unique identifier
+ */
+ removeContextMenuHandler: function(id) {delete this.config.contextMenuHandlers[id];},
+
+
+
+ /**
+ * Sets width of editor.
+ *
+ * @param {String} w Valid CSS value for style.width. For example, "100%", "200px".
+ */
+ setWidth: function(w) {
+ this.outmostWrapper.style.width = w;
+ },
+
+
+
+ /**
+ * Sets height of editor.
+ *
+ * @param {String} h Valid CSS value for style.height. For example, "100%", "200px".
+ */
+ setHeight: function(h) {
+ this.wysiwygEditorDiv.style.height = h;
+ this.sourceEditorDiv.style.height = h;
+ },
+
+
+
+ /////////////////////////////////////////////
+ // Edit mode management
+
+ /**
+ * Returns current edit mode - wysiwyg, source
+ */
+ getCurrentEditMode: function() {
+ return this.currentEditMode;
+ },
+
+ /**
+ * Toggle edit mode between source and wysiwyg
+ */
+ toggleSourceAndWysiwygMode: function() {
+ var mode = this.getCurrentEditMode();
+ this.setEditMode(mode === 'wysiwyg' ? 'source' : 'wysiwyg');
+ },
+
+ /**
+ * Switches between WYSIWYG/Source mode.
+ *
+ * @param {String} mode 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode.
+ */
+ setEditMode: function(mode) {
+ if(typeof mode !== 'string') throw "[mode] is not a string."
+ if(['wysiwyg', 'source'].indexOf(mode) === -1) throw "Illegal [mode] value: '" + mode + "'. Use 'wysiwyg' or 'source'";
+ if(this.currentEditMode === mode) return;
+
+ // create editor frame if there's no editor frame.
+ var editorCreated = !!this.outmostWrapper;
+ if(!editorCreated) {
+ // create validator
+ this.validator = xq.validator.Base.createInstance(
+ this.doc.location.href,
+ this.config.urlValidationMode,
+ this.config.whitelist
+ );
+
+ this._fireOnStartInitialization(this);
+
+ this._createEditorFrame(mode);
+ var temp = window.setInterval(function() {
+ // wait for loading
+ if(this.getBody()) {
+ window.clearInterval(temp);
+
+ // @WORKAROUND: it is needed to fix IE6 horizontal scrollbar problem
+ if(xq.Browser.isIE6) {
+ this.rdom.getDoc().documentElement.style.overflowY='auto';
+ this.rdom.getDoc().documentElement.style.overflowX='hidden';
+ }
+
+ this.setEditMode(mode);
+ if(this.config.autoFocusOnInit) this.focus();
+
+ this.timer.start();
+ this._fireOnInitialized(this);
+ }
+ }.bind(this), 10);
+
+ return;
+ }
+
+ // switch mode
+ if(mode === 'wysiwyg') {
+ this._setEditModeToWysiwyg();
+ } else { // mode === 'source'
+ this._setEditModeToSource();
+ }
+
+ // fire event
+ var oldEditMode = this.currentEditMode;
+ this.currentEditMode = mode;
+
+ this._fireOnCurrentEditModeChanged(this, oldEditMode, this.currentEditMode);
+ },
+
+ _setEditModeToWysiwyg: function() {
+ // Turn off static content and source editor
+ this.contentElement.style.display = "none";
+ this.sourceEditorDiv.style.display = "none";
+
+ // Update contents
+ if(this.currentEditMode === 'source') {
+ // get html from source editor
+ var html = this.getSourceContent(true);
+
+ // invalidate it and load it into wysiwyg editor
+ var invalidHtml = this.validator.invalidate(html);
+ invalidHtml = this.removeUnnecessarySpaces(invalidHtml);
+ if(invalidHtml.isBlank()) {
+ this.rdom.clearRoot();
+ } else {
+ this.rdom.getRoot().innerHTML = invalidHtml;
+ this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true);
+ }
+ } else {
+ // invalidate static html and load it into wysiwyg editor
+ var invalidHtml = this.validator.invalidate(this.getStaticContent());
+ invalidHtml = this.removeUnnecessarySpaces(invalidHtml);
+ if(invalidHtml.isBlank()) {
+ this.rdom.clearRoot();
+ } else {
+ this.rdom.getRoot().innerHTML = invalidHtml;
+ this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true);
+ }
+ }
+
+ // Turn on wysiwyg editor
+ this.wysiwygEditorDiv.style.display = "block";
+ this.outmostWrapper.style.display = "block";
+
+ // Without this, xq.rdom.Base.focus() doesn't work correctly.
+ if(xq.Browser.isGecko) this.rdom.placeCaretAtStartOf(this.rdom.getRoot());
+
+ if(this.toolbar) this.toolbar.enableButtons();
+ },
+
+ _setEditModeToSource: function() {
+ // Update contents
+ var validHtml = null;
+ if(this.currentEditMode === 'wysiwyg') {
+ validHtml = this.getWysiwygContent();
+ } else {
+ validHtml = this.getStaticContent();
+ }
+ this.sourceEditorTextarea.value = validHtml
+
+ // Turn off static content and wysiwyg editor
+ this.contentElement.style.display = "none";
+ this.wysiwygEditorDiv.style.display = "none";
+
+ // Turn on source editor
+ this.sourceEditorDiv.style.display = "block";
+ this.outmostWrapper.style.display = "block";
+ if(this.toolbar) this.toolbar.disableButtons(['html']);
+ },
+
+ /**
+ * Load CSS into WYSIWYG mode document
+ *
+ * @param {string} path URL
+ */
+ loadStylesheet: function(path) {
+ var head = this.getDoc().getElementsByTagName("HEAD")[0];
+ var link = this.getDoc().createElement("LINK");
+ link.rel = "Stylesheet";
+ link.type = "text/css";
+ link.href = path;
+ head.appendChild(link);
+ },
+
+ /**
+ * Sets editor's dynamic content from static content
+ */
+ loadCurrentContentFromStaticContent: function() {
+ if(this.getCurrentEditMode() == 'wysiwyg') {
+ // update WYSIWYG editor
+ var html = this.validator.invalidate(this.getStaticContent());
+ html = this.removeUnnecessarySpaces(html);
+
+ if(html.isBlank()) {
+ this.rdom.clearRoot();
+ } else {
+ this.rdom.getRoot().innerHTML = html;
+ this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true);
+ }
+ } else { // 'source'
+ this.sourceEditorTextarea.value = this.getStaticContent();
+ }
+
+ this._fireOnCurrentContentChanged(this);
+ },
+
+ /**
+ * Removes unnecessary spaces, tabs and new lines.
+ *
+ * @param {String} html HTML string.
+ * @returns {String} Modified HTML string.
+ */
+ removeUnnecessarySpaces: function(html) {
+ var blocks = this.rdom.tree.getBlockTags().join("|");
+ var regex = new RegExp("\\s*<(/?)(" + blocks + ")>\\s*", "img");
+ return html.replace(regex, '<$1$2>');
+ },
+
+ /**
+ * Gets editor's dynamic content from current editor(source or WYSIWYG)
+ *
+ * @return {Object} HTML String
+ */
+ getCurrentContent: function() {
+ if(this.getCurrentEditMode() === 'source') {
+ return this.getSourceContent(this.config.noValidationInSourceEditMode);
+ } else {
+ return this.getWysiwygContent();
+ }
+ },
+
+ /**
+ * Gets editor's dynamic content from WYSIWYG editor
+ *
+ * @return {Object} HTML String
+ */
+ getWysiwygContent: function() {
+ return this.validator.validate(this.rdom.getRoot());
+ },
+
+ /**
+ * Gets editor's dynamic content from source editor
+ *
+ * @return {Object} HTML String
+ */
+ getSourceContent: function(noValidation) {
+ var raw = this.sourceEditorTextarea.value;
+ if(noValidation) return raw;
+
+ var tempDiv = document.createElement('div');
+ tempDiv.innerHTML = this.removeUnnecessarySpaces(raw);
+
+ var rdom = xq.rdom.Base.createInstance();
+ rdom.wrapAllInlineOrTextNodesAs("P", tempDiv, true);
+
+ return this.validator.validate(tempDiv, true);
+ },
+
+ /**
+ * Sets editor's original content
+ *
+ * @param {Object} content HTML String
+ */
+ setStaticContent: function(content) {
+ this.contentElement.value = content;
+ this._fireOnStaticContentChanged(this, content);
+ },
+
+ /**
+ * Gets editor's original content
+ *
+ * @return {Object} HTML String
+ */
+ getStaticContent: function() {
+ return this.contentElement.value;
+ },
+
+ /**
+ * Gets editor's original content as (newely created) DOM node
+ *
+ * @return {Element} DIV element
+ */
+ getStaticContentAsDOM: function() {
+ var div = this.doc.createElement('DIV');
+ div.innerHTML = this.contentElement.value;
+ return div;
+ },
+
+ /**
+ * Gives focus to editor
+ */
+ focus: function() {
+ if(this.getCurrentEditMode() === 'wysiwyg') {
+ this.rdom.focus();
+ if(this.toolbar) this.toolbar.triggerUpdate();
+ } else if(this.getCurrentEditMode() === 'source') {
+ this.sourceEditorTextarea.focus();
+ }
+ },
+
+ getWysiwygEditorDiv: function() {
+ return this.wysiwygEditorDiv;
+ },
+
+ getSourceEditorDiv: function() {
+ return this.sourceEditorDiv;
+ },
+
+ /**
+ * Returns outer iframe object
+ */
+ getOuterFrame: function() {
+ return this.outerFrame;
+ },
+
+ /**
+ * Returns outer iframe document
+ */
+ getOuterDoc: function() {
+ return this.outerFrame.contentWindow.document;
+ },
+
+ /**
+ * Returns designmode iframe object
+ */
+ getFrame: function() {
+ return this.editorFrame;
+ },
+
+ /**
+ * Returns designmode window object
+ */
+ getWin: function() {
+ return this.rdom.getWin();
+ },
+
+ /**
+ * Returns designmode document object
+ */
+ getDoc: function() {
+ return this.rdom.getDoc();
+ },
+
+ /**
+ * Returns designmode body object
+ */
+ getBody: function() {
+ return this.rdom.getRoot();
+ },
+
+ /**
+ * Returns outmost wrapper element
+ */
+ getOutmostWrapper: function() {
+ return this.outmostWrapper;
+ },
+
+ _createIFrame: function(doc, width, height) {
+ var frame = doc.createElement("iframe");
+
+ // IE displays warning when a protocol is HTTPS, because IE6 treats IFRAME
+ // without SRC attribute as insecure.
+ if(xq.Browser.isIE) frame.src = 'javascript:""';
+
+ frame.style.width = width || "100%";
+ frame.style.height = height || "100%";
+ frame.setAttribute("frameBorder", "0");
+ frame.setAttribute("marginWidth", "0");
+ frame.setAttribute("marginHeight", "0");
+ frame.setAttribute("allowTransparency", "auto");
+ return frame;
+ },
+
+ _createDoc: function(frame, head, cssList, bodyId, bodyClass, body) {
+ var sb = [];
+ if(!xq.Browser.isTrident) {
+ // @WORKAROUND: IE6/7 has caret movement and scrolling problem if I include following DTD.
+ sb.push('');
+ }
+ sb.push('');
+ sb.push('');
+ sb.push('');
+ if(head) sb.push(head);
+
+ if(cssList) for(var i = 0; i < cssList.length; i++) {
+ sb.push('');
+ }
+ sb.push('');
+ sb.push('');
+ if(body) sb.push(body);
+ sb.push('');
+ sb.push('');
+
+ var doc = frame.contentWindow.document;
+ doc.open();
+ doc.write(sb.join(""));
+ doc.close();
+ return doc;
+ },
+
+ _createEditorFrame: function(mode) {
+ // turn off static content
+ this.contentElement.style.display = "none";
+
+ // create outer DIV
+ this.outmostWrapper = this.doc.createElement('div');
+ this.outmostWrapper.className = "xquared";
+ this.contentElement.parentNode.insertBefore(this.outmostWrapper, this.contentElement);
+
+ // create toolbar
+ if(this.toolbarContainer || this.config.generateDefaultToolbar) {
+ this.toolbar = new xq.ui.Toolbar(
+ this,
+ this.toolbarContainer,
+ this.outmostWrapper,
+ this.config.defaultToolbarButtonMap,
+ this.config.imagePathForDefaultToolbar,
+ function() {
+ var element = this.getCurrentEditMode() === 'wysiwyg' ? this.lastFocusElement : null;
+ return element && element.nodeName != "BODY" ? this.rdom.collectStructureAndStyle(element) : null;
+ }.bind(this)
+ );
+ }
+
+ // create source editor div
+ this.sourceEditorDiv = this.doc.createElement('div');
+ this.sourceEditorDiv.className = "editor source_editor"; //TODO: remove editor
+ this.sourceEditorDiv.style.display = "none";
+ this.outmostWrapper.appendChild(this.sourceEditorDiv);
+
+ // create TEXTAREA for source editor
+ this.sourceEditorTextarea = this.doc.createElement('textarea');
+ this.sourceEditorDiv.appendChild(this.sourceEditorTextarea);
+
+ // create WYSIWYG editor div
+ this.wysiwygEditorDiv = this.doc.createElement('div');
+ this.wysiwygEditorDiv.className = "editor wysiwyg_editor"; //TODO: remove editor
+ this.outmostWrapper.appendChild(this.wysiwygEditorDiv);
+
+ // create outer iframe for WYSIWYG editor
+ this.outerFrame = this._createIFrame(document);
+ this.wysiwygEditorDiv.appendChild(this.outerFrame);
+ var outerDoc = this._createDoc(
+ this.outerFrame,
+ ''
+ );
+
+ // create designmode iframe for WYSIWYG editor
+ this.editorFrame = this._createIFrame(outerDoc);
+
+ outerDoc.body.appendChild(this.editorFrame);
+ var editorDoc = this._createDoc(
+ this.editorFrame,
+ '' +
+ (!xq.Browser.isTrident ? '' : '') + // @WORKAROUND: it is needed to force href of pasted content to be an absolute url
+ (this.config.changeCursorOnLink ? '' : ''),
+ this.config.contentCssList,
+ this.config.bodyId,
+ this.config.bodyClass,
+ ''
+ );
+ this.rdom.setWin(this.editorFrame.contentWindow);
+ this.editHistory = new xq.EditHistory(this.rdom);
+
+ // turn on designmode
+ this.rdom.getDoc().designMode = "On";
+
+ // turn off Firefox's table editing feature
+ if(xq.Browser.isGecko) {
+ try {this.rdom.getDoc().execCommand("enableInlineTableEditing", false, "false")} catch(ignored) {}
+ }
+
+ // register event handlers
+ this._registerEventHandlers();
+
+ // hook onsubmit of form
+ if(this.config.automaticallyHookSubmitEvent && this.contentElement.form) {
+ var original = this.contentElement.form.onsubmit;
+ this.contentElement.form.onsubmit = function() {
+ this.contentElement.value = this.getCurrentContent();
+ return original ? original.bind(this.contentElement.form)() : true;
+ }.bind(this);
+ }
+ },
+
+
+
+ /////////////////////////////////////////////
+ // Event Management
+
+ _registerEventHandlers: function() {
+ var events = [this.platformDepedentKeyEventType, 'click', 'keyup', 'mouseup', 'contextmenu'];
+
+ if(xq.Browser.isTrident && this.config.changeCursorOnLink) events.push('mousemove');
+
+ var handler = this._handleEvent.bindAsEventListener(this);
+ for(var i = 0; i < events.length; i++) {
+ xq.observe(this.getDoc(), events[i], handler);
+ }
+
+ if(xq.Browser.isGecko) {
+ xq.observe(this.getDoc(), "focus", handler);
+ xq.observe(this.getDoc(), "blur", handler);
+ xq.observe(this.getDoc(), "scroll", handler);
+ xq.observe(this.getDoc(), "dragdrop", handler);
+ } else {
+ xq.observe(this.getWin(), "focus", handler);
+ xq.observe(this.getWin(), "blur", handler);
+ xq.observe(this.getWin(), "scroll", handler);
+ }
+ },
+
+ _handleEvent: function(e) {
+ this._fireOnBeforeEvent(this, e);
+ if(e.stopProcess) {
+ xq.stopEvent(e);
+ return false;
+ }
+
+ // Trident only
+ if(e.type === 'mousemove') {
+ if(!this.config.changeCursorOnLink) return true;
+
+ var link = !!this.rdom.getParentElementOf(e.srcElement, ["A"]);
+
+ var editable = this.getBody().contentEditable;
+ editable = editable === 'inherit' ? false : editable;
+
+ if(editable !== link && !this.rdom.hasSelection()) this.getBody().contentEditable = !link;
+ return true;
+ }
+
+ var stop = false;
+ var modifiedByCorrection = false;
+ if(e.type === this.platformDepedentKeyEventType) {
+ var undoPerformed = false;
+ modifiedByCorrection = this.rdom.correctParagraph();
+ for(var key in this.config.shortcuts) {
+ if(!this.config.shortcuts[key].event.matches(e)) continue;
+
+ var handler = this.config.shortcuts[key].handler;
+ var xed = this;
+ stop = (typeof handler === "function") ? handler(this) : eval(handler);
+
+ if(key === "undo") undoPerformed = true;
+ }
+ } else if(e.type === 'click' && e.button === 0 && this.config.enableLinkClick) {
+ var a = this.rdom.getParentElementOf(e.target || e.srcElement, ["A"]);
+ if(a) stop = this.handleClick(e, a);
+ } else if(["keyup", "mouseup"].indexOf(e.type) !== -1) {
+ modifiedByCorrection = this.rdom.correctParagraph();
+ } else if(["contextmenu"].indexOf(e.type) !== -1) {
+ this._handleContextMenu(e);
+ } else if("focus" == e.type) {
+ this.rdom.focused = true;
+ } else if("blur" == e.type) {
+ this.rdom.focused = false;
+ }
+
+ if(stop) xq.stopEvent(e);
+
+ this._fireOnCurrentContentChanged(this);
+ this._fireOnAfterEvent(this, e);
+
+ if(!undoPerformed && !modifiedByCorrection) this.editHistory.onEvent(e);
+
+ return !stop;
+ },
+
+ /**
+ * TODO: remove dup with handleAutocompletion
+ */
+ handleAutocorrection: function() {
+ var block = this.rdom.getCurrentBlockElement();
+ // TODO: use complete unescape algorithm
+ var text = this.rdom.getInnerText(block).replace(/ /gi, " ");
+
+ var acs = this.config.autocorrections;
+ var performed = false;
+
+ var stop = false;
+ for(var key in acs) {
+ var ac = acs[key];
+ if(ac.criteria(text)) {
+ try {
+ this.editHistory.onCommand();
+ this.editHistory.disable();
+ if(typeof ac.handler === "String") {
+ var xed = this;
+ var rdom = this.rdom;
+ eval(ac.handler);
+ } else {
+ stop = ac.handler(this, this.rdom, block, text);
+ }
+ this.editHistory.enable();
+ } catch(ignored) {}
+
+ block = this.rdom.getCurrentBlockElement();
+ text = this.rdom.getInnerText(block);
+
+ performed = true;
+ if(stop) break;
+ }
+ }
+
+ return stop;
+ },
+
+ /**
+ * TODO: remove dup with handleAutocorrection
+ */
+ handleAutocompletion: function() {
+ var acs = this.config.autocompletions;
+ if(xq.isEmptyHash(acs)) return;
+
+ if(this.rdom.hasSelection()) {
+ var text = this.rdom.getSelectionAsText();
+ this.rdom.deleteSelection();
+ var wrapper = this.rdom.insertNode(this.rdom.createElement("SPAN"));
+ wrapper.innerHTML = text;
+
+ var marker = this.rdom.pushMarker();
+
+ var filtered = [];
+ for(var key in acs) {
+ filtered.push([key, acs[key].criteria(text)]);
+ }
+ filtered = filtered.findAll(function(elem) {
+ return elem[1] !== -1;
+ });
+
+ if(filtered.length === 0) {
+ this.rdom.popMarker(true);
+ return;
+ }
+
+ var minIndex = 0;
+ var min = filtered[0][1];
+ for(var i = 0; i < filtered.length; i++) {
+ if(filtered[i][1] < min) {
+ minIndex = i;
+ min = filtered[i][1];
+ }
+ }
+
+ var ac = acs[filtered[minIndex][0]];
+
+ this.editHistory.disable();
+ this.rdom.selectElement(wrapper);
+ } else {
+ var marker = this.rdom.pushMarker();
+
+ var filtered = [];
+ for(var key in acs) {
+ filtered.push([key, this.rdom.testSmartWrap(marker, acs[key].criteria).textIndex]);
+ }
+ filtered = filtered.findAll(function(elem) {
+ return elem[1] !== -1;
+ });
+
+ if(filtered.length === 0) {
+ this.rdom.popMarker(true);
+ return;
+ }
+
+ var minIndex = 0;
+ var min = filtered[0][1];
+ for(var i = 0; i < filtered.length; i++) {
+ if(filtered[i][1] < min) {
+ minIndex = i;
+ min = filtered[i][1];
+ }
+ }
+
+ var ac = acs[filtered[minIndex][0]];
+
+ this.editHistory.disable();
+
+ var wrapper = this.rdom.smartWrap(marker, "SPAN", ac.criteria);
+ }
+
+ var block = this.rdom.getCurrentBlockElement();
+
+ // TODO: use complete unescape algorithm
+ var text = this.rdom.getInnerText(wrapper).replace(/ /gi, " ");
+
+ try {
+ // call handler
+ if(typeof ac.handler === "String") {
+ var xed = this;
+ var rdom = this.rdom;
+ eval(ac.handler);
+ } else {
+ ac.handler(this, this.rdom, block, wrapper, text);
+ }
+ } catch(ignored) {}
+
+ try {
+ this.rdom.unwrapElement(wrapper);
+ } catch(ignored) {}
+
+ if(this.rdom.isEmptyBlock(block)) this.rdom.correctEmptyElement(block);
+
+ this.editHistory.enable();
+ this.editHistory.onCommand();
+
+ this.rdom.popMarker(true);
+ },
+
+ /**
+ * Handles click event
+ *
+ * @param {Event} e click event
+ * @param {Element} target target element(usually has A tag)
+ */
+ handleClick: function(e, target) {
+ var href = decodeURI(target.href);
+ if(!xq.Browser.isTrident) {
+ if(!e.ctrlKey && !e.shiftKey && e.button !== 1) {
+ window.location.href = href;
+ return true;
+ }
+ } else {
+ if(e.shiftKey) {
+ window.open(href, "_blank");
+ } else {
+ window.location.href = href;
+ }
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Show link dialog
+ *
+ * TODO: should support modify/unlink
+ * TODO: Add selenium test
+ */
+ handleLink: function() {
+ var text = this.rdom.getSelectionAsText() || '';
+ var dialog = new xq.ui.FormDialog(
+ this,
+ xq.ui_templates.basicLinkDialog,
+ function(dialog) {
+ if(text) {
+ dialog.form.text.value = text;
+ dialog.form.url.focus();
+ dialog.form.url.select();
+ }
+ },
+ function(data) {
+ this.focus();
+
+ if(xq.Browser.isTrident) {
+ var rng = this.rdom.rng();
+ rng.moveToBookmark(bm);
+ rng.select();
+ }
+
+ if(!data) return;
+ this.handleInsertLink(false, data.url, data.text, data.text);
+ }.bind(this)
+ );
+
+ if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
+
+ dialog.show({position: 'centerOfEditor'});
+
+ return true;
+ },
+
+ /**
+ * Inserts link or apply link into selected area
+ * @TODO Add selenium test
+ *
+ * @param {boolean} autoSelection if set true and there's no selection, automatically select word to link(if possible)
+ * @param {String} url url
+ * @param {String} title title of link
+ * @param {String} text text of link. If there's a selection(manually or automatically), it will be replaced with this text
+ *
+ * @returns {Element} created element
+ */
+ handleInsertLink: function(autoSelection, url, title, text) {
+ if(autoSelection && !this.rdom.hasSelection()) {
+ var marker = this.rdom.pushMarker();
+ var a = this.rdom.smartWrap(marker, "A", function(text) {
+ var index = text.lastIndexOf(" ");
+ return index === -1 ? index : index + 1;
+ });
+ a.href = url;
+ a.title = title;
+ if(text) {
+ a.innerHTML = ""
+ a.appendChild(this.rdom.createTextNode(text));
+ } else if(!a.hasChildNodes()) {
+ this.rdom.deleteNode(a);
+ }
+ this.rdom.popMarker(true);
+ } else {
+ text = text || (this.rdom.hasSelection() ? this.rdom.getSelectionAsText() : null);
+ if(!text) return;
+
+ this.rdom.deleteSelection();
+
+ var a = this.rdom.createElement('A');
+ a.href = url;
+ a.title = title;
+ a.appendChild(this.rdom.createTextNode(text));
+ this.rdom.insertNode(a);
+ }
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * @TODO Add selenium test
+ */
+ handleSpace: function() {
+ // If it has selection, perform default action.
+ if(this.rdom.hasSelection()) return false;
+
+ // Trident performs URL replacing automatically
+ if(!xq.Browser.isTrident) {
+ this.replaceUrlToLink();
+ }
+
+ return false;
+ },
+
+ /**
+ * Called when enter key pressed.
+ * @TODO Add selenium test
+ *
+ * @param {boolean} skipAutocorrection if set true, skips autocorrection
+ * @param {boolean} forceInsertParagraph if set true, inserts paragraph
+ */
+ handleEnter: function(skipAutocorrection, forceInsertParagraph) {
+ // If it has selection, perform default action.
+ if(this.rdom.hasSelection()) return false;
+
+ // @WORKAROUND:
+ // If caret is in HR, default action should be performed and
+ // this._handleEvent() will correct broken HTML
+ if(xq.Browser.isTrident && this.rdom.tree.isBlockOnlyContainer(this.rdom.getCurrentElement()) && this.rdom.recentHR) {
+ this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.recentHR, "before");
+ this.rdom.recentHR = null;
+ return true;
+ }
+
+ // Perform autocorrection
+ if(!skipAutocorrection && this.handleAutocorrection()) return true;
+
+ var block = this.rdom.getCurrentBlockElement();
+ var info = this.rdom.collectStructureAndStyle(block);
+
+ // Perform URL replacing. Trident performs URL replacing automatically
+ if(!xq.Browser.isTrident) {
+ this.replaceUrlToLink();
+ }
+
+ var atEmptyBlock = this.rdom.isCaretAtEmptyBlock();
+ var atStart = atEmptyBlock || this.rdom.isCaretAtBlockStart();
+ var atEnd = atEmptyBlock || (!atStart && this.rdom.isCaretAtBlockEnd());
+ var atEdge = atEmptyBlock || atStart || atEnd;
+
+ if(!atEdge) {
+ var marker = this.rdom.pushMarker();
+
+ if(this.rdom.isFirstLiWithNestedList(block) && !forceInsertParagraph) {
+ var parent = block.parentNode;
+ this.rdom.unwrapElement(block);
+ block = parent;
+ } else if(block.nodeName !== "LI" && this.rdom.tree.isBlockContainer(block)) {
+ block = this.rdom.wrapAllInlineOrTextNodesAs("P", block, true).first();
+ }
+ this.rdom.splitElementUpto(marker, block);
+
+ this.rdom.popMarker(true);
+ } else if(atEmptyBlock) {
+ this._handleEnterAtEmptyBlock();
+
+ if(!xq.Browser.isWebkit) {
+ if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize);
+ if(info.fontName) this.handleFontFace(info.fontName);
+ }
+ } else {
+ this._handleEnterAtEdge(atStart, forceInsertParagraph);
+
+ if(!xq.Browser.isWebkit) {
+ if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize);
+ if(info.fontName) this.handleFontFace(info.fontName);
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Moves current block upward or downward
+ *
+ * @param {boolean} up moves current block upward
+ */
+ handleMoveBlock: function(up) {
+ var block = this.rdom.moveBlock(this.rdom.getCurrentBlockElement(), up);
+ if(block) {
+ this.rdom.selectElement(block, false);
+ if(this.rdom.isEmptyBlock(block)) this.rdom.collapseSelection(true);
+
+ block.scrollIntoView(false);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+ }
+ return true;
+ },
+
+ /**
+ * Called when tab key pressed
+ * @TODO: Add selenium test
+ */
+ handleTab: function() {
+ var hasSelection = this.rdom.hasSelection();
+ var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]);
+
+ if(hasSelection) {
+ this.handleIndent();
+ } else if (table && table.className === "datatable") {
+ this.handleMoveToNextCell();
+ } else if (this.rdom.isCaretAtBlockStart()) {
+ this.handleIndent();
+ } else {
+ this.handleInsertTab();
+ }
+
+ return true;
+ },
+
+ /**
+ * Called when shift+tab key pressed
+ * @TODO: Add selenium test
+ */
+ handleShiftTab: function() {
+ var hasSelection = this.rdom.hasSelection();
+ var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]);
+
+ if(hasSelection) {
+ this.handleOutdent();
+ } else if (table && table.className === "datatable") {
+ this.handleMoveToPreviousCell();
+ } else {
+ this.handleOutdent();
+ }
+
+ return true;
+ },
+
+ /**
+ * Inserts three non-breaking spaces
+ * @TODO: Add selenium test
+ */
+ handleInsertTab: function() {
+ this.rdom.insertHtml(' ');
+ this.rdom.insertHtml(' ');
+ this.rdom.insertHtml(' ');
+
+ return true;
+ },
+
+ /**
+ * Called when delete key pressed
+ * @TODO: Add selenium test
+ */
+ handleDelete: function() {
+ if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockEnd()) return false;
+ return this._handleMerge(true);
+ },
+
+ /**
+ * Called when backspace key pressed
+ * @TODO: Add selenium test
+ */
+ handleBackspace: function() {
+ if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockStart()) return false;
+ return this._handleMerge(false);
+ },
+
+ _handleMerge: function(withNext) {
+ var block = this.rdom.getCurrentBlockElement();
+
+ if(this.rdom.isEmptyBlock(block) && !this.rdom.tree.isBlockContainer(block.nextSibling) && withNext) {
+ var blockToMove = this.rdom.removeBlock(block);
+ this.rdom.placeCaretAtStartOf(blockToMove);
+ blockToMove.scrollIntoView(false);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ } else {
+ // save caret position;
+ var marker = this.rdom.pushMarker();
+
+ // perform merge
+ var merged = this.rdom.mergeElement(block, withNext, withNext);
+ if(!merged && !withNext) this.rdom.extractOutElementFromParent(block);
+
+ // restore caret position
+ this.rdom.popMarker(true);
+ if(merged) this.rdom.correctEmptyElement(merged);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+ return !!merged;
+ }
+ },
+
+ /**
+ * (in table) Moves caret to the next cell
+ * @TODO: Add selenium test
+ */
+ handleMoveToNextCell: function() {
+ this._handleMoveToCell("next");
+ },
+
+ /**
+ * (in table) Moves caret to the previous cell
+ * @TODO: Add selenium test
+ */
+ handleMoveToPreviousCell: function() {
+ this._handleMoveToCell("prev");
+ },
+
+ /**
+ * (in table) Moves caret to the above cell
+ * @TODO: Add selenium test
+ */
+ handleMoveToAboveCell: function() {
+ this._handleMoveToCell("above");
+ },
+
+ /**
+ * (in table) Moves caret to the below cell
+ * @TODO: Add selenium test
+ */
+ handleMoveToBelowCell: function() {
+ this._handleMoveToCell("below");
+ },
+
+ _handleMoveToCell: function(dir) {
+ var block = this.rdom.getCurrentBlockElement();
+ var cell = this.rdom.getParentElementOf(block, ["TD", "TH"]);
+ var table = this.rdom.getParentElementOf(cell, ["TABLE"]);
+ var rtable = new xq.RichTable(this.rdom, table);
+ var target = null;
+
+ if(["next", "prev"].indexOf(dir) !== -1) {
+ var toNext = dir === "next";
+ target = toNext ? rtable.getNextCellOf(cell) : rtable.getPreviousCellOf(cell);
+ } else {
+ var toBelow = dir === "below";
+ target = toBelow ? rtable.getBelowCellOf(cell) : rtable.getAboveCellOf(cell);
+ }
+
+ if(!target) {
+ var finder = function(node) {return ['TD', 'TH'].indexOf(node.nodeName) === -1 && this.tree.isBlock(node) && !this.tree.hasBlocks(node);}.bind(this.rdom);
+ var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this.rdom);
+
+ target = (toNext || toBelow) ?
+ this.rdom.tree.findForward(cell, finder, exitCondition) :
+ this.rdom.tree.findBackward(table, finder, exitCondition);
+ }
+
+ if(target) this.rdom.placeCaretAtStartOf(target);
+ },
+
+ /**
+ * Applies STRONG tag
+ * @TODO: Add selenium test
+ */
+ handleStrongEmphasis: function() {
+ this.rdom.applyStrongEmphasis();
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Applies EM tag
+ * @TODO: Add selenium test
+ */
+ handleEmphasis: function() {
+ this.rdom.applyEmphasis();
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Applies EM.underline tag
+ * @TODO: Add selenium test
+ */
+ handleUnderline: function() {
+ this.rdom.applyUnderline();
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Applies SPAN.strike tag
+ * @TODO: Add selenium test
+ */
+ handleStrike: function() {
+ this.rdom.applyStrike();
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Removes all style
+ * @TODO: Add selenium test
+ */
+ handleRemoveFormat: function() {
+ this.rdom.applyRemoveFormat();
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Remove link
+ * @TODO: Add selenium test
+ */
+ handleRemoveLink: function() {
+ this.rdom.applyRemoveLink();
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Inserts table
+ * @TODO: Add selenium test
+ *
+ * @param {Number} cols number of columns
+ * @param {Number} rows number of rows
+ * @param {String} headerPosition position of THs. "T" or "L" or "TL". "T" means top, "L" means left.
+ */
+ handleTable: function(cols, rows, headerPositions) {
+ var cur = this.rdom.getCurrentBlockElement();
+ if(this.rdom.getParentElementOf(cur, ["TABLE"])) return true;
+
+ var rtable = xq.RichTable.create(this.rdom, cols, rows, headerPositions);
+ if(this.rdom.tree.isBlockContainer(cur)) {
+ var wrappers = this.rdom.wrapAllInlineOrTextNodesAs("P", cur, true);
+ cur = wrappers.last();
+ }
+ var tableDom = this.rdom.insertNodeAt(rtable.getDom(), cur, "after");
+ this.rdom.placeCaretAtStartOf(rtable.getCellAt(0, 0));
+
+ if(this.rdom.isEmptyBlock(cur)) this.rdom.deleteNode(cur, true);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ handleInsertNewRowAt: function(where) {
+ var cur = this.rdom.getCurrentBlockElement();
+ var tr = this.rdom.getParentElementOf(cur, ["TR"]);
+ if(!tr) return true;
+
+ var table = this.rdom.getParentElementOf(tr, ["TABLE"]);
+ var rtable = new xq.RichTable(this.rdom, table);
+ var row = rtable.insertNewRowAt(tr, where);
+
+ this.rdom.placeCaretAtStartOf(row.cells[0]);
+ return true;
+ },
+
+ /**
+ * @TODO: Add selenium test
+ */
+ handleInsertNewColumnAt: function(where) {
+ var cur = this.rdom.getCurrentBlockElement();
+ var td = this.rdom.getParentElementOf(cur, ["TD"], true);
+ if(!td) return true;
+
+ var table = this.rdom.getParentElementOf(td, ["TABLE"]);
+ var rtable = new xq.RichTable(this.rdom, table);
+ rtable.insertNewCellAt(td, where);
+
+ this.rdom.placeCaretAtStartOf(cur);
+ return true;
+ },
+
+ /**
+ * @TODO: Add selenium test
+ */
+ handleDeleteRow: function() {
+ var cur = this.rdom.getCurrentBlockElement();
+ var tr = this.rdom.getParentElementOf(cur, ["TR"]);
+ if(!tr) return true;
+
+ var table = this.rdom.getParentElementOf(tr, ["TABLE"]);
+ var rtable = new xq.RichTable(this.rdom, table);
+ var blockToMove = rtable.deleteRow(tr);
+
+ this.rdom.placeCaretAtStartOf(blockToMove);
+ return true;
+ },
+
+ /**
+ * @TODO: Add selenium test
+ */
+ handleDeleteColumn: function() {
+ var cur = this.rdom.getCurrentBlockElement();
+ var td = this.rdom.getParentElementOf(cur, ["TD"], true);
+ if(!td) return true;
+
+ var table = this.rdom.getParentElementOf(td, ["TABLE"]);
+ var rtable = new xq.RichTable(this.rdom, table);
+ rtable.deleteCell(td);
+
+ //this.rdom.placeCaretAtStartOf(table);
+ return true;
+ },
+
+ /**
+ * Performs block indentation
+ * @TODO: Add selenium test
+ */
+ handleIndent: function() {
+ if(this.rdom.hasSelection()) {
+ var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
+ if(blocks.first() !== blocks.last()) {
+ var affected = this.rdom.indentElements(blocks.first(), blocks.last());
+ this.rdom.selectBlocksBetween(affected.first(), affected.last());
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ }
+ }
+
+ var block = this.rdom.getCurrentBlockElement();
+ var affected = this.rdom.indentElement(block);
+
+ if(affected) {
+ this.rdom.placeCaretAtStartOf(affected);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+ }
+
+ return true;
+ },
+
+ /**
+ * Performs block outdentation
+ * @TODO: Add selenium test
+ */
+ handleOutdent: function() {
+ if(this.rdom.hasSelection()) {
+ var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
+ if(blocks.first() !== blocks.last()) {
+ var affected = this.rdom.outdentElements(blocks.first(), blocks.last());
+ this.rdom.selectBlocksBetween(affected.first(), affected.last());
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ }
+ }
+
+ var block = this.rdom.getCurrentBlockElement();
+ var affected = this.rdom.outdentElement(block);
+
+ if(affected) {
+ this.rdom.placeCaretAtStartOf(affected);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+ }
+
+ return true;
+ },
+
+ /**
+ * Applies list.
+ * @TODO: Add selenium test
+ *
+ * @param {String} type "UL" or "OL"
+ * @param {String} CSS class name
+ */
+ handleList: function(type, className) {
+ if(this.rdom.hasSelection()) {
+ var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
+ if(blocks.first() !== blocks.last()) {
+ blocks = this.rdom.applyLists(blocks.first(), blocks.last(), type, className);
+ } else {
+ blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type, className);
+ }
+ this.rdom.selectBlocksBetween(blocks.first(), blocks.last());
+ } else {
+ var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type, className);
+ this.rdom.placeCaretAtStartOf(block);
+ }
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Applies justification
+ * @TODO: Add selenium test
+ *
+ * @param {String} dir "left", "center", "right" or "both"
+ */
+ handleJustify: function(dir) {
+ if(this.rdom.hasSelection()) {
+ var blocks = this.rdom.getSelectedBlockElements();
+ var dir = (dir === "left" || dir === "both") && (blocks[0].style.textAlign === "left" || blocks[0].style.textAlign === "") ? "both" : dir;
+ this.rdom.justifyBlocks(blocks, dir);
+ this.rdom.selectBlocksBetween(blocks.first(), blocks.last());
+ } else {
+ var block = this.rdom.getCurrentBlockElement();
+ var dir = (dir === "left" || dir === "both") && (block.style.textAlign === "left" || block.style.textAlign === "") ? "both" : dir;
+ this.rdom.justifyBlock(block, dir);
+ }
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Removes current block element
+ * @TODO: Add selenium test
+ */
+ handleRemoveBlock: function() {
+ var block = this.rdom.getCurrentBlockElement();
+ var blockToMove = this.rdom.removeBlock(block);
+ this.rdom.placeCaretAtStartOf(blockToMove);
+ blockToMove.scrollIntoView(false);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Applies background color
+ * @TODO: Add selenium test
+ *
+ * @param {String} color CSS color string
+ */
+ handleBackgroundColor: function(color) {
+ if(color) {
+ this.rdom.applyBackgroundColor(color);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+ } else {
+ var dialog = new xq.ui.FormDialog(
+ this,
+ xq.ui_templates.basicColorPickerDialog,
+ function(dialog) {},
+ function(data) {
+ this.focus();
+
+ if(xq.Browser.isTrident) {
+ var rng = this.rdom.rng();
+ rng.moveToBookmark(bm);
+ rng.select();
+ }
+
+ if(!data) return;
+
+ this.handleBackgroundColor(data.color);
+ }.bind(this)
+ );
+
+ if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
+
+ dialog.show({position: 'centerOfEditor'});
+ }
+ return true;
+ },
+
+ /**
+ * Applies foreground color
+ * @TODO: Add selenium test
+ *
+ * @param {String} color CSS color string
+ */
+ handleForegroundColor: function(color) {
+ if(color) {
+ this.rdom.applyForegroundColor(color);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+ } else {
+ var dialog = new xq.ui.FormDialog(
+ this,
+ xq.ui_templates.basicColorPickerDialog,
+ function(dialog) {},
+ function(data) {
+ this.focus();
+
+ if(xq.Browser.isTrident) {
+ var rng = this.rdom.rng();
+ rng.moveToBookmark(bm);
+ rng.select();
+ }
+
+ if(!data) return;
+
+ this.handleForegroundColor(data.color);
+ }.bind(this)
+ );
+
+ if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
+
+ dialog.show({position: 'centerOfEditor'});
+ }
+ return true;
+ },
+
+ /**
+ * Applies font face
+ * @TODO: Add selenium test
+ *
+ * @param {String} face font face
+ */
+ handleFontFace: function(face) {
+ if(face) {
+ this.rdom.applyFontFace(face);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+ } else {
+ //TODO: popup font dialog
+ }
+ return true;
+ },
+
+ /**
+ * Applies font size
+ *
+ * @param {Number} font size (1 to 6)
+ */
+ handleFontSize: function(size) {
+ if(size) {
+ this.rdom.applyFontSize(size);
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+ } else {
+ //TODO: popup font dialog
+ }
+ return true;
+ },
+
+ /**
+ * Applies superscription
+ * @TODO: Add selenium test
+ */
+ handleSuperscription: function() {
+ this.rdom.applySuperscription();
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Applies subscription
+ * @TODO: Add selenium test
+ */
+ handleSubscription: function() {
+ this.rdom.applySubscription();
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Change or wrap current block(or selected blocks)'s tag
+ * @TODO: Add selenium test
+ *
+ * @param {String} [tagName] Name of tag. If not provided, it does not modify current tag name
+ * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed.
+ */
+ handleApplyBlock: function(tagName, className) {
+ if(!tagName && !className) return true;
+
+ // if current selection contains multi-blocks
+ if(this.rdom.hasSelection()) {
+ var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
+ if(blocks.first() !== blocks.last()) {
+ var applied = this.rdom.applyTagIntoElements(tagName, blocks.first(), blocks.last(), className);
+ this.rdom.selectBlocksBetween(applied.first(), applied.last());
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ }
+ }
+
+ // else
+ var block = this.rdom.getCurrentBlockElement();
+ this.rdom.pushMarker();
+ var applied =
+ this.rdom.applyTagIntoElement(tagName, block, className) ||
+ block;
+ this.rdom.popMarker(true);
+
+ if(this.rdom.isEmptyBlock(applied)) {
+ this.rdom.correctEmptyElement(applied);
+ this.rdom.placeCaretAtStartOf(applied);
+ }
+
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Inserts seperator (HR)
+ * @TODO: Add selenium test
+ */
+ handleSeparator: function() {
+ this.rdom.collapseSelection();
+
+ var curBlock = this.rdom.getCurrentBlockElement();
+ var atStart = this.rdom.isCaretAtBlockStart();
+ if(this.rdom.tree.isBlockContainer(curBlock)) curBlock = this.rdom.wrapAllInlineOrTextNodesAs("P", curBlock, true)[0];
+
+ this.rdom.insertNodeAt(this.rdom.createElement("HR"), curBlock, atStart ? "before" : "after");
+ this.rdom.placeCaretAtStartOf(curBlock);
+
+ // add undo history
+ var historyAdded = this.editHistory.onCommand();
+ this._fireOnCurrentContentChanged(this);
+
+ return true;
+ },
+
+ /**
+ * Performs UNDO
+ * @TODO: Add selenium test
+ */
+ handleUndo: function() {
+ var performed = this.editHistory.undo();
+ this._fireOnCurrentContentChanged(this);
+
+ var curBlock = this.rdom.getCurrentBlockElement();
+ if(!xq.Browser.isTrident && curBlock) {
+ curBlock.scrollIntoView(false);
+ }
+ return true;
+ },
+
+ /**
+ * Performs REDO
+ * @TODO: Add selenium test
+ */
+ handleRedo: function() {
+ var performed = this.editHistory.redo();
+ this._fireOnCurrentContentChanged(this);
+
+ var curBlock = this.rdom.getCurrentBlockElement();
+ if(!xq.Browser.isTrident && curBlock) {
+ curBlock.scrollIntoView(false);
+ }
+ return true;
+ },
+
+
+
+ _handleContextMenu: function(e) {
+ if (xq.Browser.isWebkit) {
+ if (e.metaKey || xq.isLeftClick(e)) return false;
+ } else if (e.shiftKey || e.ctrlKey || e.altKey) {
+ return false;
+ }
+
+ var point = xq.getEventPoint(e);
+ var x = point.x;
+ var y = point.y;
+
+ var pos = xq.getCumulativeOffset(this.wysiwygEditorDiv);
+ x += pos.left;
+ y += pos.top;
+ this._contextMenuTargetElement = e.target || e.srcElement;
+
+ if (!xq.Browser.isTrident) {
+ var doc = this.getDoc();
+ var body = this.getBody();
+
+ x -= doc.documentElement.scrollLeft;
+ y -= doc.documentElement.scrollTop;
+
+ x -= body.scrollLeft;
+ y -= body.scrollTop;
+ }
+
+ for(var cmh in this.config.contextMenuHandlers) {
+ var stop = this.config.contextMenuHandlers[cmh].handler(this, this._contextMenuTargetElement, x, y);
+ if(stop) {
+ xq.stopEvent(e);
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ showContextMenu: function(menuItems, x, y) {
+ if (!menuItems || menuItems.length <= 0) return;
+
+ if (!this.contextMenuContainer) {
+ this.contextMenuContainer = this.doc.createElement('UL');
+ this.contextMenuContainer.className = 'xqContextMenu';
+ this.contextMenuContainer.style.display='none';
+
+ xq.observe(this.doc, 'click', this._contextMenuClicked.bindAsEventListener(this));
+ xq.observe(this.rdom.getDoc(), 'click', this.hideContextMenu.bindAsEventListener(this));
+
+ this.body.appendChild(this.contextMenuContainer);
+ } else {
+ while (this.contextMenuContainer.childNodes.length > 0)
+ this.contextMenuContainer.removeChild(this.contextMenuContainer.childNodes[0]);
+ }
+
+ for (var i=0; i < menuItems.length; i++) {
+ menuItems[i]._node = this._addContextMenuItem(menuItems[i]);
+ }
+
+ this.contextMenuContainer.style.display='block';
+ this.contextMenuContainer.style.left = Math.min(Math.max(this.doc.body.scrollWidth, this.doc.documentElement.clientWidth) - this.contextMenuContainer.offsetWidth, x) + 'px';
+ this.contextMenuContainer.style.top = Math.min(Math.max(this.doc.body.scrollHeight, this.doc.documentElement.clientHeight) - this.contextMenuContainer.offsetHeight, y) + 'px';
+
+ this.contextMenuItems = menuItems;
+ },
+
+ hideContextMenu: function() {
+ if (this.contextMenuContainer)
+ this.contextMenuContainer.style.display='none';
+ },
+
+ _addContextMenuItem: function(item) {
+ if (!this.contextMenuContainer) throw "No conext menu container exists";
+
+ var node = this.doc.createElement('LI');
+ if (item.disabled) node.className += ' disabled';
+
+ if (item.title === '----') {
+ node.innerHTML = ' ';
+ node.className = 'separator';
+ } else {
+ if(item.handler) {
+ node.innerHTML = ''+(item.title.toString().escapeHTML())+'';
+ } else {
+ node.innerHTML = (item.title.toString().escapeHTML());
+ }
+ }
+
+ if(item.className) node.className = item.className;
+
+ this.contextMenuContainer.appendChild(node);
+
+ return node;
+ },
+
+ _contextMenuClicked: function(e) {
+ this.hideContextMenu();
+
+ if (!this.contextMenuContainer) return;
+
+ var node = e.srcElement || e.target;
+ while(node && node.nodeName !== "LI") {
+ node = node.parentNode;
+ }
+ if (!node || !this.rdom.tree.isDescendantOf(this.contextMenuContainer, node)) return;
+
+ for (var i=0; i < this.contextMenuItems.length; i++) {
+ if (this.contextMenuItems[i]._node === node) {
+ var handler = this.contextMenuItems[i].handler;
+ if (!this.contextMenuItems[i].disabled && handler) {
+ var xed = this;
+ var element = this._contextMenuTargetElement;
+ if(typeof handler === "function") {
+ handler(xed, element);
+ } else {
+ eval(handler);
+ }
+ }
+ break;
+ }
+ }
+ },
+
+ /**
+ * Inserts HTML template
+ * @TODO: Add selenium test
+ *
+ * @param {String} html Template string. It should have single root element
+ * @returns {Element} inserted element
+ */
+ insertTemplate: function(html) {
+ return this.rdom.insertHtml(this._processTemplate(html));
+ },
+
+ /**
+ * Places given HTML template nearby target.
+ * @TODO: Add selenium test
+ *
+ * @param {String} html Template string. It should have single root element
+ * @param {Node} target Target node.
+ * @param {String} where Possible values: "before", "start", "end", "after"
+ *
+ * @returns {Element} Inserted element.
+ */
+ insertTemplateAt: function(html, target, where) {
+ return this.rdom.insertHtmlAt(this._processTemplate(html), target, where);
+ },
+
+ _processTemplate: function(html) {
+ // apply template processors
+ var tps = this.getTemplateProcessors();
+ for(var key in tps) {
+ var value = tps[key];
+ html = value.handler(html);
+ }
+
+ // remove all whitespace characters between block tags
+ return this.removeUnnecessarySpaces(html);
+ },
+
+
+
+ /** @private */
+ _handleEnterAtEmptyBlock: function() {
+ var block = this.rdom.getCurrentBlockElement();
+ if(this.rdom.tree.isTableCell(block) && this.rdom.isFirstBlockOfBody(block)) {
+ block = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start");
+ } else {
+ block =
+ this.rdom.outdentElement(block) ||
+ this.rdom.extractOutElementFromParent(block) ||
+ this.rdom.replaceTag("P", block) ||
+ this.rdom.insertNewBlockAround(block);
+ }
+
+ this.rdom.placeCaretAtStartOf(block);
+ if(!xq.Browser.isTrident) block.scrollIntoView(false);
+ },
+
+ /** @private */
+ _handleEnterAtEdge: function(atStart, forceInsertParagraph) {
+ var block = this.rdom.getCurrentBlockElement();
+ var blockToPlaceCaret;
+
+ if(atStart && this.rdom.isFirstBlockOfBody(block)) {
+ blockToPlaceCaret = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start");
+ } else {
+ if(this.rdom.tree.isTableCell(block)) forceInsertParagraph = true;
+ var newBlock = this.rdom.insertNewBlockAround(block, atStart, forceInsertParagraph ? "P" : null);
+ blockToPlaceCaret = !atStart ? newBlock : newBlock.nextSibling;
+ }
+
+ this.rdom.placeCaretAtStartOf(blockToPlaceCaret);
+ if(!xq.Browser.isTrident) blockToPlaceCaret.scrollIntoView(false);
+ },
+
+ /**
+ * Replace URL text nearby caret into a link
+ * @TODO: Add selenium test
+ */
+ replaceUrlToLink: function() {
+ // If there's link nearby caret, nothing happens
+ if(this.rdom.getParentElementOf(this.rdom.getCurrentElement(), ["A"])) return;
+
+ var marker = this.rdom.pushMarker();
+ var criteria = function(text) {
+ var m = /(http|https|ftp|mailto)\:\/\/[^\s]+$/.exec(text);
+ return m ? m.index : -1;
+ };
+
+ var test = this.rdom.testSmartWrap(marker, criteria);
+ if(test.textIndex !== -1) {
+ var a = this.rdom.smartWrap(marker, "A", criteria);
+ a.href = encodeURI(test.text);
+ }
+ this.rdom.popMarker(true);
+ }
+});
diff --git a/modules/editor/skins/xquared/javascripts/Json2.js b/modules/editor/skins/xquared/javascripts/Json2.js
new file mode 100644
index 000000000..6fc8ed622
--- /dev/null
+++ b/modules/editor/skins/xquared/javascripts/Json2.js
@@ -0,0 +1,275 @@
+/*
+ json2.js
+ 2008-02-14
+
+ Public Domain
+
+ No warranty expressed or implied. Use at your own risk.
+
+ See http://www.JSON.org/js.html
+
+ This file creates a global JSON object containing two methods:
+
+ JSON.stringify(value, whitelist)
+ value any JavaScript value, usually an object or array.
+
+ whitelist an optional array parameter that determines how object
+ values are stringified.
+
+ This method produces a JSON text from a JavaScript value.
+ There are three possible ways to stringify an object, depending
+ on the optional whitelist parameter.
+
+ If an object has a toJSON method, then the toJSON() method will be
+ called. The value returned from the toJSON method will be
+ stringified.
+
+ Otherwise, if the optional whitelist parameter is an array, then
+ the elements of the array will be used to select members of the
+ object for stringification.
+
+ Otherwise, if there is no whitelist parameter, then all of the
+ members of the object will be stringified.
+
+ Values that do not have JSON representaions, such as undefined or
+ functions, will not be serialized. Such values in objects will be
+ dropped; in arrays will be replaced with null.
+ JSON.stringify(undefined) returns undefined. Dates will be
+ stringified as quoted ISO dates.
+
+ Example:
+
+ var text = JSON.stringify(['e', {pluribus: 'unum'}]);
+ // text is '["e",{"pluribus":"unum"}]'
+
+ JSON.parse(text, filter)
+ This method parses a JSON text to produce an object or
+ array. It can throw a SyntaxError exception.
+
+ The optional filter parameter is a function that can filter and
+ transform the results. It receives each of the keys and values, and
+ its return value is used instead of the original value. If it
+ returns what it received, then structure is not modified. If it
+ returns undefined then the member is deleted.
+
+ Example:
+
+ // Parse the text. If a key contains the string 'date' then
+ // convert the value to a date.
+
+ myData = JSON.parse(text, function (key, value) {
+ return key.indexOf('date') >= 0 ? new Date(value) : value;
+ });
+
+ This is a reference implementation. You are free to copy, modify, or
+ redistribute.
+
+ Use your own copy. It is extremely unwise to load third party
+ code into your pages.
+*/
+
+/*jslint evil: true */
+
+/*global JSON */
+
+/*members "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+ charCodeAt, floor, getUTCDate, getUTCFullYear, getUTCHours,
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, length,
+ parse, propertyIsEnumerable, prototype, push, replace, stringify, test,
+ toJSON, toString
+*/
+
+if (!this.JSON) {
+
+ JSON = function () {
+
+ function f(n) { // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ Date.prototype.toJSON = function () {
+
+// Eventually, this method will be based on the date.toISOString method.
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+
+ var m = { // table of character substitutions
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ };
+
+ function stringify(value, whitelist) {
+ var a, // The array holding the partial texts.
+ i, // The loop counter.
+ k, // The member key.
+ l, // Length.
+ r = /["\\\x00-\x1f\x7f-\x9f]/g,
+ v; // The member value.
+
+ switch (typeof value) {
+ case 'string':
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe sequences.
+
+ return r.test(value) ?
+ '"' + value.replace(r, function (a) {
+ var c = m[a];
+ if (c) {
+ return c;
+ }
+ c = a.charCodeAt();
+ return '\\u00' + Math.floor(c / 16).toString(16) +
+ (c % 16).toString(16);
+ }) + '"' :
+ '"' + value + '"';
+
+ case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+ return String(value);
+
+ case 'object':
+
+// Due to a specification blunder in ECMAScript,
+// typeof null is 'object', so watch out for that case.
+
+ if (!value) {
+ return 'null';
+ }
+
+// If the object has a toJSON method, call it, and stringify the result.
+
+ if (typeof value.toJSON === 'function') {
+ return stringify(value.toJSON());
+ }
+ a = [];
+ if (typeof value.length === 'number' &&
+ !(value.propertyIsEnumerable('length'))) {
+
+// The object is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+ l = value.length;
+ for (i = 0; i < l; i += 1) {
+ a.push(stringify(value[i], whitelist) || 'null');
+ }
+
+// Join all of the elements together and wrap them in brackets.
+
+ return '[' + a.join(',') + ']';
+ }
+ if (whitelist) {
+
+// If a whitelist (array of keys) is provided, use it to select the components
+// of the object.
+
+ l = whitelist.length;
+ for (i = 0; i < l; i += 1) {
+ k = whitelist[i];
+ if (typeof k === 'string') {
+ v = stringify(value[k], whitelist);
+ if (v) {
+ a.push(stringify(k) + ':' + v);
+ }
+ }
+ }
+ } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+ for (k in value) {
+ if (typeof k === 'string') {
+ v = stringify(value[k], whitelist);
+ if (v) {
+ a.push(stringify(k) + ':' + v);
+ }
+ }
+ }
+ }
+
+// Join all of the member texts together and wrap them in braces.
+
+ return '{' + a.join(',') + '}';
+ }
+ }
+
+ return {
+ stringify: stringify,
+ parse: function (text, filter) {
+ var j;
+
+ function walk(k, v) {
+ var i, n;
+ if (v && typeof v === 'object') {
+ for (i in v) {
+ if (Object.prototype.hasOwnProperty.apply(v, [i])) {
+ n = walk(i, v[i]);
+ if (n !== undefined) {
+ v[i] = n;
+ } else {
+ delete v[i];
+ }
+ }
+ }
+ }
+ return filter(k, v);
+ }
+
+
+// Parsing happens in three stages. In the first stage, we run the text against
+// regular expressions that look for non-JSON patterns. We are especially
+// concerned with '()' and 'new' because they can cause invocation, and '='
+// because it can cause mutation. But just to be safe, we want to reject all
+// unexpected forms.
+
+// We split the first stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace all backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+ if (/^[\],:{}\s]*$/.test(text.replace(/\\./g, '@').
+replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the second stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+ j = eval('(' + text + ')');
+
+// In the optional third stage, we recursively walk the new structure, passing
+// each name/value pair to a filter function for possible transformation.
+
+ return typeof filter === 'function' ? walk('', j) : j;
+ }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+ throw new SyntaxError('parseJSON');
+ }
+ };
+ }();
+}
diff --git a/modules/editor/skins/xquared/javascripts/Layer.js b/modules/editor/skins/xquared/javascripts/Layer.js
new file mode 100644
index 000000000..9ecf0f657
--- /dev/null
+++ b/modules/editor/skins/xquared/javascripts/Layer.js
@@ -0,0 +1,79 @@
+/**
+ * @requires Xquared.js
+ * @requires Editor.js
+ */
+xq.Layer = xq.Class(/** @lends xq.Layer.prototype */{
+ /**
+ * @constructs
+ *
+ * @param {xq.Editor} editor editor instance
+ * @param {Element} element designMode document's element. Layer instance will be attached to this element
+ * @param {String} html HTML for body.
+ */
+ initialize: function(editor, element, html) {
+ xq.addToFinalizeQueue(this);
+
+ this.margin = 4;
+ this.editor = editor;
+ this.element = element;
+ this.frame = this.editor._createIFrame(this.editor.getOuterDoc(), this.element.offsetWidth - (this.margin * 2) + "px", this.element.offsetHeight + (this.margin * 2) + "px");
+ this.editor.getOuterDoc().body.appendChild(this.frame);
+ this.doc = editor._createDoc(
+ this.frame,
+ '',
+ [], null, null, html
+ );
+ this.frame.style.position = "absolute";
+ this.updatePosition();
+ },
+
+ getFrame: function() {
+ return this.frame;
+ },
+
+ getDoc: function() {
+ return this.doc;
+ },
+
+ getBody: function() {
+ return this.doc.body;
+ },
+
+ isValid: function() {
+ return this.element && this.element.parentNode && this.element.offsetParent;
+ },
+
+ detach: function() {
+ this.frame.parentNode.removeChild(this.frame);
+
+ this.frame = null;
+ this.element = null;
+ },
+
+ updatePosition: function() {
+ // calculate element position
+ var offset = xq.getCumulativeOffset(this.element, this.editor.rdom.getRoot());
+
+ // and scroll position
+ var doc = this.editor.getDoc();
+ var body = this.editor.getBody();
+ offset.left -= doc.documentElement.scrollLeft + body.scrollLeft - this.margin;
+ offset.top -= doc.documentElement.scrollTop + body.scrollTop - this.margin;
+
+ // apply new position
+ this.frame.style.left = offset.left + "px";
+ this.frame.style.top = offset.top + "px";
+
+ // perform autofit
+ var newWidth = this.doc.body.scrollWidth + (this.margin - 1) * 2;
+ var newHeight = this.doc.body.scrollHeight + (this.margin - 1) * 2;
+
+ // without -1, the element increasing slowly.
+ this.element.width = newWidth;
+ this.element.height = newHeight;
+
+ // resize frame
+ this.frame.style.width = this.element.offsetWidth - (this.margin * 2) + "px";
+ this.frame.style.height = this.element.offsetHeight - (this.margin * 2) + "px";
+ }
+});
diff --git a/modules/editor/skins/xquared/javascripts/RichTable.js b/modules/editor/skins/xquared/javascripts/RichTable.js
new file mode 100644
index 000000000..e5af9e1dd
--- /dev/null
+++ b/modules/editor/skins/xquared/javascripts/RichTable.js
@@ -0,0 +1,215 @@
+/**
+ * @requires Xquared.js
+ * @requires rdom/Base.js
+ */
+xq.RichTable = xq.Class(/** @lends xq.RichTable.prototype */{
+ /**
+ * TODO: Add description
+ *
+ * @constructs
+ */
+ initialize: function(rdom, table) {
+ xq.addToFinalizeQueue(this);
+
+ this.rdom = rdom;
+ this.table = table;
+ },
+ insertNewRowAt: function(tr, where) {
+ var row = this.rdom.createElement("TR");
+ var cells = tr.cells;
+ for(var i = 0; i < cells.length; i++) {
+ var cell = this.rdom.createElement(cells[i].nodeName);
+ this.rdom.correctEmptyElement(cell);
+ row.appendChild(cell);
+ }
+ return this.rdom.insertNodeAt(row, tr, where);
+ },
+ insertNewCellAt: function(cell, where) {
+ // collect cells;
+ var cells = [];
+ var x = this.getXIndexOf(cell);
+ var y = 0;
+ while(true) {
+ var cur = this.getCellAt(x, y);
+ if(!cur) break;
+ cells.push(cur);
+ y++;
+ }
+
+ // insert new cells
+ for(var i = 0; i < cells.length; i++) {
+ var cell = this.rdom.createElement(cells[i].nodeName);
+ this.rdom.correctEmptyElement(cell);
+ this.rdom.insertNodeAt(cell, cells[i], where);
+ }
+ },
+ deleteRow: function(tr) {
+ return this.rdom.removeBlock(tr);
+ },
+ deleteCell: function(cell) {
+ if(!cell.previousSibling && !cell.nextSibling) {
+ this.rdom.deleteNode(this.table);
+ return;
+ }
+
+ // collect cells;
+ var cells = [];
+ var x = this.getXIndexOf(cell);
+ var y = 0;
+ while(true) {
+ var cur = this.getCellAt(x, y);
+ if(!cur) break;
+ cells.push(cur);
+ y++;
+ }
+
+ for(var i = 0; i < cells.length; i++) {
+ this.rdom.deleteNode(cells[i]);
+ }
+ },
+ getPreviousCellOf: function(cell) {
+ if(cell.previousSibling) return cell.previousSibling;
+ var adjRow = this.getPreviousRowOf(cell.parentNode);
+ if(adjRow) return adjRow.lastChild;
+ return null;
+ },
+ getNextCellOf: function(cell) {
+ if(cell.nextSibling) return cell.nextSibling;
+ var adjRow = this.getNextRowOf(cell.parentNode);
+ if(adjRow) return adjRow.firstChild;
+ return null;
+ },
+ getPreviousRowOf: function(row) {
+ if(row.previousSibling) return row.previousSibling;
+ var rowContainer = row.parentNode;
+ if(rowContainer.previousSibling && rowContainer.previousSibling.lastChild) return rowContainer.previousSibling.lastChild;
+ return null;
+ },
+ getNextRowOf: function(row) {
+ if(row.nextSibling) return row.nextSibling;
+ var rowContainer = row.parentNode;
+ if(rowContainer.nextSibling && rowContainer.nextSibling.firstChild) return rowContainer.nextSibling.firstChild;
+ return null;
+ },
+ getAboveCellOf: function(cell) {
+ var row = this.getPreviousRowOf(cell.parentNode);
+ if(!row) return null;
+
+ var x = this.getXIndexOf(cell);
+ return row.cells[x];
+ },
+ getBelowCellOf: function(cell) {
+ var row = this.getNextRowOf(cell.parentNode);
+ if(!row) return null;
+
+ var x = this.getXIndexOf(cell);
+ return row.cells[x];
+ },
+ getXIndexOf: function(cell) {
+ var row = cell.parentNode;
+ for(var i = 0; i < row.cells.length; i++) {
+ if(row.cells[i] === cell) return i;
+ }
+
+ return -1;
+ },
+ getYIndexOf: function(cell) {
+ var y = -1;
+
+ // find y
+ var group = row.parentNode;
+ for(var i = 0; i col) ? row.cells[col] : null;
+ },
+ getRowAt: function(index) {
+ if(this.hasHeadingAtTop()) {
+ return index === 0 ? this.table.tHead.rows[0] : this.table.tBodies[0].rows[index - 1];
+ } else {
+ var rows = this.table.tBodies[0].rows;
+ return (rows.length > index) ? rows[index] : null;
+ }
+ },
+ getDom: function() {
+ return this.table;
+ },
+ hasHeadingAtTop: function() {
+ return !!(this.table.tHead && this.table.tHead.rows[0]);
+ },
+ hasHeadingAtLeft: function() {
+ return this.table.tBodies[0].rows[0].cells[0].nodeName === "TH";
+ },
+ correctEmptyCells: function() {
+ var cells = xq.$A(this.table.getElementsByTagName("TH"));
+ var tds = xq.$A(this.table.getElementsByTagName("TD"));
+ for(var i = 0; i < tds.length; i++) {
+ cells.push(tds[i]);
+ }
+
+ for(var i = 0; i < cells.length; i++) {
+ if(this.rdom.isEmptyBlock(cells[i])) this.rdom.correctEmptyElement(cells[i])
+ }
+ }
+});
+
+xq.RichTable.create = function(rdom, cols, rows, headerPositions) {
+ if(["t", "tl", "lt"].indexOf(headerPositions) !== -1) var headingAtTop = true
+ if(["l", "tl", "lt"].indexOf(headerPositions) !== -1) var headingAtLeft = true
+
+ var sb = []
+ sb.push('