diff --git a/modules/editor/skins/xquared/LICENSE b/modules/editor/skins/xquared/LICENSE new file mode 100644 index 000000000..b47e925fd --- /dev/null +++ b/modules/editor/skins/xquared/LICENSE @@ -0,0 +1,14 @@ +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 + * 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 diff --git a/modules/editor/skins/xquared/README b/modules/editor/skins/xquared/README new file mode 100644 index 000000000..dcdd58778 --- /dev/null +++ b/modules/editor/skins/xquared/README @@ -0,0 +1,9 @@ +Xquared is a Javascript based, XHTML complaint, easily extensible +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/ + +Thanks for using Xquared. \ No newline at end of file diff --git a/modules/editor/skins/xquared/css/default.css b/modules/editor/skins/xquared/css/default.css new file mode 100644 index 000000000..bd9c47182 --- /dev/null +++ b/modules/editor/skins/xquared/css/default.css @@ -0,0 +1,18 @@ +@charset "utf-8"; + +.xeEditor .fileAttach { position:relative; top:20px; padding:0 1em .5em 1em;} +.xeEditor .fileAttach .preview { padding:5px; width:110px; height:110px; border:1px solid #e1e1dd; background:#fbfbfb; float:left; margin-right:.5em;} +.xeEditor .fileAttach .preview img { width:110px; height:110px; float:left; display:block;} +.xeEditor .fileAttach .fileListArea { float:left; width:50%; margin-right:.7em; padding-bottom:.5em; margin-bottom:1em} +.xeEditor .fileAttach .fileListArea .fileList { overflow:auto; width:100%; height:auto; border:1px solid; border-color:#a6a6a6 #d8d8d8 #d8d8d8 #a6a6a6; margin-bottom:.3em; font-size:11px;} +.xeEditor .fileAttach .fileListArea .fileList option { line-height:100%; padding-left:.5em;} +.xeEditor .fileAttach .fileListArea span.file_attach_info { color:#3f4040; font-size:11px; text-align:left;} +.xeEditor .fileAttach .fileListArea span.file_attach_info strong { color:#ff6600; font-size:11px; font-weight:bold; } +.xeEditor .fileAttach .fileUploadControl { margin-bottom:5px; } + +.xeEditor .fileAttach .fileUploadControl .uploaderButton { display:block; cursor:pointer; background:url(../images/buttonTypeBCenter.gif) repeat-x left center; line-height:100%; overflow:visible; color:#3f4040; margin:0 1px; font-size:.9em; white-space:nowrap;} +.xeEditor .fileAttach .fileUploadControl .uploaderButton:hover { text-decoration:none;} +.xeEditor .fileAttach .fileUploadControl .uploaderButton img { vertical-align:middle;} +.xeEditor .fileAttach .fileUploadControl .uploaderButton .leftCap { width:2px; height:21px; background:url(../images/buttonTypeBLeft.gif) no-repeat; margin:0 .3em 0 0; position:relative; left:-1px;} +.xeEditor .fileAttach .fileUploadControl .uploaderButton .rightCap { width:2px; height:21px; background:url(../images/buttonTypeBRight.gif) no-repeat; margin:0 -1px 0 .4em;} +.xeEditor .fileAttach .fileUploadControl .uploaderButton .icon { margin:0 .2em;} diff --git a/modules/editor/skins/xquared/doc/api/_01.html b/modules/editor/skins/xquared/doc/api/_01.html new file mode 100644 index 000000000..bd7000e14 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_01.html @@ -0,0 +1,45 @@ + + + + + + JsDoc: Browser.js + + + + +
+
+ +
Library: Browser.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + +
Functions
+ + +
Objects
+ +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:20 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_02.html b/modules/editor/skins/xquared/doc/api/_02.html new file mode 100644 index 000000000..bc396fa0a --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_02.html @@ -0,0 +1,143 @@ + + + + + + JsDoc: + + + + +
+
+ +
Library: Controls.js
+
+
+ Overview +
+
+
xq.controls provides common UI elements such as dialog.
+
+ +
+
+
+ +
Constructors
+ + + + + + + + +
Functions
+ + +
+
+ + + + this.form.onsubmit() + +
+ + + + + + + + + + +
+ + + +
+
+ + + + cancelButton.onclick() + +
+ + + + + + + + + + +
+ + + +
+
+ + + + this.param.renderItem(item) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + item + + +
+ + + + + +
+ + + +
Objects
+ + + + + + + + + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:20 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_03.html b/modules/editor/skins/xquared/doc/api/_03.html new file mode 100644 index 000000000..527f10740 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_03.html @@ -0,0 +1,161 @@ + + + + + + JsDoc: DomTree.js + + + + +
+
+ +
Library: DomTree.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + + + + + +
Functions
+ + + + +
+
+ + + + findLeft(el) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + el + + +
+ + + + + +
+ + + +
+
+ + + + findRight(el) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + el + + +
+ + + + + +
+ + + +
Objects
+ + +
+
+ + + + + xq.DomTree + +
+ + +
Provide various tree operations. + +TODO: Add specs
+ + + + + +
+ + + + + + + + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:21 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_04.html b/modules/editor/skins/xquared/doc/api/_04.html new file mode 100644 index 000000000..080339233 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_04.html @@ -0,0 +1,45 @@ + + + + + + JsDoc: + + + + +
+
+ +
Library: EditHistory.js
+
+
+ Overview +
+
+
xq.EditHistory manages editing history and performs UNDO/REDO.
+
+ +
+
+
+ +
Constructors
+ + +
Functions
+ + +
Objects
+ +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:21 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_05.html b/modules/editor/skins/xquared/doc/api/_05.html new file mode 100644 index 000000000..4b8bee6a9 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_05.html @@ -0,0 +1,796 @@ + + + + + + JsDoc: + + + + +
+
+ +
Library: Editor.js
+
+
+ Overview +
+
+
xq.Editor manages configurations such as autocompletion and autocorrection, edit mode/normal mode switching, handles editing commands, keyboard shortcuts and other events.
+
+ +
+
+
+ +
Constructors
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Functions
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + criteria(text) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + text + + +
+ + + + + +
+ + + +
+
+ + + + criteria(text) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + text + + +
+ + + + + +
+ + + +
+
+ + + + this.contentElement.form.onsubmit() + +
+ + + + + + + + + + +
+ + + +
+
+ + + + cancelMousedown(e) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + e + + +
+ + + + + +
+ + + +
+
+ + + + finder(node) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + node + + +
+ + + + + +
+ + + +
+
+ + + + exitCondition(node) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + node + + +
+ + + + + +
+ + + +
Objects
+ + +
+
+ + + object + + + + this.config + +
+ + +
Editor's configuration
+ + + + + +
+ + + + +
+
+ + + Element + + + + this.contentElement + +
+ + +
Original content element
+ + + + + +
+ + + + +
+
+ + + Document + + + + this.doc + +
+ + +
Owner document of content element
+ + + + + +
+ + + + +
+
+ + + Element + + + + this.body + +
+ + +
Body of content element
+ + + + + +
+ + + + +
+
+ + + Object + + + + this.currentEditMode + +
+ + +
False or 'readonly' means read-only mode, true or 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode.
+ + + + + +
+ + + + +
+
+ + + xq.RichDom + + + + this.rdom + +
+ + +
RichDom instance
+ + + + + +
+ + + + +
+
+ + + xq.Validator + + + + this.validator + +
+ + +
Validator instance
+ + + + + +
+ + + + +
+
+ + + Element + + + + this.outmostWrapper + +
+ + +
Outmost wrapper div
+ + + + + +
+ + + + +
+
+ + + Element + + + + this.sourceEditorDiv + +
+ + +
Source editor container
+ + + + + +
+ + + + +
+
+ + + Element + + + + this.sourceEditorTextarea + +
+ + +
Source editor textarea
+ + + + + +
+ + + + +
+
+ + + Element + + + + this.wysiwygEditorDiv + +
+ + +
WYSIWYG editor container
+ + + + + +
+ + + + +
+
+ + + IFrame + + + + this.editorFrame + +
+ + +
Design mode iframe
+ + + + + +
+ + + + +
+
+ + + Window + + + + this.editorWin + +
+ + +
Window that contains design mode iframe
+ + + + + +
+ + + + +
+
+ + + Document + + + + this.editorDoc + +
+ + +
Document that contained by design mode iframe
+ + + + + +
+ + + + +
+
+ + + Element + + + + this.editorBody + +
+ + +
Body that contained by design mode iframe
+ + + + + +
+ + + + +
+
+ + + Element + + + + this.toolbarContainer + +
+ + +
Toolbar container
+ + + + + +
+ + + + +
+
+ + + Array + + + + this.toolbarButtons + +
+ + +
Toolbar buttons
+ + + + + +
+ + + + +
+
+ + + xq.EditHistory + + + + this.editHistory + +
+ + +
Undo/redo manager
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:34 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_06.html b/modules/editor/skins/xquared/doc/api/_06.html new file mode 100644 index 000000000..3aa42a640 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_06.html @@ -0,0 +1,216 @@ + + + + + + JsDoc: RichDom.js + + + + +
+
+ +
Library: RichDom.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + + + + + + + + + +
Functions
+ + + + + + +
+
+ + + + finder(node) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + node + + +
+ + + + + +
+ + + +
+
+ + + + exitCondition(node) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + node + + +
+ + + + + +
+ + + +
+
+ + + + xq.RichDom.createInstance() + +
+ + +
Creates and returns instance of browser specific implementation.
+ + + + + + + + + +
+ + + +
Objects
+ + +
+
+ + + + + xq.RichDom + +
+ + +
Encapsulates browser incompatibility problem and provides rich set of DOM manipulation API. + +RichDom provides basic CRUD + Advanced DOM manipulation API, various query methods and caret/selection management API
+ + + + + +
+ + + + +
+
+ + + + + this.tree + +
+ + +
instance of DomTree
+ + + + + +
+ + + + + + + + + + + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:48 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_07.html b/modules/editor/skins/xquared/doc/api/_07.html new file mode 100644 index 000000000..58e74f9aa --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_07.html @@ -0,0 +1,71 @@ + + + + + + JsDoc: RichDomGecko.js + + + + +
+
+ +
Library: RichDomGecko.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + +
Functions
+ + + + +
Objects
+ + +
+
+ + + + + xq.RichDomGecko + +
+ + +
RichDom for Gecko
+ + + + + +
+ + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:48 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_08.html b/modules/editor/skins/xquared/doc/api/_08.html new file mode 100644 index 000000000..56f0609f3 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_08.html @@ -0,0 +1,71 @@ + + + + + + JsDoc: RichDomTrident.js + + + + +
+
+ +
Library: RichDomTrident.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + +
Functions
+ + + + +
Objects
+ + +
+
+ + + + + xq.RichDomTrident + +
+ + +
RichDom for Internet Explorer 6 and 7
+ + + + + +
+ + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:48 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_09.html b/modules/editor/skins/xquared/doc/api/_09.html new file mode 100644 index 000000000..5a3a1fe0f --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_09.html @@ -0,0 +1,71 @@ + + + + + + JsDoc: RichDomW3.js + + + + +
+
+ +
Library: RichDomW3.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + +
Functions
+ + + + +
Objects
+ + +
+
+ + + + + xq.RichDomW3 + +
+ + +
RichDom for W3C Standard Engine
+ + + + + +
+ + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:49 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_10.html b/modules/editor/skins/xquared/doc/api/_10.html new file mode 100644 index 000000000..9c4670246 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_10.html @@ -0,0 +1,71 @@ + + + + + + JsDoc: RichDomWebkit.js + + + + +
+
+ +
Library: RichDomWebkit.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + +
Functions
+ + + + +
Objects
+ + +
+
+ + + + + xq.RichDomWebkit + +
+ + +
RichDom for Webkit
+ + + + + +
+ + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:49 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_11.html b/modules/editor/skins/xquared/doc/api/_11.html new file mode 100644 index 000000000..c5409a591 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_11.html @@ -0,0 +1,125 @@ + + + + + + JsDoc: RichTable.js + + + + +
+
+ +
Library: RichTable.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + +
Functions
+ + +
+
+ + + + xq.RichTable.create(rdom, cols, rows, headerPositions) + +
+ + + + + + +
parameters
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + rdom + + +
+ + + cols + + +
+ + + rows + + +
+ + + headerPositions + + +
+ + + + + +
+ + + +
Objects
+ + + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:49 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_12.html b/modules/editor/skins/xquared/doc/api/_12.html new file mode 100644 index 000000000..2d0bff85a --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_12.html @@ -0,0 +1,189 @@ + + + + + + JsDoc: Shortcut.js + + + + +
+
+ +
Library: Shortcut.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + + + + + +
Functions
+ + +
+
+ + + + xq.Shortcut.interprete(expression) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + expression + + +
+ + + + + +
+ + + +
+
+ + + + xq.Shortcut._interpreteModifier(expression, modifierName) + +
+ + + + + + +
parameters
+ + + + + + + + + + + + + + +
+ + + expression + + +
+ + + modifierName + + +
+ + + + + +
+ + + +
+
+ + + + xq.Shortcut._interpreteWhich(keyName) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + keyName + + +
+ + + + + +
+ + + +
Objects
+ + + + + + + + + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:49 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_13.html b/modules/editor/skins/xquared/doc/api/_13.html new file mode 100644 index 000000000..46fe53499 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_13.html @@ -0,0 +1,153 @@ + + + + + + JsDoc: Validator.js + + + + +
+
+ +
Library: Validator.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + + + +
Functions
+ + + + +
+
+ + + + xq.Validator.createInstance(curUrl, urlValidationMode, allowedTags, allowedAttrs) + +
+ + +
Creates and returns instance of browser specific implementation.
+ + + + + +
parameters
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + curUrl + + +
+ + + urlValidationMode + + +
+ + + allowedTags + + +
+ + + allowedAttrs + + +
+ + + + + +
+ + + +
Objects
+ + +
+
+ + + + + xq.Validator + +
+ + +
Validates and invalidates designmode contents
+ + + + + +
+ + + + + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:49 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_14.html b/modules/editor/skins/xquared/doc/api/_14.html new file mode 100644 index 000000000..3a15872b4 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_14.html @@ -0,0 +1,71 @@ + + + + + + JsDoc: ValidatorGecko.js + + + + +
+
+ +
Library: ValidatorGecko.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + +
Functions
+ + + + +
Objects
+ + +
+
+ + + + + xq.ValidatorGecko + +
+ + +
Validator for Gecko Engine
+ + + + + +
+ + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:49 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_15.html b/modules/editor/skins/xquared/doc/api/_15.html new file mode 100644 index 000000000..ee41915a5 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_15.html @@ -0,0 +1,71 @@ + + + + + + JsDoc: ValidatorTrident.js + + + + +
+
+ +
Library: ValidatorTrident.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + +
Functions
+ + + + +
Objects
+ + +
+
+ + + + + xq.ValidatorTrident + +
+ + +
Validator for Internet Explorer 6 and 7
+ + + + + +
+ + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:50 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_16.html b/modules/editor/skins/xquared/doc/api/_16.html new file mode 100644 index 000000000..5b7576800 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_16.html @@ -0,0 +1,71 @@ + + + + + + JsDoc: ValidatorW3.js + + + + +
+
+ +
Library: ValidatorW3.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + +
Functions
+ + + + +
Objects
+ + +
+
+ + + + + xq.ValidatorW3 + +
+ + +
Validator for W3C Standard Engine
+ + + + + +
+ + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:50 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_17.html b/modules/editor/skins/xquared/doc/api/_17.html new file mode 100644 index 000000000..b4e3818df --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_17.html @@ -0,0 +1,71 @@ + + + + + + JsDoc: ValidatorWebkit.js + + + + +
+
+ +
Library: ValidatorWebkit.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + +
Functions
+ + + + +
Objects
+ + +
+
+ + + + + xq.ValidatorWebkit + +
+ + +
Validator for Webkit
+ + + + + +
+ + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:50 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_18.html b/modules/editor/skins/xquared/doc/api/_18.html new file mode 100644 index 000000000..88d060871 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_18.html @@ -0,0 +1,512 @@ + + + + + + JsDoc: XQuared.js + + + + +
+
+ +
Library: XQuared.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Functions
+ + + + +
+
+ + + + xq.asEventSource(object, prefix, events) + +
+ + +
Make given object as event source
+ + + + + +
parameters
+ + + + + + + + + + + + + + + + + + + + +
+ Object + + object + + target object +
+ String + + prefix + + prefix for generated functions +
+ Array + + events + + array of string which contains name of events +
+ + + + + +
+ + + +
+
+ + + Array.indexOf(n) + + +
+ + +
Returns the index of given element
+ + + + + +
parameters
+ + + + + + + + +
+ + + n + + +
+ + + +
returns
+ + + + + + + +
+ Number + + index or -1 +
+ + + +
+ + + +
+
+ + + + Date.pass(msec) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + msec + + +
+ + + + + +
+ + + +
+
+ + + + Date.get() + +
+ + + + + + + + + + +
+ + + +
+
+ + + Date.elapsed(msec) + + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + msec + + +
+ + + + + +
+ + + +
+
+ + + String.merge(data) + + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + data + + +
+ + + + + +
+ + + +
+
+ + + String.parseURL() + + +
+ + + + + + + + + + +
+ + + +
+
+ + + + xq.findXquaredScript() + +
+ + + + + + + + + + +
+ + + +
+
+ + + + xq.shouldLoadOthers() + +
+ + + + + + + + + + +
+ + + +
+
+ + + + xq.loadScript(url) + +
+ + + + + + +
parameters
+ + + + + + + + +
+ + + url + + +
+ + + + + +
+ + + +
+
+ + + + xq.loadOthers() + +
+ + + + + + + + + + +
+ + + +
Objects
+ + +
+
+ + + + + xq + +
+ + +
Namespace for entire Xquared classes
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:50 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/_19.html b/modules/editor/skins/xquared/doc/api/_19.html new file mode 100644 index 000000000..4616b926c --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/_19.html @@ -0,0 +1,45 @@ + + + + + + JsDoc: _ui_templates.js + + + + +
+
+ +
Library: _ui_templates.js
+
+
+ Overview +
+
+
+
+ +
+
+
+ +
Constructors
+ + +
Functions
+ + +
Objects
+ +
+
+
+ Generated by JsDoc Toolkit 1.3.1 on Wed, 21 Nov 2007 09:57:50 GMT. +
+ + diff --git a/modules/editor/skins/xquared/doc/api/constructor.gif b/modules/editor/skins/xquared/doc/api/constructor.gif new file mode 100644 index 000000000..ba779972c Binary files /dev/null and b/modules/editor/skins/xquared/doc/api/constructor.gif differ diff --git a/modules/editor/skins/xquared/doc/api/default.css b/modules/editor/skins/xquared/doc/api/default.css new file mode 100644 index 000000000..cfe837beb --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/default.css @@ -0,0 +1,116 @@ +body,a +{ + color: #000; + font: 12px verdana; +} + +ul +{ + list-style-type: none; + margin-left: 10px; + padding-left: 10px; +} + +.content { } +.docs { } +.signature { font-weight: normal; } +.code { + font: 11px monaco,monospace; + padding: 4px; + margin-left: 18px; + border: 1px dashed #ccc; +} + +.itemTitle +{ + font-size: 12px; + font-weight: bold; + height: 16px; +} + +.item { } + +.sectionHead +{ + font-size: 18px; + font-weight: bold; + background-color: #C0C1DE; + color: #fff; + margin-top: 18px; + padding: 2px 4px 2px 4px; +} + +.section +{ + padding: 8px; + border: 1px #8A92BC solid; + margin: 4px; +} + +.detailHead +{ + border-bottom: 1px #8FB685 dotted; + font-size: 12px; + font-weight: bold; + color: #798E73; + margin-top: 18px; +} + +.desc { padding: 8px; } + +.fileHead +{ + background-image: url(file.gif); + background-repeat: no-repeat; + padding-left: 20px; + font-weight: bold; + font-size: 14px; + line-height: 20px; +} + +.overview .itemTitle +{ + background-image: url(overview.gif); + background-repeat: no-repeat; + padding-left: 20px; +} + +.constructor .itemTitle +{ + background-image: url(constructor.gif); + background-repeat: no-repeat; + padding-left: 20px; +} + +.function .itemTitle +{ + background-image: url(function.gif); + background-repeat: no-repeat; + padding-left: 20px; +} + +.object .itemTitle +{ + background-image: url(object.gif); + background-repeat: no-repeat; + padding-left: 20px; +} + +.type +{ + font-style: italic; + color: #999; + font-weight: normal; +} + +.itemTitle a.type { font-weight: bold; } + +.finePrint +{ + color: #878787; + font-family: verdana; + font-size: 10px; + text-align: right; +} + +.params td { padding-right: 10px; } \ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/file.gif b/modules/editor/skins/xquared/doc/api/file.gif new file mode 100644 index 000000000..9c7446e41 Binary files /dev/null and b/modules/editor/skins/xquared/doc/api/file.gif differ diff --git a/modules/editor/skins/xquared/doc/api/file_list.html b/modules/editor/skins/xquared/doc/api/file_list.html new file mode 100644 index 000000000..f6c3a20f5 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/file_list.html @@ -0,0 +1,149 @@ + + + + + JsDoc + + + + +
File Index
+ + + \ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/function.gif b/modules/editor/skins/xquared/doc/api/function.gif new file mode 100644 index 000000000..be00b964b Binary files /dev/null and b/modules/editor/skins/xquared/doc/api/function.gif differ diff --git a/modules/editor/skins/xquared/doc/api/index.html b/modules/editor/skins/xquared/doc/api/index.html new file mode 100644 index 000000000..3df8e5bfc --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/index.html @@ -0,0 +1,13 @@ + + + + + JsDoc + + + + + + + \ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/object.gif b/modules/editor/skins/xquared/doc/api/object.gif new file mode 100644 index 000000000..409b58e33 Binary files /dev/null and b/modules/editor/skins/xquared/doc/api/object.gif differ diff --git a/modules/editor/skins/xquared/doc/api/overview.gif b/modules/editor/skins/xquared/doc/api/overview.gif new file mode 100644 index 000000000..241c2574e Binary files /dev/null and b/modules/editor/skins/xquared/doc/api/overview.gif differ diff --git a/modules/editor/skins/xquared/doc/api/splash.html b/modules/editor/skins/xquared/doc/api/splash.html new file mode 100644 index 000000000..8c7495195 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/splash.html @@ -0,0 +1,7 @@ + + + JsDoc + + + + \ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_01.html b/modules/editor/skins/xquared/doc/api/src_01.html new file mode 100644 index 000000000..4d8272800 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_01.html @@ -0,0 +1,24 @@ +
  1 xq.Browser = {
+  2 	// By Layout Engines
+  3 	isTrident: navigator.appName == "Microsoft Internet Explorer",
+  4 	isWebkit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
+  5 	isGecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
+  6 	isKHTML: navigator.userAgent.indexOf('KHTML') != -1,
+  7 	isPresto: navigator.appName == "Opera",
+  8 	
+  9 	// By Platforms
+ 10 	isMac: navigator.userAgent.indexOf("Macintosh") != -1,
+ 11 	isUbuntu: navigator.userAgent.indexOf('Ubuntu') != -1,
+ 12 
+ 13 	// By Browsers
+ 14 	isIE: navigator.appName == "Microsoft Internet Explorer",
+ 15 	isIE6: navigator.userAgent.indexOf('MSIE 6') != -1,
+ 16 	isIE7: navigator.userAgent.indexOf('MSIE 7') != -1
+ 17 };
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_02.html b/modules/editor/skins/xquared/doc/api/src_02.html new file mode 100644 index 000000000..7d1de91f7 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_02.html @@ -0,0 +1,344 @@ +
  1 /**
+  2  * @fileOverview xq.controls provides common UI elements such as dialog.
+  3  */
+  4 xq.controls = {};
+  5 
+  6 
+  7 
+  8 xq.controls.FormDialog = Class.create({
+  9 	/**
+ 10      * @constructor
+ 11      *
+ 12      * @param {String} html HTML string which contains FORM
+ 13      * @param {Function} [onLoadHandler] callback function to be called when the form is loaded
+ 14 	 */
+ 15 	initialize: function(xed, html, onLoadHandler, onCloseHandler) {
+ 16 		this.xed = xed;
+ 17 		this.html = html;
+ 18 		this.onLoadHandler = onLoadHandler || function() {};
+ 19 		this.onCloseHandler = onCloseHandler || function() {};
+ 20 		this.form = null;
+ 21 	},
+ 22 	/**
+ 23 	 * Show dialog
+ 24 	 *
+ 25 	 * @param {Object} [options] collection of options
+ 26 	 */
+ 27 	show: function(options) {
+ 28 		options = options || {};
+ 29 		options.position = options.position || 'centerOfWindow';
+ 30 		options.mode = options.mode || 'modal';
+ 31 		options.cancelOnEsc = options.cancelOnEsc || true;
+ 32 		
+ 33 		var self = this;
+ 34 		
+ 35 		// create and append container
+ 36 		var container = $(document.createElement('DIV'));
+ 37 		container.style.display = 'none';
+ 38 		document.body.appendChild(container);
+ 39 		
+ 40 		// initialize form
+ 41 		container.innerHTML = this.html;
+ 42 		this.form = $(container.getElementsByTagName('FORM')[0]);
+ 43 		
+ 44 		this.form.onsubmit = function() {
+ 45 			self.onCloseHandler($(this).serialize(true));
+ 46 			self.close();
+ 47 			return false;
+ 48 		};
+ 49 		
+ 50 		var cancelButton = this.form.getElementsByClassName('cancel')[0];
+ 51 		cancelButton.onclick = function() {
+ 52 			self.onCloseHandler();
+ 53 			self.close();
+ 54 		};
+ 55 		
+ 56 		// append dialog
+ 57 		document.body.appendChild(this.form);
+ 58 		container.parentNode.removeChild(container);
+ 59 		
+ 60 		// place dialog to center of window
+ 61 		this.setPosition(options.position);
+ 62 		
+ 63 		// give focus
+ 64 		var elementToFocus = this.form.getElementsByClassName('initialFocus');
+ 65 		if(elementToFocus.length > 0) elementToFocus[0].focus();
+ 66 		
+ 67 		// handle cancelOnEsc option
+ 68 		if(options.cancelOnEsc) {
+ 69 			Event.observe(this.form, 'keydown', function(e) {
+ 70 				if(e.keyCode == 27) {
+ 71 					this.onCloseHandler();
+ 72 					this.close();
+ 73 				}
+ 74 				return false;
+ 75 			}.bind(this));
+ 76 		}
+ 77 		
+ 78 		this.onLoadHandler(this);
+ 79 	},
+ 80 	close: function() {
+ 81 		this.form.parentNode.removeChild(this.form);
+ 82 	},
+ 83 	setPosition: function(target) {
+ 84 		var targetElement;
+ 85 		
+ 86 		if(target == 'centerOfWindow') {
+ 87 			targetElement = document.documentElement;
+ 88 		} else if(target == 'centerOfEditor') {
+ 89 			targetElement = this.xed.getDoc()[xq.Browser.isTrident ? "body" : "documentElement"];
+ 90 		} else if(target == 'nearbyCaret') {
+ 91 			throw "Not implemented yet";
+ 92 		} else {
+ 93 			throw "Invalid argument: " + target;
+ 94 		}
+ 95 		
+ 96 		var targetWidth = targetElement.clientWidth;
+ 97 		var targetHeight = targetElement.clientHeight;
+ 98 		var dialogWidth = this.form.clientWidth;
+ 99 		var dialogHeight = this.form.clientHeight;
+100 		
+101 		var x = parseInt((targetWidth - dialogWidth) / 2);
+102 		var y = parseInt((targetHeight - dialogHeight) / 2);
+103 		
+104 		this.form.style.left = x + "px";
+105 		this.form.style.top = y + "px";
+106 	}
+107 })
+108 
+109 
+110 
+111 xq.controls.QuickSearchDialog = Class.create({
+112 	/**
+113      * @constructor
+114 	 */
+115 	initialize: function(xed, param) {
+116 		this.xed = xed;
+117 		
+118 		this.rdom = xq.RichDom.createInstance();
+119 		this.rdom.setRoot(document.body);
+120 		
+121 		this.param = param;
+122 		if(!this.param.renderItem) this.param.renderItem = function(item) {
+123 			return this.rdom.getInnerText(item);
+124 		}.bind(this);
+125 		
+126 		this.container = null;
+127 	},
+128 	
+129 	getQuery: function() {
+130 		if(!this.container) return "";
+131 		return this._getInputField().value;
+132 	},
+133 	
+134 	onSubmit: function(e) {
+135 		if(this.matchCount() > 0) {
+136 			this.param.onSelect(this.xed, this.list[this._getSelectedIndex()]);
+137 		}
+138 		
+139 		this.close();
+140 		Event.stop(e);
+141 		return false;
+142 	},
+143 	
+144 	onCancel: function(e) {
+145 		if(this.param.onCancel) this.param.onCancel(this.xed);
+146 		this.close();
+147 	},
+148 	
+149 	onBlur: function(e) {
+150 		// TODO: Ugly
+151 		setTimeout(function() {this.onCancel(e)}.bind(this), 400);
+152 	},
+153 	
+154 	onKey: function(e) {
+155 		var esc = new xq.Shortcut("ESC");
+156 		var enter = new xq.Shortcut("ENTER");
+157 		var up = new xq.Shortcut("UP");
+158 		var down = new xq.Shortcut("DOWN");
+159 		
+160 		if(esc.matches(e)) {
+161 			this.onCancel(e);
+162 		} else if(enter.matches(e)) {
+163 			this.onSubmit(e);
+164 		} else if(up.matches(e)) {
+165 			this._moveSelectionUp();
+166 		} else if(down.matches(e)) {
+167 			this._moveSelectionDown();
+168 		} else {
+169 			this.updateList();
+170 		}
+171 	},
+172 	
+173 	onClick: function(e) {
+174 		var target = e.srcElement || e.target;
+175 		if(target.nodeName == "LI") {
+176 			
+177 			var index = this._getIndexOfLI(target);
+178 			this.param.onSelect(this.xed, this.list[index]);
+179 		}
+180 	},
+181 	
+182 	onList: function(list) {
+183 		this.list = list;
+184 		this.renderList(list);
+185 	},
+186 	
+187 	updateList: function() {
+188 		window.setTimeout(function() {
+189 			this.param.listProvider(this.getQuery(), this.xed, this.onList.bind(this));
+190 		}.bind(this), 0);
+191 	},
+192 	
+193 	renderList: function(list) 
+194 	{
+195 		var ol = this._getListContainer();
+196 		ol.innerHTML = "";
+197 		
+198 		for(var i = 0; i < list.length; i++) {
+199 			var li = this.rdom.createElement('LI');
+200 			li.innerHTML = this.param.renderItem(list[i]);
+201 			ol.appendChild(li);
+202 		}
+203 		
+204 		if(ol.hasChildNodes()) {
+205 			ol.firstChild.className = "selected";
+206 		}
+207 	},
+208 	
+209 	show: function() {
+210 		if(!this.container) this.container = this._create();
+211 		
+212 		var dialog = this.rdom.insertNodeAt(this.container, this.rdom.getRoot(), "end");
+213 		this.setPosition('centerOfEditor');
+214 		this.updateList();
+215 		this.focus();
+216 	},
+217 	
+218 	close: function() {
+219 		this.rdom.deleteNode(this.container);
+220 	},
+221 	
+222 	focus: function() {
+223 		this._getInputField().focus();
+224 	},
+225 	
+226 	setPosition: function(target) {
+227 		var targetElement;
+228 		
+229 		if(target == 'centerOfWindow') {
+230 			targetElement = document.documentElement;
+231 		} else if(target == 'centerOfEditor') {
+232 			targetElement = this.xed.getDoc().documentElement;
+233 		} else if(target == 'nearbyCaret') {
+234 			throw "Not implemented yet";
+235 		} else {
+236 			throw "Invalid argument: " + target;
+237 		}
+238 		
+239 		var targetWidth = targetElement.clientWidth;
+240 		var targetHeight = targetElement.clientHeight;
+241 		var dialogWidth = this.container.clientWidth;
+242 		var dialogHeight = this.container.clientHeight;
+243 		
+244 		var x = parseInt((targetWidth - dialogWidth) / 2);
+245 		var y = parseInt((targetHeight - dialogHeight) / 2);
+246 		this.container.style.left = x + "px";
+247 		this.container.style.top = y + "px";
+248 	},
+249 	
+250 	matchCount: function() {
+251 		return this.list ? this.list.length : 0;
+252 	},
+253 	
+254 	_create: function() {
+255 		// make container
+256 		var container = this.rdom.createElement("DIV");
+257 		container.className = "xqQuickSearch";
+258 		
+259 		// make title
+260 		if(this.param.title) {
+261 			var title = this.rdom.createElement("H1");
+262 			title.innerHTML = this.param.title;
+263 			container.appendChild(title);
+264 		}
+265 		
+266 		// make input field
+267 		var inputWrapper = this.rdom.createElement("DIV");
+268 		inputWrapper.className = "input";
+269 		var form = this.rdom.createElement("FORM");
+270 		var input = this.rdom.createElement("INPUT");
+271 		input.type = "text";
+272 		input.value = "";
+273     	form.appendChild(input);
+274 		inputWrapper.appendChild(form);
+275 		container.appendChild(inputWrapper);
+276 		
+277 		// make list
+278 		var list = this.rdom.createElement("OL");
+279 
+280 	    Event.observe(input, 'blur', this.onBlur.bindAsEventListener(this));
+281     	Event.observe(input, 'keypress', this.onKey.bindAsEventListener(this));
+282     	Event.observe(list, 'click', this.onClick.bindAsEventListener(this), true);
+283     	Event.observe(form, 'submit', this.onSubmit.bindAsEventListener(this));
+284     	Event.observe(form, 'reset', this.onCancel.bindAsEventListener(this));
+285 
+286 		container.appendChild(list);
+287 		return container;
+288 	},
+289 	
+290 	_getInputField: function() {
+291 		return this.container.getElementsByTagName('INPUT')[0];
+292 	},
+293 	
+294 	_getListContainer: function() {
+295 		return this.container.getElementsByTagName('OL')[0];
+296 	},
+297 	
+298 	_getSelectedIndex: function() {
+299 		var ol = this._getListContainer();
+300 		for(var i = 0; i < ol.childNodes.length; i++) {
+301 			if(ol.childNodes[i].className == 'selected') return i;
+302 		}
+303 	},
+304 	
+305 	_getIndexOfLI: function(li) {
+306 		var ol = this._getListContainer();
+307 		for(var i = 0; i < ol.childNodes.length; i++) {
+308 			if(ol.childNodes[i] == li) return i;
+309 		}
+310 	},
+311 	
+312 	_moveSelectionUp: function() {
+313 		var count = this.matchCount();
+314 		if(count == 0) return;
+315 		var index = this._getSelectedIndex();
+316 		var ol = this._getListContainer();
+317 		ol.childNodes[index].className = "";
+318 		
+319 		index--;
+320 		if(index < 0) index = count - 1;
+321 
+322 		ol.childNodes[index].className = "selected";
+323 	},
+324 	
+325 	_moveSelectionDown: function() {
+326 		var count = this.matchCount();
+327 		if(count == 0) return;
+328 		var index = this._getSelectedIndex();
+329 		var ol = this._getListContainer();
+330 		ol.childNodes[index].className = "";
+331 
+332 		index++;
+333 		if(index >= count) index = 0;
+334 		
+335 		ol.childNodes[index].className = "selected";
+336 	}
+337 });
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_03.html b/modules/editor/skins/xquared/doc/api/src_03.html new file mode 100644 index 000000000..076de09f1 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_03.html @@ -0,0 +1,325 @@ +
  1 /**
+  2  * Provide various tree operations.
+  3  *
+  4  * TODO: Add specs
+  5  */
+  6 xq.DomTree = Class.create({
+  7 	initialize: function() {
+  8 		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"];
+  9 		this._blockContainerTags = ["DIV", "DD", "LI", "BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"];
+ 10 		this._listContainerTags = ["OL", "UL", "DL"];
+ 11 		this._tableCellTags = ["TH", "TD"];
+ 12 		this._blockOnlyContainerTags = ["BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR"];
+ 13 		this._atomicTags = ["IMG", "OBJECT", "BR", "HR"];
+ 14 	},
+ 15 	
+ 16 	getBlockTags: function() {
+ 17 		return this._blockTags;
+ 18 	},
+ 19 	
+ 20 	/**
+ 21 	 * Find common ancestor(parent) and his immediate children(left and right).
+ 22 	 *
+ 23 	 * A --- B -+- C -+- D -+- E
+ 24 	 *          |
+ 25 	 *          +- F -+- G
+ 26 	 *
+ 27 	 * For example:
+ 28 	 * > findCommonAncestorAndImmediateChildrenOf("E", "G")
+ 29 	 *
+ 30 	 * will return
+ 31 	 *
+ 32 	 * > {parent:"B", left:"C", right:"F"}
+ 33 	 */
+ 34 	findCommonAncestorAndImmediateChildrenOf: function(left, right) {
+ 35 		if(left.parentNode == right.parentNode) {
+ 36 			return {
+ 37 				left:left,
+ 38 				right:right,
+ 39 				parent:left.parentNode
+ 40 			};
+ 41 		} else {
+ 42 			var parentsOfLeft = this.collectParentsOf(left, true);
+ 43 			var parentsOfRight = this.collectParentsOf(right, true);
+ 44 			var ca = this.getCommonAncestor(parentsOfLeft, parentsOfRight);
+ 45 	
+ 46 			var leftAncestor = parentsOfLeft.find(function(node) {return node.parentNode == ca});
+ 47 			var rightAncestor = parentsOfRight.find(function(node) {return node.parentNode == ca});
+ 48 			
+ 49 			return {
+ 50 				left:leftAncestor,
+ 51 				right:rightAncestor,
+ 52 				parent:ca
+ 53 			};
+ 54 		}
+ 55 	},
+ 56 	
+ 57 	/**
+ 58 	 * Find leaves at edge.
+ 59 	 *
+ 60 	 * A --- B -+- C -+- D -+- E
+ 61 	 *          |
+ 62 	 *          +- F -+- G
+ 63 	 *
+ 64 	 * For example:
+ 65 	 * > getLeavesAtEdge("A")
+ 66 	 *
+ 67 	 * will return
+ 68 	 *
+ 69 	 * > ["E", "G"]
+ 70 	 */
+ 71 	getLeavesAtEdge: function(element) {
+ 72 		if(!element.hasChildNodes()) return [null, null];
+ 73 		
+ 74 		var findLeft = function(el) {
+ 75 			for (var i = 0; i < el.childNodes.length; i++) {
+ 76 				if (el.childNodes[i].nodeType == 1 && this.isBlock(el.childNodes[i])) return findLeft(el.childNodes[i]);
+ 77 			}
+ 78 			return el;
+ 79 		}.bind(this);
+ 80 		
+ 81 		var findRight=function(el) {
+ 82 			for (var i = el.childNodes.length; i--;) {
+ 83 				if (el.childNodes[i].nodeType == 1 && this.isBlock(el.childNodes[i])) return findRight(el.childNodes[i]);
+ 84 			}
+ 85 			return el;
+ 86 		}.bind(this);
+ 87 		
+ 88 		var left = findLeft(element);
+ 89 		var right = findRight(element);
+ 90 		return [left == element ? null : left, right == element ? null : right];
+ 91 	},
+ 92 	
+ 93 	getCommonAncestor: function(parents1, parents2) {
+ 94 		for(var i = 0; i < parents1.length; i++) {
+ 95 			for(var j = 0; j < parents2.length; j++) {
+ 96 				if(parents1[i] == parents2[j]) return parents1[i];
+ 97 			}
+ 98 		}
+ 99 	},
+100 	
+101 	collectParentsOf: function(node, includeSelf, exitCondition) {
+102 		var parents = [];
+103 		if(includeSelf) parents.push(node);
+104 		
+105 		while((node = node.parentNode) && (node.nodeName != "HTML") && !(typeof exitCondition == "function" && exitCondition(node))) parents.push(node);
+106 		return parents;
+107 	},
+108 	
+109 	isDescendantOf: function(parent, child) {
+110 		if(parent.length > 0) {
+111 			for(var i = 0; i < parent.length; i++) {
+112 				if(this.isDescendantOf(parent[i], child)) return true;
+113 			}
+114 			return false;
+115 		}
+116 		
+117 		if(parent == child) return false;
+118 		
+119 	    while (child = child.parentNode)
+120 	      if (child == parent) return true;
+121 	    return false;
+122 	},
+123 	
+124 	/**
+125 	 * Perform tree walking (foreward)
+126 	 */
+127 	walkForward: function(node) {
+128 		if(node.hasChildNodes()) return node.firstChild;
+129 		if(node.nextSibling) return node.nextSibling;
+130 		
+131 		while(node = node.parentNode) {
+132 			if(node.nextSibling) return node.nextSibling;
+133 		}
+134 		
+135 		return null;
+136 	},
+137 	
+138 	/**
+139 	 * Perform tree walking (backward)
+140 	 */
+141 	walkBackward: function(node) {
+142 		if(node.previousSibling) {
+143 			node = node.previousSibling;
+144 			while(node.hasChildNodes()) {node = node.lastChild;}
+145 			return node;
+146 		}
+147 		
+148 		return node.parentNode;
+149 	},
+150 	
+151 	/**
+152 	 * Perform tree walking (to next siblings)
+153 	 */
+154 	walkNext: function(node) {return node.nextSibling},
+155 	
+156 	/**
+157 	 * Perform tree walking (to next siblings)
+158 	 */
+159 	walkPrev: function(node) {return node.previousSibling},
+160 	
+161 	/**
+162 	 * Returns true if target is followed by start
+163 	 */
+164 	checkTargetForward: function(start, target) {
+165 		return this._check(start, this.walkForward, target);
+166 	},
+167 
+168 	/**
+169 	 * Returns true if start is followed by target
+170 	 */
+171 	checkTargetBackward: function(start, target) {
+172 		return this._check(start, this.walkBackward, target);
+173 	},
+174 	
+175 	findForward: function(start, condition, exitCondition) {
+176 		return this._find(start, this.walkForward, condition, exitCondition);
+177 	},
+178 	
+179 	findBackward: function(start, condition, exitCondition) {
+180 		return this._find(start, this.walkBackward, condition, exitCondition);
+181 	},
+182 	
+183 	/** @private */
+184 	_check: function(start, direction, target) {
+185 		if(start == target) return false;
+186 		
+187 		while(start = direction(start)) {
+188 			if(start == target) return true;
+189 		}
+190 		return false;
+191 	},
+192 	
+193 	/** @private */
+194 	_find: function(start, direction, condition, exitCondition) {
+195 		while(start = direction(start)) {
+196 			if(exitCondition && exitCondition(start)) return null;
+197 			if(condition(start)) return start;
+198 		}
+199 		return null;
+200 	},
+201 
+202 	/**
+203 	 * Walks Forward through DOM tree from start to end, and collects all nodes that matches with a filter.
+204 	 * If no filter provided, it just collects all nodes.
+205 	 *
+206 	 * @param function filter a filter function
+207 	 */
+208 	collectNodesBetween: function(start, end, filter) {
+209 		if(start == end) return [start, end].findAll(filter || function() {return true});
+210 		
+211 		var nodes = this.collectForward(start, function(node) {return node == end}, filter);
+212 		if(
+213 			start != end &&
+214 			typeof filter == "function" &&
+215 			filter(end)
+216 		) nodes.push(end);
+217 		
+218 		return nodes;
+219 	},
+220 
+221 	collectForward: function(start, exitCondition, filter) {
+222 		return this.collect(start, this.walkForward, exitCondition, filter);
+223 	},
+224 	
+225 	collectBackward: function(start, exitCondition, filter) {
+226 		return this.collect(start, this.walkBackward, exitCondition, filter);
+227 	},
+228 	
+229 	collectNext: function(start, exitCondition, filter) {
+230 		return this.collect(start, this.walkNext, exitCondition, filter);
+231 	},
+232 	
+233 	collectPrev: function(start, exitCondition, filter) {
+234 		return this.collect(start, this.walkPrev, exitCondition, filter);
+235 	},
+236 	
+237 	collect: function(start, next, exitCondition, filter) {
+238 		var nodes = [start];
+239 
+240 		while(true) {
+241 			start = next(start);
+242 			if(
+243 				(start == null) ||
+244 				(typeof exitCondition == "function" && exitCondition(start))
+245 			) break;
+246 			
+247 			nodes.push(start);
+248 		}
+249 
+250 		return (typeof filter == "function") ? nodes.findAll(filter) : nodes;
+251 	},
+252 
+253 
+254 	hasBlocks: function(element) {
+255 		var nodes = element.childNodes;
+256 		for(var i = 0; i < nodes.length; i++) {
+257 			if(this.isBlock(nodes[i])) return true;
+258 		}
+259 		return false;
+260 	},
+261 	
+262 	hasMixedContents: function(element) {
+263 		if(!this.isBlock(element)) return false;
+264 		if(!this.isBlockContainer(element)) return false;
+265 		
+266 		var hasTextOrInline = false;
+267 		var hasBlock = false;
+268 		for(var i = 0; i < element.childNodes.length; i++) {
+269 			var node = element.childNodes[i];
+270 			if(!hasTextOrInline && this.isTextOrInlineNode(node)) hasTextOrInline = true;
+271 			if(!hasBlock && this.isBlock(node)) hasBlock = true;
+272 			
+273 			if(hasTextOrInline && hasBlock) break;
+274 		}
+275 		if(!hasTextOrInline || !hasBlock) return false;
+276 		
+277 		return true;
+278 	},
+279 	
+280 	isBlockOnlyContainer: function(element) {
+281 		if(!element) return false;
+282 		return this._blockOnlyContainerTags.include(typeof element == 'string' ? element : element.nodeName);
+283 	},
+284 	
+285 	isTableCell: function(element) {
+286 		if(!element) return false;
+287 		return this._tableCellTags.include(typeof element == 'string' ? element : element.nodeName);
+288 	},
+289 	
+290 	isBlockContainer: function(element) {
+291 		if(!element) return false;
+292 		return this._blockContainerTags.include(typeof element == 'string' ? element : element.nodeName);
+293 	},
+294 	
+295 	isHeading: function(element) {
+296 		if(!element) return false;
+297 		return (typeof element == 'string' ? element : element.nodeName).match(/H\d/);
+298 	},
+299 	
+300 	isBlock: function(element) {
+301 		if(!element) return false;
+302 		return this._blockTags.include(typeof element == 'string' ? element : element.nodeName);
+303 	},
+304 	
+305 	isAtomic: function(element) {
+306 		if(!element) return false;
+307 		return this._atomicTags.include(typeof element == 'string' ? element : element.nodeName);
+308 	},
+309 	
+310 	isListContainer: function(element) {
+311 		if(!element) return false;
+312 		return this._listContainerTags.include(typeof element == 'string' ? element : element.nodeName);
+313 	},
+314 	
+315 	isTextOrInlineNode: function(node) {
+316 		return node && (node.nodeType == 3 || !this.isBlock(node));
+317 	}
+318 });
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_04.html b/modules/editor/skins/xquared/doc/api/src_04.html new file mode 100644 index 000000000..50db4821a --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_04.html @@ -0,0 +1,163 @@ +
  1 /**
+  2  * @fileOverview xq.EditHistory manages editing history and performs UNDO/REDO.
+  3  */
+  4 xq.EditHistory = Class.create({
+  5     /**
+  6 	 * Initializer
+  7 	 *
+  8      * @constructor
+  9 	 * @param {xq.RichDom} rdom RichDom instance
+ 10 	 * @param {Number} [max] maximum UNDO buffer size(default value is 100).
+ 11 	 */
+ 12 	initialize: function(rdom, max) {
+ 13 		if (!rdom) throw "IllegalArgumentException";
+ 14 
+ 15 		this.disabled = false;
+ 16 		this.max = max || 100;
+ 17 		this.rdom = rdom;
+ 18 		this.root = rdom.getRoot();
+ 19 		this.clear();
+ 20 		
+ 21 		this.lastModified = Date.get();
+ 22 	},
+ 23 	getLastModifiedDate: function() {
+ 24 		return this.lastModified;
+ 25 	},
+ 26 	isUndoable: function() {
+ 27 		return this.queue.length > 0 && this.index > 0;
+ 28 	},
+ 29 	isRedoable: function() {
+ 30 		return this.queue.length > 0 && this.index < this.queue.length - 1;
+ 31 	},
+ 32 	disable: function() {
+ 33 		this.disabled = true;
+ 34 	},
+ 35 	enable: function() {
+ 36 		this.disabled = false;
+ 37 	},
+ 38 	undo: function() {
+ 39 		this.pushContent();
+ 40 		
+ 41 		if (this.isUndoable()) {
+ 42 			this.index--;
+ 43 			this.popContent();
+ 44 			return true;
+ 45 		} else {
+ 46 			return false;
+ 47 		}
+ 48 	},
+ 49 	redo: function() {
+ 50 		if (this.isRedoable()) {
+ 51 			this.index++;
+ 52 			this.popContent();
+ 53 			return true;
+ 54 		} else {
+ 55 			return false;
+ 56 		}
+ 57 	},
+ 58 	onCommand: function() {
+ 59 		this.lastModified = Date.get();
+ 60 		if(this.disabled) return false;
+ 61 
+ 62 		return this.pushContent();
+ 63 	},
+ 64 	onEvent: function(event) {
+ 65 		this.lastModified = Date.get();
+ 66 		if(this.disabled) return false;
+ 67 
+ 68 		// ignore normal keys
+ 69 		if('keydown' == event.type && !(event.ctrlKey || event.metaKey)) return false;
+ 70 		if(['keydown', 'keyup', 'keypress'].include(event.type) && !event.ctrlKey && !event.altKey && !event.metaKey && ![33,34,35,36,37,38,39,40].include(event.keyCode)) return false;
+ 71 		if(['keydown', 'keyup', 'keypress'].include(event.type) && (event.ctrlKey || event.metaKey) && [89,90].include(event.keyCode)) return false;
+ 72 		
+ 73 		// ignore ctrl/shift/alt/meta keys
+ 74 		if([16,17,18,224].include(event.keyCode)) return false;
+ 75 		
+ 76 		return this.pushContent();
+ 77 	},
+ 78 	popContent: function() {
+ 79 		this.lastModified = Date.get();
+ 80 		var entry = this.queue[this.index];
+ 81 		if (entry.caret > 0) {
+ 82 			var html=entry.html.substring(0, entry.caret) + '<span id="caret_marker_00700"></span>' + entry.html.substring(entry.caret);
+ 83 			this.root.innerHTML = html;
+ 84 		} else {
+ 85 			this.root.innerHTML = entry.html;
+ 86 		}
+ 87 		this.restoreCaret();
+ 88 	},
+ 89 	pushContent: function(ignoreCaret) {
+ 90 		if(xq.Browser.isTrident && !ignoreCaret && !this.rdom.hasFocus()) return false;
+ 91 		if(!this.rdom.getCurrentElement()) return false;
+ 92 		
+ 93 		var html = this.root.innerHTML;
+ 94 		if(html == (this.queue[this.index] ? this.queue[this.index].html : null)) return false;
+ 95 		var caret = ignoreCaret ? -1 : this.saveCaret();
+ 96 		
+ 97 		if(this.queue.length >= this.max) {
+ 98 			this.queue.shift();
+ 99 		} else {
+100 			this.index++;
+101 		}
+102 		
+103 		this.queue.splice(this.index, this.queue.length - this.index, {html:html, caret:caret});
+104 		return true;
+105 	},
+106 	clear: function() {
+107 		this.index = -1;
+108 		this.queue = [];
+109 		this.pushContent(true);
+110 	},
+111 	saveCaret: function() {
+112 		if(this.rdom.hasSelection()) return null;
+113 
+114 		// FF on Mac has a caret problem with these lines. --2007/11/19
+115 		var marker = this.rdom.pushMarker();
+116 		var str = xq.Browser.isTrident ? '<SPAN class='+marker.className : '<span class="'+marker.className+'"';
+117 		var caret = this.rdom.getRoot().innerHTML.indexOf(str);
+118 		this.rdom.popMarker(true);
+119 
+120 		return caret;
+121 
+122 /*
+123 		// This is old code. It also has same problem.
+124 		
+125 		if(this.rdom.hasSelection()) return null;
+126 		
+127 		var bookmark = this.rdom.saveSelection();
+128 		var marker = this.rdom.pushMarker();
+129 		
+130 		var str = xq.Browser.isTrident ? '<SPAN class='+marker.className : '<span class="'+marker.className+'"';
+131 		var caret = this.rdom.getRoot().innerHTML.indexOf(str);
+132 		
+133 		this.rdom.popMarker();
+134 		this.rdom.restoreSelection(bookmark);
+135 		
+136 		return caret;
+137 */
+138 	},
+139 	restoreCaret: function() {
+140 		var marker = this.rdom.$('caret_marker_00700');
+141 		
+142 		if(marker) {
+143 			this.rdom.selectElement(marker, true);
+144 			this.rdom.collapseSelection(false);
+145 			this.rdom.deleteNode(marker);
+146 		} else {
+147 			var node = this.rdom.tree.findForward(this.rdom.getRoot(), function(node) {
+148 				return this.isBlock(node) && !this.hasBlocks(node);
+149 			}.bind(this.rdom.tree));
+150 			this.rdom.selectElement(node, false);
+151 			this.rdom.collapseSelection(false);
+152 			
+153 		}
+154 	}
+155 });
+156 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_05.html b/modules/editor/skins/xquared/doc/api/src_05.html new file mode 100644 index 000000000..c8bbff96a --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_05.html @@ -0,0 +1,2216 @@ +
  1 /**
+  2  * @fileOverview xq.Editor manages configurations such as autocompletion and autocorrection, edit mode/normal mode switching, handles editing commands, keyboard shortcuts and other events.
+  3  */
+  4 xq.Editor = Class.create({
+  5 	/**
+  6 	 * Initialize editor but it doesn't automatically start designMode. setEditMode should be called after initialization.
+  7 	 *
+  8      * @constructor
+  9 	 * @param {Element} contentElement HTML element(TEXTAREA or normal block element such as DIV) to be replaced with editable area
+ 10 	 * @param {Element} toolbarContainer HTML element which contains toolbar icons
+ 11 	 */
+ 12 	initialize: function(contentElement, toolbarContainer) {
+ 13 		if(!contentElement) throw "[contentElement] is null";
+ 14 		if(contentElement.nodeType != 1) throw "[contentElement] is not an element";
+ 15 		
+ 16 		xq.asEventSource(this, "Editor", ["ElementChanged", "BeforeEvent", "AfterEvent", "CurrentContentChanged", "StaticContentChanged", "CurrentEditModeChanged"]);
+ 17 		
+ 18 		/**
+ 19 		 * Editor's configuration
+ 20 		 * @type object
+ 21 		 */
+ 22 		this.config = {};
+ 23 		this.config.enableLinkClick = false;
+ 24 		this.config.changeCursorOnLink = false;
+ 25 		this.config.generateDefaultToolbar = true;
+ 26 		this.config.defaultToolbarButtonMap = [
+ 27 			[
+ 28 				{className:"foregroundColor", title:"Foreground color", handler:"xed.handleForegroundColor()"},
+ 29 				{className:"backgroundColor", title:"Background color", handler:"xed.handleBackgroundColor()"}
+ 30 			],
+ 31 			[
+ 32 				{className:"link", title:"Link", handler:"xed.handleLink()"},
+ 33 				{className:"strongEmphasis", title:"Strong emphasis", handler:"xed.handleStrongEmphasis()"},
+ 34 				{className:"emphasis", title:"Emphasis", handler:"xed.handleEmphasis()"},
+ 35 				{className:"underline", title:"Underline", handler:"xed.handleUnderline()"},
+ 36 				{className:"strike", title:"Strike", handler:"xed.handleStrike()"},
+ 37 				{className:"superscription", title:"Superscription", handler:"xed.handleSuperscription()"},
+ 38 				{className:"subscription", title:"Subscription", handler:"xed.handleSubscription()"}
+ 39 			],
+ 40 			[
+ 41 				{className:"removeFormat", title:"Remove format", handler:"xed.handleRemoveFormat()"}
+ 42 			],
+ 43 			[
+ 44 				{className:"justifyLeft", title:"Justify left", handler:"xed.handleJustify('left')"},
+ 45 				{className:"justifyCenter", title:"Justify center", handler:"xed.handleJustify('center')"},
+ 46 				{className:"justifyRight", title:"Justify right", handler:"xed.handleJustify('right')"},
+ 47 				{className:"justifyBoth", title:"Justify both", handler:"xed.handleJustify('both')"}
+ 48 			],
+ 49 			[
+ 50 				{className:"indent", title:"Indent", handler:"xed.handleIndent()"},
+ 51 				{className:"outdent", title:"Outdent", handler:"xed.handleOutdent()"}
+ 52 			],
+ 53 			[
+ 54 				{className:"unorderedList", title:"Unordered list", handler:"xed.handleList('UL')"},
+ 55 				{className:"orderedList", title:"Ordered list", handler:"xed.handleList('OL')"}
+ 56 			],
+ 57 			[
+ 58 				{className:"paragraph", title:"Paragraph", handler:"xed.handleApplyBlock('P')"},
+ 59 				{className:"heading1", title:"Heading 1", handler:"xed.handleApplyBlock('H1')"},
+ 60 				{className:"blockquote", title:"Blockquote", handler:"xed.handleApplyBlock('BLOCKQUOTE')"},
+ 61 				{className:"code", title:"Code", handler:"xed.handleList('CODE')"},
+ 62 				{className:"division", title:"Division", handler:"xed.handleApplyBlock('DIV')"}
+ 63 			],
+ 64 			[
+ 65 				{className:"table", title:"Table", handler:"xed.handleTable(3,3,'tl')"},
+ 66 				{className:"separator", title:"Separator", handler:"xed.handleSeparator()"}
+ 67 			],
+ 68 			[
+ 69 				{className:"html", title:"Edit source", handler:"xed.toggleSourceAndWysiwygMode()"}
+ 70 			],
+ 71 			[
+ 72 				{className:"undo", title:"Undo", handler:"xed.handleUndo()"},
+ 73 				{className:"redo", title:"Redo", handler:"xed.handleRedo()"}
+ 74 			]
+ 75 		];
+ 76 		
+ 77 		this.config.imagePathForDefaultToobar = 'img/toolbar/';
+ 78 		
+ 79 		// relative | host_relative | absolute | browser_default
+ 80 		this.config.urlValidationMode = 'absolute';
+ 81 		
+ 82 		this.config.automaticallyHookSubmitEvent = true;
+ 83 		
+ 84 		this.config.allowedTags = ['a', 'abbr', 'acronym', 'address', 'blockquote', 'br', 'caption', 'cite', 'code', 'dd', 'dfn', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'span', 'sup', 'sub', 'strong', 'table', 'thead', 'tbody', 'td', 'th', 'tr', 'ul', 'var'];
+ 85 		this.config.allowedAttributes = ['alt', 'cite', 'class', 'datetime', 'height', 'href', 'id', 'rel', 'rev', 'src', 'style', 'title', 'width'];
+ 86 		
+ 87 		this.config.shortcuts = {};
+ 88 		this.config.autocorrections = {};
+ 89 		this.config.autocompletions = {};
+ 90 		this.config.templateProcessors = {};
+ 91 		this.config.contextMenuHandlers = {};
+ 92 		
+ 93 		/**
+ 94 		 * Original content element
+ 95 		 * @type Element
+ 96 		 */
+ 97 		this.contentElement = contentElement;
+ 98 		
+ 99 		/**
+100 		 * Owner document of content element
+101 		 * @type Document
+102 		 */
+103 		this.doc = this.contentElement.ownerDocument;
+104 		
+105 		/**
+106 		 * Body of content element
+107 		 * @type Element
+108 		 */
+109 		this.body = this.doc.body;
+110 		
+111 		/**
+112 		 * False or 'readonly' means read-only mode, true or 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode.
+113 		 * @type Object
+114 		 */
+115 		this.currentEditMode = 'readonly';
+116 		
+117 		/**
+118 		 * RichDom instance
+119 		 * @type xq.RichDom
+120 		 */
+121 		this.rdom = xq.RichDom.createInstance();
+122 		
+123 		/**
+124 		 * Validator instance
+125 		 * @type xq.Validator
+126 		 */
+127 		this.validator = null;
+128 		
+129 		/**
+130 		 * Outmost wrapper div
+131 		 * @type Element
+132 		 */
+133 		this.outmostWrapper = null;
+134 		
+135 		/**
+136 		 * Source editor container
+137 		 * @type Element
+138 		 */
+139 		this.sourceEditorDiv = null;
+140 		
+141 		/**
+142 		 * Source editor textarea
+143 		 * @type Element
+144 		 */
+145 		this.sourceEditorTextarea = null;
+146 		
+147 		/**
+148 		 * WYSIWYG editor container
+149 		 * @type Element
+150 		 */
+151 		this.wysiwygEditorDiv = null;
+152 		
+153 		/**
+154 		 * Design mode iframe
+155 		 * @type IFrame
+156 		 */
+157 		this.editorFrame = null;
+158 		
+159 		/**
+160 		 * Window that contains design mode iframe
+161 		 * @type Window
+162 		 */
+163 		this.editorWin = null;
+164 		
+165 		/**
+166 		 * Document that contained by design mode iframe
+167 		 * @type Document
+168 		 */
+169 		this.editorDoc = null;
+170 		
+171 		/**
+172 		 * Body that contained by design mode iframe
+173 		 * @type Element
+174 		 */
+175 		this.editorBody = null;
+176 		
+177 		/**
+178 		 * Toolbar container
+179 		 * @type Element
+180 		 */
+181 		this.toolbarContainer = toolbarContainer;
+182 		
+183 		/**
+184 		 * Toolbar buttons
+185 		 * @type Array
+186 		 */
+187 		this.toolbarButtons = null;
+188 		
+189 		/**
+190 		 * Undo/redo manager
+191 		 * @type xq.EditHistory
+192 		 */
+193 		this.editHistory = null;
+194 		
+195 		this._contextMenuContainer = null;
+196 		this._contextMenuItems = null;
+197 		
+198 		this._validContentCache = null;
+199 		this._lastModified = null;
+200 		
+201 		this.addShortcuts(this._getDefaultShortcuts());
+202 		this.addTemplateProcessors(this._getDefaultTemplateProcessors());
+203 		
+204 		this.addListener({
+205 			onEditorCurrentContentChanged: function(xed) {
+206 				var curFocusElement = xed.rdom.getCurrentElement();
+207 				if(!curFocusElement) return;
+208 				
+209 				if(xed._lastFocusElement != curFocusElement) {
+210 					if(!xed.rdom.tree.isBlockOnlyContainer(xed._lastFocusElement) && xed.rdom.tree.isBlock(xed._lastFocusElement)) {
+211 						xed.rdom.removeTrailingWhitespace(xed._lastFocusElement);
+212 					}
+213 					xed._fireOnElementChanged(xed._lastFocusElement, curFocusElement);
+214 					xed._lastFocusElement = curFocusElement;
+215 				}
+216 
+217 				xed.updateAllToolbarButtonsStatus(curFocusElement);
+218 			}
+219 		});
+220 	},
+221 	
+222 	
+223 	
+224 	/////////////////////////////////////////////
+225 	// Configuration Management
+226 	
+227 	_getDefaultShortcuts: function() {
+228 		if(xq.Browser.isMac) {
+229 			// Mac FF & Safari
+230 			return [
+231 				{event:"Ctrl+Shift+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
+232 				{event:"ENTER", handler:"this.handleEnter(false, false)"},
+233 				{event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
+234 				{event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
+235 				{event:"TAB", handler:"this.handleTab()"},
+236 				{event:"Shift+TAB", handler:"this.handleShiftTab()"},
+237 				{event:"DELETE", handler:"this.handleDelete()"},
+238 				{event:"BACKSPACE", handler:"this.handleBackspace()"},
+239 				
+240 				{event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
+241 				{event:"Ctrl+I", handler:"this.handleEmphasis()"},
+242 				{event:"Ctrl+U", handler:"this.handleUnderline()"},
+243 				{event:"Ctrl+K", handler:"this.handleStrike()"},
+244 				{event:"Meta+Z", handler:"this.handleUndo()"},
+245 				{event:"Meta+Shift+Z", handler:"this.handleRedo()"},
+246 				{event:"Meta+Y", handler:"this.handleRedo()"}
+247 			];
+248 		} else if(xq.Browser.isUbuntu) {
+249 			//  Ubunto FF
+250 			return [
+251 				{event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
+252 				{event:"ENTER", handler:"this.handleEnter(false, false)"},
+253 				{event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
+254 				{event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
+255 				{event:"TAB", handler:"this.handleTab()"},
+256 				{event:"Shift+TAB", handler:"this.handleShiftTab()"},
+257 				{event:"DELETE", handler:"this.handleDelete()"},
+258 				{event:"BACKSPACE", handler:"this.handleBackspace()"},
+259 			
+260 				{event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
+261 				{event:"Ctrl+I", handler:"this.handleEmphasis()"},
+262 				{event:"Ctrl+U", handler:"this.handleUnderline()"},
+263 				{event:"Ctrl+K", handler:"this.handleStrike()"},
+264 				{event:"Ctrl+Z", handler:"this.handleUndo()"},
+265 				{event:"Ctrl+Y", handler:"this.handleRedo()"}
+266 			];
+267 		} else {
+268 			// Win IE & FF
+269 			return [
+270 				{event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
+271 				{event:"ENTER", handler:"this.handleEnter(false, false)"},
+272 				{event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
+273 				{event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
+274 				{event:"TAB", handler:"this.handleTab()"},
+275 				{event:"Shift+TAB", handler:"this.handleShiftTab()"},
+276 				{event:"DELETE", handler:"this.handleDelete()"},
+277 				{event:"BACKSPACE", handler:"this.handleBackspace()"},
+278 			
+279 				{event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
+280 				{event:"Ctrl+I", handler:"this.handleEmphasis()"},
+281 				{event:"Ctrl+U", handler:"this.handleUnderline()"},
+282 				{event:"Ctrl+K", handler:"this.handleStrike()"},
+283 				{event:"Ctrl+Z", handler:"this.handleUndo()"},
+284 				{event:"Ctrl+Y", handler:"this.handleRedo()"}
+285 			];
+286 		}
+287 	},
+288 	
+289 	_getDefaultTemplateProcessors: function() {
+290 		return [
+291 			{
+292 				id:"predefinedKeywordProcessor",
+293 				handler:function(html) {
+294 					var today = Date.get();
+295 					var keywords = {
+296 						year: today.getFullYear(),
+297 						month: today.getMonth() + 1,
+298 						date: today.getDate(),
+299 						hour: today.getHours(),
+300 						min: today.getMinutes(),
+301 						sec: today.getSeconds()
+302 					};
+303 					
+304 					return html.replace(/\{xq:(year|month|date|hour|min|sec)\}/img, function(text, keyword) {
+305 						return keywords[keyword] || keyword;
+306 					});
+307 				}
+308 			}
+309 		];
+310 	},
+311 	
+312 	/**
+313 	 * Adds or replaces keyboard shortcut.
+314 	 *
+315 	 * @param {String} shortcut keymap expression like "CTRL+Space"
+316 	 * @param {Object} handler string or function to be evaluated or called
+317 	 */
+318 	addShortcut: function(shortcut, handler) {
+319 		this.config.shortcuts[shortcut] = {"event":new xq.Shortcut(shortcut), "handler":handler};
+320 	},
+321 	
+322 	/**
+323 	 * Adds several keyboard shortcuts at once.
+324 	 *
+325 	 * @param {Array} list of shortcuts. each element should have following structure: {event:"keymap expression", handler:handler}
+326 	 */
+327 	addShortcuts: function(list) {
+328 		list.each(function(shortcut) {
+329 			this.addShortcut(shortcut.event, shortcut.handler);
+330 		}.bind(this));
+331 	},
+332 
+333 	/**
+334 	 * Returns keyboard shortcut matches with given keymap expression.
+335 	 *
+336 	 * @param {String} shortcut keymap expression like "CTRL+Space"
+337 	 */
+338 	getShortcut: function(shortcut) {return this.config.shortcuts[shortcut];},
+339 
+340 	/**
+341 	 * Returns entire keyboard shortcuts' map
+342 	 */
+343 	getShortcuts: function() {return this.config.shortcuts;},
+344 	
+345 	/**
+346 	 * Remove keyboard shortcut matches with given keymap expression.
+347 	 *
+348 	 * @param {String} shortcut keymap expression like "CTRL+Space"
+349 	 */
+350 	removeShortcut: function(shortcut) {delete this.config.shortcuts[shortcut];},
+351 	
+352 	/**
+353 	 * Adds or replaces autocorrection handler.
+354 	 *
+355 	 * @param {String} id unique identifier
+356 	 * @param {Object} criteria regex pattern or function to be used as a criterion for match
+357 	 * @param {Object} handler string or function to be evaluated or called when criteria met
+358 	 */
+359 	addAutocorrection: function(id, criteria, handler) {
+360 		if(criteria.exec) {
+361 			var pattern = criteria;
+362 			criteria = function(text) {return text.match(pattern)};
+363 		}
+364 		this.config.autocorrections[id] = {"criteria":criteria, "handler":handler};
+365 	},
+366 	
+367 	/**
+368 	 * Adds several autocorrection handlers at once.
+369 	 *
+370 	 * @param {Array} list of autocorrection. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler}
+371 	 */
+372 	addAutocorrections: function(list) {
+373 		list.each(function(ac) {
+374 			this.addAutocorrection(ac.id, ac.criteria, ac.handler);
+375 		}.bind(this));
+376 	},
+377 	
+378 	/**
+379 	 * Returns autocorrection handler matches with given id
+380 	 *
+381 	 * @param {String} id unique identifier
+382 	 */
+383 	getAutocorrection: function(id) {return this.config.autocorrection[id];},
+384 	
+385 	/**
+386 	 * Returns entire autocorrections' map
+387 	 */
+388 	getAutocorrections: function() {return this.config.autocorrections;},
+389 	
+390 	/**
+391 	 * Removes autocorrection handler matches with given id
+392 	 *
+393 	 * @param {String} id unique identifier
+394 	 */
+395 	removeAutocorrection: function(id) {delete this.config.autocorrections[id];},
+396 	
+397 	/**
+398 	 * Adds or replaces autocompletion handler.
+399 	 *
+400 	 * @param {String} id unique identifier
+401 	 * @param {Object} criteria regex pattern or function to be used as a criterion for match
+402 	 * @param {Object} handler string or function to be evaluated or called when criteria met
+403 	 */
+404 	addAutocompletion: function(id, criteria, handler) {
+405 		if(criteria.exec) {
+406 			var pattern = criteria;
+407 			criteria = function(text) {
+408 				var m = pattern.exec(text);
+409 				return m ? m.index : -1;
+410 			};
+411 		}
+412 		this.config.autocompletions[id] = {"criteria":criteria, "handler":handler};
+413 	},
+414 	
+415 	/**
+416 	 * Adds several autocompletion handlers at once.
+417 	 *
+418 	 * @param {Array} list of autocompletion. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler}
+419 	 */
+420 	addAutocompletions: function(list) {
+421 		list.each(function(ac) {
+422 			this.addAutocompletion(ac.id, ac.criteria, ac.handler);
+423 		}.bind(this));
+424 	},
+425 	
+426 	/**
+427 	 * Returns autocompletion handler matches with given id
+428 	 *
+429 	 * @param {String} id unique identifier
+430 	 */
+431 	getAutocompletion: function(id) {return this.config.autocompletions[id];},
+432 	
+433 	/**
+434 	 * Returns entire autocompletions' map
+435 	 */
+436 	getAutocompletions: function() {return this.config.autocompletions;},
+437 	
+438 	/**
+439 	 * Removes autocompletion handler matches with given id
+440 	 *
+441 	 * @param {String} id unique identifier
+442 	 */
+443 	removeAutocompletion: function(id) {delete this.config.autocompletions[id];},
+444 	
+445 	/**
+446 	 * Adds or replaces template processor.
+447 	 *
+448 	 * @param {String} id unique identifier
+449 	 * @param {Object} handler string or function to be evaluated or called when template inserted
+450 	 */
+451 	addTemplateProcessor: function(id, handler) {
+452 		this.config.templateProcessors[id] = {"handler":handler};
+453 	},
+454 	
+455 	/**
+456 	 * Adds several template processors at once.
+457 	 *
+458 	 * @param {Array} list of template processors. Each element should have following structure: {id:"identifier", handler:handler}
+459 	 */
+460 	addTemplateProcessors: function(list) {
+461 		list.each(function(tp) {
+462 			this.addTemplateProcessor(tp.id, tp.handler);
+463 		}.bind(this));
+464 	},
+465 	
+466 	/**
+467 	 * Returns template processor matches with given id
+468 	 *
+469 	 * @param {String} id unique identifier
+470 	 */
+471 	getTemplateProcessor: function(id) {return this.config.templateProcessors[id];},
+472 
+473 	/**
+474 	 * Returns entire template processors' map
+475 	 */
+476 	getTemplateProcessors: function() {return this.config.templateProcessors;},
+477 
+478 	/**
+479 	 * Removes template processor matches with given id
+480 	 *
+481 	 * @param {String} id unique identifier
+482 	 */
+483 	removeTemplateProcessor: function(id) {delete this.config.templateProcessors[id];},
+484 
+485 
+486 
+487 	/**
+488 	 * Adds or replaces context menu handler.
+489 	 *
+490 	 * @param {String} id unique identifier
+491 	 * @param {Object} handler string or function to be evaluated or called when onContextMenu occured
+492 	 */
+493 	addContextMenuHandler: function(id, handler) {
+494 		this.config.contextMenuHandlers[id] = {"handler":handler};
+495 	},
+496 	
+497 	/**
+498 	 * Adds several context menu handlers at once.
+499 	 *
+500 	 * @param {Array} list of handlers. Each element should have following structure: {id:"identifier", handler:handler}
+501 	 */
+502 	addContextMenuHandlers: function(list) {
+503 		list.each(function(mh) {
+504 			this.addContextMenuHandler(mh.id, mh.handler);
+505 		}.bind(this));
+506 	},
+507 	
+508 	/**
+509 	 * Returns context menu handler matches with given id
+510 	 *
+511 	 * @param {String} id unique identifier
+512 	 */
+513 	getContextMenuHandler: function(id) {return this.config.contextMenuHandlers[id];},
+514 
+515 	/**
+516 	 * Returns entire context menu handlers' map
+517 	 */
+518 	getContextMenuHandlers: function() {return this.config.contextMenuHandlers;},
+519 
+520 	/**
+521 	 * Removes context menu handler matches with given id
+522 	 *
+523 	 * @param {String} id unique identifier
+524 	 */
+525 	removeContextMenuHandler: function(id) {delete this.config.contextMenuHandlers[id];},
+526 	
+527 	
+528 	
+529 	/////////////////////////////////////////////
+530 	// Edit mode management
+531 	
+532 	/**
+533 	 * Returns current edit mode - readonly, wysiwyg, source
+534 	 */
+535 	getCurrentEditMode: function() {
+536 		return this.currentEditMode;
+537 	},
+538 	
+539 	toggleSourceAndWysiwygMode: function() {
+540 		var mode = this.getCurrentEditMode();
+541 		if(mode == 'readonly') return;
+542 		this.setEditMode(mode == 'wysiwyg' ? 'source' : 'wysiwyg');
+543 		
+544 		return true;
+545 	},
+546 	
+547 	/**
+548 	 * Switches between edit-mode/normal mode.
+549 	 *
+550 	 * @param {Object} mode false or 'readonly' means read-only mode, true or 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode.
+551 	 */
+552 	setEditMode: function(mode) {
+553 		if(this.currentEditMode == mode) return;
+554 		
+555 		var firstCall = mode != false && mode != 'readonly' && !this.outmostWrapper;
+556 		if(firstCall) {
+557 			// Create editor element if needed
+558 			this._createEditorFrame();
+559 			this._registerEventHandlers();
+560 			
+561 			this.loadCurrentContentFromStaticContent();
+562 			this.editHistory = new xq.EditHistory(this.rdom);
+563 		}
+564 		
+565 		if(mode == 'wysiwyg') {
+566 			// Update contents
+567 			if(this.currentEditMode == 'source') this.setStaticContent(this.getSourceContent());
+568 			this.loadCurrentContentFromStaticContent();
+569 			
+570 			// Make static content invisible
+571 			this.contentElement.style.display = "none";
+572 			
+573 			// Make WYSIWYG editor visible
+574 			this.sourceEditorDiv.style.display = "none";
+575 			this.wysiwygEditorDiv.style.display = "block";
+576 			this.outmostWrapper.style.display = "block";
+577 			
+578 			this.currentEditMode = mode;
+579 			
+580 			if(!xq.Browser.isTrident) {
+581 				window.setTimeout(function() {
+582 					if(this.getDoc().designMode == 'On') return;
+583 					
+584 					// Without it, Firefox doesn't display embedded SWF
+585 					this.getDoc().designMode = 'On';
+586 					
+587 					// turn off Firefox's table editing feature
+588 					try {this.getDoc().execCommand("enableInlineTableEditing", false, "false")} catch(ignored) {}
+589 				}.bind(this), 0);
+590 			}
+591 			
+592 			this.enableToolbarButtons();
+593 			if(!firstCall) this.focus();
+594 		} else if(mode == 'source') {
+595 			// Update contents
+596 			if(this.currentEditMode == 'wysiwyg') this.setStaticContent(this.getWysiwygContent());
+597 			this.loadCurrentContentFromStaticContent();
+598 			
+599 			// Make static content invisible
+600 			this.contentElement.style.display = "none";
+601 			
+602 			// Make source editor visible
+603 			this.sourceEditorDiv.style.display = "block";
+604 			this.wysiwygEditorDiv.style.display = "none";
+605 			this.outmostWrapper.style.display = "block";
+606 			
+607 			this.currentEditMode = mode;
+608 
+609 			this.disableToolbarButtons(['html']);
+610 			if(!firstCall) this.focus();
+611 		} else {
+612 			// Update contents
+613 			this.setStaticContent(this.getCurrentContent());
+614 			this.loadCurrentContentFromStaticContent();
+615 
+616 			// Make editor and toolbar invisible
+617 			this.outmostWrapper.style.display = "none";
+618 			
+619 			// Make static content visible
+620 			this.contentElement.style.display = "block";
+621 			
+622 			this.currentEditMode = mode;
+623 		}
+624 		
+625 		this._fireOnCurrentEditModeChanged(this, mode);
+626 	},
+627 	
+628 	/**
+629 	 * Load CSS into editing-mode document
+630 	 *
+631 	 * @param {string} path URL
+632 	 */
+633 	loadStylesheet: function(path) {
+634 		var head = this.editorDoc.getElementsByTagName("HEAD")[0];
+635 		var link = this.editorDoc.createElement("LINK");
+636 		link.rel = "Stylesheet";
+637 		link.type = "text/css";
+638 		link.href = path;
+639 		head.appendChild(link);
+640 	},
+641 	
+642 	/**
+643 	 * Sets editor's dynamic content from static content
+644 	 */
+645 	loadCurrentContentFromStaticContent: function() {
+646 		// update WYSIWYG editor
+647 		var html = this.validator.invalidate(this.getStaticContentAsDOM());
+648 		html = this.removeUnnecessarySpaces(html);
+649 		
+650 		if(html.blank()) {
+651 			this.rdom.clearRoot();
+652 		} else {
+653 			this.rdom.getRoot().innerHTML = html;
+654 		}
+655 		this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true);
+656 		
+657 		// update source editor
+658 		var source = this.getWysiwygContent(true, true);
+659 		
+660 		this.sourceEditorTextarea.value = source;
+661 		if(xq.Browser.isWebkit) {
+662 			this.sourceEditorTextarea.innerHTML = source;
+663 		}
+664 		
+665 		this._fireOnCurrentContentChanged(this);
+666 	},
+667 	
+668 	/**
+669 	 * Enables all toolbar buttons
+670 	 *
+671 	 * @param {Array} [exceptions] array of string containing classnames to exclude
+672 	 */
+673 	enableToolbarButtons: function(exceptions) {
+674 		if(!this.toolbarContainer) return;
+675 		
+676 		this._execForAllToolbarButtons(exceptions, function(li, exception) {
+677 			li.firstChild.className = !exception ? '' : 'disabled';
+678 		});
+679 		
+680 		// Toolbar image icon disappears without following code:
+681 		if(xq.Browser.isIE6) {
+682 			this.toolbarContainer.style.display = 'none';
+683 			setTimeout(function() {this.toolbarContainer.style.display = 'block';}.bind(this), 0);
+684 		}
+685 	},
+686 	
+687 	/**
+688 	 * Disables all toolbar buttons
+689 	 *
+690 	 * @param {Array} [exceptions] array of string containing classnames to exclude
+691 	 */
+692 	disableToolbarButtons: function(exceptions) {
+693 		this._execForAllToolbarButtons(exceptions, function(li, exception) {
+694 			li.firstChild.className = exception ? '' : 'disabled';
+695 		});
+696 	},
+697 	
+698 	_execForAllToolbarButtons: function(exceptions, exec) {
+699 		if(!this.toolbarContainer) return;
+700 		exceptions = exceptions || [];
+701 		
+702 		$(this.toolbarContainer).select('li').each(function(li) {
+703 			var buttonsClassName = li.classNames().find(function(name) {return name != 'xq_separator'});
+704 			var exception = exceptions.include(buttonsClassName);
+705 			exec(li, exception);
+706 		});
+707 	},
+708 
+709 	_updateToolbarButtonStatus: function(buttonClassName, selected) {
+710 		var button = this.toolbarButtons.get(buttonClassName);
+711 		if(button) button.firstChild.firstChild.className = selected ? 'selected' : '';
+712 	},
+713 	
+714 	updateAllToolbarButtonsStatus: function(element) {
+715 		if(!this.toolbarContainer) return;
+716 		if(!this.toolbarButtons) {
+717 			var classNames = [
+718 				"emphasis", "strongEmphasis", "underline", "strike", "superscription", "subscription",
+719 				"justifyLeft", "justifyCenter", "justifyRight", "justifyBoth",
+720 				"unorderedList", "orderedList", "code",
+721 				"paragraph", "heading1", "heading2", "heading3", "heading4", "heading5", "heading6"
+722 			];
+723 			
+724 			this.toolbarButtons = $H({});
+725 			
+726 			classNames.each(function(className) {
+727 				var found = $(this.toolbarContainer).getElementsBySelector("." + className);
+728 				var button = found && found.length > 0 ? found[0] : null;
+729 				if(button) this.toolbarButtons.set(className, button);
+730 			}.bind(this));
+731 		}
+732 		
+733 		var buttons = this.toolbarButtons;
+734 		
+735 		var info = this.rdom.collectStructureAndStyle(element);
+736 		
+737 		this._updateToolbarButtonStatus('emphasis', info.em);
+738 		this._updateToolbarButtonStatus('strongEmphasis', info.strong);
+739 		this._updateToolbarButtonStatus('underline', info.underline);
+740 		this._updateToolbarButtonStatus('strike', info.strike);
+741 		this._updateToolbarButtonStatus('superscription', info.superscription);
+742 		this._updateToolbarButtonStatus('subscription', info.subscription);
+743 		
+744 		this._updateToolbarButtonStatus('justifyLeft', info.justification == 'left');
+745 		this._updateToolbarButtonStatus('justifyCenter', info.justification == 'center');
+746 		this._updateToolbarButtonStatus('justifyRight', info.justification == 'right');
+747 		this._updateToolbarButtonStatus('justifyBoth', info.justification == 'justify');
+748 		
+749 		this._updateToolbarButtonStatus('orderedList', info.list == 'OL');
+750 		this._updateToolbarButtonStatus('unorderedList', info.list == 'UL');
+751 		this._updateToolbarButtonStatus('code', info.list == 'CODE');
+752 		
+753 		this._updateToolbarButtonStatus('paragraph', info.block == 'P');
+754 		this._updateToolbarButtonStatus('heading1', info.block == 'H1');
+755 		this._updateToolbarButtonStatus('heading2', info.block == 'H2');
+756 		this._updateToolbarButtonStatus('heading3', info.block == 'H3');
+757 		this._updateToolbarButtonStatus('heading4', info.block == 'H4');
+758 		this._updateToolbarButtonStatus('heading5', info.block == 'H5');
+759 		this._updateToolbarButtonStatus('heading6', info.block == 'H6');
+760 	},
+761 	
+762 	removeUnnecessarySpaces: function(html) {
+763 		var blocks = this.rdom.tree.getBlockTags().join("|");
+764 		var regex = new RegExp("\\s*<(/?)(" + blocks + ")>\\s*", "img");
+765 		return html.replace(regex, '<$1$2>');
+766 	},
+767 	
+768 	/**
+769 	 * Gets editor's dynamic content from current editor(source or WYSIWYG)
+770 	 * 
+771 	 * @return {Object} HTML String
+772 	 */
+773 	getCurrentContent: function(performFullValidation) {
+774 		if(this.getCurrentEditMode() == 'source') {
+775 			return this.getSourceContent(performFullValidation);
+776 		} else {
+777 			return this.getWysiwygContent(performFullValidation);
+778 		}
+779 	},
+780 	
+781 	/**
+782 	 * Gets editor's dynamic content from WYSIWYG editor
+783 	 * 
+784 	 * @return {Object} HTML String
+785 	 */
+786 	getWysiwygContent: function(performFullValidation, dontUseCache) {
+787 		if(dontUseCache || !performFullValidation) return this.validator.validate(this.rdom.getRoot(), performFullValidation);
+788 		
+789 		var lastModified = this.editHistory.getLastModifiedDate();
+790 		if(this._lastModified != lastModified) {
+791 			this._validContentCache = this.validator.validate(this.rdom.getRoot(), performFullValidation);
+792 			this._lastModified = lastModified;
+793 		}
+794 		return this._validContentCache;
+795 	},
+796 	
+797 	/**
+798 	 * Gets editor's dynamic content from source editor
+799 	 * 
+800 	 * @return {Object} HTML String
+801 	 */
+802 	getSourceContent: function(performFullValidation) {
+803 		var raw = this.sourceEditorTextarea[xq.Browser.isWebkit ? 'innerHTML' : 'value'];
+804 		var tempDiv = document.createElement('div');
+805 		tempDiv.innerHTML = this.removeUnnecessarySpaces(raw);
+806 
+807 		var rdom = xq.RichDom.createInstance();
+808 		rdom.setRoot(document.body);
+809 		rdom.wrapAllInlineOrTextNodesAs("P", tempDiv, true);
+810 		
+811 		return this.validator.validate(tempDiv, performFullValidation);
+812 	},
+813 	
+814 	/**
+815 	 * Sets editor's original content
+816 	 *
+817 	 * @param {Object} content HTML String
+818 	 */
+819 	setStaticContent: function(content) {
+820 		if(this.contentElement.nodeName == 'TEXTAREA') {
+821 			this.contentElement.value = content;
+822 			if(xq.Browser.isWebkit) {
+823 				this.contentElement.innerHTML = content;
+824 			}
+825 		} else {
+826 			this.contentElement.innerHTML = content;
+827 		}
+828 		this._fireOnStaticContentChanged(this, content);
+829 	},
+830 	
+831 	/**
+832 	 * Gets editor's original content
+833 	 *
+834 	 * @return {Object} HTML String
+835 	 */
+836 	getStaticContent: function() {
+837 		var content;
+838 		if(this.contentElement.nodeName == 'TEXTAREA') {
+839 			content = this.contentElement[xq.Browser.isWebkit ? 'innerHTML' : 'value'];
+840 		} else {
+841 			content = this.contentElement.innerHTML;
+842 		}
+843 		return content;
+844 	},
+845 	
+846 	/**
+847 	 * Gets editor's original content as DOM node
+848 	 *
+849 	 * @return {Object} HTML String
+850 	 */
+851 	getStaticContentAsDOM: function() {
+852 		if(this.contentElement.nodeName == 'TEXTAREA') {
+853 			var div = this.doc.createElement('DIV');
+854 			div.innerHTML = this.contentElement[xq.Browser.isWebkit ? 'innerHTML' : 'value'];
+855 			return div;
+856 		} else {
+857 			return this.contentElement;
+858 		}
+859 	},
+860 	
+861 	/**
+862 	 * Gives focus to editor
+863 	 */
+864 	focus: function() {
+865 		if(this.getCurrentEditMode() == 'wysiwyg') {
+866 			this.rdom.focus();
+867 			window.setTimeout(function() {
+868 				this.updateAllToolbarButtonsStatus(this.rdom.getCurrentElement());
+869 			}.bind(this), 0);
+870 		} else if(this.getCurrentEditMode() == 'source') {
+871 			this.sourceEditorTextarea.focus();
+872 		}
+873 	},
+874 	
+875 	/**
+876 	 * Returns designmode iframe object
+877 	 */
+878 	getFrame: function() {
+879 		return this.editorFrame;
+880 	},
+881 	
+882 	/**
+883 	 * Returns designmode window object
+884 	 */
+885 	getWin: function() {
+886 		return this.editorWin;
+887 	},
+888 	
+889 	/**
+890 	 * Returns designmode document object
+891 	 */
+892 	getDoc: function() {
+893 		return this.editorDoc;
+894 	},
+895 	
+896 	/**
+897 	 * Returns outmost wrapper element
+898 	 */
+899 	getOutmostWrapper: function() {
+900 		return this.outmostWrapper;
+901 	},
+902 	
+903 	/**
+904 	 * Returns designmode body object
+905 	 */
+906 	getBody: function() {
+907 		return this.editorBody;
+908 	},
+909 	
+910 	_createEditorFrame: function() {
+911 		// create outer DIV
+912 		this.outmostWrapper = this.doc.createElement('div');
+913 		this.outmostWrapper.className = "xquared";
+914 		
+915 		this.contentElement.parentNode.insertBefore(this.outmostWrapper, this.contentElement);
+916 		
+917 		// create toolbar is needed
+918 		if(!this.toolbarContainer && this.config.generateDefaultToolbar) {
+919 			this.toolbarContainer = this._generateDefaultToolbar();
+920 			this.outmostWrapper.appendChild(this.toolbarContainer);
+921 		}
+922 		
+923 		// create source editor div
+924 		this.sourceEditorDiv = this.doc.createElement('div');
+925 		this.sourceEditorDiv.className = "editor source_editor"; //TODO: remove editor
+926 		this.sourceEditorDiv.style.display = "none";
+927 		this.outmostWrapper.appendChild(this.sourceEditorDiv);
+928 		
+929 		// create TEXTAREA for source editor
+930 		this.sourceEditorTextarea = this.doc.createElement('textarea');
+931 		this.sourceEditorDiv.appendChild(this.sourceEditorTextarea);
+932 		
+933 		// create WYSIWYG editor div
+934 		this.wysiwygEditorDiv = this.doc.createElement('div');
+935 		this.wysiwygEditorDiv.className = "editor wysiwyg_editor"; //TODO: remove editor
+936 		this.wysiwygEditorDiv.style.display = "none";
+937 		this.outmostWrapper.appendChild(this.wysiwygEditorDiv);
+938 		
+939 		// create designmode iframe for WYSIWYG editor
+940 		this.editorFrame = this.doc.createElement('iframe');
+941 		this.rdom.setAttributes(this.editorFrame, {
+942 			"frameBorder": "0",
+943 			"marginWidth": "0",
+944 			"marginHeight": "0",
+945 			"leftMargin": "0",
+946 			"topMargin": "0",
+947 			"allowTransparency": "true"
+948 		});
+949 		this.wysiwygEditorDiv.appendChild(this.editorFrame);
+950 		
+951 		var doc = this.editorFrame.contentWindow.document;
+952 		if(xq.Browser.isTrident) doc.designMode = 'On';
+953 		
+954 		doc.open();
+955 		doc.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">');
+956 		doc.write('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko">');
+957 		doc.write('<head>');
+958 		
+959 		// it is needed to force href of pasted content to be an absolute url
+960 		if(!xq.Browser.isTrident) doc.write('<base href="./" />');
+961 		
+962 		doc.write('<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />');
+963 		doc.write('<title>XQuared</title>');
+964 		if(this.config.changeCursorOnLink) doc.write('<style>.xed a {cursor: pointer !important;}</style>');
+965 		doc.write('</head>');
+966 		doc.write('<body><p>' + this.rdom.makePlaceHolderString() + '</p></body>');
+967 		doc.write('</html>');
+968 		doc.close();
+969 		
+970 		this.editorWin = this.editorFrame.contentWindow;
+971 		this.editorDoc = this.editorWin.document;
+972 		this.editorBody = this.editorDoc.body;
+973 		this.editorBody.className = "xed";
+974 		
+975 		// it is needed to fix IE6 horizontal scrollbar problem
+976 		if(xq.Browser.isIE6) {
+977 			this.editorDoc.documentElement.style.overflowY='auto';
+978 			this.editorDoc.documentElement.style.overflowX='hidden';
+979 		}
+980 		
+981 		this.rdom.setWin(this.editorWin);
+982 		this.rdom.setRoot(this.editorBody);
+983 		this.validator = xq.Validator.createInstance(this.doc.location.href, this.config.urlValidationMode, this.config.allowedTags, this.config.allowedAttributes);
+984 		
+985 		// hook onsubmit of form
+986 		if(this.config.automaticallyHookSubmitEvent && this.contentElement.nodeName == 'TEXTAREA' && this.contentElement.form) {
+987 			var original = this.contentElement.form.onsubmit;
+988 			
+989 			this.contentElement.form.onsubmit = function() {
+990 				this.contentElement.value = this.getCurrentContent(true);
+991 				if(original) {
+992 					return original();
+993 				} else {
+994 					return true;
+995 				}
+996 			}.bind(this);
+997 		}
+998 	},
+999 	
+1000 	_addStyleRule: function(selector, rule) {
+1001 		if(!this.dynamicStyle) {
+1002 			if(xq.Browser.isTrident) {
+1003 			    this.dynamicStyle = this.doc.createStyleSheet();
+1004 			} else {
+1005 	    		var style = this.doc.createElement('style');
+1006 	    		this.doc.body.appendChild(style);
+1007 		    	this.dynamicStyle = $A(this.doc.styleSheets).last();
+1008 			}
+1009 		}
+1010 		
+1011 		if(xq.Browser.isTrident) {
+1012 			this.dynamicStyle.addRule(selector, rule);
+1013 		} else {
+1014 	    	this.dynamicStyle.insertRule(selector + " {" + rule + "}", this.dynamicStyle.cssRules.length);
+1015     	}
+1016 	},
+1017 	
+1018 	_generateDefaultToolbar: function() {
+1019 		// override image path
+1020 		this._addStyleRule(".xquared div.toolbar", "background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarBg.gif)");
+1021 		this._addStyleRule(".xquared ul.buttons li", "background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarButtonBg.gif)");
+1022 		this._addStyleRule(".xquared ul.buttons li.xq_separator", "background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarSeparator.gif)");
+1023 		
+1024 		// outmost container
+1025 		var container = this.doc.createElement('div');
+1026 		container.className = 'toolbar';
+1027 		
+1028 		// button container
+1029 		var buttons = this.doc.createElement('ul');
+1030 		buttons.className = 'buttons';
+1031 		container.appendChild(buttons);
+1032 		
+1033 		// Generate buttons from map and append it to button container
+1034 		var cancelMousedown = function(e) {Event.stop(e); return false};
+1035 		var map = this.config.defaultToolbarButtonMap;
+1036 		for(var i = 0; i < map.length; i++) {
+1037 			for(var j = 0; j < map[i].length; j++) {
+1038 				var buttonConfig = map[i][j];
+1039 
+1040 				var li = this.doc.createElement('li');
+1041 				buttons.appendChild(li);
+1042 				li.className = buttonConfig.className;
+1043 				
+1044 				var span = this.doc.createElement('span');
+1045 				li.appendChild(span);
+1046 				
+1047 				var a = this.doc.createElement('a');
+1048 				span.appendChild(a);
+1049 				a.href = '#';
+1050 				a.title = buttonConfig.title;
+1051 				a.handler = buttonConfig.handler;
+1052 				a.xed = this;
+1053 				Event.observe(a, 'mousedown', cancelMousedown);
+1054 				Event.observe(a, 'click', function(e) {
+1055 					var xed = this.xed;
+1056 					
+1057 					if($(this.parentNode).hasClassName('disabled') || xed.toolbarContainer.hasClassName('disabled')) {
+1058 						Event.stop(e);
+1059 						return false;
+1060 					}
+1061 					
+1062 					if(xq.Browser.isTrident) xed.focus();
+1063 					
+1064 					var handler = this.handler;
+1065 					var stop = (typeof handler == "function") ? handler(xed) : eval(handler);
+1066 					if(stop) {
+1067 						Event.stop(e);
+1068 						return false;
+1069 					} else {
+1070 						return true;
+1071 					}
+1072 				}.bind(a));
+1073 				
+1074 				var img = this.doc.createElement('img');
+1075 				a.appendChild(img);
+1076 				img.src = this.config.imagePathForDefaultToobar + buttonConfig.className + '.gif';
+1077 
+1078 				if(j == 0 && i != 0) li.className += ' xq_separator';
+1079 			}
+1080 		}
+1081 		
+1082 		return container;
+1083 	},
+1084 	
+1085 	
+1086 	
+1087 	/////////////////////////////////////////////
+1088 	// Event Management
+1089 	
+1090 	_registerEventHandlers: function() {
+1091 		var events = ['keydown', 'click', 'keyup', 'mouseup', 'contextmenu', 'scroll'];
+1092 		
+1093 		if(xq.Browser.isTrident && this.config.changeCursorOnLink) events.push('mousemove');
+1094 		if(xq.Browser.isMac && xq.Browser.isGecko) events.push('keypress');
+1095 		
+1096 		for(var i = 0; i < events.length; i++) {
+1097 			Event.observe(this.getDoc(), events[i], this._handleEvent.bindAsEventListener(this));
+1098 		}
+1099 	},
+1100 	
+1101 	_handleEvent: function(e) {
+1102 		this._fireOnBeforeEvent(this, e);
+1103 		
+1104 		var stop = false;
+1105 		
+1106 		var modifiedByCorrection = false;
+1107 		
+1108 		if(e.type == 'mousemove' && this.config.changeCursorOnLink) {
+1109 			// Trident only
+1110 			var link = !!this.rdom.getParentElementOf(e.srcElement, ["A"]);
+1111 			if(this.editorBody.contentEditable != link && !this.rdom.hasSelection()) this.editorBody.contentEditable = !link;
+1112 		} else if(e.type == 'click' && e.button == 0 && this.config.enableLinkClick) {
+1113 			var a = this.rdom.getParentElementOf(e.target || e.srcElement, ["A"]);
+1114 			if(a) stop = this.handleClick(e, a);
+1115 		} else if(e.type == (xq.Browser.isMac && xq.Browser.isGecko ? "keypress" : "keydown")) {
+1116 			var undoPerformed = false;
+1117 			
+1118 			modifiedByCorrection = this.rdom.correctParagraph();
+1119 			for(var key in this.config.shortcuts) {
+1120 				if(!this.config.shortcuts[key].event.matches(e)) continue;
+1121 				
+1122 				var handler = this.config.shortcuts[key].handler;
+1123 				var xed = this;
+1124 				stop = (typeof handler == "function") ? handler(this) : eval(handler);
+1125 				
+1126 				if(key == "undo") undoPerformed = true;
+1127 			}
+1128 		} else if(["mouseup", "keyup"].include(e.type)) {
+1129 			modifiedByCorrection = this.rdom.correctParagraph();
+1130 		} else if(["contextmenu"].include(e.type)) {
+1131 			this._handleContextMenu(e);
+1132 		}
+1133 		
+1134 		if(stop) Event.stop(e);
+1135 		
+1136 		this._fireOnCurrentContentChanged(this);
+1137 		this._fireOnAfterEvent(this, e);
+1138 		
+1139 		if(!undoPerformed && !modifiedByCorrection) this.editHistory.onEvent(e);
+1140 		
+1141 		return !stop;
+1142 	},
+1143 
+1144 	/**
+1145 	 * TODO: remove dup with handleAutocompletion
+1146 	 */
+1147 	handleAutocorrection: function() {
+1148 		var block = this.rdom.getCurrentBlockElement();
+1149 		
+1150 		// TODO: use complete unescape algorithm
+1151 		var text = this.rdom.getInnerText(block).replace(/ /gi, " ");
+1152 		
+1153 		var acs = this.config.autocorrections;
+1154 		var performed = false;
+1155 		
+1156 		var stop = false;
+1157 		for(var key in acs) {
+1158 			var ac = acs[key];
+1159 			if(ac.criteria(text)) {
+1160 				try {
+1161 					this.editHistory.onCommand();
+1162 					this.editHistory.disable();
+1163 					if(typeof ac.handler == "String") {
+1164 						var xed = this;
+1165 						var rdom = this.rdom;
+1166 						eval(ac.handler);
+1167 					} else {
+1168 						stop = ac.handler(this, this.rdom, block, text);
+1169 					}
+1170 					this.editHistory.enable();
+1171 				} catch(ignored) {}
+1172 				
+1173 				block = this.rdom.getCurrentBlockElement();
+1174 				text = this.rdom.getInnerText(block);
+1175 				
+1176 				performed = true;
+1177 				if(stop) break;
+1178 			}
+1179 		}
+1180 		
+1181 		return stop;
+1182 	},
+1183 	
+1184 	/**
+1185 	 * TODO: remove dup with handleAutocorrection
+1186 	 */
+1187 	handleAutocompletion: function() {
+1188 		var acs = $H(this.config.autocompletions);
+1189 		if(acs.size() == 0) return;
+1190 
+1191 		if(this.rdom.hasSelection()) {
+1192 			var text = this.rdom.getSelectionAsText();
+1193 			this.rdom.deleteSelection();
+1194 			var wrapper = this.rdom.insertNode(this.rdom.createElement("SPAN"));
+1195 			wrapper.innerHTML = text;
+1196 			
+1197 			var marker = this.rdom.pushMarker();
+1198 
+1199 			var filtered = 
+1200 				acs.map(function(pair) {
+1201 					return [pair.key, pair.value.criteria(text)];
+1202 				}.bind(this)).findAll(function(elem) {
+1203 					return elem[1] != -1;
+1204 				}).sortBy(function(elem) {
+1205 					return elem[1];
+1206 				});
+1207 			
+1208 			if(filtered.length == 0) {
+1209 				this.rdom.popMarker(true);
+1210 				return;
+1211 			}
+1212 			var ac = acs.get(filtered[0][0]);
+1213 			
+1214 			this.editHistory.disable();
+1215 		} else {
+1216 			var marker = this.rdom.pushMarker();
+1217 			
+1218 			var filtered = 
+1219 				acs.map(function(pair) {
+1220 					return [pair.key, this.rdom.testSmartWrap(marker, pair.value.criteria).textIndex];
+1221 				}.bind(this)).findAll(function(elem) {
+1222 					return elem[1] != -1;
+1223 				}).sortBy(function(elem) {
+1224 					return elem[1];
+1225 				});
+1226 			
+1227 			if(filtered.length == 0) {
+1228 				this.rdom.popMarker(true);
+1229 				return;
+1230 			}
+1231 			
+1232 			var ac = acs.get(filtered[0][0]);
+1233 			
+1234 			this.editHistory.disable();
+1235 			
+1236 			var wrapper = this.rdom.smartWrap(marker, "SPAN", ac.criteria);
+1237 		}
+1238 		
+1239 		var block = this.rdom.getCurrentBlockElement();
+1240 		
+1241 		// TODO: use complete unescape algorithm
+1242 		var text = this.rdom.getInnerText(wrapper).replace(/ /gi, " ");
+1243 		
+1244 		try {
+1245 			// call handler
+1246 			if(typeof ac.handler == "String") {
+1247 				var xed = this;
+1248 				var rdom = this.rdom;
+1249 				eval(ac.handler);
+1250 			} else {
+1251 				ac.handler(this, this.rdom, block, wrapper, text);
+1252 			}
+1253 		} catch(ignored) {}
+1254 		
+1255 		try {
+1256 			this.rdom.unwrapElement(wrapper);
+1257 		} catch(ignored) {}
+1258 
+1259 		
+1260 		if(this.rdom.isEmptyBlock(block)) this.rdom.correctEmptyElement(block);
+1261 		
+1262 		this.editHistory.enable();
+1263 		this.editHistory.onCommand();
+1264 		
+1265 		this.rdom.popMarker(true);
+1266 	},
+1267 
+1268 	/**
+1269 	 * Handles click event
+1270 	 *
+1271 	 * @param {Event} e click event
+1272 	 * @param {Element} target target element(usually has A tag)
+1273 	 */
+1274 	handleClick: function(e, target) {
+1275 		var href = decodeURI(target.href);
+1276 		if(!xq.Browser.isTrident) {
+1277 			if(!e.ctrlKey && !e.shiftKey && e.button != 1) {
+1278 				window.location.href = href;
+1279 				return true;
+1280 			}
+1281 		} else {
+1282 			if(e.shiftKey) {
+1283 				window.open(href, "_blank");
+1284 			} else {
+1285 				window.location.href = href;
+1286 			}
+1287 			return true;
+1288 		}
+1289 		
+1290 		return false;
+1291 	},
+1292 
+1293 	/**
+1294 	 * Show link dialog
+1295 	 *
+1296 	 * TODO: should support modify/unlink
+1297 	 */
+1298 	handleLink: function() {
+1299 		var text = this.rdom.getSelectionAsText() || '';
+1300 		var dialog = new xq.controls.FormDialog(
+1301 			this,
+1302 			xq.ui_templates.basicLinkDialog,
+1303 			function(dialog) {
+1304 				if(text) {
+1305 					dialog.form.text.value = text;
+1306 					dialog.form.url.focus();
+1307 					dialog.form.url.select();
+1308 				}
+1309 			},
+1310 			function(data) {
+1311 				this.focus();
+1312 				
+1313 				if(xq.Browser.isTrident) {
+1314 					var rng = this.rdom.rng();
+1315 					rng.moveToBookmark(bm);
+1316 					rng.select();
+1317 				}
+1318 				
+1319 				if(!data) return;
+1320 				this.handleInsertLink(false, data.url, data.text, data.text);
+1321 			}.bind(this)
+1322 		);
+1323 		
+1324 		if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
+1325 		
+1326 		dialog.show({position: 'centerOfEditor'});
+1327 		
+1328 		return true;
+1329 	},
+1330 	
+1331 	/**
+1332 	 * Inserts link or apply link into selected area
+1333 	 * 
+1334 	 * @param {boolean} autoSelection if set true and there's no selection, automatically select word to link(if possible)
+1335 	 * @param {String} url url
+1336 	 * @param {String} title title of link
+1337 	 * @param {String} text text of link. If there's a selection(manually or automatically), it will be replaced with this text
+1338 	 *
+1339 	 * @returns {Element} created element
+1340 	 */
+1341 	handleInsertLink: function(autoSelection, url, title, text) {
+1342 		if(autoSelection && !this.rdom.hasSelection()) {
+1343 			var marker = this.rdom.pushMarker();
+1344 			var a = this.rdom.smartWrap(marker, "A", function(text) {
+1345 				var index = text.lastIndexOf(" ");
+1346 				return index == -1 ? index : index + 1;
+1347 			});
+1348 			a.href = url;
+1349 			a.title = title;
+1350 			if(text) {
+1351 				a.innerHTML = ""
+1352 				a.appendChild(this.rdom.createTextNode(text));
+1353 			} else if(!a.hasChildNodes()) {
+1354 				this.rdom.deleteNode(a);
+1355 			}
+1356 			this.rdom.popMarker(true);
+1357 		} else {
+1358 			text = text || (this.rdom.hasSelection() ? this.rdom.getSelectionAsText() : null);
+1359 			if(!text) return;
+1360 			
+1361 			this.rdom.deleteSelection();
+1362 			
+1363 			var a = this.rdom.createElement('A');
+1364 			a.href = url;
+1365 			a.title = title;
+1366 			a.appendChild(this.rdom.createTextNode(text));
+1367 			this.rdom.insertNode(a);
+1368 		}
+1369 		
+1370 		var historyAdded = this.editHistory.onCommand();
+1371 		this._fireOnCurrentContentChanged(this);
+1372 		
+1373 		return true;
+1374 	},
+1375 	
+1376 	/**
+1377 	 * Called when enter key pressed.
+1378 	 *
+1379 	 * @param {boolean} skipAutocorrection if set true, skips autocorrection
+1380 	 * @param {boolean} forceInsertParagraph if set true, inserts paragraph
+1381 	 */
+1382 	handleEnter: function(skipAutocorrection, forceInsertParagraph) {
+1383 		// If it has selection, perform default action.
+1384 		if(this.rdom.hasSelection()) return false;
+1385 		
+1386 		// Perform autocorrection
+1387 		if(!skipAutocorrection && this.handleAutocorrection()) return true;
+1388 		
+1389 		var atEmptyBlock = this.rdom.isCaretAtEmptyBlock();
+1390 		var atStart = atEmptyBlock || this.rdom.isCaretAtBlockStart();
+1391 		var atEnd = atEmptyBlock || (!atStart && this.rdom.isCaretAtBlockEnd());
+1392 		var atEdge = atEmptyBlock || atStart || atEnd;
+1393 		
+1394 		if(!atEdge) {
+1395 			var block = this.rdom.getCurrentBlockElement();
+1396 			var marker = this.rdom.pushMarker();
+1397 			
+1398 			if(this.rdom.isFirstLiWithNestedList(block) && !forceInsertParagraph) {
+1399 				var parent = block.parentNode;
+1400 				this.rdom.unwrapElement(block);
+1401 				block = parent;
+1402 			} else if(block.nodeName != "LI" && this.rdom.tree.isBlockContainer(block)) {
+1403 				block = this.rdom.wrapAllInlineOrTextNodesAs("P", block, true).first();
+1404 			}
+1405 			this.rdom.splitElementUpto(marker, block);
+1406 			
+1407 			this.rdom.popMarker(true);
+1408 		} else if(atEmptyBlock) {
+1409 			this._handleEnterAtEmptyBlock();
+1410 		} else {
+1411 			this._handleEnterAtEdge(atStart, forceInsertParagraph);
+1412 		}
+1413 		
+1414 		return true;
+1415 	},
+1416 	
+1417 	/**
+1418 	 * Moves current block upward or downward
+1419 	 *
+1420 	 * @param {boolean} up moves current block upward
+1421 	 */
+1422 	handleMoveBlock: function(up) {
+1423 		var block = this.rdom.moveBlock(this.rdom.getCurrentBlockElement(), up);
+1424 		if(block) {
+1425 			this.rdom.selectElement(block, false);
+1426 			block.scrollIntoView(false);
+1427 			
+1428 			var historyAdded = this.editHistory.onCommand();
+1429 			this._fireOnCurrentContentChanged(this);
+1430 		}
+1431 		return true;
+1432 	},
+1433 	
+1434 	/**
+1435 	 * Called when tab key pressed
+1436 	 */
+1437 	handleTab: function() {
+1438 		var hasSelection = this.rdom.hasSelection();
+1439 		var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]);
+1440 		
+1441 		if(hasSelection) {
+1442 			this.handleIndent();
+1443 		} else if (table && table.className == "datatable") {
+1444 			this.handleMoveToNextCell();
+1445 		} else if (this.rdom.isCaretAtBlockStart()) {
+1446 			this.handleIndent();
+1447 		} else {
+1448 			this.handleInsertTab();
+1449 		}
+1450 
+1451 		return true;
+1452 	},
+1453 	
+1454 	/**
+1455 	 * Called when shift+tab key pressed
+1456 	 */
+1457 	handleShiftTab: function() {
+1458 		var hasSelection = this.rdom.hasSelection();
+1459 		var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]);
+1460 		
+1461 		if(hasSelection) {
+1462 			this.handleOutdent();
+1463 		} else if (table && table.className == "datatable") {
+1464 			this.handleMoveToPreviousCell();
+1465 		} else {
+1466 			this.handleOutdent();
+1467 		}
+1468 		
+1469 		return true;
+1470 	},
+1471 	
+1472 	/**
+1473 	 * Inserts three non-breaking spaces
+1474 	 */
+1475 	handleInsertTab: function() {
+1476 		this.rdom.insertHtml(' ');
+1477 		this.rdom.insertHtml(' ');
+1478 		this.rdom.insertHtml(' ');
+1479 		
+1480 		return true;
+1481 	},
+1482 	
+1483 	/**
+1484 	 * Called when delete key pressed
+1485 	 */
+1486 	handleDelete: function() {
+1487 		if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockEnd()) return false;
+1488 		return this._handleMerge(true);
+1489 	},
+1490 	
+1491 	/**
+1492 	 * Called when backspace key pressed
+1493 	 */
+1494 	handleBackspace: function() {
+1495 		if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockStart()) return false;
+1496 		return this._handleMerge(false);
+1497 	},
+1498 	
+1499 	_handleMerge: function(withNext) {
+1500 		var block = this.rdom.getCurrentBlockElement();
+1501 		
+1502 		// save caret position;
+1503 		var marker = this.rdom.pushMarker();
+1504 		
+1505 		// perform merge
+1506 		var merged = this.rdom.mergeElement(block, withNext, withNext);
+1507 		if(!merged && !withNext) this.rdom.extractOutElementFromParent(block);
+1508 		
+1509 		// restore caret position
+1510 		this.rdom.popMarker(true);
+1511 		if(merged) this.rdom.correctEmptyElement(merged);
+1512 		
+1513 		var historyAdded = this.editHistory.onCommand();
+1514 		this._fireOnCurrentContentChanged(this);
+1515 		
+1516 		return !!merged;
+1517 	},
+1518 	
+1519 	/**
+1520 	 * (in table) Moves caret to the next cell
+1521 	 */
+1522 	handleMoveToNextCell: function() {
+1523 		this._handleMoveToCell("next");
+1524 	},
+1525 
+1526 	/**
+1527 	 * (in table) Moves caret to the previous cell
+1528 	 */
+1529 	handleMoveToPreviousCell: function() {
+1530 		this._handleMoveToCell("prev");
+1531 	},
+1532 
+1533 	/**
+1534 	 * (in table) Moves caret to the above cell
+1535 	 */
+1536 	handleMoveToAboveCell: function() {
+1537 		this._handleMoveToCell("above");
+1538 	},
+1539 
+1540 	/**
+1541 	 * (in table) Moves caret to the below cell
+1542 	 */
+1543 	handleMoveToBelowCell: function() {
+1544 		this._handleMoveToCell("below");
+1545 	},
+1546 
+1547 	_handleMoveToCell: function(dir) {
+1548 		var block = this.rdom.getCurrentBlockElement();
+1549 		var cell = this.rdom.getParentElementOf(block, ["TD", "TH"]);
+1550 		var table = this.rdom.getParentElementOf(cell, ["TABLE"]);
+1551 		var rtable = new xq.RichTable(this.rdom, table);
+1552 		var target = null;
+1553 		
+1554 		if(["next", "prev"].include(dir)) {
+1555 			var toNext = dir == "next";
+1556 			target = toNext ? rtable.getNextCellOf(cell) : rtable.getPreviousCellOf(cell);
+1557 		} else {
+1558 			var toBelow = dir == "below";
+1559 			target = toBelow ? rtable.getBelowCellOf(cell) : rtable.getAboveCellOf(cell);
+1560 		}
+1561 
+1562 		if(!target) {
+1563 			var finder = function(node) {return !['TD', 'TH'].include(node.nodeName) && this.tree.isBlock(node) && !this.tree.hasBlocks(node);}.bind(this.rdom);
+1564 			var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this.rdom);
+1565 			
+1566 			target = (toNext || toBelow) ? 
+1567 				this.rdom.tree.findForward(cell, finder, exitCondition) :
+1568 				this.rdom.tree.findBackward(table, finder, exitCondition);
+1569 		}
+1570 		
+1571 		if(target) this.rdom.placeCaretAtStartOf(target);
+1572 	},
+1573 	
+1574 	/**
+1575 	 * Applies STRONG tag
+1576 	 */
+1577 	handleStrongEmphasis: function() {
+1578 		this.rdom.applyStrongEmphasis();
+1579 		
+1580 		var historyAdded = this.editHistory.onCommand();
+1581 		this._fireOnCurrentContentChanged(this);
+1582 		
+1583 		return true;
+1584 	},
+1585 	
+1586 	/**
+1587 	 * Applies EM tag
+1588 	 */
+1589 	handleEmphasis: function() {
+1590 		this.rdom.applyEmphasis();
+1591 		
+1592 		var historyAdded = this.editHistory.onCommand();
+1593 		this._fireOnCurrentContentChanged(this);
+1594 		
+1595 		return true;
+1596 	},
+1597 	
+1598 	/**
+1599 	 * Applies EM.underline tag
+1600 	 */
+1601 	handleUnderline: function() {
+1602 		this.rdom.applyUnderline();
+1603 		
+1604 		var historyAdded = this.editHistory.onCommand();
+1605 		this._fireOnCurrentContentChanged(this);
+1606 		
+1607 		return true;
+1608 	},
+1609 	
+1610 	/**
+1611 	 * Applies SPAN.strike tag
+1612 	 */
+1613 	handleStrike: function() {
+1614 		this.rdom.applyStrike();
+1615 
+1616 		var historyAdded = this.editHistory.onCommand();
+1617 		this._fireOnCurrentContentChanged(this);
+1618 
+1619 		return true;
+1620 	},
+1621 	
+1622 	/**
+1623 	 * Removes all style
+1624 	 */
+1625 	handleRemoveFormat: function() {
+1626 		this.rdom.applyRemoveFormat();
+1627 
+1628 		var historyAdded = this.editHistory.onCommand();
+1629 		this._fireOnCurrentContentChanged(this);
+1630 
+1631 		return true;
+1632 	},
+1633 	
+1634 	/**
+1635 	 * Inserts table
+1636 	 *
+1637 	 * @param {Number} cols number of columns
+1638 	 * @param {Number} rows number of rows
+1639 	 * @param {String} headerPosition position of THs. "T" or "L" or "TL". "T" means top, "L" means left.
+1640 	 */
+1641 	handleTable: function(cols, rows, headerPositions) {
+1642 		var cur = this.rdom.getCurrentBlockElement();
+1643 		if(this.rdom.getParentElementOf(cur, ["TABLE"])) return true;
+1644 		
+1645 		var rtable = xq.RichTable.create(this.rdom, cols, rows, headerPositions);
+1646 		if(this.rdom.tree.isBlockContainer(cur)) {
+1647 			var wrappers = this.rdom.wrapAllInlineOrTextNodesAs("P", cur, true);
+1648 			cur = wrappers.last();
+1649 		}
+1650 		var tableDom = this.rdom.insertNodeAt(rtable.getDom(), cur, "after");
+1651 		this.rdom.placeCaretAtStartOf(rtable.getCellAt(0, 0));
+1652 		
+1653 		if(this.rdom.isEmptyBlock(cur)) this.rdom.deleteNode(cur, true);
+1654 		
+1655 		var historyAdded = this.editHistory.onCommand();
+1656 		this._fireOnCurrentContentChanged(this);
+1657 		
+1658 		return true;
+1659 	},
+1660 	
+1661 	handleInsertNewRowAt: function(where) {
+1662 		var cur = this.rdom.getCurrentBlockElement();
+1663 		var tr = this.rdom.getParentElementOf(cur, ["TR"]);
+1664 		if(!tr) return true;
+1665 		
+1666 		var table = this.rdom.getParentElementOf(tr, ["TABLE"]);
+1667 		var rtable = new xq.RichTable(this.rdom, table);
+1668 		var row = rtable.insertNewRowAt(tr, where);
+1669 		
+1670 		this.rdom.placeCaretAtStartOf(row.cells[0]);
+1671 		return true;
+1672 	},
+1673 	handleInsertNewColumnAt: function(where) {
+1674 		var cur = this.rdom.getCurrentBlockElement();
+1675 		var td = this.rdom.getParentElementOf(cur, ["TD"], true);
+1676 		if(!td) return true;
+1677 		
+1678 		var table = this.rdom.getParentElementOf(td, ["TABLE"]);
+1679 		var rtable = new xq.RichTable(this.rdom, table);
+1680 		rtable.insertNewCellAt(td, where);
+1681 		
+1682 		this.rdom.placeCaretAtStartOf(cur);
+1683 		return true;
+1684 	},
+1685 	
+1686 	handleDeleteRow: function() {
+1687 		var cur = this.rdom.getCurrentBlockElement();
+1688 		var tr = this.rdom.getParentElementOf(cur, ["TR"]);
+1689 		if(!tr) return true;
+1690 
+1691 		var table = this.rdom.getParentElementOf(tr, ["TABLE"]);
+1692 		var rtable = new xq.RichTable(this.rdom, table);
+1693 		var blockToMove = rtable.deleteRow(tr);
+1694 		
+1695 		this.rdom.placeCaretAtStartOf(blockToMove);
+1696 		return true;
+1697 	},
+1698 	
+1699 	handleDeleteColumn: function() {
+1700 		var cur = this.rdom.getCurrentBlockElement();
+1701 		var td = this.rdom.getParentElementOf(cur, ["TD"], true);
+1702 		if(!td) return true;
+1703 
+1704 		var table = this.rdom.getParentElementOf(td, ["TABLE"]);
+1705 		var rtable = new xq.RichTable(this.rdom, table);
+1706 		rtable.deleteCell(td);
+1707 
+1708 		return true;
+1709 	},
+1710 	
+1711 	/**
+1712 	 * Performs block indentation
+1713 	 */
+1714 	handleIndent: function() {
+1715 		if(this.rdom.hasSelection()) {
+1716 			var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
+1717 			if(blocks.first() != blocks.last()) {
+1718 				var affected = this.rdom.indentElements(blocks.first(), blocks.last());
+1719 				this.rdom.selectBlocksBetween(affected.first(), affected.last());
+1720 				
+1721 				var historyAdded = this.editHistory.onCommand();
+1722 				this._fireOnCurrentContentChanged(this);
+1723 				
+1724 				return true;
+1725 			}
+1726 		}
+1727 		
+1728 		var block = this.rdom.getCurrentBlockElement();
+1729 		var affected = this.rdom.indentElement(block);
+1730 		
+1731 		if(affected) {
+1732 			this.rdom.placeCaretAtStartOf(affected);
+1733 			
+1734 			var historyAdded = this.editHistory.onCommand();
+1735 			this._fireOnCurrentContentChanged(this);
+1736 		}
+1737 		
+1738 		return true;
+1739 	},
+1740 
+1741 	/**
+1742 	 * Performs block outdentation
+1743 	 */
+1744 	handleOutdent: function() {
+1745 		if(this.rdom.hasSelection()) {
+1746 			var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
+1747 			if(blocks.first() != blocks.last()) {
+1748 				var affected = this.rdom.outdentElements(blocks.first(), blocks.last());
+1749 				this.rdom.selectBlocksBetween(affected.first(), affected.last());
+1750 				
+1751 				var historyAdded = this.editHistory.onCommand();
+1752 				this._fireOnCurrentContentChanged(this);
+1753 				
+1754 				return true;
+1755 			}
+1756 		}
+1757 		
+1758 		var block = this.rdom.getCurrentBlockElement();
+1759 		var affected = this.rdom.outdentElement(block);
+1760 		
+1761 		if(affected) {
+1762 			this.rdom.placeCaretAtStartOf(affected);
+1763 
+1764 			var historyAdded = this.editHistory.onCommand();
+1765 			this._fireOnCurrentContentChanged(this);
+1766 		}
+1767 		
+1768 		return true;
+1769 	},
+1770 	
+1771 	/**
+1772 	 * Applies list.
+1773 	 *
+1774 	 * @param {String} type "UL" or "OL" or "CODE". CODE generates OL.code
+1775 	 */
+1776 	handleList: function(type) {
+1777 		if(this.rdom.hasSelection()) {
+1778 			var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
+1779 			if(blocks.first() != blocks.last()) {
+1780 				blocks = this.rdom.applyLists(blocks.first(), blocks.last(), type);
+1781 			} else {
+1782 				blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type);
+1783 			}
+1784 			this.rdom.selectBlocksBetween(blocks.first(), blocks.last());
+1785 		} else {
+1786 			var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type);
+1787 			this.rdom.placeCaretAtStartOf(block);
+1788 		}
+1789 		var historyAdded = this.editHistory.onCommand();
+1790 		this._fireOnCurrentContentChanged(this);
+1791 		
+1792 		return true;
+1793 	},
+1794 	
+1795 	/**
+1796 	 * Applies justification
+1797 	 *
+1798 	 * @param {String} dir "left", "center", "right" or "both"
+1799 	 */
+1800 	handleJustify: function(dir) {
+1801 		var block = this.rdom.getCurrentBlockElement();
+1802 		var dir = (dir == "left" || dir == "both") && (block.style.textAlign == "left" || block.style.textAlign == "") ? "both" : dir;
+1803 		
+1804 		if(this.rdom.hasSelection()) {
+1805 			var blocks = this.rdom.getSelectedBlockElements();
+1806 			this.rdom.justifyBlocks(blocks, dir);
+1807 			this.rdom.selectBlocksBetween(blocks.first(), blocks.last());
+1808 		} else {
+1809 			this.rdom.justifyBlock(block, dir);
+1810 		}
+1811 		var historyAdded = this.editHistory.onCommand();
+1812 		this._fireOnCurrentContentChanged(this);
+1813 		
+1814 		return true;
+1815 	},
+1816 	
+1817 	/**
+1818 	 * Removes current block element
+1819 	 */
+1820 	handleRemoveBlock: function() {
+1821 		var block = this.rdom.getCurrentBlockElement();
+1822 		var blockToMove = this.rdom.removeBlock(block);
+1823 		this.rdom.placeCaretAtStartOf(blockToMove);
+1824 		blockToMove.scrollIntoView(false);
+1825 	},
+1826 	
+1827 	/**
+1828 	 * Applies background color
+1829 	 *
+1830 	 * @param {String} color CSS color string
+1831 	 */
+1832 	handleBackgroundColor: function(color) {
+1833 		if(color) {
+1834 			this.rdom.applyBackgroundColor(color);
+1835 
+1836 			var historyAdded = this.editHistory.onCommand();
+1837 			this._fireOnCurrentContentChanged(this);
+1838 		} else {
+1839 			var dialog = new xq.controls.FormDialog(
+1840 				this,
+1841 				xq.ui_templates.basicColorPickerDialog,
+1842 				function(dialog) {},
+1843 				function(data) {
+1844 					this.focus();
+1845 					
+1846 					if(xq.Browser.isTrident) {
+1847 						var rng = this.rdom.rng();
+1848 						rng.moveToBookmark(bm);
+1849 						rng.select();
+1850 					}
+1851 					
+1852 					if(!data) return;
+1853 					
+1854 					this.handleBackgroundColor(data.color);
+1855 				}.bind(this)
+1856 			);
+1857 			
+1858 			if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
+1859 			
+1860 			dialog.show({position: 'centerOfEditor'});
+1861 		}
+1862 		return true;
+1863 	},
+1864 	
+1865 	/**
+1866 	 * Applies foreground color
+1867 	 *
+1868 	 * @param {String} color CSS color string
+1869 	 */
+1870 	handleForegroundColor: function(color) {
+1871 		if(color) {
+1872 			this.rdom.applyForegroundColor(color);
+1873 
+1874 			var historyAdded = this.editHistory.onCommand();
+1875 			this._fireOnCurrentContentChanged(this);
+1876 		} else {
+1877 			var dialog = new xq.controls.FormDialog(
+1878 				this,
+1879 				xq.ui_templates.basicColorPickerDialog,
+1880 				function(dialog) {},
+1881 				function(data) {
+1882 					this.focus();
+1883 					
+1884 					if(xq.Browser.isTrident) {
+1885 						var rng = this.rdom.rng();
+1886 						rng.moveToBookmark(bm);
+1887 						rng.select();
+1888 					}
+1889 					
+1890 					if(!data) return;
+1891 					
+1892 					this.handleForegroundColor(data.color);
+1893 				}.bind(this)
+1894 			);
+1895 			
+1896 			if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
+1897 			
+1898 			dialog.show({position: 'centerOfEditor'});
+1899 		}
+1900 		return true;
+1901 	},
+1902 
+1903 	/**
+1904 	 * Applies superscription
+1905 	 */	
+1906 	handleSuperscription: function() {
+1907 		this.rdom.applySuperscription();
+1908 
+1909 		var historyAdded = this.editHistory.onCommand();
+1910 		this._fireOnCurrentContentChanged(this);
+1911 
+1912 		return true;
+1913 	},
+1914 	
+1915 	/**
+1916 	 * Applies subscription
+1917 	 */	
+1918 	handleSubscription: function() {
+1919 		this.rdom.applySubscription();
+1920 
+1921 		var historyAdded = this.editHistory.onCommand();
+1922 		this._fireOnCurrentContentChanged(this);
+1923 
+1924 		return true;
+1925 	},
+1926 	
+1927 	/**
+1928 	 * Change of wrap current block's tag
+1929 	 */	
+1930 	handleApplyBlock: function(tagName) {
+1931 		if(this.rdom.hasSelection()) {
+1932 			var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
+1933 			if(blocks.first() != blocks.last()) {
+1934 				var applied = this.rdom.applyTagIntoElements(tagName, blocks.first(), blocks.last());
+1935 				this.rdom.selectBlocksBetween(applied.first(), applied.last());
+1936 				
+1937 				var historyAdded = this.editHistory.onCommand();
+1938 				this._fireOnCurrentContentChanged(this);
+1939 				
+1940 				return true;
+1941 			}
+1942 		}
+1943 		
+1944 		var block = this.rdom.getCurrentBlockElement();
+1945 		this.rdom.pushMarker();
+1946 		var applied =
+1947 			this.rdom.applyTagIntoElement(tagName, block) ||
+1948 			block;
+1949 		this.rdom.popMarker(true);
+1950 
+1951 		if(this.rdom.isEmptyBlock(applied)) {
+1952 			this.rdom.correctEmptyElement(applied);
+1953 			this.rdom.placeCaretAtStartOf(applied);
+1954 		}
+1955 		
+1956 		var historyAdded = this.editHistory.onCommand();
+1957 		this._fireOnCurrentContentChanged(this);
+1958 		
+1959 		return true;
+1960 	},
+1961 
+1962 	/**
+1963 	 * Inserts seperator (HR)
+1964 	 */
+1965 	handleSeparator: function() {
+1966 		this.rdom.collapseSelection();
+1967 		
+1968 		var curBlock = this.rdom.getCurrentBlockElement();
+1969 		var atStart = this.rdom.isCaretAtBlockStart();
+1970 		if(this.rdom.tree.isBlockContainer(curBlock)) curBlock = this.rdom.wrapAllInlineOrTextNodesAs("P", curBlock, true)[0];
+1971 		
+1972 		this.rdom.insertNodeAt(this.rdom.createElement("HR"), curBlock, atStart ? "before" : "after");
+1973 		this.rdom.placeCaretAtStartOf(curBlock);
+1974 
+1975 		// add undo history
+1976 		var historyAdded = this.editHistory.onCommand();
+1977 		this._fireOnCurrentContentChanged(this);
+1978 		
+1979 		return true;
+1980 	},
+1981 	
+1982 	/**
+1983 	 * Performs UNDO
+1984 	 */
+1985 	handleUndo: function() {
+1986 		var performed = this.editHistory.undo();
+1987 		this._fireOnCurrentContentChanged(this);
+1988 		
+1989 		var curBlock = this.rdom.getCurrentBlockElement();
+1990 		if(!xq.Browser.isTrident && curBlock) {
+1991 			curBlock.scrollIntoView(false);
+1992 		}
+1993 		return true;
+1994 	},
+1995 	
+1996 	/**
+1997 	 * Performs REDO
+1998 	 */
+1999 	handleRedo: function() {
+2000 		var performed = this.editHistory.redo();
+2001 		this._fireOnCurrentContentChanged(this);
+2002 		
+2003 		var curBlock = this.rdom.getCurrentBlockElement();
+2004 		if(!xq.Browser.isTrident && curBlock) {
+2005 			curBlock.scrollIntoView(false);
+2006 		}
+2007 		return true;
+2008 	},
+2009 	
+2010 	
+2011 	
+2012 	_handleContextMenu: function(e) {
+2013 		if (xq.Browser.isWebkit) {
+2014 			if (e.metaKey || Event.isLeftClick(e)) return false;
+2015 		} else if (e.shiftKey || e.ctrlKey || e.altKey) {
+2016 			return false;
+2017 		}
+2018 		
+2019 		var x=Event.pointerX(e);
+2020 		var y=Event.pointerY(e);
+2021 		var pos=Position.cumulativeOffset(this.getFrame());
+2022 		x+=pos[0];
+2023 		y+=pos[1];
+2024 		this._contextMenuTargetElement = e.target || e.srcElement;
+2025 		
+2026 		//TODO: Safari on Windows doesn't work with context key(app key)
+2027 		if (!x || !y || xq.Browser.isTrident) {
+2028 			var pos = Position.cumulativeOffset(this._contextMenuTargetElement);
+2029 			var posFrame = Position.cumulativeOffset(this.getFrame());
+2030 			x = pos[0] + posFrame[0] - this.getDoc().documentElement.scrollLeft;
+2031 			y = pos[1] + posFrame[1] - this.getDoc().documentElement.scrollTop;
+2032 		}
+2033 		
+2034 		if (!xq.Browser.isTrident) {
+2035 			var doc = this.getDoc();
+2036 			var body = this.getBody();
+2037 			
+2038 			x -= doc.documentElement.scrollLeft;
+2039 			y -= doc.documentElement.scrollTop;
+2040 			
+2041 			if (doc != body) {
+2042 				x -= body.scrollLeft;
+2043 				y -= body.scrollTop;
+2044 			}
+2045 		}
+2046 		
+2047 		for(var cmh in this.config.contextMenuHandlers) {
+2048 			var stop = this.config.contextMenuHandlers[cmh].handler(this, this._contextMenuTargetElement, x, y);
+2049 			if(stop) {
+2050 				Event.stop(e);
+2051 				return true;
+2052 			}
+2053 		}
+2054 		
+2055 		return false;
+2056 	},
+2057 	
+2058 	showContextMenu: function(menuItems, x, y) {
+2059 		if (!menuItems || menuItems.length <= 0) return;
+2060 		
+2061 		if (!this._contextMenuContainer) {
+2062 			this._contextMenuContainer = this.doc.createElement('UL');
+2063 			this._contextMenuContainer.className = 'xqContextMenu';
+2064 			this._contextMenuContainer.style.display='none';
+2065 			
+2066 			Event.observe(this.doc, 'click', this._contextMenuClicked.bindAsEventListener(this));
+2067 			Event.observe(this.rdom.getDoc(), 'click', this.hideContextMenu.bindAsEventListener(this));
+2068 			
+2069 			this.body.appendChild(this._contextMenuContainer);
+2070 		} else {
+2071 			while (this._contextMenuContainer.childNodes.length > 0)
+2072 				this._contextMenuContainer.removeChild(this._contextMenuContainer.childNodes[0]);
+2073 		}
+2074 		
+2075 		for (var i=0; i < menuItems.length; i++) {
+2076 			menuItems[i]._node = this._addContextMenuItem(menuItems[i]);
+2077 		}
+2078 
+2079 		this._contextMenuContainer.style.display='block';
+2080 		this._contextMenuContainer.style.left=Math.min(Math.max(this.doc.body.scrollWidth, this.doc.documentElement.clientWidth)-this._contextMenuContainer.offsetWidth, x)+'px';
+2081 		this._contextMenuContainer.style.top=Math.min(Math.max(this.doc.body.scrollHeight, this.doc.documentElement.clientHeight)-this._contextMenuContainer.offsetHeight, y)+'px';
+2082 
+2083 		this._contextMenuItems = menuItems;
+2084 	},
+2085 	
+2086 	hideContextMenu: function() {
+2087 		if (this._contextMenuContainer)
+2088 			this._contextMenuContainer.style.display='none';
+2089 	},
+2090 	
+2091 	_addContextMenuItem: function(item) {
+2092 		if (!this._contextMenuContainer) throw "No conext menu container exists";
+2093 		
+2094 		var node = this.doc.createElement('LI');
+2095 		if (item.disabled) node.className += ' disabled'; 
+2096 		
+2097 		if (item.title == '----') {
+2098 			node.innerHTML = ' ';
+2099 			node.className = 'separator';
+2100 		} else {
+2101 			if(item.handler) {
+2102 				node.innerHTML = '<a href="javascript:;" onclick="return false;">'+(item.title.toString().escapeHTML())+'</a>';
+2103 			} else {
+2104 				node.innerHTML = (item.title.toString().escapeHTML());
+2105 			}
+2106 		}
+2107 		
+2108 		if(item.className) node.className = item.className;
+2109 		
+2110 		this._contextMenuContainer.appendChild(node);
+2111 		
+2112 		return node;
+2113 	},
+2114 	
+2115 	_contextMenuClicked: function(e) {
+2116 		this.hideContextMenu();
+2117 		
+2118 		if (!this._contextMenuContainer) return;
+2119 		
+2120 		var node = Event.findElement(e, 'LI');
+2121 		if (!node || !this.rdom.tree.isDescendantOf(this._contextMenuContainer, node)) return;
+2122 
+2123 		for (var i=0; i < this._contextMenuItems.length; i++) {
+2124 			if (this._contextMenuItems[i]._node == node) {
+2125 				var handler = this._contextMenuItems[i].handler;
+2126 				if (!this._contextMenuItems[i].disabled && handler) {
+2127 					var xed = this;
+2128 					var element = this._contextMenuTargetElement;
+2129 					if(typeof handler == "function") {
+2130 						handler(xed, element);
+2131 					} else {
+2132 						eval(handler);
+2133 					}
+2134 				}
+2135 				break;
+2136 			}
+2137 		}
+2138 	},
+2139 	
+2140 	/**
+2141 	 * Inserts HTML template
+2142 	 *
+2143 	 * @param {String} html Template string. It should have single root element
+2144 	 * @returns {Element} inserted element
+2145 	 */
+2146 	insertTemplate: function(html) {
+2147 		return this.rdom.insertHtml(this._processTemplate(html));
+2148 	},
+2149 	
+2150 	/**
+2151 	 * Places given HTML template nearby target.
+2152 	 *
+2153 	 * @param {String} html Template string. It should have single root element
+2154 	 * @param {Node} target Target node.
+2155 	 * @param {String} where Possible values: "before", "start", "end", "after"
+2156 	 *
+2157 	 * @returns {Element} Inserted element.
+2158 	 */
+2159 	insertTemplateAt: function(html, target, where) {
+2160 		return this.rdom.insertHtmlAt(this._processTemplate(html), target, where);
+2161 	},
+2162 	
+2163 	_processTemplate: function(html) {
+2164 		// apply template processors
+2165 		var tps = $H(this.getTemplateProcessors()).values();
+2166 		for(var i = 0; i < tps.length; i++) {
+2167 			html = tps[i].handler(html);
+2168 		}
+2169 		
+2170 		// remove all whitespace characters between block tags
+2171 		return html = this.removeUnnecessarySpaces(html);
+2172 	},
+2173 	
+2174 	
+2175 	
+2176 	/** @private */
+2177 	_handleEnterAtEmptyBlock: function() {
+2178 		var block = this.rdom.getCurrentBlockElement();
+2179 		if(this.rdom.tree.isTableCell(block) && this.rdom.isFirstBlockOfBody(block)) {
+2180 			block = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start");
+2181 		} else {
+2182 			block = 
+2183 				this.rdom.outdentElement(block) ||
+2184 				this.rdom.extractOutElementFromParent(block) ||
+2185 				this.rdom.replaceTag("P", block) ||
+2186 				this.rdom.insertNewBlockAround(block);
+2187 		}
+2188 		
+2189 		this.rdom.placeCaretAtStartOf(block);
+2190 		if(!xq.Browser.isTrident) block.scrollIntoView(false);
+2191 	},
+2192 	
+2193 	/** @private */
+2194 	_handleEnterAtEdge: function(atStart, forceInsertParagraph) {
+2195 		var block = this.rdom.getCurrentBlockElement();
+2196 		var blockToPlaceCaret;
+2197 		
+2198 		if(atStart && this.rdom.isFirstBlockOfBody(block)) {
+2199 			blockToPlaceCaret = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start");
+2200 		} else {
+2201 			if(this.rdom.tree.isTableCell(block)) forceInsertParagraph = true;
+2202 			var newBlock = this.rdom.insertNewBlockAround(block, atStart, forceInsertParagraph ? "P" : null);
+2203 			blockToPlaceCaret = !atStart ? newBlock : newBlock.nextSibling;
+2204 		}
+2205 		
+2206 		this.rdom.placeCaretAtStartOf(blockToPlaceCaret);
+2207 		if(!xq.Browser.isTrident) blockToPlaceCaret.scrollIntoView(false);
+2208 	}
+2209 });
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_06.html b/modules/editor/skins/xquared/doc/api/src_06.html new file mode 100644 index 000000000..32b20a15b --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_06.html @@ -0,0 +1,2262 @@ +
  1 /**
+  2  * Encapsulates browser incompatibility problem and provides rich set of DOM manipulation API.
+  3  *
+  4  * RichDom provides basic CRUD + Advanced DOM manipulation API, various query methods and caret/selection management API
+  5  */
+  6 xq.RichDom = Class.create({
+  7 	/**
+  8 	 * Initialize RichDom. Target window and root element should be set after initialization. See setWin and setRoot.
+  9 	 *
+ 10      * @constructor
+ 11 	 */
+ 12 	initialize: function() {
+ 13 		/**
+ 14 		 * {xq.DomTree} instance of DomTree
+ 15 		 */
+ 16 		this.tree = new xq.DomTree();
+ 17 		
+ 18 		this._lastMarkerId = 0;
+ 19 	},
+ 20 	
+ 21 	
+ 22 	
+ 23 	/**
+ 24 	 * @param {Window} win Browser's window object
+ 25 	 */
+ 26 	setWin: function(win) {
+ 27 		if(!win) throw "[win] is null";
+ 28 		this.win = win;
+ 29 	},
+ 30 	
+ 31 	/**
+ 32 	 * @param {Element} root Root element
+ 33 	 */
+ 34 	setRoot: function(root) {
+ 35 		if(!root) throw "[root] is null";
+ 36 		if(this.win && (root.ownerDocument != this.win.document)) throw "root.ownerDocument != this.win.document";
+ 37 		this.root = root;
+ 38 		this.doc = this.root.ownerDocument;
+ 39 	},
+ 40 	
+ 41 	/**
+ 42 	 * @returns Browser's window object.
+ 43 	 */
+ 44 	getWin: function() {return this.win},
+ 45 	
+ 46 	/**
+ 47 	 * @returns Document object of root element.
+ 48 	 */
+ 49 	getDoc: function() {return this.doc},
+ 50 	
+ 51 	/**
+ 52 	 * @returns Root element.
+ 53 	 */
+ 54 	getRoot: function() {return this.root},
+ 55 	
+ 56 	
+ 57 	
+ 58 	/////////////////////////////////////////////
+ 59 	// CRUDs
+ 60 	
+ 61 	clearRoot: function() {
+ 62 		this.root.innerHTML = "";
+ 63 		this.root.appendChild(this.makeEmptyParagraph());
+ 64 	},
+ 65 	
+ 66 	/**
+ 67 	 * Removes place holders and empty text nodes of given element.
+ 68 	 *
+ 69 	 * @param {Element} element target element
+ 70 	 */
+ 71 	removePlaceHoldersAndEmptyNodes: function(element) {
+ 72 		var children = element.childNodes;
+ 73 		if(!children) return;
+ 74 		var stopAt = this.getBottommostLastChild(element);
+ 75 		if(!stopAt) return;
+ 76 		stopAt = this.tree.walkForward(stopAt);
+ 77 		
+ 78 		while(true) {
+ 79 			if(!element || element == stopAt) break;
+ 80 			
+ 81 			if(
+ 82 				this.isPlaceHolder(element) ||
+ 83 				(element.nodeType == 3 && element.nodeValue == "") ||
+ 84 				(!this.getNextSibling(element) && element.nodeType == 3 && element.nodeValue.strip() == "")
+ 85 			) {
+ 86 				var deleteTarget = element;
+ 87 				element = this.tree.walkForward(element);
+ 88 				
+ 89 				this.deleteNode(deleteTarget);
+ 90 			} else {
+ 91 				element = this.tree.walkForward(element);
+ 92 			}
+ 93 		}
+ 94 	},
+ 95 	
+ 96 	/**
+ 97 	 * Sets multiple attributes into element at once
+ 98 	 *
+ 99 	 * @param {Element} element target element
+100 	 * @param {Object} map key-value pairs
+101 	 */
+102 	setAttributes: function(element, map) {
+103 		for(key in map) element.setAttribute(key, map[key]);
+104 	},
+105 
+106 	/**
+107 	 * Creates textnode by given node value.
+108 	 *
+109 	 * @param {String} value value of textnode
+110 	 * @returns {Node} Created text node
+111 	 */	
+112 	createTextNode: function(value) {return this.doc.createTextNode(value);},
+113 
+114 	/**
+115 	 * Creates empty element by given tag name.
+116 	 *
+117 	 * @param {String} tagName name of tag
+118 	 * @returns {Element} Created element
+119 	 */	
+120 	createElement: function(tagName) {return this.doc.createElement(tagName);},
+121 
+122 	/**
+123 	 * Creates element from HTML string
+124 	 * 
+125 	 * @param {String} html HTML string
+126 	 * @returns {Element} Created element
+127 	 */
+128 	createElementFromHtml: function(html) {
+129 		var node = this.createElement("div");
+130 		node.innerHTML = html;
+131 		if(node.childNodes.length != 1) {
+132 			throw "Illegal HTML fragment";
+133 		}
+134 		return this.getFirstChild(node);
+135 	},
+136 	
+137 	/**
+138 	 * Deletes node from DOM tree.
+139 	 *
+140 	 * @param {Node} node Target node which should be deleted
+141 	 * @param {boolean} deleteEmptyParentsRecursively Recursively delete empty parent elements
+142 	 * @param {boolean} correctEmptyParent Call #correctEmptyElement on empty parent element after deletion
+143 	 */	
+144 	deleteNode: function(node, deleteEmptyParentsRecursively, correctEmptyParent) {
+145 		if(!node || !node.parentNode) return;
+146 		
+147 		var parent = node.parentNode;
+148 		parent.removeChild(node);
+149 		
+150 		if(deleteEmptyParentsRecursively) {
+151 			while(!parent.hasChildNodes()) {
+152 				node = parent;
+153 				parent = node.parentNode;
+154 				if(!parent || this.getRoot() == node) break;
+155 				parent.removeChild(node);
+156 			}
+157 		}
+158 		
+159 		if(correctEmptyParent && this.isEmptyBlock(parent)) {
+160 			parent.innerHTML = "";
+161 			this.correctEmptyElement(parent);
+162 		}
+163 	},
+164 
+165 	/**
+166 	 * Inserts given node into current caret position
+167 	 *
+168 	 * @param {Node} node Target node
+169 	 * @returns {Node} Inserted node. It could be different with given node.
+170 	 */
+171 	insertNode: function(node) {throw "Not implemented"},
+172 
+173 	/**
+174 	 * Inserts given html into current caret position
+175 	 *
+176 	 * @param {String} html HTML string
+177 	 * @returns {Node} Inserted node. It could be different with given node.
+178 	 */
+179 	insertHtml: function(html) {
+180 		return this.insertNode(this.createElementFromHtml(html));
+181 	},
+182 	
+183 	/**
+184 	 * Creates textnode from given text and inserts it into current caret position
+185 	 *
+186 	 * @param {String} text Value of textnode
+187 	 * @returns {Node} Inserted node
+188 	 */
+189 	insertText: function(text) {
+190 		this.insertNode(this.createTextNode(text));
+191 	},
+192 	
+193 	/**
+194 	 * Places given node nearby target.
+195 	 *
+196 	 * @param {Node} node Node to be inserted.
+197 	 * @param {Node} target Target node.
+198 	 * @param {String} where Possible values: "before", "start", "end", "after"
+199 	 * @param {boolean} performValidation Validate node if needed. For example when P placed into UL, its tag name automatically replaced with LI
+200 	 *
+201 	 * @returns {Node} Inserted node. It could be different with given node.
+202 	 */
+203 	insertNodeAt: function(node, target, where, performValidation) {
+204 		if(
+205 			["HTML", "HEAD"].include(target.nodeName) ||
+206 			["BODY"].include(target.nodeName) && ["before", "after"].include(where)
+207 		) throw "Illegal argument. Cannot move node[" + node.nodeName + "] to '" + where + "' of target[" + target.nodeName + "]"
+208 		
+209 		var object;
+210 		var message;
+211 		var secondParam;
+212 		
+213 		switch(where.toLowerCase()) {
+214 			case "before":
+215 				object = target.parentNode;
+216 				message = 'insertBefore';
+217 				secondParam = target;
+218 				break
+219 			case "start":
+220 				if(target.firstChild) {
+221 					object = target;
+222 					message = 'insertBefore';
+223 					secondParam = target.firstChild;
+224 				} else {
+225 					object = target;
+226 					message = 'appendChild';
+227 				}
+228 				break
+229 			case "end":
+230 				object = target;
+231 				message = 'appendChild';
+232 				break
+233 			case "after":
+234 				if(target.nextSibling) {
+235 					object = target.parentNode;
+236 					message = 'insertBefore';
+237 					secondParam = target.nextSibling;
+238 				} else {
+239 					object = target.parentNode;
+240 					message = 'appendChild';
+241 				}
+242 				break
+243 		}
+244 
+245 		if(performValidation && this.tree.isListContainer(object) && node.nodeName != "LI") {
+246 			var li = this.createElement("LI");
+247 			li.appendChild(node);
+248 			node = li;
+249 			object[message](node, secondParam);		
+250 		} else if(performValidation && !this.tree.isListContainer(object) && node.nodeName == "LI") {
+251 			this.wrapAllInlineOrTextNodesAs("P", node, true);
+252 			var div = this.createElement("DIV");
+253 			this.moveChildNodes(node, div);
+254 			this.deleteNode(node);
+255 			object[message](div, secondParam);
+256 			node = this.unwrapElement(div, true);
+257 		} else {
+258 			object[message](node, secondParam);
+259 		}
+260 		
+261 		return node;
+262 	},
+263 
+264 	/**
+265 	 * Creates textnode from given text and places given node nearby target.
+266 	 *
+267 	 * @param {String} text Text to be inserted.
+268 	 * @param {Node} target Target node.
+269 	 * @param {String} where Possible values: "before", "start", "end", "after"
+270 	 *
+271 	 * @returns {Node} Inserted node.
+272 	 */
+273 	insertTextAt: function(text, target, where) {
+274 		return this.insertNodeAt(this.createTextNode(text), target, where);
+275 	},
+276 
+277 	/**
+278 	 * Creates element from given HTML string and places given it nearby target.
+279 	 *
+280 	 * @param {String} html HTML to be inserted.
+281 	 * @param {Node} target Target node.
+282 	 * @param {String} where Possible values: "before", "start", "end", "after"
+283 	 *
+284 	 * @returns {Node} Inserted node.
+285 	 */
+286 	insertHtmlAt: function(html, target, where) {
+287 		return this.insertNodeAt(this.createElementFromHtml(html), target, where);
+288 	},
+289 
+290 	/**
+291 	 * Replaces element's tag by removing current element and creating new element by given tag name.
+292 	 *
+293 	 * @param {String} tag New tag name
+294 	 * @param {Element} element Target element
+295 	 *
+296 	 * @returns {Element} Replaced element
+297 	 */	
+298 	replaceTag: function(tag, element) {
+299 		if(element.nodeName == tag) return null;
+300 		if(this.tree.isTableCell(element)) return null;
+301 		
+302 		var newElement = this.createElement(tag);
+303 		this.moveChildNodes(element, newElement);
+304 		this.copyAttributes(element, newElement, true);
+305 		element.parentNode.replaceChild(newElement, element);
+306 		
+307 		if(!newElement.hasChildNodes()) this.correctEmptyElement(newElement);
+308 		
+309 		return newElement;
+310 	},
+311 
+312 	/**
+313 	 * Unwraps unnecessary paragraph.
+314 	 *
+315 	 * Unnecessary paragraph is P which is the only child of given container element.
+316 	 * For example, P which is contained by LI and is the only child is the unnecessary paragraph.
+317 	 * But if given container element is a block-only-container(BLOCKQUOTE, BODY), this method does nothing.
+318 	 *
+319 	 * @param {Element} element Container element
+320 	 * @returns {boolean} True if unwrap performed.
+321 	 */
+322 	unwrapUnnecessaryParagraph: function(element) {
+323 		if(!element) return false;
+324 		
+325 		if(!this.tree.isBlockOnlyContainer(element) && element.childNodes.length == 1 && element.firstChild.nodeName == "P" && !this.hasImportantAttributes(element.firstChild)) {
+326 			var p = element.firstChild;
+327 			this.moveChildNodes(p, element);
+328 			this.deleteNode(p);
+329 			return true;
+330 		}
+331 		return false;
+332 	},
+333 	
+334 	/**
+335 	 * Unwraps element by extracting all children out and removing the element.
+336 	 *
+337 	 * @param {Element} element Target element
+338 	 * @param {boolean} wrapInlineAndTextNodes Wrap all inline and text nodes with P before unwrap
+339 	 * @returns {Node} First child of unwrapped element
+340 	 */
+341 	unwrapElement: function(element, wrapInlineAndTextNodes) {
+342 		if(wrapInlineAndTextNodes) this.wrapAllInlineOrTextNodesAs("P", element);
+343 		
+344 		var nodeToReturn = element.firstChild;
+345 		
+346 		while(element.firstChild) this.insertNodeAt(element.firstChild, element, "before");
+347 		this.deleteNode(element);
+348 		
+349 		return nodeToReturn;
+350 	},
+351 	
+352 	/**
+353 	 * Wraps element by given tag
+354 	 *
+355 	 * @param {String} tag tag name
+356 	 * @param {Element} element target element to wrap
+357 	 * @returns {Element} wrapper
+358 	 */
+359 	wrapElement: function(tag, element) {
+360 		var wrapper = this.insertNodeAt(this.createElement(tag), element, "before");
+361 		wrapper.appendChild(element);
+362 		return wrapper;
+363 	},
+364 	
+365 	/**
+366 	 * Tests #smartWrap with given criteria but doesn't change anything
+367 	 */
+368 	testSmartWrap: function(endElement, criteria) {
+369 		return this.smartWrap(endElement, null, criteria, true);
+370 	},
+371 	
+372 	/**
+373 	 * Create inline element with given tag name and wraps nodes nearby endElement by given criteria
+374 	 *
+375 	 * @param {Element} endElement Boundary(end point, exclusive) of wrapper.
+376 	 * @param {String} tag Tag name of wrapper.
+377 	 * @param {Object} function which returns text index of start boundary.
+378 	 * @param {boolean} testOnly just test boundary and do not perform actual wrapping.
+379 	 *
+380 	 * @returns {Element} wrapper
+381 	 */
+382 	smartWrap: function(endElement, tag, criteria, testOnly) {
+383 		var block = this.getParentBlockElementOf(endElement);
+384 
+385 		tag = tag || "SPAN";
+386 		criteria = criteria || function(text) {return -1};
+387 		
+388 		// check for empty wrapper
+389 		if(!testOnly && (!endElement.previousSibling || this.isEmptyBlock(block))) {
+390 			var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before");
+391 			return wrapper;
+392 		}
+393 		
+394 		// collect all textnodes
+395 		var textNodes = this.tree.collectForward(block, function(node) {return node == endElement}, function(node) {return node.nodeType == 3});
+396 		
+397 		// find textnode and break-point
+398 		var nodeIndex = 0;
+399 		var nodeValues = textNodes.pluck("nodeValue");
+400 		var textToWrap = nodeValues.join("");
+401 		var textIndex = criteria(textToWrap)
+402 		var breakPoint = textIndex;
+403 		
+404 		if(breakPoint == -1) {
+405 			breakPoint = 0;
+406 		} else {
+407 			textToWrap = textToWrap.substring(breakPoint);
+408 		}
+409 		
+410 		for(var i = 0; i < textNodes.length; i++) {
+411 			if(breakPoint > nodeValues[i].length) {
+412 				breakPoint -= nodeValues[i].length;
+413 			} else {
+414 				nodeIndex = i;
+415 				break;
+416 			}
+417 		}
+418 		
+419 		if(testOnly) return {text:textToWrap, textIndex:textIndex, nodeIndex:nodeIndex, breakPoint:breakPoint};
+420 		
+421 		// break textnode if necessary 
+422 		if(breakPoint != 0) {
+423 			var splitted = textNodes[nodeIndex].splitText(breakPoint);
+424 			nodeIndex++;
+425 			textNodes.splice(nodeIndex, 0, splitted);
+426 		}
+427 		var startElement = textNodes[nodeIndex] || block.firstChild;
+428 		
+429 		// split inline elements up to parent block if necessary
+430 		var family = this.tree.findCommonAncestorAndImmediateChildrenOf(startElement, endElement);
+431 		var ca = family.parent;
+432 		if(ca) {
+433 			if(startElement.parentNode != ca) startElement = this.splitElementUpto(startElement, ca, true);
+434 			if(endElement.parentNode != ca) endElement = this.splitElementUpto(endElement, ca, true);
+435 			
+436 			var prevStart = startElement.previousSibling;
+437 			var nextEnd = endElement.nextSibling;
+438 			
+439 			// remove empty inline elements
+440 			if(prevStart && prevStart.nodeType == 1 && this.isEmptyBlock(prevStart)) this.deleteNode(prevStart);
+441 			if(nextEnd && nextEnd.nodeType == 1 && this.isEmptyBlock(nextEnd)) this.deleteNode(nextEnd);
+442 			
+443 			// wrap
+444 			var wrapper = this.insertNodeAt(this.createElement(tag), startElement, "before");
+445 			while(wrapper.nextSibling != endElement) wrapper.appendChild(wrapper.nextSibling);
+446 			return wrapper;
+447 		} else {
+448 			// wrap
+449 			var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before");
+450 			return wrapper;
+451 		}
+452 	},
+453 	
+454 	/**
+455 	 * Wraps all adjust inline elements and text nodes into block element.
+456 	 *
+457 	 * TODO: empty element should return empty array when it is not forced and (at least) single item array when forced
+458 	 *
+459 	 * @param {String} tag Tag name of wrapper
+460 	 * @param {Element} element Target element
+461 	 * @param {boolean} force Force wrapping. If it is set to false, this method do not makes unnecessary wrapper.
+462 	 *
+463 	 * @returns {Array} Array of wrappers. If nothing performed it returns empty array
+464 	 */
+465 	wrapAllInlineOrTextNodesAs: function(tag, element, force) {
+466 		var wrappers = [];
+467 		
+468 		if(!force && !this.tree.hasMixedContents(element)) return wrappers;
+469 		
+470 		var node = element.firstChild;
+471 		while(node) {
+472 			if(this.tree.isTextOrInlineNode(node)) {
+473 				var wrapper = this.wrapInlineOrTextNodesAs(tag, node);
+474 				wrappers.push(wrapper);
+475 				node = wrapper.nextSibling;
+476 			} else {
+477 				node = node.nextSibling;
+478 			}
+479 		}
+480 
+481 		return wrappers;
+482 	},
+483 
+484 	/**
+485 	 * Wraps node and its adjust next siblings into an element
+486 	 */
+487 	wrapInlineOrTextNodesAs: function(tag, node) {
+488 		var wrapper = this.createElement(tag);
+489 		var from = node;
+490 
+491 		from.parentNode.replaceChild(wrapper, from);
+492 		wrapper.appendChild(from);
+493 
+494 		// move nodes into wrapper
+495 		while(wrapper.nextSibling && this.tree.isTextOrInlineNode(wrapper.nextSibling)) wrapper.appendChild(wrapper.nextSibling);
+496 
+497 		return wrapper;
+498 	},
+499 	
+500 	/**
+501 	 * Turns block element into list item
+502 	 *
+503 	 * @param {Element} element Target element
+504 	 * @param {String} type One of "UL", "OL", "CODE". "CODE" is same with "OL" but it gives "OL" a class name "code"
+505 	 *
+506 	 * @return {Element} LI element
+507 	 */
+508 	turnElementIntoListItem: function(element, type) {
+509 		type = type.toUpperCase();
+510 		
+511 		var container = this.createElement(type == "UL" ? "UL" : "OL");
+512 		if(type == "CODE") container.className = "code";
+513 		
+514 		if(this.tree.isTableCell(element)) {
+515 			var p = this.wrapAllInlineOrTextNodesAs("P", element, true)[0];
+516 			container = this.insertNodeAt(container, element, "start");
+517 			var li = this.insertNodeAt(this.createElement("LI"), container, "start");
+518 			li.appendChild(p);
+519 		} else {
+520 			container = this.insertNodeAt(container, element, "after");
+521 			var li = this.insertNodeAt(this.createElement("LI"), container, "start");
+522 			li.appendChild(element);
+523 		}
+524 		
+525 		this.unwrapUnnecessaryParagraph(li);
+526 		this.mergeAdjustLists(container);
+527 		
+528 		return li;
+529 	},
+530 	
+531 	/**
+532 	 * Extracts given element out from its parent element.
+533 	 * 
+534 	 * @param {Element} element Target element
+535 	 */
+536 	extractOutElementFromParent: function(element) {
+537 		if(element == this.root || this.root == element.parentNode || !element.offsetParent) return null;
+538 		
+539 		if(element.nodeName == "LI") {
+540 			this.wrapAllInlineOrTextNodesAs("P", element, true);
+541 			element = element.firstChild;
+542 		}
+543 
+544 		var container = element.parentNode;
+545 		var nodeToReturn = null;
+546 		
+547 		if(container.nodeName == "LI" && container.parentNode.parentNode.nodeName == "LI") {
+548 			// nested list item
+549 			if(element.previousSibling) {
+550 				this.splitContainerOf(element, true);
+551 				this.correctEmptyElement(element);
+552 			}
+553 			
+554 			this.outdentListItem(element);
+555 			nodeToReturn = element;
+556 		} else if(container.nodeName == "LI") {
+557 			// not-nested list item
+558 			
+559 			if(this.tree.isListContainer(element.nextSibling)) {
+560 				// 1. split listContainer
+561 				var listContainer = container.parentNode;
+562 				this.splitContainerOf(container, true);
+563 				this.correctEmptyElement(element);
+564 				
+565 				// 2. extract out LI's children
+566 				nodeToReturn = container.firstChild;
+567 				while(container.firstChild) {
+568 					this.insertNodeAt(container.firstChild, listContainer, "before");
+569 				}
+570 				
+571 				// 3. remove listContainer and merge adjust lists
+572 				var prevContainer = listContainer.previousSibling;
+573 				this.deleteNode(listContainer);
+574 				if(prevContainer && this.tree.isListContainer(prevContainer)) this.mergeAdjustLists(prevContainer);
+575 			} else {
+576 				// 1. split LI
+577 				this.splitContainerOf(element, true);
+578 				this.correctEmptyElement(element);
+579 				
+580 				// 2. split list container
+581 				var listContainer = this.splitContainerOf(container);
+582 				
+583 				// 3. extract out
+584 				this.insertNodeAt(element, listContainer.parentNode, "before");
+585 				this.deleteNode(listContainer.parentNode);
+586 				
+587 				nodeToReturn = element;
+588 			}
+589 		} else if(this.tree.isTableCell(container) || this.tree.isTableCell(element)) {
+590 			// do nothing
+591 		} else {
+592 			// normal block
+593 			this.splitContainerOf(element, true);
+594 			this.correctEmptyElement(element);
+595 			nodeToReturn = this.insertNodeAt(element, container, "before");
+596 			
+597 			this.deleteNode(container);
+598 		}
+599 		
+600 		return nodeToReturn;
+601 	},
+602 	
+603 	/**
+604 	 * Insert new block above or below given element.
+605 	 *
+606 	 * @param {Element} block Target block
+607 	 * @param {boolean} before Insert new block above(before) target block
+608 	 * @param {String} forceTag New block's tag name. If omitted, target block's tag name will be used.
+609 	 *
+610 	 * @returns {Element} Inserted block
+611 	 */
+612 	insertNewBlockAround: function(block, before, forceTag) {
+613 		var isListItem = block.nodeName == "LI" || block.parentNode.nodeName == "LI";
+614 		
+615 		this.removeTrailingWhitespace(block);
+616 		if(this.isFirstLiWithNestedList(block) && !forceTag && before) {
+617 			var li = this.getParentElementOf(block, ["LI"]);
+618 			var newBlock = this._insertNewBlockAround(li, before);
+619 			return newBlock;
+620 		} else if(isListItem && !forceTag) {
+621 			var li = this.getParentElementOf(block, ["LI"]);
+622 			var newBlock = this._insertNewBlockAround(block, before);
+623 			if(li != block) newBlock = this.splitContainerOf(newBlock, false, "prev");
+624 			return newBlock;
+625 		} else if(this.tree.isBlockContainer(block)) {
+626 			this.wrapAllInlineOrTextNodesAs("P", block, true);
+627 			return this._insertNewBlockAround(block.firstChild, before, forceTag);
+628 		} else {
+629 			return this._insertNewBlockAround(block, before, this.tree.isHeading(block) ? "P" : forceTag);
+630 		}
+631 	},
+632 	
+633 	/**
+634 	 * @private
+635 	 *
+636 	 * TODO: Rename
+637 	 */
+638 	_insertNewBlockAround: function(element, before, tagName) {
+639 		var newElement = this.createElement(tagName || element.nodeName);
+640 		this.copyAttributes(element, newElement, false);
+641 		this.correctEmptyElement(newElement);
+642 		newElement = this.insertNodeAt(newElement, element, before ? "before" : "after");
+643 		return newElement;
+644 	},
+645 	
+646 	/**
+647 	 * Wrap or replace element with given tag name.
+648 	 *
+649 	 * @param {String} tag Tag name
+650 	 * @param {Element} element Target element
+651 	 *
+652 	 * @return {Element} wrapper element or replaced element.
+653 	 */
+654 	applyTagIntoElement: function(tag, element) {
+655 		if(this.tree.isBlockOnlyContainer(tag)) {
+656 			return this.wrapBlock(tag, element);
+657 		} else if(this.tree.isBlockContainer(element)) {
+658 			var wrapper = this.createElement(tag);
+659 			this.moveChildNodes(element, wrapper);
+660 			return this.insertNodeAt(wrapper, element, "start");
+661 		} else {
+662 			if(this.tree.isBlockContainer(tag) && this.hasImportantAttributes(element)) {
+663 				return this.wrapBlock(tag, element);
+664 			} else {
+665 				return this.replaceTag(tag, element);
+666 			}
+667 		}
+668 		
+669 		throw "IllegalArgumentException - [" + tag + ", " + element + "]";
+670 	},
+671 	
+672 	/**
+673 	 * Wrap or replace elements with given tag name.
+674 	 *
+675 	 * @param {String} tag Tag name
+676 	 * @param {Element} from Start boundary (inclusive)
+677 	 * @param {Element} to End boundary (inclusive)
+678 	 *
+679 	 * @returns {Array} Array of wrappers or replaced elements
+680 	 */
+681 	applyTagIntoElements: function(tagName, from, to) {
+682 		var applied = [];
+683 		
+684 		if(this.tree.isBlockContainer(tagName)) {
+685 			var family = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to);
+686 			var node = family.left;
+687 			var wrapper = this.insertNodeAt(this.createElement(tagName), node, "before");
+688 			
+689 			var coveringWholeList =
+690 				family.parent.nodeName == "LI" &&
+691 				family.parent.parentNode.childNodes.length == 1 &&
+692 				!family.left.previousSilbing &&
+693 				!family.right.nextSibling;
+694 				
+695 			if(coveringWholeList) {
+696 				var ul = node.parentNode.parentNode;
+697 				this.insertNodeAt(wrapper, ul, "before");
+698 				wrapper.appendChild(ul);
+699 			} else {
+700 				while(node != family.right) {
+701 					next = node.nextSibling;
+702 					wrapper.appendChild(node);
+703 					node = next;
+704 				}
+705 				wrapper.appendChild(family.right);
+706 			}
+707 			applied.push(wrapper);
+708 		} else {
+709 			// is normal tagName
+710 			var elements = this.getBlockElementsBetween(from, to);
+711 			for(var i = 0; i < elements.length; i++) {
+712 				if(this.tree.isBlockContainer(elements[i])) {
+713 					applied.push(this.wrapAllInlineOrTextNodesAs(tagName, elements[i], true));
+714 				} else {
+715 					applied.push(this.replaceTag(tagName, elements[i]));
+716 				}
+717 			}
+718 		}
+719 		return applied.flatten();
+720 	},
+721 	
+722 	/**
+723 	 * Moves block up or down
+724 	 *
+725 	 * @param {Element} block Target block
+726 	 * @param {boolean} up Move up if true
+727 	 * 
+728 	 * @returns {Element} Moved block. It could be different with given block.
+729 	 */
+730 	moveBlock: function(block, up) {
+731 		// if block is table cell or contained by table cell, select its row as mover
+732 		block = this.getParentElementOf(block, ["TR"]) || block;
+733 		
+734 		// if block is only child, select its parent as mover
+735 		while(block.nodeName != "TR" && block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
+736 			block = block.parentNode;
+737 		}
+738 		
+739 		// find target and where
+740 		var target, where;
+741 		if (up) {
+742 			target = block.previousSibling;
+743 			
+744 			if(target) {
+745 				var singleNodeLi = target.nodeName == 'LI' && ((target.childNodes.length == 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target));
+746 				var table = ['TABLE', 'TR'].include(target.nodeName);
+747 
+748 				where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "end" : "before";
+749 			} else if(block.parentNode != this.getRoot()) {
+750 				target = block.parentNode;
+751 				where = "before";
+752 			}
+753 		} else {
+754 			target = block.nextSibling;
+755 			
+756 			if(target) {
+757 				var singleNodeLi = target.nodeName == 'LI' && ((target.childNodes.length == 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target));
+758 				var table = ['TABLE', 'TR'].include(target.nodeName);
+759 				
+760 				where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "start" : "after";
+761 			} else if(block.parentNode != this.getRoot()) {
+762 				target = block.parentNode;
+763 				where = "after";
+764 			}
+765 		}
+766 		
+767 		
+768 		// no way to go?
+769 		if(!target) return null;
+770 		if(["TBODY", "THEAD"].include(target.nodeName)) return null;
+771 		
+772 		// normalize
+773 		this.wrapAllInlineOrTextNodesAs("P", target, true);
+774 		
+775 		// make placeholder if needed
+776 		if(this.isFirstLiWithNestedList(block)) {
+777 			this.insertNewBlockAround(block, false, "P");
+778 		}
+779 		
+780 		// perform move
+781 		var parent = block.parentNode;
+782 		var moved = this.insertNodeAt(block, target, where, true);
+783 		
+784 		// cleanup
+785 		if(!parent.hasChildNodes()) this.deleteNode(parent, true);
+786 		this.unwrapUnnecessaryParagraph(moved);
+787 		this.unwrapUnnecessaryParagraph(target);
+788 
+789 		// remove placeholder
+790 		if(up) {
+791 			if(moved.previousSibling && this.isEmptyBlock(moved.previousSibling) && !moved.previousSibling.previousSibling && moved.parentNode.nodeName == "LI" && this.tree.isListContainer(moved.nextSibling)) {
+792 				this.deleteNode(moved.previousSibling);
+793 			}
+794 		} else {
+795 			if(moved.nextSibling && this.isEmptyBlock(moved.nextSibling) && !moved.previousSibling && moved.parentNode.nodeName == "LI" && this.tree.isListContainer(moved.nextSibling.nextSibling)) {
+796 				this.deleteNode(moved.nextSibling);
+797 			}
+798 		}
+799 		
+800 		return moved;
+801 	},
+802 	
+803 	/**
+804 	 * Remove given block
+805 	 *
+806 	 * @param {Element} block Target block
+807 	 * @returns {Element} Nearest block of remove element
+808 	 */
+809 	removeBlock: function(block) {
+810 		var blockToMove;
+811 
+812 		// if block is only child, select its parent as mover
+813 		while(block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
+814 			block = block.parentNode;
+815 		}
+816 		
+817 		var finder = function(node) {return this.tree.isBlock(node) && !this.tree.isAtomic(node) && !this.tree.isDescendantOf(block, node) && !this.tree.hasBlocks(node);}.bind(this);
+818 		var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this);
+819 		
+820 		if(this.isFirstLiWithNestedList(block)) {
+821 			blockToMove = this.outdentListItem(block.nextSibling.firstChild);
+822 			this.deleteNode(blockToMove.previousSibling, true);
+823 		} else if(this.tree.isTableCell(block)) {
+824 			var rtable = new xq.RichTable(this, this.getParentElementOf(block, ["TABLE"]));
+825 			blockToMove = rtable.getBelowCellOf(block);
+826 			
+827 			// should not delete row when there's thead and the row is the only child of tbody
+828 			if(
+829 				block.parentNode.parentNode.nodeName == "TBODY" &&
+830 				rtable.hasHeadingAtTop() &&
+831 				rtable.getDom().tBodies[0].rows.length == 1) return blockToMove;
+832 			
+833 			blockToMove = blockToMove ||
+834 				this.tree.findForward(block, finder, exitCondition) ||
+835 				this.tree.findBackward(block, finder, exitCondition);
+836 			
+837 			this.deleteNode(block.parentNode, true);
+838 		} else {
+839 			blockToMove = blockToMove ||
+840 				this.tree.findForward(block, finder, exitCondition) ||
+841 				this.tree.findBackward(block, finder, exitCondition);
+842 			
+843 			if(!blockToMove) blockToMove = this.insertNodeAt(this.makeEmptyParagraph(), block, "after");
+844 			
+845 			this.deleteNode(block, true);
+846 		}
+847 		if(!this.getRoot().hasChildNodes()) {
+848 			blockToMove = this.createElement("P");
+849 			this.getRoot().appendChild(blockToMove);
+850 			this.correctEmptyElement(blockToMove);
+851 		}
+852 		
+853 		return blockToMove;
+854 	},
+855 	
+856 	/**
+857 	 * Removes trailing whitespaces of given block
+858 	 *
+859 	 * @param {Element} block Target block
+860 	 */
+861 	removeTrailingWhitespace: function(block) {throw "Not implemented"},
+862 	
+863 	/**
+864 	 * Extract given list item out and change its container's tag
+865 	 *
+866 	 * @param {Element} element LI or P which is a child of LI
+867 	 * @param {String} type "OL", "UL", or "CODE"
+868 	 *
+869 	 * @returns {Element} changed element
+870 	 */
+871 	changeListTypeTo: function(element, type) {
+872 		type = type.toUpperCase();
+873 		
+874 		var li = this.getParentElementOf(element, ["LI"]);
+875 		if(!li) throw "IllegalArgumentException";
+876 		
+877 		var container = li.parentNode;
+878 
+879 		this.splitContainerOf(li);
+880 		
+881 		var newContainer = this.insertNodeAt(this.createElement(type == "UL" ? "UL" : "OL"), container, "before");
+882 		if(type == "CODE") newContainer.className = "code";
+883 		
+884 		this.insertNodeAt(li, newContainer, "start");
+885 		this.deleteNode(container);
+886 		
+887 		this.mergeAdjustLists(newContainer);
+888 		
+889 		return element;
+890 	},
+891 	
+892 	/**
+893 	 * Split container of element into (maxium) three pieces.
+894 	 */
+895 	splitContainerOf: function(element, preserveElementItself, dir) {
+896 		if([element, element.parentNode].include(this.getRoot())) return element;
+897 
+898 		var container = element.parentNode;
+899 		if(element.previousSibling && (!dir || dir.toLowerCase() == "prev")) {
+900 			var prev = this.createElement(container.nodeName);
+901 			this.copyAttributes(container, prev);
+902 			while(container.firstChild != element) {
+903 				prev.appendChild(container.firstChild);
+904 			}
+905 			this.insertNodeAt(prev, container, "before");
+906 			this.unwrapUnnecessaryParagraph(prev);
+907 		}
+908 		
+909 		if(element.nextSibling && (!dir || dir.toLowerCase() == "next")) {
+910 			var next = this.createElement(container.nodeName);
+911 			this.copyAttributes(container, next);
+912 			while(container.lastChild != element) {
+913 				this.insertNodeAt(container.lastChild, next, "start");
+914 			}
+915 			this.insertNodeAt(next, container, "after");
+916 			this.unwrapUnnecessaryParagraph(next);
+917 		}
+918 		
+919 		if(!preserveElementItself) element = this.unwrapUnnecessaryParagraph(container) ? container : element;
+920 		return element;
+921 	},
+922 
+923 	/**
+924 	 * TODO: Add specs
+925 	 */
+926 	splitParentElement: function(seperator) {
+927 		var parent = seperator.parentNode;
+928 		if(["HTML", "HEAD", "BODY"].include(parent.nodeName)) throw "Illegal argument. Cannot seperate element[" + parent.nodeName + "]";
+929 
+930 		var previousSibling = seperator.previousSibling;
+931 		var nextSibling = seperator.nextSibling;
+932 		
+933 		var newElement = this.insertNodeAt(this.createElement(parent.nodeName), parent, "after");
+934 		
+935 		var next;
+936 		while(next = seperator.nextSibling) newElement.appendChild(next);
+937 		
+938 		this.insertNodeAt(seperator, newElement, "start");
+939 		this.copyAttributes(parent, newElement);
+940 		
+941 		return newElement;
+942 	},
+943 	
+944 	/**
+945 	 * TODO: Add specs
+946 	 */
+947 	splitElementUpto: function(seperator, element, excludeElement) {
+948 		while(seperator.previousSibling != element) {
+949 			if(excludeElement && seperator.parentNode == element) break;
+950 			seperator = this.splitParentElement(seperator);
+951 		}
+952 		return seperator;
+953 	},
+954 	
+955 	/**
+956 	 * Merges two adjust elements
+957 	 *
+958 	 * @param {Element} element base element
+959 	 * @param {boolean} withNext merge base element with next sibling
+960 	 * @param {boolean} skip skip merge steps
+961 	 */
+962 	mergeElement: function(element, withNext, skip) {
+963 		this.wrapAllInlineOrTextNodesAs("P", element.parentNode, true);
+964 		
+965 		// find two block
+966 		if(withNext) {
+967 			var prev = element;
+968 			var next = this.tree.findForward(
+969 				element,
+970 				function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node != element.parentNode}.bind(this)
+971 			);
+972 		} else {
+973 			var next = element;
+974 			var prev = this.tree.findBackward(
+975 				element,
+976 				function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node != element.parentNode}.bind(this)
+977 			);
+978 		}
+979 		
+980 		// normalize next block
+981 		if(next && this.tree.isDescendantOf(this.getRoot(), next)) {
+982 			var nextContainer = next.parentNode;
+983 			if(this.tree.isBlockContainer(next)) {
+984 				nextContainer = next;
+985 				this.wrapAllInlineOrTextNodesAs("P", nextContainer, true);
+986 				next = nextContainer.firstChild;
+987 			}
+988 		} else {
+989 			next = null;
+990 		}
+991 		
+992 		// normalize prev block
+993 		if(prev && this.tree.isDescendantOf(this.getRoot(), prev)) {
+994 			var prevContainer = prev.parentNode;
+995 			if(this.tree.isBlockContainer(prev)) {
+996 				prevContainer = prev;
+997 				this.wrapAllInlineOrTextNodesAs("P", prevContainer, true);
+998 				prev = prevContainer.lastChild;
+999 			}
+1000 		} else {
+1001 			prev = null;
+1002 		}
+1003 		
+1004 		try {
+1005 			var containersAreTableCell =
+1006 				prevContainer && (this.tree.isTableCell(prevContainer) || ['TR', 'THEAD', 'TBODY'].include(prevContainer.nodeName)) &&
+1007 				nextContainer && (this.tree.isTableCell(nextContainer) || ['TR', 'THEAD', 'TBODY'].include(nextContainer.nodeName));
+1008 			
+1009 			if(containersAreTableCell && prevContainer != nextContainer) return null;
+1010 			
+1011 			// if next has margin, perform outdent
+1012 			if((!skip || !prev) && next && this.outdentElement(next)) return element;
+1013 
+1014 			// nextContainer is first li and next of it is list container
+1015 			if(nextContainer && nextContainer.nodeName == 'LI' && this.tree.isListContainer(next.nextSibling)) {
+1016 				this.extractOutElementFromParent(nextContainer);
+1017 				return prev;
+1018 			}
+1019 			
+1020 			// merge two list containers
+1021 			if(nextContainer && nextContainer.nodeName == 'LI' && this.tree.isListContainer(nextContainer.parentNode.previousSibling)) {
+1022 				this.mergeAdjustLists(nextContainer.parentNode.previousSibling, true, "next");
+1023 				return prev;
+1024 			}
+1025 
+1026 			if(next && !containersAreTableCell && prevContainer && prevContainer.nodeName == 'LI' && nextContainer && nextContainer.nodeName == 'LI' && prevContainer.parentNode.nextSibling == nextContainer.parentNode) {
+1027 				var nextContainerContainer = nextContainer.parentNode;
+1028 				this.moveChildNodes(nextContainer.parentNode, prevContainer.parentNode);
+1029 				this.deleteNode(nextContainerContainer);
+1030 				return prev;
+1031 			}
+1032 			
+1033 			// merge two containers
+1034 			if(next && !containersAreTableCell && prevContainer && prevContainer.nextSibling == nextContainer && ((skip && prevContainer.nodeName != "LI") || (!skip && prevContainer.nodeName == "LI"))) {
+1035 				this.moveChildNodes(nextContainer, prevContainer);
+1036 				return prev;
+1037 			}
+1038 
+1039 			// unwrap container
+1040 			if(nextContainer && nextContainer.nodeName != "LI" && !this.getParentElementOf(nextContainer, ["TABLE"]) && !this.tree.isListContainer(nextContainer) && nextContainer != this.getRoot() && !next.previousSibling) {
+1041 				return this.unwrapElement(nextContainer, true);
+1042 			}
+1043 			
+1044 			// delete table
+1045 			if(withNext && nextContainer && nextContainer.nodeName == "TABLE") {
+1046 				this.deleteNode(nextContainer, true);
+1047 				return prev;
+1048 			} else if(!withNext && prevContainer && this.tree.isTableCell(prevContainer) && !this.tree.isTableCell(nextContainer)) {
+1049 				this.deleteNode(this.getParentElementOf(prevContainer, ["TABLE"]), true);
+1050 				return next;
+1051 			}
+1052 			
+1053 			// if prev is same with next, do nothing
+1054 			if(prev == next) return null;
+1055 
+1056 			// if there is a null block, do nothing
+1057 			if(!prev || !next || !prevContainer || !nextContainer) return null;
+1058 			
+1059 			// if two blocks are not in the same table cell, do nothing
+1060 			if(this.getParentElementOf(prev, ["TD", "TH"]) != this.getParentElementOf(next, ["TD", "TH"])) return null;
+1061 			
+1062 			var prevIsEmpty = false;
+1063 			
+1064 			// cleanup empty block before merge
+1065 
+1066 			// 1. cleanup prev node which ends with marker +  
+1067 			if(
+1068 				xq.Browser.isTrident &&
+1069 				prev.childNodes.length >= 2 &&
+1070 				this.isMarker(prev.lastChild.previousSibling) &&
+1071 				prev.lastChild.nodeType == 3 &&
+1072 				prev.lastChild.nodeValue.length == 1 &&
+1073 				prev.lastChild.nodeValue.charCodeAt(0) == 160
+1074 			) {
+1075 				this.deleteNode(prev.lastChild);
+1076 			}
+1077 
+1078 			// 2. cleanup prev node (if prev is empty, then replace prev's tag with next's)
+1079 			this.removePlaceHoldersAndEmptyNodes(prev);
+1080 			if(this.isEmptyBlock(prev)) {
+1081 				// replace atomic block with normal block so that following code don't need to care about atomic block
+1082 				if(this.tree.isAtomic(prev)) prev = this.replaceTag("P", prev);
+1083 				
+1084 				prev = this.replaceTag(next.nodeName, prev) || prev;
+1085 				prev.innerHTML = "";
+1086 			} else if(prev.firstChild == prev.lastChild && this.isMarker(prev.firstChild)) {
+1087 				prev = this.replaceTag(next.nodeName, prev) || prev;
+1088 			}
+1089 			
+1090 			// 3. cleanup next node
+1091 			if(this.isEmptyBlock(next)) {
+1092 				// replace atomic block with normal block so that following code don't need to care about atomic block
+1093 				if(this.tree.isAtomic(next)) next = this.replaceTag("P", next);
+1094 				
+1095 				next.innerHTML = "";
+1096 			}
+1097 			
+1098 			// perform merge
+1099 			this.moveChildNodes(next, prev);
+1100 			this.deleteNode(next);
+1101 			return prev;
+1102 		} finally {
+1103 			// cleanup
+1104 			if(prevContainer && this.isEmptyBlock(prevContainer)) this.deleteNode(prevContainer, true);
+1105 			if(nextContainer && this.isEmptyBlock(nextContainer)) this.deleteNode(nextContainer, true);
+1106 			
+1107 			if(prevContainer) this.unwrapUnnecessaryParagraph(prevContainer);
+1108 			if(nextContainer) this.unwrapUnnecessaryParagraph(nextContainer);
+1109 		}
+1110 	},
+1111 	
+1112 	/**
+1113 	 * Merges adjust list containers which has same tag name
+1114 	 *
+1115 	 * @param {Element} container target list container
+1116 	 * @param {boolean} force force adjust list container even if they have different list type
+1117 	 * @param {String} dir Specify merge direction: PREV or NEXT. If not supplied it will be merged with both direction.
+1118 	 */
+1119 	mergeAdjustLists: function(container, force, dir) {
+1120 		var prev = container.previousSibling;
+1121 		var isPrevSame = prev && (prev.nodeName == container.nodeName && prev.className == container.className);
+1122 		if((!dir || dir.toLowerCase() == 'prev') && (isPrevSame || (force && this.tree.isListContainer(prev)))) {
+1123 			while(prev.lastChild) {
+1124 				this.insertNodeAt(prev.lastChild, container, "start");
+1125 			}
+1126 			this.deleteNode(prev);
+1127 		}
+1128 		
+1129 		var next = container.nextSibling;
+1130 		var isNextSame = next && (next.nodeName == container.nodeName && next.className == container.className);
+1131 		if((!dir || dir.toLowerCase() == 'next') && (isNextSame || (force && this.tree.isListContainer(next)))) {
+1132 			while(next.firstChild) {
+1133 				this.insertNodeAt(next.firstChild, container, "end");
+1134 			}
+1135 			this.deleteNode(next);
+1136 		}
+1137 	},
+1138 	
+1139 	/**
+1140 	 * Moves child nodes from one element into another.
+1141 	 *
+1142 	 * @param {Elemet} from source element
+1143 	 * @param {Elemet} to target element
+1144 	 */
+1145 	moveChildNodes: function(from, to) {
+1146 		if(this.tree.isDescendantOf(from, to) || ["HTML", "HEAD"].include(to.nodeName))
+1147 			throw "Illegal argument. Cannot move children of element[" + from.nodeName + "] to element[" + to.nodeName + "]";
+1148 		
+1149 		if(from == to) return;
+1150 		
+1151 		while(from.firstChild) to.appendChild(from.firstChild);
+1152 	},
+1153 	
+1154 	/**
+1155 	 * Copies attributes from one element into another.
+1156 	 *
+1157 	 * @param {Element} from source element
+1158 	 * @param {Element} to target element
+1159 	 * @param {boolean} copyId copy ID attribute of source element
+1160 	 */
+1161 	copyAttributes: function(from, to, copyId) {
+1162 		// IE overrides this
+1163 		
+1164 		var attrs = from.attributes;
+1165 		if(!attrs) return;
+1166 		
+1167 		for(var i = 0; i < attrs.length; i++) {
+1168 			if(attrs[i].nodeName == "class" && attrs[i].nodeValue) {
+1169 				to.className = attrs[i].nodeValue;
+1170 			} else if((copyId || !["id"].include(attrs[i].nodeName)) && attrs[i].nodeValue) {
+1171 				to.setAttribute(attrs[i].nodeName, attrs[i].nodeValue);
+1172 			}
+1173 		}
+1174 	},
+1175 
+1176 	_indentElements: function(node, blocks, affect) {
+1177 		for (var i=0; i < affect.length; i++) {
+1178 			if (affect[i] == node || this.tree.isDescendantOf(affect[i], node))
+1179 				return;
+1180 		}
+1181 		leaves = this.tree.getLeavesAtEdge(node);
+1182 		
+1183 		if (blocks.include(leaves[0])) {
+1184 			var affected = this.indentElement(node, true);
+1185 			if (affected) {
+1186 				affect.push(affected);
+1187 				return;
+1188 			}
+1189 		}
+1190 		
+1191 		if (blocks.include(node)) {
+1192 			var affected = this.indentElement(node, true);
+1193 			if (affected) {
+1194 				affect.push(affected);
+1195 				return;
+1196 			}
+1197 		}
+1198 
+1199 		var children=$A(node.childNodes);
+1200 		for (var i=0; i < children.length; i++)
+1201 			this._indentElements(children[i], blocks, affect);
+1202 		return;
+1203 	},
+1204 
+1205 	indentElements: function(from, to) {
+1206 		var blocks = this.getBlockElementsBetween(from, to);
+1207 		var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to);
+1208 		
+1209 		var affect = [];
+1210 		
+1211 		leaves = this.tree.getLeavesAtEdge(top.parent);
+1212 		if (blocks.include(leaves[0])) {
+1213 			var affected = this.indentElement(top.parent);
+1214 			if (affected)
+1215 				return [affected];
+1216 		}
+1217 		
+1218 		var children = $A(top.parent.childNodes);
+1219 		for (var i=0; i < children.length; i++) {
+1220 			this._indentElements(children[i], blocks, affect);
+1221 		}
+1222 		
+1223 		affect = affect.flatten()
+1224 		return affect.length > 0 ? affect : blocks;
+1225 	},
+1226 	
+1227 	outdentElementsCode: function(node) {
+1228 		if (node.tagName == 'LI')
+1229 			node = node.parentNode;
+1230 		if (node.tagName == 'OL' && node.className == 'code')
+1231 			return true;
+1232 		return false;
+1233 	},
+1234 	
+1235 	_outdentElements: function(node, blocks, affect) {
+1236 		for (var i=0; i < affect.length; i++) {
+1237 			if (affect[i] == node || this.tree.isDescendantOf(affect[i], node))
+1238 				return;
+1239 		}
+1240 		leaves = this.tree.getLeavesAtEdge(node);
+1241 		
+1242 		if (blocks.include(leaves[0]) && !this.outdentElementsCode(leaves[0])) {
+1243 			var affected = this.outdentElement(node, true);
+1244 			if (affected) {
+1245 				affect.push(affected);
+1246 				return;
+1247 			}
+1248 		}
+1249 		
+1250 		if (blocks.include(node)) {
+1251 			var children = $A(node.parentNode.childNodes);
+1252 			var isCode = this.outdentElementsCode(node);
+1253 			var affected = this.outdentElement(node, true, isCode);
+1254 			if (affected) {
+1255 				if (children.include(affected) && this.tree.isListContainer(node.parentNode) && !isCode) {
+1256 					for (var i=0; i < children.length; i++) {
+1257 						if (blocks.include(children[i]) && !affect.include(children[i]))
+1258 							affect.push(children[i]);
+1259 					}
+1260 				}else
+1261 					affect.push(affected);
+1262 				return;
+1263 			}
+1264 		}
+1265 
+1266 		var children=$A(node.childNodes);
+1267 		for (var i=0; i < children.length; i++)
+1268 			this._outdentElements(children[i], blocks, affect);
+1269 		return;
+1270 	},
+1271 
+1272 	outdentElements: function(from, to) {
+1273 		var start, end;
+1274 		
+1275 		if (from.parentNode.tagName == 'LI') start=from.parentNode;
+1276 		if (to.parentNode.tagName == 'LI') end=to.parentNode;
+1277 		
+1278 		var blocks = this.getBlockElementsBetween(from, to);
+1279 		var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to);
+1280 		
+1281 		var affect = [];
+1282 		
+1283 		leaves = this.tree.getLeavesAtEdge(top.parent);
+1284 		if (blocks.include(leaves[0]) && !this.outdentElementsCode(top.parent)) {
+1285 			var affected = this.outdentElement(top.parent);
+1286 			if (affected)
+1287 				return [affected];
+1288 		}
+1289 		
+1290 		var children = $A(top.parent.childNodes);
+1291 		for (var i=0; i < children.length; i++) {
+1292 			this._outdentElements(children[i], blocks, affect);
+1293 		}
+1294 
+1295 		if (from.offsetParent && to.offsetParent) {
+1296 			start = from;
+1297 			end = to;
+1298 		}else if (blocks.first().offsetParent && blocks.last().offsetParent) {
+1299 			start = blocks.first();
+1300 			end = blocks.last();
+1301 		}
+1302 		
+1303 		affect = affect.flatten()
+1304 		if (!start || !start.offsetParent)
+1305 			start = affect.first();
+1306 		if (!end || !end.offsetParent)
+1307 			end = affect.last();
+1308 		
+1309 		return this.getBlockElementsBetween(start, end);
+1310 	},
+1311 	
+1312 	/**
+1313 	 * Performs indent by increasing element's margin-left
+1314 	 */	
+1315 	indentElement: function(element, noParent, forceMargin) {
+1316 		if(
+1317 			!forceMargin &&
+1318 			(element.nodeName == "LI" || (!this.tree.isListContainer(element) && !element.previousSibling && element.parentNode.nodeName == "LI"))
+1319 		) return this.indentListItem(element, noParent);
+1320 		
+1321 		var root = this.getRoot();
+1322 		if(!element || element == root) return null;
+1323 		
+1324 		if (element.parentNode != root && !element.previousSibling && !noParent) element=element.parentNode;
+1325 		
+1326 		var margin = element.style.marginLeft;
+1327 		var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"};
+1328 		
+1329 		cssValue.value += 2;
+1330 		element.style.marginLeft = cssValue.value + cssValue.unit;
+1331 		
+1332 		return element;
+1333 	},
+1334 	
+1335 	/**
+1336 	 * Performs outdent by decreasing element's margin-left
+1337 	 */	
+1338 	outdentElement: function(element, noParent, forceMargin) {
+1339 		if(!forceMargin && element.nodeName == "LI") return this.outdentListItem(element, noParent);
+1340 		
+1341 		var root = this.getRoot();
+1342 		if(!element || element == root) return null;
+1343 		
+1344 		var margin = element.style.marginLeft;
+1345 		
+1346 		var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"};
+1347 		if(cssValue.value == 0) {
+1348 			return element.previousSibling || forceMargin ?
+1349 				null :
+1350 				this.outdentElement(element.parentNode, noParent);
+1351 		}
+1352 		
+1353 		cssValue.value -= 2;
+1354 		element.style.marginLeft = cssValue.value <= 0 ? "" : cssValue.value + cssValue.unit;
+1355 		if(element.style.cssText == "") element.removeAttribute("style");
+1356 		
+1357 		return element;
+1358 	},
+1359 	
+1360 	/**
+1361 	 * Performs indent for list item
+1362 	 */
+1363 	indentListItem: function(element, treatListAsNormalBlock) {
+1364 		var li = this.getParentElementOf(element, ["LI"]);
+1365 		var container = li.parentNode;
+1366 		var prev = li.previousSibling;
+1367 		if(!li.previousSibling) return this.indentElement(container);
+1368 		
+1369 		if(li.parentNode.nodeName == "OL" && li.parentNode.className == "code") return this.indentElement(li, treatListAsNormalBlock, true);
+1370 		
+1371 		if(!prev.lastChild) prev.appendChild(this.makePlaceHolder());
+1372 		
+1373 		var targetContainer = 
+1374 			this.tree.isListContainer(prev.lastChild) ?
+1375 			// if there's existing list container, select it as target container
+1376 			prev.lastChild :
+1377 			// if there's nothing, create new one
+1378 			this.insertNodeAt(this.createElement(container.nodeName), prev, "end");
+1379 		
+1380 		this.wrapAllInlineOrTextNodesAs("P", prev, true);
+1381 		
+1382 		// perform move
+1383 		targetContainer.appendChild(li);
+1384 		
+1385 		// flatten nested list
+1386 		if(!treatListAsNormalBlock && li.lastChild && this.tree.isListContainer(li.lastChild)) {
+1387 			var childrenContainer = li.lastChild;
+1388 			var child;
+1389 			while(child = childrenContainer.lastChild) {
+1390 				this.insertNodeAt(child, li, "after");
+1391 			}
+1392 			this.deleteNode(childrenContainer);
+1393 		}
+1394 		
+1395 		this.unwrapUnnecessaryParagraph(li);
+1396 		
+1397 		return li;
+1398 	},
+1399 	
+1400 	/**
+1401 	 * Performs outdent for list item
+1402 	 *
+1403 	 * @return {Element} outdented list item or null if no outdent performed
+1404 	 */
+1405 	outdentListItem: function(element, treatListAsNormalBlock) {
+1406 		var li = this.getParentElementOf(element, ["LI"]);
+1407 		var container = li.parentNode;
+1408 
+1409 		if(!li.previousSibling) {
+1410 			var performed = this.outdentElement(container);
+1411 			if(performed) return performed;
+1412 		}
+1413 
+1414 		if(li.parentNode.nodeName == "OL" && li.parentNode.className == "code") return this.outdentElement(li, treatListAsNormalBlock, true);
+1415 		
+1416 		var parentLi = container.parentNode;
+1417 		if(parentLi.nodeName != "LI") return null;
+1418 		
+1419 		if(treatListAsNormalBlock) {
+1420 			while(container.lastChild != li) {
+1421 				this.insertNodeAt(container.lastChild, parentLi, "after");
+1422 			}
+1423 		} else {
+1424 			// make next siblings as children
+1425 			if(li.nextSibling) {
+1426 				var targetContainer =
+1427 					li.lastChild && this.tree.isListContainer(li.lastChild) ?
+1428 						// if there's existing list container, select it as target container
+1429 						li.lastChild :
+1430 						// if there's nothing, create new one
+1431 						this.insertNodeAt(this.createElement(container.nodeName), li, "end");
+1432 				
+1433 				this.copyAttributes(container, targetContainer);
+1434 				
+1435 				var sibling;
+1436 				while(sibling = li.nextSibling) {
+1437 					targetContainer.appendChild(sibling);
+1438 				}
+1439 			}
+1440 		}
+1441 		
+1442 		// move current LI into parent LI's next sibling
+1443 		li = this.insertNodeAt(li, parentLi, "after");
+1444 		
+1445 		// remove empty container
+1446 		if(container.childNodes.length == 0) this.deleteNode(container);
+1447 		
+1448 		if(li.firstChild && this.tree.isListContainer(li.firstChild)) {
+1449 			this.insertNodeAt(this.makePlaceHolder(), li, "start");
+1450 		}
+1451 		
+1452 		this.wrapAllInlineOrTextNodesAs("P", li);
+1453 		this.unwrapUnnecessaryParagraph(parentLi);
+1454 		
+1455 		return li;
+1456 	},
+1457 	
+1458 	/**
+1459 	 * Performs justification
+1460 	 *
+1461 	 * @param {Element} block target element
+1462 	 * @param {String} dir one of "LEFT", "CENTER", "RIGHT", "BOTH"
+1463 	 */
+1464 	justifyBlock: function(block, dir) {
+1465 		// if block is only child, select its parent as mover
+1466 		while(block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
+1467 			block = block.parentNode;
+1468 		}
+1469 		
+1470 		var styleValue = dir.toLowerCase() == "both" ? "justify" : dir;
+1471 		if(styleValue == "left") {
+1472 			block.style.textAlign = "";
+1473 			if(block.style.cssText == "") block.removeAttribute("style");
+1474 		} else {
+1475 			block.style.textAlign = styleValue;
+1476 		}
+1477 		return block;
+1478 	},
+1479 	
+1480 	justifyBlocks: function(blocks, dir) {
+1481 		blocks.each(function(block) {
+1482 			this.justifyBlock(block, dir);
+1483 		}.bind(this));
+1484 		
+1485 		return blocks;
+1486 	},
+1487 	
+1488 	/**
+1489      * Turn given element into list. If the element is a list already, it will be reversed into normal element.
+1490 	 *
+1491 	 * @param {Element} element target element
+1492 	 * @param {String} type one of "UL", "OL"
+1493 	 * @returns {Element} affected element
+1494 	 */
+1495 	applyList: function(element, type) {
+1496 		type = type.toUpperCase();
+1497 		var containerTag = type == "UL" ? "UL" : "OL";
+1498 		
+1499 		if(element.nodeName == "LI" || (element.parentNode.nodeName == "LI" && !element.previousSibling)) {
+1500 			var element = this.getParentElementOf(element, ["LI"]);
+1501 			var container = element.parentNode;
+1502 			if(container.nodeName == containerTag) {
+1503 				return this.extractOutElementFromParent(element);
+1504 			} else {
+1505 				return this.changeListTypeTo(element, type);
+1506 			}
+1507 		} else {
+1508 			return this.turnElementIntoListItem(element, type);
+1509 		}
+1510 	},
+1511 	
+1512 	applyLists: function(from, to, type) {
+1513 		type = type.toUpperCase();
+1514 		var containerTag = type == "UL" ? "UL" : "OL";
+1515 		var blocks = this.getBlockElementsBetween(from, to);
+1516 		
+1517 		// LIs or Non-containing blocks
+1518 		var whole = blocks.findAll(function(e) {
+1519 			return e.nodeName == "LI" || !this.tree.isBlockContainer(e);
+1520 		}.bind(this));
+1521 		
+1522 		// LIs
+1523 		var listItems = whole.findAll(function(e) {return e.nodeName == "LI"}.bind(this));
+1524 		
+1525 		// Non-containing blocks which is not a descendant of any LIs selected above(listItems).
+1526 		var normalBlocks = whole.findAll(function(e) {
+1527 			return e.nodeName != "LI" &&
+1528 				!(e.parentNode.nodeName == "LI" && !e.previousSibling && !e.nextSibling) &&
+1529 				!this.tree.isDescendantOf(listItems, e)
+1530 		}.bind(this));
+1531 		
+1532 		var diffListItems = listItems.findAll(function(e) {
+1533 			return e.parentNode.nodeName != containerTag;
+1534 		}.bind(this));
+1535 		
+1536 		// Conditions needed to determine mode
+1537 		var hasNormalBlocks = normalBlocks.length > 0;
+1538 		var hasDifferentListStyle = diffListItems.length > 0;
+1539 		
+1540 		var blockToHandle = null;
+1541 		
+1542 		if(hasNormalBlocks) {
+1543 			blockToHandle = normalBlocks;
+1544 		} else if(hasDifferentListStyle) {
+1545 			blockToHandle = diffListItems;
+1546 		} else {
+1547 			blockToHandle = listItems;
+1548 		}
+1549 		
+1550 		// perform operation
+1551 		for(var i = 0; i < blockToHandle.length; i++) {
+1552 			var block = blockToHandle[i];
+1553 			
+1554 			// preserve original index to restore selection
+1555 			var originalIndex = blocks.indexOf(block);
+1556 			blocks[originalIndex] = this.applyList(block, type);
+1557 		}
+1558 		
+1559 		return blocks;
+1560 	},
+1561 
+1562 	/**
+1563 	 * Insert place-holder for given empty element. Empty element does not displayed and causes many editing problems.
+1564 	 *
+1565 	 * @param {Element} element empty element
+1566 	 */
+1567 	correctEmptyElement: function(element) {throw "Not implemented"},
+1568 
+1569 	/**
+1570 	 * Corrects current block-only-container to do not take any non-block element or node.
+1571 	 */
+1572 	correctParagraph: function() {throw "Not implemented"},
+1573 	
+1574 	/**
+1575 	 * Makes place-holder for empty element.
+1576 	 *
+1577 	 * @returns {Node} Platform specific place holder
+1578 	 */
+1579 	makePlaceHolder: function() {throw "Not implemented"},
+1580 	
+1581 	/**
+1582 	 * Makes place-holder string.
+1583 	 *
+1584 	 * @returns {String} Platform specific place holder string
+1585 	 */
+1586 	makePlaceHolderString: function() {throw "Not implemented"},
+1587 	
+1588 	/**
+1589 	 * Makes empty paragraph which contains only one place-holder
+1590 	 */
+1591 	makeEmptyParagraph: function() {throw "Not implemented"},
+1592 
+1593 	/**
+1594 	 * Applies background color to selected area
+1595 	 *
+1596 	 * @param {Object} color valid CSS color value
+1597 	 */
+1598 	applyBackgroundColor: function(color) {throw "Not implemented";},
+1599 
+1600 	/**
+1601 	 * Applies foreground color to selected area
+1602 	 *
+1603 	 * @param {Object} color valid CSS color value
+1604 	 */
+1605 	applyForegroundColor: function(color) {
+1606 		this.execCommand("forecolor", color);
+1607 	},
+1608 	
+1609 	execCommand: function(commandId, param) {throw "Not implemented";},
+1610 	
+1611 	applyRemoveFormat: function() {throw "Not implemented";},
+1612 	applyEmphasis: function() {throw "Not implemented";},
+1613 	applyStrongEmphasis: function() {throw "Not implemented";},
+1614 	applyStrike: function() {throw "Not implemented";},
+1615 	applyUnderline: function() {throw "Not implemented";},
+1616 	applySuperscription: function() {
+1617 		this.execCommand("superscript");
+1618 	},
+1619 	applySubscription: function() {
+1620 		this.execCommand("subscript");
+1621 	},
+1622 	indentBlock: function(element, treatListAsNormalBlock) {
+1623 		return (!element.previousSibling && element.parentNode.nodeName == "LI") ?
+1624 			this.indentListItem(element, treatListAsNormalBlock) :
+1625 			this.indentElement(element);
+1626 	},
+1627 	outdentBlock: function(element, treatListAsNormalBlock) {
+1628 		while(true) {
+1629 			if(!element.previousSibling && element.parentNode.nodeName == "LI") {
+1630 				element = this.outdentListItem(element, treatListAsNormalBlock);
+1631 				return element;
+1632 			} else {
+1633 				var performed = this.outdentElement(element);
+1634 				if(performed) return performed;
+1635 				
+1636 				// first-child can outdent container
+1637 				if(!element.previousSibling) {
+1638 					element = element.parentNode;
+1639 				} else {
+1640 					break;
+1641 				}
+1642 			}
+1643 		}
+1644 		
+1645 		return null;
+1646 	},
+1647 	wrapBlock: function(tag, start, end) {
+1648 		if(!this.tree._blockTags.include(tag)) throw "Unsuppored block container: [" + tag + "]";
+1649 		if(!start) start = this.getCurrentBlockElement();
+1650 		if(!end) end = start;
+1651 		
+1652 		// Check if the selection captures valid fragement
+1653 		var validFragment = false;
+1654 		
+1655 		if(start == end) {
+1656 			// are they same block?
+1657 			validFragment = true;
+1658 		} else if(start.parentNode == end.parentNode && !start.previousSibling && !end.nextSibling) {
+1659 			// are they covering whole parent?
+1660 			validFragment = true;
+1661 			start = end = start.parentNode;
+1662 		} else {
+1663 			// are they siblings of non-LI blocks?
+1664 			validFragment =
+1665 				(start.parentNode == end.parentNode) &&
+1666 				(start.nodeName != "LI");
+1667 		}
+1668 		
+1669 		if(!validFragment) return null;
+1670 		
+1671 		var wrapper = this.createElement(tag);
+1672 		
+1673 		if(start == end) {
+1674 			// They are same.
+1675 			if(this.tree.isBlockContainer(start) && !this.tree.isListContainer(start)) {
+1676 				// It's a block container. Wrap its contents.
+1677 				if(this.tree.isBlockOnlyContainer(wrapper)) {
+1678 					this.correctEmptyElement(start);
+1679 					this.wrapAllInlineOrTextNodesAs("P", start, true);
+1680 				}
+1681 				this.moveChildNodes(start, wrapper);
+1682 				start.appendChild(wrapper);
+1683 			} else {
+1684 				// It's not a block container. Wrap itself.
+1685 				wrapper = this.insertNodeAt(wrapper, start, "after");
+1686 				wrapper.appendChild(start);
+1687 			}
+1688 			
+1689 			this.correctEmptyElement(wrapper);
+1690 		} else {
+1691 			// They are siblings. Wrap'em all.
+1692 			wrapper = this.insertNodeAt(wrapper, start, "before");
+1693 			var node = start;
+1694 			
+1695 			while(node != end) {
+1696 				next = node.nextSibling;
+1697 				wrapper.appendChild(node);
+1698 				node = next;
+1699 			}
+1700 			wrapper.appendChild(node);
+1701 		}
+1702 		
+1703 		return wrapper;
+1704 	},
+1705 
+1706 
+1707 	
+1708 	/////////////////////////////////////////////
+1709 	// Focus/Caret/Selection
+1710 	
+1711 	/**
+1712 	 * Gives focus to root element's window
+1713 	 */
+1714 	focus: function() {throw "Not implemented";},
+1715 
+1716 	/**
+1717 	 * Returns selection object
+1718 	 */
+1719 	sel: function() {throw "Not implemented";},
+1720 	
+1721 	/**
+1722 	 * Returns range object
+1723 	 */
+1724 	rng: function() {throw "Not implemented";},
+1725 	
+1726 	/**
+1727 	 * Returns true if DOM has selection
+1728 	 */
+1729 	hasSelection: function() {throw "Not implemented";},
+1730 
+1731 	/**
+1732 	 * Returns true if root element's window has selection
+1733 	 */
+1734 	hasFocus: function() {
+1735 		var cur = this.getCurrentElement();
+1736 		return (cur && cur.ownerDocument == this.getDoc());
+1737 	},
+1738 	
+1739 	/**
+1740 	 * Adjust scrollbar to make the element visible in current viewport.
+1741 	 *
+1742 	 * @param {Element} element Target element
+1743 	 * @param {boolean} toTop Align element to top of the viewport
+1744 	 * @param {boolean} moveCaret Move caret to the element
+1745 	 */
+1746 	scrollIntoView: function(element, toTop, moveCaret) {
+1747 		element.scrollIntoView(toTop);
+1748 		if(moveCaret) this.placeCaretAtStartOf(element);
+1749 	},
+1750 	
+1751 	/**
+1752 	 * Select all document
+1753 	 */
+1754 	selectAll: function() {
+1755 		return this.execCommand('selectall');
+1756 	},
+1757 	
+1758 	/**
+1759 	 * Select specified element.
+1760 	 *
+1761 	 * @param {Element} element element to select
+1762 	 * @param {boolean} entireElement true to select entire element, false to select inner content of element 
+1763 	 */
+1764 	selectElement: function(node, entireElement) {throw "Not implemented"},
+1765 	
+1766 	/**
+1767 	 * Select all elements between two blocks(inclusive).
+1768 	 *
+1769 	 * @param {Element} start start of selection
+1770 	 * @param {Element} end end of selection
+1771 	 */
+1772 	selectBlocksBetween: function(start, end) {throw "Not implemented"},
+1773 	
+1774 	/**
+1775 	 * Delete selected area
+1776 	 */
+1777 	deleteSelection: function() {throw "Not implemented"},
+1778 	
+1779 	/**
+1780 	 * Collapses current selection.
+1781 	 *
+1782 	 * @param {boolean} toStart true to move caret to start of selected area.
+1783 	 */
+1784 	collapseSelection: function(toStart) {throw "Not implemented"},
+1785 	
+1786 	/**
+1787 	 * Returns selected area as HTML string
+1788 	 */
+1789 	getSelectionAsHtml: function() {throw "Not implemented"},
+1790 	
+1791 	/**
+1792 	 * Returns selected area as text string
+1793 	 */
+1794 	getSelectionAsText: function() {throw "Not implemented"},
+1795 	
+1796 	/**
+1797 	 * Places caret at start of the element
+1798 	 *
+1799 	 * @param {Element} element Target element
+1800 	 */
+1801 	placeCaretAtStartOf: function(element) {throw "Not implemented"},
+1802 	
+1803 	/**
+1804 	 * Checks if the node is empty-text-node or not
+1805 	 */
+1806 	isEmptyTextNode: function(node) {
+1807 		return node.nodeType == 3 && node.nodeValue.length == 0;
+1808 	},
+1809 	
+1810 	/**
+1811 	 * Checks if the caret is place in empty block element
+1812 	 */
+1813 	isCaretAtEmptyBlock: function() {
+1814 		return this.isEmptyBlock(this.getCurrentBlockElement());
+1815 	},
+1816 	
+1817 	/**
+1818 	 * Checks if the caret is place at start of the block
+1819 	 */
+1820 	isCaretAtBlockStart: function() {throw "Not implemented"},
+1821 
+1822 	/**
+1823 	 * Checks if the caret is place at end of the block
+1824 	 */
+1825 	isCaretAtBlockEnd: function() {throw "Not implemented"},
+1826 	
+1827 	/**
+1828 	 * Saves current selection info
+1829 	 *
+1830 	 * @returns {Object} Bookmark for selection
+1831 	 */
+1832 	saveSelection: function() {throw "Not implemented"},
+1833 	
+1834 	/**
+1835 	 * Restores current selection info
+1836 	 *
+1837 	 * @param {Object} bookmark Bookmark
+1838 	 */
+1839 	restoreSelection: function(bookmark) {throw "Not implemented"},
+1840 	
+1841 	/**
+1842 	 * Create marker
+1843 	 */
+1844 	createMarker: function() {
+1845 		var marker = this.createElement("SPAN");
+1846 		marker.id = "xquared_marker_" + (this._lastMarkerId++);
+1847 		marker.className = "xquared_marker";
+1848 		return marker;
+1849 	},
+1850 
+1851 	/**
+1852 	 * Create and insert marker into current caret position.
+1853 	 * Marker is an inline element which has no child nodes. It can be used with many purposes.
+1854 	 * For example, You can push marker to mark current caret position.
+1855 	 *
+1856 	 * @returns {Element} marker
+1857 	 */
+1858 	pushMarker: function() {
+1859 		var marker = this.createMarker();
+1860 		return this.insertNode(marker);
+1861 	},
+1862 	
+1863 	/**
+1864 	 * Removes last marker
+1865 	 *
+1866 	 * @params {boolean} moveCaret move caret into marker before delete.
+1867 	 */
+1868 	popMarker: function(moveCaret) {
+1869 		var id = "xquared_marker_" + (--this._lastMarkerId);
+1870 		var marker = this.$(id);
+1871 		if(!marker) return;
+1872 		
+1873 		if(moveCaret) {
+1874 			this.selectElement(marker, true);
+1875 			this.collapseSelection(false);
+1876 		}
+1877 		
+1878 		this.deleteNode(marker);
+1879 	},
+1880 	
+1881 	
+1882 	
+1883 	/////////////////////////////////////////////
+1884 	// Query methods
+1885 	
+1886 	isMarker: function(node) {
+1887 		return (node.nodeType == 1 && node.nodeName == "SPAN" && node.className == "xquared_marker");
+1888 	},
+1889 	
+1890 	isFirstBlockOfBody: function(block) {
+1891 		var root = this.getRoot();
+1892 		var found = this.tree.findBackward(
+1893 			block,
+1894 			function(node) {return (node == root) || node.previousSibling;}.bind(this)
+1895 		);
+1896 		
+1897 		return found == root;
+1898 	},
+1899 	
+1900 	/**
+1901 	 * Returns outer HTML of given element
+1902 	 */
+1903 	getOuterHTML: function(element) {throw "Not implemented"},
+1904 	
+1905 	/**
+1906 	 * Returns inner text of given element
+1907 	 * 
+1908 	 * @param {Element} element Target element
+1909 	 * @returns {String} Text string
+1910 	 */
+1911 	getInnerText: function(element) {
+1912 		return element.innerHTML.stripTags();
+1913 	},
+1914 
+1915 	/**
+1916 	 * Checks if given node is place holder or not.
+1917 	 * 
+1918 	 * @param {Node} node DOM node
+1919 	 */
+1920 	isPlaceHolder: function(node) {throw "Not implemented"},
+1921 	
+1922 	/**
+1923 	 * Checks if given block is the first LI whose next sibling is a nested list.
+1924 	 *
+1925 	 * @param {Element} block Target block
+1926 	 */
+1927 	isFirstLiWithNestedList: function(block) {
+1928 		return !block.previousSibling &&
+1929 			block.parentNode.nodeName == "LI" &&
+1930 			this.tree.isListContainer(block.nextSibling);
+1931 	},
+1932 	
+1933 	/**
+1934 	 * Search all links within given element
+1935 	 *
+1936 	 * @param {Element} [element] Container element. If not given, the root element will be used.
+1937 	 * @param {Array} [found] if passed, links will be appended into this array.
+1938 	 * @returns {Array} Array of anchors. It returns empty array if there's no links.
+1939 	 */
+1940 	searchAnchors: function(element, found) {
+1941 		if(!element) element = this.getRoot();
+1942 		if(!found) found = [];
+1943 
+1944 		var anchors = element.getElementsByTagName("A");
+1945 		for(var i = 0; i < anchors.length; i++) {
+1946 			found.push(anchors[i]);
+1947 		}
+1948 
+1949 		return found;
+1950 	},
+1951 	
+1952 	/**
+1953 	 * Search all headings within given element
+1954 	 *
+1955 	 * @param {Element} [element] Container element. If not given, the root element will be used.
+1956 	 * @param {Array} [found] if passed, headings will be appended into this array.
+1957 	 * @returns {Array} Array of headings. It returns empty array if there's no headings.
+1958 	 */
+1959 	searchHeadings: function(element, found) {
+1960 		if(!element) element = this.getRoot();
+1961 		if(!found) found = [];
+1962 
+1963 		var regexp = /^h[1-6]/ig;
+1964 
+1965 		if (!element.childNodes) return [];
+1966 		$A(element.childNodes).each(function(child) {
+1967 			var isContainer = child && this.tree._blockContainerTags.include(child.nodeName);
+1968 			var isHeading = child && child.nodeName.match(regexp);
+1969 
+1970 			if (isContainer) {
+1971 				this.searchHeadings(child, found);
+1972 			} else if (isHeading) {
+1973 				found.push(child);
+1974 			}
+1975 		}.bind(this));
+1976 
+1977 		return found;
+1978 	},
+1979 	
+1980 	/**
+1981 	 * Collect structure and style informations of given element.
+1982 	 *
+1983 	 * @param {Element} element target element
+1984 	 * @returns {Object} object that contains information: {em: true, strong: false, block: "p", list: "ol", ...}
+1985 	 */
+1986 	collectStructureAndStyle: function(element) {
+1987 		if(!element || element.nodeName == "#document") return {};
+1988 
+1989 		var block = this.getParentBlockElementOf(element);
+1990 		var parents = this.tree.collectParentsOf(element, true, function(node) {return block.parentNode == node});
+1991 		var blockName = block.nodeName;
+1992 
+1993 		var info = {};
+1994 		
+1995 		var doc = this.getDoc();
+1996 		var em = doc.queryCommandState("Italic");
+1997 		var strong = doc.queryCommandState("Bold");
+1998 		var strike = doc.queryCommandState("Strikethrough");
+1999 		var underline = doc.queryCommandState("Underline") && !this.getParentElementOf(element, ["A"]);
+2000 		var superscription = doc.queryCommandState("superscript");
+2001 		var subscription = doc.queryCommandState("subscript");
+2002 		
+2003 		// if block is only child, select its parent
+2004 		while(block.parentNode && block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) {
+2005 			block = block.parentNode;
+2006 		}
+2007 
+2008 		var list = false;
+2009 		if(block.nodeName == "LI") {
+2010 			var parent = block.parentNode;
+2011 			var isCode = parent.nodeName == "OL" && parent.className == "code";
+2012 			list = isCode ? "CODE" : parent.nodeName;
+2013 		}
+2014 		
+2015 		var justification = block.style.textAlign || "left";
+2016 		
+2017 		return {
+2018 			block:blockName,
+2019 			em: em,
+2020 			strong: strong,
+2021 			strike: strike,
+2022 			underline: underline,
+2023 			superscription: superscription,
+2024 			subscription: subscription,
+2025 			list: list,
+2026 			justification: justification
+2027 		};
+2028 	},
+2029 	
+2030 	/**
+2031 	 * Find elements by CSS selector.
+2032 	 *
+2033 	 * WARNING: Use this method carefully since prototype.js doesn't work well with designMode DOM.
+2034 	 */
+2035 	findBySelector: function(selector) {
+2036 		return Element.getElementsBySelector(this.root, selector);
+2037 	},
+2038 	
+2039 	/**
+2040 	 * Find elements by attribute.
+2041 	 * 
+2042 	 * This method will be deprecated when findBySelector get stabilized.
+2043 	 */
+2044 	findByAttribute: function(name, value) {
+2045 		var nodes = [];
+2046 		this._findByAttribute(nodes, this.root, name, value);
+2047 		return nodes;
+2048 	},
+2049 	
+2050 	/** @private */
+2051 	_findByAttribute: function(nodes, element, name, value) {
+2052 		if(element.getAttribute(name) == value) nodes.push(element);
+2053 		if(!element.hasChildNodes()) return;
+2054 		
+2055 		var children = element.childNodes;
+2056 		for(var i = 0; i < children.length; i++) {
+2057 			if(children[i].nodeType == 1) this._findByAttribute(nodes, children[i], name, value);
+2058 		}
+2059 	},
+2060 	
+2061 	/**
+2062 	 * Checks if the element has one or more important attributes: id, class, style
+2063 	 *
+2064 	 * @param {Element} element Target element
+2065 	 */
+2066 	hasImportantAttributes: function(element) {throw "Not implemented"},
+2067 	
+2068 	/**
+2069 	 * Checks if the element is empty or not. Place-holder is not counted as a child.
+2070 	 *
+2071 	 * @param {Element} element Target element
+2072 	 */
+2073 	isEmptyBlock: function(element) {throw "Not implemented"},
+2074 	
+2075 	/**
+2076 	 * Returns element that contains caret.
+2077 	 */
+2078 	getCurrentElement: function() {throw "Not implemented"},
+2079 	
+2080 	/**
+2081 	 * Returns block element that contains caret.
+2082 	 */
+2083 	getCurrentBlockElement: function() {
+2084 		var cur = this.getCurrentElement();
+2085 		if(!cur) return null;
+2086 		
+2087 		var block = this.getParentBlockElementOf(cur);
+2088 		if(!block) return null;
+2089 		
+2090 		return (block.nodeName == "BODY") ? null : block;
+2091 	},
+2092 	
+2093 	/**
+2094 	 * Returns parent block element of parameter.
+2095 	 * If the parameter itself is a block, it will be returned.
+2096 	 *
+2097 	 * @param {Element} element Target element
+2098 	 *
+2099 	 * @returns {Element} Element or null
+2100 	 */
+2101 	getParentBlockElementOf: function(element) {
+2102 		while(element) {
+2103 			if(this.tree._blockTags.include(element.nodeName)) return element;
+2104 			element = element.parentNode;
+2105 		}
+2106 		return null;
+2107 	},
+2108 	
+2109 	/**
+2110 	 * Returns parent element of parameter which has one of given tag name.
+2111 	 * If the parameter itself has the same tag name, it will be returned.
+2112 	 *
+2113 	 * @param {Element} element Target element
+2114 	 * @param {Array} tagNames Array of string which contains tag names
+2115 	 *
+2116 	 * @returns {Element} Element or null
+2117 	 */
+2118 	getParentElementOf: function(element, tagNames) {
+2119 		while(element) {
+2120 			if(tagNames.include(element.nodeName)) return element;
+2121 			element = element.parentNode;
+2122 		}
+2123 		return null;
+2124 	},
+2125 	
+2126 	/**
+2127 	 * Collects all block elements between two elements
+2128 	 *
+2129 	 * @param {Element} from Start element(inclusive)
+2130 	 * @param {Element} to End element(inclusive)
+2131 	 */
+2132 	getBlockElementsBetween: function(from, to) {
+2133 		return this.tree.collectNodesBetween(from, to, function(node) {
+2134 			return node.nodeType == 1 && this.tree.isBlock(node);
+2135 		}.bind(this));
+2136 	},
+2137 	
+2138 	/**
+2139 	 * Returns block element that contains selection start.
+2140 	 *
+2141 	 * This method will return exactly same result with getCurrentBlockElement method
+2142 	 * when there's no selection.
+2143 	 */
+2144 	getBlockElementAtSelectionStart: function() {throw "Not implemented"},
+2145 	
+2146 	/**
+2147 	 * Returns block element that contains selection end.
+2148 	 *
+2149 	 * This method will return exactly same result with getCurrentBlockElement method
+2150 	 * when there's no selection.
+2151 	 */
+2152 	getBlockElementAtSelectionEnd: function() {throw "Not implemented"},
+2153 	
+2154 	/**
+2155 	 * Returns blocks at each edge of selection(start and end).
+2156 	 *
+2157 	 * TODO: implement ignoreEmptyEdges for FF
+2158 	 *
+2159 	 * @param {boolean} naturalOrder Mak the start element always comes before the end element
+2160 	 * @param {boolean} ignoreEmptyEdges Prevent some browser(Gecko) from selecting one more block than expected
+2161 	 */
+2162 	getBlockElementsAtSelectionEdge: function(naturalOrder, ignoreEmptyEdges) {throw "Not implemented"},
+2163 	
+2164 	/**
+2165 	 * Returns array of selected block elements
+2166 	 */
+2167 	getSelectedBlockElements: function() {
+2168 		var selectionEdges = this.getBlockElementsAtSelectionEdge(true, true);
+2169 		var start = selectionEdges[0];
+2170 		var end = selectionEdges[1];
+2171 		
+2172 		return this.tree.collectNodesBetween(start, end, function(node) {
+2173 			return node.nodeType == 1 && this.tree.isBlock(node);
+2174 		}.bind(this));
+2175 	},
+2176 	
+2177 	/**
+2178 	 * Get element by ID
+2179 	 *
+2180 	 * @param {String} id Element's ID
+2181 	 * @returns {Element} element or null
+2182 	 */
+2183 	getElementById: function(id) {return this.doc.getElementById(id)},
+2184 	
+2185 	/**
+2186 	 * Shortcut for #getElementById
+2187 	 */
+2188 	$: function(id) {return this.getElementById(id)},
+2189 	
+2190 	/**
+2191 	  * Returns first "valid" child of given element. It ignores empty textnodes.
+2192 	  *
+2193 	  * @param {Element} element Target element
+2194 	  * @returns {Node} first child node or null
+2195 	  */
+2196 	getFirstChild: function(element) {
+2197 		if(!element) return null;
+2198 		
+2199 		var nodes = $A(element.childNodes);
+2200 		for(var i = 0; i < nodes.length; i++) {
+2201 			if(!this.isEmptyTextNode(nodes[i])) return nodes[i];
+2202 		}
+2203 		return null;
+2204 	},
+2205 	
+2206 	/**
+2207 	  * Returns last "valid" child of given element. It ignores empty textnodes and place-holders.
+2208 	  *
+2209 	  * @param {Element} element Target element
+2210 	  * @returns {Node} last child node or null
+2211 	  */
+2212 	getLastChild: function(element) {throw "Not implemented"},
+2213 
+2214 	getNextSibling: function(node) {
+2215 		while(node = node.nextSibling) {
+2216 			if(node.nodeType != 3 || node.nodeValue.strip() != "") break;
+2217 		}
+2218 		return node;
+2219 	},
+2220 
+2221 	getBottommostFirstChild: function(node) {
+2222 		while(node.firstChild && node.nodeType == 1) node = node.firstChild;
+2223 		return node;
+2224 	},
+2225 	
+2226 	getBottommostLastChild: function(node) {
+2227 		while(node.lastChild && node.nodeType == 1) node = node.lastChild;
+2228 		return node;
+2229 	},
+2230 
+2231 	/** @private */
+2232 	_getCssValue: function(str, defaultUnit) {
+2233 		if(!str || str.length == 0) return {value:0, unit:defaultUnit};
+2234 		
+2235 		var tokens = str.match(/(\d+)(.*)/);
+2236 		return {
+2237 			value:parseInt(tokens[1]),
+2238 			unit:tokens[2] || defaultUnit
+2239 		};
+2240 	}
+2241 });
+2242 
+2243 /**
+2244  * Creates and returns instance of browser specific implementation.
+2245  */
+2246 xq.RichDom.createInstance = function() {
+2247 	if(xq.Browser.isTrident) {
+2248 		return new xq.RichDomTrident();
+2249 	} else if(xq.Browser.isWebkit) {
+2250 		return new xq.RichDomWebkit();
+2251 	} else {
+2252 		return new xq.RichDomGecko();
+2253 	}
+2254 }
+2255 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_07.html b/modules/editor/skins/xquared/doc/api/src_07.html new file mode 100644 index 000000000..6b89ca0d2 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_07.html @@ -0,0 +1,54 @@ +
  1 /**
+  2  * RichDom for Gecko
+  3  */
+  4 xq.RichDomGecko = Class.create(xq.RichDomW3, {
+  5 	makePlaceHolder: function() {
+  6 		var holder = this.createElement("BR");
+  7 		holder.setAttribute("type", "_moz");
+  8 		return holder;
+  9 	},
+ 10 	
+ 11 	makePlaceHolderString: function() {
+ 12 		return '<br type="_moz" />';
+ 13 	},
+ 14 	
+ 15 	makeEmptyParagraph: function() {
+ 16 		return this.createElementFromHtml('<p><br type="_moz" /></p>');
+ 17 	},
+ 18 
+ 19 	isPlaceHolder: function(node) {
+ 20 		if(node.nodeType != 1) return false;
+ 21 		
+ 22 		var typeMatches = node.nodeName == "BR" && node.getAttribute("type") == "_moz";
+ 23 		if(typeMatches) return true;
+ 24 		
+ 25 		var positionMatches = node.nodeName == "BR" && !this.getNextSibling(node);
+ 26 		if(positionMatches) return true;
+ 27 		
+ 28 		return false;
+ 29 	},
+ 30 
+ 31 	selectElement: function(element, entireElement) {
+ 32 		if(!element) throw "[element] is null";
+ 33 		if(element.nodeType != 1) throw "[element] is not an element";
+ 34 
+ 35 		// required to avoid Windows FF selection bug.
+ 36 		try {
+ 37 			if(!xq.Browser.isMac) this.doc.execCommand("SelectAll", false, null);
+ 38 		} catch(ignored) {}
+ 39 		
+ 40 		if(entireElement) {
+ 41 			this.rng().selectNode(element);
+ 42 		} else {
+ 43 			this.rng().selectNodeContents(element);
+ 44 		}
+ 45 	}
+ 46 });
+ 47 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_08.html b/modules/editor/skins/xquared/doc/api/src_08.html new file mode 100644 index 000000000..ce19b8514 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_08.html @@ -0,0 +1,369 @@ +
  1 /**
+  2  * RichDom for Internet Explorer 6 and 7
+  3  */
+  4 xq.RichDomTrident = Class.create(xq.RichDom, {
+  5 	makePlaceHolder: function() {
+  6 		return this.createTextNode(" ");
+  7 	},
+  8 	
+  9 	makePlaceHolderString: function() {
+ 10 		return ' ';
+ 11 	},
+ 12 	
+ 13 	makeEmptyParagraph: function() {
+ 14 		return this.createElementFromHtml("<p> </p>");
+ 15 	},
+ 16 
+ 17 	isPlaceHolder: function(node) {
+ 18 		return false;
+ 19 	},
+ 20 
+ 21 	getOuterHTML: function(element) {
+ 22 		return element.outerHTML;
+ 23 	},
+ 24 	
+ 25 	insertNode: function(node) {
+ 26 		if(this.hasSelection()) this.collapseSelection(true);
+ 27 		
+ 28 		this.rng().pasteHTML('<span id="xquared_temp"></span>');
+ 29 		var marker = this.$('xquared_temp');
+ 30 		if(node.id == 'xquared_temp') return marker;
+ 31 		
+ 32 		marker.replaceNode(node);
+ 33 		return node;
+ 34 	},
+ 35 	
+ 36 	removeTrailingWhitespace: function(block) {
+ 37 		if(!block) return;
+ 38 		
+ 39 		// TODO: reimplement to handle atomic tags and so on. (use DomTree)
+ 40 		if(this.tree.isBlockContainer(block)) return;
+ 41 		if(this.isEmptyBlock(block)) return;
+ 42 		
+ 43 		var text = block.innerText;
+ 44 		var lastCharCode = text.charCodeAt(text.length - 1);
+ 45 		if(text.length <= 1 || ![32,160].include(lastCharCode)) return;
+ 46 		
+ 47 		var node = block;
+ 48 		
+ 49 		while(node && node.nodeType != 3) node = node.lastChild;
+ 50 		
+ 51 		if(!node) return;
+ 52 		
+ 53 		// DO NOT REMOVE OR MODIFY FOLLOWING CODE:
+ 54 		//
+ 55 		// Modifying following code crash IE7
+ 56 		var nodeValue = node.nodeValue;
+ 57 		if(nodeValue.length <= 1) {
+ 58 			this.deleteNode(node, true);
+ 59 		} else {
+ 60 			node.nodeValue = nodeValue.substring(0, nodeValue.length - 1);
+ 61 		}
+ 62 	},
+ 63 	
+ 64 	correctEmptyElement: function(element) {
+ 65 		if(!element || element.nodeType != 1 || this.tree.isAtomic(element)) return;
+ 66 		
+ 67 		if(element.firstChild) {
+ 68 			this.correctEmptyElement(element.firstChild);
+ 69 		} else {
+ 70 			element.innerHTML = " ";
+ 71 		}
+ 72 	},
+ 73 
+ 74 	copyAttributes: function(from, to, copyId) {
+ 75 		to.mergeAttributes(from, !copyId);
+ 76 	},
+ 77 
+ 78 	correctParagraph: function() {
+ 79 		if(!this.hasFocus()) return false;
+ 80 		if(this.hasSelection()) return false;
+ 81 		
+ 82 		var block = this.getCurrentElement();
+ 83 		
+ 84 		if(block.nodeName == "BODY") {
+ 85 			// check for atomic block element such as HR
+ 86 			block = this.insertNode(this.makeEmptyParagraph());
+ 87 			var next = block.nextSibling;
+ 88 			if(this.tree.isAtomic(next)) {
+ 89 				block = this.insertNodeAt(block, next, "after");
+ 90 				this.placeCaretAtStartOf(block);
+ 91 				
+ 92 				var nextBlock = this.tree.findForward(
+ 93 					block,
+ 94 					function(node) {return this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)}.bind(this)
+ 95 				);
+ 96 				if(nextBlock) {
+ 97 					this.deleteNode(block);
+ 98 					this.placeCaretAtStartOf(nextBlock);
+ 99 				}
+100 				return true;
+101 			} else {
+102 				var nextBlock = this.tree.findForward(
+103 					block,
+104 					function(node) {return this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)}.bind(this)
+105 				);
+106 				if(nextBlock) {
+107 					this.deleteNode(block);
+108 					this.placeCaretAtStartOf(nextBlock);
+109 				}
+110 				return true;
+111 			}
+112 		} else {
+113 			block = this.getCurrentBlockElement();
+114 			if(block.nodeType == 3) block = block.parentNode;
+115 			
+116 			if(this.tree.hasMixedContents(block)) {
+117 				var marker = this.pushMarker();
+118 				this.wrapAllInlineOrTextNodesAs("P", block, true);
+119 				this.popMarker(true);
+120 				return true;
+121 			} else if((this.tree.isTextOrInlineNode(block.previousSibling) || this.tree.isTextOrInlineNode(block.nextSibling)) && this.tree.hasMixedContents(block.parentNode)) {
+122 				// IE?서??Block?Inline/Text??접??경우 getCurrentElement ?이 ?작?한??
+123 				// ?라???재 Block 주?까? ?번???아주어???다.
+124 				this.wrapAllInlineOrTextNodesAs("P", block.parentNode, true);
+125 				return true;
+126 			} else {
+127 				return false;
+128 			}
+129 		}
+130 	},
+131 	
+132 	
+133 	
+134 	//////
+135 	// Commands
+136 	execCommand: function(commandId, param) {
+137 		return this.doc.execCommand(commandId, false, param);
+138 	},
+139 	
+140 	applyBackgroundColor: function(color) {
+141 		this.execCommand("BackColor", color);
+142 	},
+143 	
+144 	applyEmphasis: function() {
+145 		// Generate <i> tag. It will be replaced with <emphasis> tag during cleanup phase.
+146 		this.execCommand("Italic");
+147 	},
+148 	applyStrongEmphasis: function() {
+149 		// Generate <b> tag. It will be replaced with <strong> tag during cleanup phase.
+150 		this.execCommand("Bold");
+151 	},
+152 	applyStrike: function() {
+153 		// Generate <strike> tag. It will be replaced with <style class="strike"> tag during cleanup phase.
+154 		this.execCommand("strikethrough");
+155 	},
+156 	applyUnderline: function() {
+157 		// Generate <u> tag. It will be replaced with <em class="underline"> tag during cleanup phase.
+158 		this.execCommand("underline");
+159 	},
+160 	applyRemoveFormat: function() {
+161 		this.execCommand("RemoveFormat");
+162 		this.execCommand("Unlink");
+163 	},
+164 	execHeading: function(level) {
+165 		this.execCommand("FormatBlock", "<H" + level + ">");
+166 	},
+167 
+168 
+169 
+170 	//////
+171 	// Focus/Caret/Selection
+172 	
+173 	focus: function() {
+174 		this.win.focus();
+175 		
+176 		// ?게 ?으?초기??caret??P 밖에 ?치?면??		// getCurrentElement??면 P?리턴?는 기이???상??발생.
+177 		if(!this._focusedBefore) {
+178 			this.correctParagraph();
+179 			this.placeCaretAtStartOf(this.getCurrentBlockElement());
+180 			this._focusedBefore = true;
+181 		}
+182 	},
+183 
+184 	sel: function() {
+185 		return this.doc.selection;
+186 	},
+187 	
+188 	rng: function() {
+189 		try {
+190 			var sel = this.sel();
+191 			return (sel == null) ? null : sel.createRange();
+192 		} catch(ignored) {
+193 			// IE often fails
+194 			return null;
+195 		}
+196 	},
+197 	
+198 	hasSelection: function() {
+199 		var selectionType = this.sel().type.toLowerCase();
+200 		if("none" == selectionType) return false;
+201 		if("text" == selectionType && this.getSelectionAsHtml().length == 0) return false;
+202 		return true;
+203 	},
+204 	deleteSelection: function() {
+205 		if(this.getSelectionAsText() != "") this.sel().clear();
+206 	},
+207 	
+208 	placeCaretAtStartOf: function(element) {
+209 		// If there's no empty span, caret sometimes moves into a previous node.
+210 		var ph = this.insertNodeAt(this.createElement("SPAN"), element, "start");
+211 		this.selectElement(ph);
+212 		this.collapseSelection(false);
+213 		this.deleteNode(ph);
+214 	},
+215 	
+216 	selectElement: function(element, entireElement) {
+217 		if(!element) throw "[element] is null";
+218 		if(element.nodeType != 1) throw "[element] is not an element";
+219 		
+220 		var rng = this.rng();
+221 		rng.moveToElementText(element);
+222 		rng.select();
+223 	},
+224 
+225 	selectBlocksBetween: function(start, end) {
+226 		var rng = this.rng();
+227 		var rngTemp = this.rng();
+228 
+229 		rngTemp.moveToElementText(start);
+230 		rng.setEndPoint("StartToStart", rngTemp);
+231 		
+232 		rngTemp.moveToElementText(end);
+233 		rng.setEndPoint("EndToEnd", rngTemp);
+234 		
+235 		rng.select();
+236 	},
+237 	
+238 	collapseSelection: function(toStart) {
+239 		var rng = this.rng();
+240 		rng.collapse(toStart);
+241 		rng.select();
+242 	},
+243 	
+244 	getSelectionAsHtml: function() {
+245 		var rng = this.rng()
+246 		return rng && rng.htmlText ? rng.htmlText : ""
+247 	},
+248 	
+249 	getSelectionAsText: function() {
+250 		var rng = this.rng();
+251 		return rng && rng.text ? rng.text : "";
+252 	},
+253 	
+254 	hasImportantAttributes: function(element) {
+255 		return !!(element.id || element.className || element.style.cssText);
+256 	},
+257 
+258 	isEmptyBlock: function(element) {
+259 		if(!element.hasChildNodes()) return true;
+260 		if(element.nodeType == 3 && !element.nodeValue) return true;
+261 		if([" ", " ", ""].include(element.innerHTML)) return true;
+262 		
+263 		return false;
+264 	},
+265 	
+266 	getLastChild: function(element) {
+267 		if(!element || !element.hasChildNodes()) return null;
+268 		
+269 		var nodes = $A(element.childNodes).reverse();
+270 		
+271 		for(var i = 0; i < nodes.length; i++) {
+272 			if(nodes[i].nodeType != 3 || nodes[i].nodeValue.length != 0) return nodes[i];
+273 		}
+274 		
+275 		return null;
+276 	},
+277 	
+278 	getCurrentElement: function() {
+279 		if(this.sel().type.toLowerCase() == "control") return this.rng().item(0);
+280 		return this.rng().parentElement();
+281 	},
+282 	
+283 	getBlockElementAtSelectionStart: function() {
+284 		var rng = this.rng();
+285 		var dup = rng.duplicate();
+286 		dup.collapse(true);
+287 		
+288 		var result = this.getParentBlockElementOf(dup.parentElement());
+289 		if(result.nodeName == "BODY") result = result.firstChild;
+290 		
+291 		return result;
+292 	},
+293 	
+294 	getBlockElementAtSelectionEnd: function() {
+295 		var rng = this.rng();
+296 		var dup = rng.duplicate();
+297 		dup.collapse(false);
+298 		
+299 		var result = this.getParentBlockElementOf(dup.parentElement());
+300 		if(result.nodeName == "BODY") result = result.lastChild;
+301 
+302 		return result;
+303 	},
+304 	
+305 	getBlockElementsAtSelectionEdge: function(naturalOrder, ignoreEmptyEdges) {
+306 		return [
+307 			this.getBlockElementAtSelectionStart(),
+308 			this.getBlockElementAtSelectionEnd()
+309 		];
+310 	},
+311 	
+312 	isCaretAtBlockStart: function() {
+313 		if(this.isCaretAtEmptyBlock()) return true;
+314 		if(this.hasSelection()) return false;
+315 		var node = this.getCurrentBlockElement();
+316 		var marker = this.pushMarker();
+317 		
+318 		var isTrue = false;
+319 		while (node = this.getFirstChild(node)) {
+320 			if (node == marker) {
+321 				isTrue = true;
+322 				break;
+323 			}
+324 		}
+325 		
+326 		this.popMarker();
+327 		
+328 		return isTrue;
+329 	},
+330 	isCaretAtBlockEnd: function() {
+331 		if(this.isCaretAtEmptyBlock()) return true;
+332 		if(this.hasSelection()) return false;
+333 		var node = this.getCurrentBlockElement();
+334 		var marker = this.pushMarker();
+335 		var isTrue = false;
+336 		while (node = this.getLastChild(node)) {
+337 			var nodeValue = node.nodeValue;
+338 			
+339 			if (node == marker) {
+340 				isTrue = true;
+341 				break;
+342 			} else if(
+343 				node.nodeType == 3 &&
+344 				node.previousSibling == marker &&
+345 				(nodeValue == " " || (nodeValue.length == 1 && nodeValue.charCodeAt(0) == 160))
+346 			) {
+347 				isTrue = true;
+348 				break;
+349 			}
+350 		}
+351 		
+352 		this.popMarker();
+353 		return isTrue;
+354 	},
+355 	saveSelection: function() {
+356 		return this.rng();
+357 	},
+358 	restoreSelection: function(bookmark) {
+359 		bookmark.select();
+360 	}
+361 });
+362 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_09.html b/modules/editor/skins/xquared/doc/api/src_09.html new file mode 100644 index 000000000..030d7b252 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_09.html @@ -0,0 +1,382 @@ +
  1 /**
+  2  * RichDom for W3C Standard Engine
+  3  */
+  4 xq.RichDomW3 = Class.create(xq.RichDom, {
+  5 	insertNode: function(node) {
+  6 		var rng = this.rng();
+  7 		rng.insertNode(node);
+  8 		rng.selectNode(node);
+  9 		rng.collapse(false);
+ 10 		return node;
+ 11 	},
+ 12 
+ 13 	removeTrailingWhitespace: function(block) {
+ 14 		// TODO: do nothing
+ 15 	},
+ 16 
+ 17 	getOuterHTML: function(element) {
+ 18 		var div = element.ownerDocument.createElement("div");
+ 19 		div.appendChild(element.cloneNode(true));
+ 20 		return div.innerHTML;
+ 21 	},
+ 22 	
+ 23 	correctEmptyElement: function(element) {
+ 24 		if(!element || element.nodeType != 1 || this.tree.isAtomic(element)) return;
+ 25 		
+ 26 		if(element.firstChild)
+ 27 			this.correctEmptyElement(element.firstChild);
+ 28 		else
+ 29 			element.appendChild(this.makePlaceHolder());
+ 30 	},
+ 31 	
+ 32 	correctParagraph: function() {
+ 33 		if(this.hasSelection()) return false;
+ 34 		
+ 35 		var block = this.getCurrentElement();
+ 36 		var modified = false;
+ 37 		
+ 38 		if(this.tree.isBlockOnlyContainer(block)) {
+ 39 			this.execCommand("InsertParagraph");
+ 40 			
+ 41 			// check for atomic block element such as HR
+ 42 			var newBlock = this.getCurrentElement();
+ 43 			if(this.tree.isAtomic(newBlock.previousSibling)) {
+ 44 				var nextBlock = this.tree.findForward(
+ 45 					newBlock,
+ 46 					function(node) {return this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)}.bind(this)
+ 47 				);
+ 48 				if(nextBlock) {
+ 49 					this.deleteNode(newBlock);
+ 50 					this.placeCaretAtStartOf(nextBlock);
+ 51 				}
+ 52 			}
+ 53 			modified = true;
+ 54 		} else if(this.tree.hasMixedContents(block)) {
+ 55 			this.wrapAllInlineOrTextNodesAs("P", block, true);
+ 56 			modified = true;
+ 57 		}
+ 58 		
+ 59 		block = this.getCurrentElement();
+ 60 		if(this.tree.isBlock(block) && !this._hasPlaceHolderAtEnd(block)) {
+ 61 			block.appendChild(this.makePlaceHolder());
+ 62 			modified = true;
+ 63 		}
+ 64 		
+ 65 		if(this.tree.isBlock(block)) {
+ 66 			var parentsLastChild = block.parentNode.lastChild;
+ 67 			if(this.isPlaceHolder(parentsLastChild)) {
+ 68 				this.deleteNode(parentsLastChild);
+ 69 				modified = true;
+ 70 			}
+ 71 		}
+ 72 		
+ 73 		return modified;
+ 74 	},
+ 75 	
+ 76 	_hasPlaceHolderAtEnd: function(block) {
+ 77 		if(!block.hasChildNodes()) return false;
+ 78 		return this.isPlaceHolder(block.lastChild) || this._hasPlaceHolderAtEnd(block.lastChild);
+ 79 	},
+ 80 	
+ 81 	applyBackgroundColor: function(color) {
+ 82 		this.execCommand("styleWithCSS", "true");
+ 83 		this.execCommand("hilitecolor", color);
+ 84 		this.execCommand("styleWithCSS", "false");
+ 85 		
+ 86 		// 0. Save current selection
+ 87 		var bookmark = this.saveSelection();
+ 88 		
+ 89 		// 1. Get selected blocks
+ 90 		var blocks = this.getSelectedBlockElements();
+ 91 		if(blocks.length == 0) return;
+ 92 		
+ 93 		// 2. Apply background-color to all adjust inline elements
+ 94 		// 3. Remove background-color from blocks
+ 95 		for(var i = 0; i < blocks.length; i++) {
+ 96 			if((i == 0 || i == blocks.length-1) && !blocks[i].style.backgroundColor) continue;
+ 97 			
+ 98 			var spans = this.wrapAllInlineOrTextNodesAs("SPAN", blocks[i], true);
+ 99 			for(var j = 0; j < spans.length; j++) {
+100 				spans[j].style.backgroundColor = color;
+101 			}
+102 			blocks[i].style.backgroundColor = "";
+103 		}
+104 		
+105 		// 4. Restore selection
+106 		this.restoreSelection(bookmark);
+107 	},
+108 	
+109 	
+110 	
+111 	
+112 	//////
+113 	// Commands
+114 	execCommand: function(commandId, param) {
+115 		return this.doc.execCommand(commandId, false, param || null);
+116 	},
+117 	
+118 	saveSelection: function() {
+119 		var rng = this.rng();
+120 		return [rng.startContainer, rng.startOffset, rng.endContainer, rng.endOffset];
+121 	},
+122 	
+123 	restoreSelection: function(bookmark) {
+124 		var rng = this.rng();
+125 		rng.setStart(bookmark[0], bookmark[1]);
+126 		rng.setEnd(bookmark[2], bookmark[3]);
+127 	},
+128 	
+129 	applyRemoveFormat: function() {
+130 		this.execCommand("RemoveFormat");
+131 		this.execCommand("Unlink");
+132 	},
+133 	applyEmphasis: function() {
+134 		// Generate <i> tag. It will be replaced with <emphasis> tag during cleanup phase.
+135 		this.execCommand("styleWithCSS", "false");
+136 		this.execCommand("italic");
+137 	},
+138 	applyStrongEmphasis: function() {
+139 		// Generate <b> tag. It will be replaced with <strong> tag during cleanup phase.
+140 		this.execCommand("styleWithCSS", "false");
+141 		this.execCommand("bold");
+142 	},
+143 	applyStrike: function() {
+144 		// Generate <strike> tag. It will be replaced with <style class="strike"> tag during cleanup phase.
+145 		this.execCommand("styleWithCSS", "false");
+146 		this.execCommand("strikethrough");
+147 	},
+148 	applyUnderline: function() {
+149 		// Generate <u> tag. It will be replaced with <em class="underline"> tag during cleanup phase.
+150 		this.execCommand("styleWithCSS", "false");
+151 		this.execCommand("underline");
+152 	},
+153 	execHeading: function(level) {
+154 		this.execCommand("Heading", "H" + level);
+155 	},
+156 
+157 
+158 
+159 	//////
+160 	// Focus/Caret/Selection
+161 	
+162 	focus: function() {
+163 		setTimeout(this._focus.bind(this), 0);
+164 	},
+165 	
+166 	/** @private */
+167 	_focus: function() {
+168 		this.win.focus();
+169 		if(!this.hasSelection() && this.getCurrentElement().nodeName == "HTML") {
+170 			this.selectElement(this.doc.body.firstChild);
+171 			this.collapseSelection(true);
+172 		}
+173 	},
+174 
+175 	sel: function() {
+176 		return this.win.getSelection();
+177 	},
+178 	
+179 	rng: function() {
+180 		var sel = this.sel();
+181 		return (sel == null || sel.rangeCount == 0) ? null : sel.getRangeAt(0);
+182 	},
+183 
+184 	hasSelection: function() {
+185 		var sel = this.sel();
+186 		return sel && !sel.isCollapsed;
+187 	},
+188 	
+189 	deleteSelection: function() {
+190 		this.rng().deleteContents();
+191 		this.sel().collapseToStart();
+192 	},
+193 	
+194 	selectElement: function(element, entireElement) {throw "Not implemented yet"},
+195 
+196 	selectBlocksBetween: function(start, end) {
+197 		// required to avoid FF selection bug.
+198 		try {
+199 			if(!xq.Browser.isMac) this.doc.execCommand("SelectAll", false, null);
+200 		} catch(ignored) {}
+201 		
+202 		var rng = this.rng();
+203 		rng.setStart(start.firstChild, 0);
+204 		rng.setEnd(end, end.childNodes.length);
+205 	},
+206 
+207 	collapseSelection: function(toStart) {
+208 		this.rng().collapse(toStart);
+209 	},
+210 	
+211 	placeCaretAtStartOf: function(element) {
+212 		while(this.tree.isBlock(element.firstChild)) {
+213 			element = element.firstChild;
+214 		}
+215 		this.selectElement(element, false);
+216 		this.collapseSelection(true);
+217 	},
+218 	
+219 	getSelectionAsHtml: function() {
+220 		var container = document.createElement("div");
+221 		container.appendChild(this.rng().cloneContents());
+222 		return container.innerHTML;
+223 	},
+224 	
+225 	getSelectionAsText: function() {
+226 		return this.rng().toString()
+227 	},
+228 	
+229 	hasImportantAttributes: function(element) {
+230 		return !!(element.id || element.className || element.style.cssText);
+231 	},
+232 	
+233 	isEmptyBlock: function(element) {
+234 		if(!element.hasChildNodes()) return true;
+235 		var children = element.childNodes;
+236 		for(var i = 0; i < children.length; i++) {
+237 			if(!this.isPlaceHolder(children[i]) && !this.isEmptyTextNode(children[i])) return false;
+238 		}
+239 		return true;
+240 	},
+241 	
+242 	getLastChild: function(element) {
+243 		if(!element || !element.hasChildNodes()) return null;
+244 		
+245 		var nodes = $A(element.childNodes).reverse();
+246 		
+247 		for(var i = 0; i < nodes.length; i++) {
+248 			if(!this.isPlaceHolder(nodes[i]) && !this.isEmptyTextNode(nodes[i])) return nodes[i];
+249 		}
+250 		return null;
+251 	},
+252 	
+253 	getCurrentElement: function() {
+254 		var rng = this.rng();
+255 		if(!rng) return null;
+256 		
+257 		var container = rng.startContainer;
+258 		return container.nodeType == 3 ? container.parentNode : container;
+259 	},
+260 
+261 	getBlockElementsAtSelectionEdge: function(naturalOrder, ignoreEmptyEdges) {
+262 		var start = this.getBlockElementAtSelectionStart();
+263 		var end = this.getBlockElementAtSelectionEnd();
+264 		
+265 		var reversed = false;
+266 		
+267 		if(naturalOrder && start != end && this.tree.checkTargetBackward(start, end)) {
+268 			var temp = start;
+269 			start = end;
+270 			end = temp;
+271 			
+272 			reversed = true;
+273 		}
+274 		
+275 		if(ignoreEmptyEdges && start != end) {
+276 			// TODO - Firefox sometimes selects one more block.
+277 /*
+278 			
+279 			var sel = this.sel();
+280 			if(reversed) {
+281 				if(sel.focusNode.nodeType == 1) start = start.nextSibling;
+282 				if(sel.anchorNode.nodeType == 3 && sel.focusOffset == 0) end = end.previousSibling;
+283 			} else {
+284 				if(sel.anchorNode.nodeType == 1) start = start.nextSibling;
+285 				if(sel.focusNode.nodeType == 3 && sel.focusOffset == 0) end = end.previousSibling;
+286 			}
+287 */
+288 		}
+289 		
+290 		return [start, end];
+291 	},
+292 	
+293 	getBlockElementAtSelectionStart: function() {
+294 		var block = this.getParentBlockElementOf(this.sel().anchorNode);
+295 		
+296 		// find bottom-most first block child
+297 		while(this.tree.isBlockContainer(block) && block.firstChild && this.tree.isBlock(block.firstChild)) {
+298 			block = block.firstChild;
+299 		}
+300 		
+301 		return block;
+302 	},
+303 	
+304 	getBlockElementAtSelectionEnd: function() {
+305 		var block = this.getParentBlockElementOf(this.sel().focusNode);
+306 		
+307 		// find bottom-most last block child
+308 		while(this.tree.isBlockContainer(block) && block.lastChild && this.tree.isBlock(block.lastChild)) {
+309 			block = block.lastChild;
+310 		}
+311 		
+312 		return block;
+313 	},
+314 
+315 	isCaretAtBlockStart: function() {
+316 		if(this.isCaretAtEmptyBlock()) return true;
+317 		if(this.hasSelection()) return false;
+318 		var rng = this.rng();
+319 		var node = this.getCurrentBlockElement();
+320 		var isTrue = false;
+321 		
+322 		if(node == rng.startContainer) {
+323 			var marker = this.pushMarker();
+324 			while (node = this.getFirstChild(node)) {
+325 				if (node == marker) {
+326 					isTrue = true;
+327 					break;
+328 				}
+329 			}
+330 			this.popMarker();
+331 		} else {
+332 			while (node = node.firstChild) {
+333 				if (node == rng.startContainer && rng.startOffset == 0) {
+334 					isTrue = true;
+335 					break;
+336 				}
+337 			}
+338 		}
+339 		
+340 		return isTrue;
+341 	},
+342 	
+343 	isCaretAtBlockEnd: function() {
+344 		if(this.isCaretAtEmptyBlock()) return true;
+345 		if(this.hasSelection()) return false;
+346 		
+347 		var rng = this.rng();
+348 		var node = this.getCurrentBlockElement();
+349 		var isTrue = false;
+350 		
+351 		if(node == rng.startContainer) {
+352 			var marker = this.pushMarker();
+353 			while (node = this.getLastChild(node)) {
+354 				if ((node == marker) || (this.isPlaceHolder(node) && node.previousSibling == marker)) {
+355 					isTrue = true;
+356 					break;
+357 				}
+358 			}
+359 			this.popMarker();
+360 		} else {
+361 			while (node = this.getLastChild(node)) {
+362 				if (node == rng.endContainer && rng.endContainer.nodeType == 1) {
+363 					isTrue = true;
+364 					break;
+365 				} else if (node == rng.endContainer && rng.endOffset == node.nodeValue.length) {
+366 					isTrue = true;
+367 					break;
+368 				}
+369 			}
+370 		}
+371 		
+372 		return isTrue;
+373 	}
+374 });
+375 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_10.html b/modules/editor/skins/xquared/doc/api/src_10.html new file mode 100644 index 000000000..9eb4955a5 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_10.html @@ -0,0 +1,91 @@ +
  1 /**
+  2  * RichDom for Webkit
+  3  */
+  4 xq.RichDomWebkit = Class.create(xq.RichDomW3, {
+  5 	makePlaceHolder: function() {
+  6 		var holder = this.createElement("BR");
+  7 		holder.className = "webkit-block-placeholder";
+  8 		return holder;
+  9 	},
+ 10 	
+ 11 	makePlaceHolderString: function() {
+ 12 		return '<br class="webkit-block-placeholder" />';
+ 13 	},
+ 14 	
+ 15 	makeEmptyParagraph: function() {
+ 16 		return this.createElementFromHtml('<p><br class="webkit-block-placeholder" /></p>');
+ 17 	},
+ 18 	
+ 19 	isPlaceHolder: function(node) {
+ 20 		return node.nodeName == "BR" && node.className == "webkit-block-placeholder";
+ 21 	},
+ 22 
+ 23 	rng: function() {
+ 24 		var sel = this.sel();
+ 25 		var rng = this.doc.createRange();
+ 26 		if (!this._rng ||
+ 27 			this._anchorNode != sel.anchorNode ||
+ 28 			this._anchorOffset != sel.anchorOffset ||
+ 29 			this._focusNode != sel.focusNode ||
+ 30 			this._focusOffset != sel.focusOffset ) {
+ 31 
+ 32 			if (sel.type != 'None') {
+ 33 				rng.setStart(sel.anchorNode, sel.anchorOffset);
+ 34 				rng.setEnd(sel.focusNode, sel.focusOffset);
+ 35 			}
+ 36 			this._anchorNode = sel.anchorNode;
+ 37 			this._anchorOffset = sel.anchorOffset;
+ 38 			this._focusNode = sel.focusNode;
+ 39 			this._focusOffset = sel.focusOffset;
+ 40 			this._rng = rng;
+ 41 		}
+ 42 		return this._rng;
+ 43 	},
+ 44 
+ 45 	selectElement: function(element, entireElement) {
+ 46 		if(!element) throw "[element] is null";
+ 47 		if(element.nodeType != 1) throw "[element] is not an element";
+ 48 		
+ 49 		var rng = this.rng();
+ 50 		if(entireElement) {
+ 51 			rng.selectNode(element);
+ 52 		} else {
+ 53 			rng.selectNodeContents(element);
+ 54 		}
+ 55 		this._setSelectionByRange(rng);
+ 56 	},
+ 57 
+ 58 	deleteSelection: function() {
+ 59 		this.rng().deleteContents();
+ 60 	},
+ 61 
+ 62 	collapseSelection: function(toStart) {
+ 63 		var rng = this.rng();
+ 64 		rng.collapse(toStart);
+ 65 		this._setSelectionByRange(rng);
+ 66 	},
+ 67 
+ 68 	getSelectionAsHtml: function() {
+ 69 		var container = this.createElement("div");
+ 70 		var rng = this.rng();
+ 71 		var contents = this.rng().cloneContents();
+ 72 		if(contents) container.appendChild(contents);
+ 73 		return container.innerHTML;
+ 74 	},
+ 75 	
+ 76 	_setSelectionByRange: function(rng) {
+ 77 		var sel = this.sel();
+ 78 		sel.setBaseAndExtent(rng.startContainer, rng.startOffset, rng.endContainer, rng.endOffset);
+ 79 		this._anchorNode = sel.anchorNode;
+ 80 		this._anchorOffset = sel.anchorOffset;
+ 81 		this._focusNode = sel.focusNode;
+ 82 		this._focusOffset = sel.focusOffset;
+ 83 	}
+ 84 });
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_11.html b/modules/editor/skins/xquared/doc/api/src_11.html new file mode 100644 index 000000000..58f5fb053 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_11.html @@ -0,0 +1,209 @@ +
  1 xq.RichTable = Class.create({
+  2 	initialize: function(rdom, table) {
+  3 		this.rdom = rdom;
+  4 		this.table = table;
+  5 	},
+  6 	insertNewRowAt: function(tr, where) {
+  7 		var row = this.rdom.createElement("TR");
+  8 		var cells = tr.cells;
+  9 		for(var i = 0; i < cells.length; i++) {
+ 10 			var cell = this.rdom.createElement(cells[i].nodeName);
+ 11 			this.rdom.correctEmptyElement(cell);
+ 12 			row.appendChild(cell);
+ 13 		}
+ 14 		return this.rdom.insertNodeAt(row, tr, where);
+ 15 	},
+ 16 	insertNewCellAt: function(cell, where) {
+ 17 		// collect cells;
+ 18 		var cells = [];
+ 19 		var x = this.getXIndexOf(cell);
+ 20 		var y = 0;
+ 21 		while(true) {
+ 22 			var cur = this.getCellAt(x, y);
+ 23 			if(!cur) break;
+ 24 			cells.push(cur);
+ 25 			y++;
+ 26 		}
+ 27 		
+ 28 		// insert new cells
+ 29 		for(var i = 0; i < cells.length; i++) {
+ 30 			var cell = this.rdom.createElement(cells[i].nodeName);
+ 31 			this.rdom.correctEmptyElement(cell);
+ 32 			this.rdom.insertNodeAt(cell, cells[i], where);
+ 33 		}
+ 34 	},
+ 35 	deleteRow: function(tr) {
+ 36 		return this.rdom.removeBlock(tr);
+ 37 	},
+ 38 	deleteCell: function(cell) {
+ 39 		if(!cell.previousSibling && !cell.nextSibling) {
+ 40 			this.rdom.deleteNode(this.table);
+ 41 			return;
+ 42 		}
+ 43 		
+ 44 		// collect cells;
+ 45 		var cells = [];
+ 46 		var x = this.getXIndexOf(cell);
+ 47 		var y = 0;
+ 48 		while(true) {
+ 49 			var cur = this.getCellAt(x, y);
+ 50 			if(!cur) break;
+ 51 			cells.push(cur);
+ 52 			y++;
+ 53 		}
+ 54 		
+ 55 		for(var i = 0; i < cells.length; i++) {
+ 56 			this.rdom.deleteNode(cells[i]);
+ 57 		}
+ 58 	},
+ 59 	getPreviousCellOf: function(cell) {
+ 60 		if(cell.previousSibling) return cell.previousSibling;
+ 61 		var adjRow = this.getPreviousRowOf(cell.parentNode);
+ 62 		if(adjRow) return adjRow.lastChild;
+ 63 		return null;
+ 64 	},
+ 65 	getNextCellOf: function(cell) {
+ 66 		if(cell.nextSibling) return cell.nextSibling;
+ 67 		var adjRow = this.getNextRowOf(cell.parentNode);
+ 68 		if(adjRow) return adjRow.firstChild;
+ 69 		return null;
+ 70 	},
+ 71 	getPreviousRowOf: function(row) {
+ 72 		if(row.previousSibling) return row.previousSibling;
+ 73 		var rowContainer = row.parentNode;
+ 74 		if(rowContainer.previousSibling && rowContainer.previousSibling.lastChild) return rowContainer.previousSibling.lastChild;
+ 75 		return null;
+ 76 	},
+ 77 	getNextRowOf: function(row) {
+ 78 		if(row.nextSibling) return row.nextSibling;
+ 79 		var rowContainer = row.parentNode;
+ 80 		if(rowContainer.nextSibling && rowContainer.nextSibling.firstChild) return rowContainer.nextSibling.firstChild;
+ 81 		return null;
+ 82 	},
+ 83 	getAboveCellOf: function(cell) {
+ 84 		var row = this.getPreviousRowOf(cell.parentNode);
+ 85 		if(!row) return null;
+ 86 		
+ 87 		var x = this.getXIndexOf(cell);
+ 88 		return row.cells[x];
+ 89 	},
+ 90 	getBelowCellOf: function(cell) {
+ 91 		var row = this.getNextRowOf(cell.parentNode);
+ 92 		if(!row) return null;
+ 93 		
+ 94 		var x = this.getXIndexOf(cell);
+ 95 		return row.cells[x];
+ 96 	},
+ 97 	getXIndexOf: function(cell) {
+ 98 		var row = cell.parentNode;
+ 99 		for(var i = 0; i < row.cells.length; i++) {
+100 			if(row.cells[i] == cell) return i;
+101 		}
+102 		
+103 		return -1;
+104 	},
+105 	getYIndexOf: function(cell) {
+106 		var y = -1;
+107 		
+108 		// find y
+109 		var group = row.parentNode;
+110 		for(var i = 0; i <group.rows.length; i++) {
+111 			if(group.rows[i] == row) {
+112 				y = i;
+113 				break;
+114 			}
+115 		}
+116 		if(this.hasHeadingAtTop() && group.nodeName == "TBODY") y = y + 1;
+117 		
+118 		return y;
+119 	},
+120 	/**
+121 	 * TODO: Not used. Delete or not?
+122 	 */
+123 	getLocationOf: function(cell) {
+124 		var x = this.getXIndexOf(cell);
+125 		var y = this.getYIndexOf(cell);
+126 		return {x:x, y:y};
+127 	},
+128 	getCellAt: function(col, row) {
+129 		var row = this.getRowAt(row);
+130 		return (row && row.cells.length > col) ? row.cells[col] : null;
+131 	},
+132 	getRowAt: function(index) {
+133 		if(this.hasHeadingAtTop()) {
+134 			return index == 0 ? this.table.tHead.rows[0] : this.table.tBodies[0].rows[index - 1];
+135 		} else {
+136 			var rows = this.table.tBodies[0].rows;
+137 			return (rows.length > index) ? rows[index] : null;
+138 		}
+139 	},
+140 	getDom: function() {
+141 		return this.table;
+142 	},
+143 	hasHeadingAtTop: function() {
+144 		return !!(this.table.tHead && this.table.tHead.rows[0]);
+145 	},
+146 	hasHeadingAtLeft: function() {
+147 		return this.table.tBodies[0].rows[0].cells[0].nodeName == "TH";
+148 	},
+149 	correctEmptyCells: function() {
+150 		var cells = $A(this.table.getElementsByTagName("TH"));
+151 		cells.push($A(this.table.getElementsByTagName("TD")));
+152 		cells = cells.flatten();
+153 		
+154 		for(var i = 0; i < cells.length; i++) {
+155 			if(this.rdom.isEmptyBlock(cells[i])) this.rdom.correctEmptyElement(cells[i])
+156 		}
+157 	}
+158 });
+159 
+160 xq.RichTable.create = function(rdom, cols, rows, headerPositions) {
+161 	if(["t", "tl", "lt"].include(headerPositions)) var headingAtTop = true
+162 	if(["l", "tl", "lt"].include(headerPositions)) var headingAtLeft = true
+163 
+164 	var sb = []
+165 	sb.push('<table class="datatable">')
+166 	
+167 	// thead
+168 	if(headingAtTop) {
+169 		sb.push('<thead><tr>')
+170 		for(var i = 0; i < cols; i++) sb.push('<th></th>')
+171 		sb.push('</tr></thead>')
+172 		rows -= 1
+173 	}
+174 		
+175 	// tbody
+176 	sb.push('<tbody>')
+177 	for(var i = 0; i < rows; i++) {
+178 		sb.push('<tr>')
+179 		
+180 		for(var j = 0; j < cols; j++) {
+181 			if(headingAtLeft && j == 0) {
+182 				sb.push('<th></th>')
+183 			} else {
+184 				sb.push('<td></td>')
+185 			}
+186 		}
+187 		
+188 		sb.push('</tr>')
+189 	}
+190 	sb.push('</tbody>')
+191 	
+192 	sb.push('</table>')
+193 	
+194 	// create DOM element
+195 	var container = rdom.createElement("div");
+196 	container.innerHTML = sb.join("");
+197 	
+198 	// correct empty cells and return
+199 	var rtable = new xq.RichTable(rdom, container.firstChild);
+200 	rtable.correctEmptyCells();
+201 	return rtable;
+202 }
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_12.html b/modules/editor/skins/xquared/doc/api/src_12.html new file mode 100644 index 000000000..72b700cd4 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_12.html @@ -0,0 +1,135 @@ +
  1 xq.Shortcut = Class.create({
+  2 	initialize: function(keymapOrExpression) {
+  3 		this.keymap = (typeof keymapOrExpression == "string") ?
+  4 			xq.Shortcut.interprete(keymapOrExpression).keymap :
+  5 			keymapOrExpression;
+  6 	},
+  7 	matches: function(e) {
+  8 		var which = xq.Browser.isGecko && xq.Browser.isMac ? (e.keyCode + "_" + e.charCode) : e.keyCode;
+  9 		
+ 10 		var keyMatches =
+ 11 			(this.keymap.which == which) ||
+ 12 			(this.keymap.which == 32 && which == 25); // 25 is SPACE in Type-3 keyboard.
+ 13 		
+ 14 		if(typeof e.metaKey == "undefined") e.metaKey = false;
+ 15 		
+ 16 		var modifierMatches = 
+ 17 			(typeof this.keymap.shiftKey == "undefined" || this.keymap.shiftKey == e.shiftKey) &&
+ 18 			(typeof this.keymap.altKey == "undefined" || this.keymap.altKey == e.altKey) &&
+ 19 			(typeof this.keymap.ctrlKey == "undefined" || this.keymap.ctrlKey == e.ctrlKey) &&
+ 20 			(typeof this.keymap.metaKey == "undefined" || this.keymap.metaKey == e.metaKey)
+ 21 		
+ 22 		return modifierMatches && keyMatches;
+ 23 	}
+ 24 });
+ 25 
+ 26 xq.Shortcut.interprete = function(expression) {
+ 27 	expression = expression.toUpperCase();
+ 28 	
+ 29 	var which = xq.Shortcut._interpreteWhich(expression.split("+").pop());
+ 30 	var ctrlKey = xq.Shortcut._interpreteModifier(expression, "CTRL");
+ 31 	var altKey = xq.Shortcut._interpreteModifier(expression, "ALT");
+ 32 	var shiftKey = xq.Shortcut._interpreteModifier(expression, "SHIFT");
+ 33 	var metaKey = xq.Shortcut._interpreteModifier(expression, "META");
+ 34 	
+ 35 	var keymap = {};
+ 36 	
+ 37 	keymap.which = which;
+ 38 	if(typeof ctrlKey != "undefined") keymap.ctrlKey = ctrlKey;
+ 39 	if(typeof altKey != "undefined") keymap.altKey = altKey;
+ 40 	if(typeof shiftKey != "undefined") keymap.shiftKey = shiftKey;
+ 41 	if(typeof metaKey != "undefined") keymap.metaKey = metaKey;
+ 42 	
+ 43 	return new xq.Shortcut(keymap);
+ 44 }
+ 45 
+ 46 xq.Shortcut._interpreteModifier = function(expression, modifierName) {
+ 47 	return expression.match("\\(" + modifierName + "\\)") ?
+ 48 		undefined :
+ 49 			expression.match(modifierName) ?
+ 50 			true : false;
+ 51 }
+ 52 xq.Shortcut._interpreteWhich = function(keyName) {
+ 53 	var which = keyName.length == 1 ?
+ 54 		((xq.Browser.isMac && xq.Browser.isGecko) ? "0_" + keyName.toLowerCase().charCodeAt(0) : keyName.charCodeAt(0)) :
+ 55 		xq.Shortcut._keyNames[keyName];
+ 56 	
+ 57 	if(typeof which == "undefined") throw "Unknown special key name: [" + keyName + "]"
+ 58 	
+ 59 	return which;
+ 60 }
+ 61 xq.Shortcut._keyNames =
+ 62 	xq.Browser.isMac && xq.Browser.isGecko ?
+ 63 	{
+ 64 		BACKSPACE: "8_0",
+ 65 		TAB: "9_0",
+ 66 		RETURN: "13_0",
+ 67 		ENTER: "13_0",
+ 68 		ESC: "27_0",
+ 69 		SPACE: "0_32",
+ 70 		LEFT: "37_0",
+ 71 		UP: "38_0",
+ 72 		RIGHT: "39_0",
+ 73 		DOWN: "40_0",
+ 74 		DELETE: "46_0",
+ 75 		HOME: "36_0",
+ 76 		END: "35_0",
+ 77 		PAGEUP: "33_0",
+ 78 		PAGEDOWN: "34_0",
+ 79 		COMMA: "0_44",
+ 80 		HYPHEN: "0_45",
+ 81 		EQUAL: "0_61",
+ 82 		PERIOD: "0_46",
+ 83 		SLASH: "0_47",
+ 84 		F1: "112_0",
+ 85 		F2: "113_0",
+ 86 		F3: "114_0",
+ 87 		F4: "115_0",
+ 88 		F5: "116_0",
+ 89 		F6: "117_0",
+ 90 		F7: "118_0",
+ 91 		F8: "119_0"
+ 92 	}
+ 93 	:
+ 94 	{
+ 95 		BACKSPACE: 8,
+ 96 		TAB: 9,
+ 97 		RETURN: 13,
+ 98 		ENTER: 13,
+ 99 		ESC: 27,
+100 		SPACE: 32,
+101 		LEFT: 37,
+102 		UP: 38,
+103 		RIGHT: 39,
+104 		DOWN: 40,
+105 		DELETE: 46,
+106 		HOME: 36,
+107 		END: 35,
+108 		PAGEUP: 33,
+109 		PAGEDOWN: 34,
+110 		COMMA: 188,
+111 		HYPHEN: xq.Browser.isTrident ? 189 : 109,
+112 		EQUAL: xq.Browser.isTrident ? 187 : 61,
+113 		PERIOD: 190,
+114 		SLASH: 191,
+115 		F1:112,
+116 		F2:113,
+117 		F3:114,
+118 		F4:115,
+119 		F5:116,
+120 		F6:117,
+121 		F7:118,
+122 		F8:119,
+123 		F9:120,
+124 		F10:121,
+125 		F11:122,
+126 		F12:123
+127 	}
+128 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_13.html b/modules/editor/skins/xquared/doc/api/src_13.html new file mode 100644 index 000000000..e2311e8a9 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_13.html @@ -0,0 +1,235 @@ +
  1 /**
+  2  * Validates and invalidates designmode contents
+  3  */
+  4 xq.Validator = Class.create({
+  5 	initialize: function(curUrl, urlValidationMode, allowedTags, allowedAttrs) {
+  6 		this.allowedTags = (allowedTags || ['a', 'abbr', 'acronym', 'address', 'blockquote', 'br', 'caption', 'cite', 'code', 'dd', 'dfn', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'span', 'sup', 'sub', 'strong', 'table', 'thead', 'tbody', 'td', 'th', 'tr', 'ul', 'var']).join(' ') + ' ';
+  7 		this.allowedAttrs = (allowedAttrs || ['alt', 'cite', 'class', 'datetime', 'height', 'href', 'id', 'rel', 'rev', 'src', 'style', 'title', 'width']).join(' ') + ' ';
+  8 		
+  9 		this.curUrl = curUrl;
+ 10 		this.curUrlParts = curUrl ? curUrl.parseURL() : null;
+ 11 		this.urlValidationMode = urlValidationMode;
+ 12 	},
+ 13 	
+ 14 	/**
+ 15 	 * Perform validation on given element
+ 16 	 *
+ 17 	 * @param {Element} element Target element. It is not affected by validation.
+ 18 	 * @param {boolean} fullValidation Perform full validation. If you just want to use the result to assign innerHTML, set it false
+ 19 	 *
+ 20 	 * @returns {String} Validated HTML string
+ 21 	 */
+ 22 	validate: function(element, fullValidation) {throw "Not implemented"},
+ 23 	
+ 24 	/**
+ 25 	 * Perform invalidation on given element to make the designmode works well.
+ 26 	 *
+ 27 	 * @param {Element} element Target element.
+ 28 	 * @returns {String} Invalidated HTML string
+ 29 	 */
+ 30 	invalidate: function(element) {throw "Not implemented"},
+ 31 	
+ 32 	validateStrike: function(content) {
+ 33 		content = content.replace(/<strike(>|\s+[^>]*>)/ig, "<span class=\"strike\"$1");
+ 34 		content = content.replace(/<\/strike>/ig, "</span>");
+ 35 		return content;
+ 36 	},
+ 37 	
+ 38 	validateUnderline: function(content) {
+ 39 		content = content.replace(/<u(>|\s+[^>]*>)/ig, "<em class=\"underline\"$1");
+ 40 		content = content.replace(/<\/u>/ig, "</em>");
+ 41 		return content;
+ 42 	},
+ 43 	
+ 44 	replaceTag: function(content, from, to) {
+ 45 		return content.replace(new RegExp("(</?)" + from + "(>|\\s+[^>]*>)", "ig"), "$1" + to + "$2");
+ 46 	},
+ 47 	
+ 48 	validateSelfClosingTags: function(content) {
+ 49 		return content.replace(/<(br|hr|img)([^>]*?)>/img, function(str, tag, attrs) {
+ 50 			return "<" + tag + attrs + " />"
+ 51 		});
+ 52 	},
+ 53 	
+ 54 	removeComments: function(content) {
+ 55 		return content.replace(/<!--.*?-->/img, '');
+ 56 	},
+ 57 	
+ 58 	removeDangerousElements: function(element) {
+ 59 		var scripts = $A(element.getElementsByTagName('SCRIPT')).reverse();
+ 60 		for(var i = 0; i < scripts.length; i++) {
+ 61 			scripts[i].parentNode.removeChild(scripts[i]);
+ 62 		}
+ 63 	},
+ 64 
+ 65 	// TODO: very slow
+ 66 	applyWhitelist: function(content) {
+ 67 		var allowedTags = this.allowedTags;
+ 68 		var allowedAttrs = this.allowedAttrs;
+ 69 		
+ 70 		return content.replace(new RegExp("(</?)([^>]+?)(>|\\s+([^>]*?)(\\s?/?)>)", "g"), function(str, head, tag, tail, attrs, selfClosing) {
+ 71 			if(allowedTags.indexOf(tag) == -1) return '';
+ 72 			
+ 73 			if(attrs) {
+ 74 				attrs = attrs.replace(/(^|\s")([^"=]+)(\s|$)/g, '$1$2="$2"$3'); // for IE
+ 75 				
+ 76 				var sb = [];
+ 77 				var m = attrs.match(/([^=]+)="[^"]*?"/g);
+ 78 				for(var i = 0; i < m.length; i++) {
+ 79 					m[i] = m[i].strip();
+ 80 					var name = m[i].split('=')[0];
+ 81 					if(allowedAttrs.indexOf(name) != -1) sb.push(m[i]);
+ 82 				}
+ 83 				attrs = sb.join(' ');
+ 84 				if(attrs != '') attrs = ' ' + attrs;
+ 85 				return head + tag + attrs + selfClosing + '>';
+ 86 			} else {
+ 87 				return str;
+ 88 			}
+ 89 		});
+ 90 	},
+ 91 	
+ 92 	makeUrlsRelative: function(content) {
+ 93 		var curUrl = this.curUrl;
+ 94 		var urlParts = this.curUrlParts;
+ 95 		
+ 96 		// 1. find attributes and...
+ 97 		return content.replace(/(<\w+\s+)(\/|([^>]+?)(\/?))>/g, function(str, head, ignored, attrs, tail) {
+ 98 			if(attrs) {
+ 99 				// 2. validate URL part
+100 				attrs = attrs.replace(/(href|src)="([^"]+)"/g, function(str, name, url) {
+101 					// 3. first, make it absolute
+102 					var abs = null;
+103 					if(url.charAt(0) == '#') {
+104 						abs = urlParts.includeQuery + url;
+105 					} else if(url.charAt(0) == '?') {
+106 						abs = urlParts.includePath + url;
+107 					} else if(url.charAt(0) == '/') {
+108 						abs = urlParts.includeHost + url;
+109 					} else if(url.match(/^\w+:\/\//)) {
+110 						abs = url;
+111 					} else {
+112 						abs = urlParts.includeBase + url;
+113 					}
+114 					
+115 					// 4. make it relative by removing same part
+116 					var rel = abs;
+117 					
+118 					if(abs.indexOf(urlParts.includeQuery) == 0) {
+119 						rel = abs.substring(urlParts.includeQuery.length);
+120 					} else if(abs.indexOf(urlParts.includePath) == 0) {
+121 						rel = abs.substring(urlParts.includePath.length);
+122 					} else if(abs.indexOf(urlParts.includeBase) == 0) {
+123 						rel = abs.substring(urlParts.includeBase.length);
+124 					} else if(abs.indexOf(urlParts.includeHost) == 0) {
+125 						rel = abs.substring(urlParts.includeHost.length);
+126 					}
+127 					if(rel == '') rel = '#';
+128 					
+129 					return name + '="' + rel + '"';
+130 				});
+131 				
+132 				return head + attrs + tail + '>';
+133 			} else {
+134 				return str;
+135 			}
+136 		});
+137 		
+138 		return content;
+139 	},
+140 	
+141 	makeUrlsHostRelative: function(content) {
+142 		var curUrl = this.curUrl;
+143 		var urlParts = this.curUrlParts;
+144 		
+145 		// 1. find attributes and...
+146 		return content.replace(/(<\w+\s+)(\/|([^>]+?)(\/?))>/g, function(str, head, ignored, attrs, tail) {
+147 			if(attrs) {
+148 				// 2. validate URL part
+149 				attrs = attrs.replace(/(href|src)="([^"]+)"/g, function(str, name, url) {
+150 					// 3. first, make it absolute
+151 					var abs = null;
+152 					if(url.charAt(0) == '#') {
+153 						abs = urlParts.includeQuery + url;
+154 					} else if(url.charAt(0) == '?') {
+155 						abs = urlParts.includePath + url;
+156 					} else if(url.charAt(0) == '/') {
+157 						abs = urlParts.includeHost + url;
+158 					} else if(url.match(/^\w+:\/\//)) {
+159 						abs = url;
+160 					} else {
+161 						abs = urlParts.includeBase + url;
+162 					}
+163 					
+164 					// 4. make it relative by removing same part
+165 					var rel = abs;
+166 					if(abs.indexOf(urlParts.includeHost) == 0) {
+167 						rel = abs.substring(urlParts.includeHost.length);
+168 					}
+169 					if(rel == '') rel = '#';
+170 					
+171 					return name + '="' + rel + '"';
+172 				});
+173 				
+174 				return head + attrs + tail + '>';
+175 			} else {
+176 				return str;
+177 			}
+178 		});
+179 		
+180 		return content;
+181 	},
+182 	
+183 	makeUrlsAbsolute: function(content) {
+184 		var curUrl = this.curUrl;
+185 		var urlParts = this.curUrlParts;
+186 		
+187 		// 1. find attributes and...
+188 		return content.replace(/(<\w+\s+)(\/|([^>]+?)(\/?))>/g, function(str, head, ignored, attrs, tail) {
+189 			if(attrs) {
+190 				// 2. validate URL part
+191 				attrs = attrs.replace(/(href|src)="([^"]+)"/g, function(str, name, url) {
+192 					var abs = null;
+193 					if(url.charAt(0) == '#') {
+194 						abs = urlParts.includeQuery + url;
+195 					} else if(url.charAt(0) == '?') {
+196 						abs = urlParts.includePath + url;
+197 					} else if(url.charAt(0) == '/') {
+198 						abs = urlParts.includeHost + url;
+199 					} else if(url.match(/^\w+:\/\//)) {
+200 						abs = url;
+201 					} else {
+202 						abs = urlParts.includeBase + url;
+203 					}
+204 
+205 					return name + '="' + abs + '"';
+206 				});
+207 				
+208 				return head + attrs + tail + '>';
+209 			} else {
+210 				return str;
+211 			}
+212 		});
+213 	}
+214 });
+215 
+216 /**
+217  * Creates and returns instance of browser specific implementation.
+218  */
+219 xq.Validator.createInstance = function(curUrl, urlValidationMode, allowedTags, allowedAttrs) {
+220 	if(xq.Browser.isTrident) {
+221 		return new xq.ValidatorTrident(curUrl, urlValidationMode, allowedTags, allowedAttrs);
+222 	} else if(xq.Browser.isWebkit) {
+223 		return new xq.ValidatorWebkit(curUrl, urlValidationMode, allowedTags, allowedAttrs);
+224 	} else {
+225 		return new xq.ValidatorGecko(curUrl, urlValidationMode, allowedTags, allowedAttrs);
+226 	}
+227 }
+228 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_14.html b/modules/editor/skins/xquared/doc/api/src_14.html new file mode 100644 index 000000000..9de6a044d --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_14.html @@ -0,0 +1,12 @@ +
  1 /**
+  2  * Validator for Gecko Engine
+  3  */
+  4 xq.ValidatorGecko = Class.create(xq.ValidatorW3, {
+  5 });
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_15.html b/modules/editor/skins/xquared/doc/api/src_15.html new file mode 100644 index 000000000..a67012aa6 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_15.html @@ -0,0 +1,147 @@ +
  1 /**
+  2  * Validator for Internet Explorer 6 and 7
+  3  */
+  4 xq.ValidatorTrident = Class.create(xq.Validator, {
+  5 	validate: function(element, fullValidation) {
+  6 		element = element.cloneNode(true);
+  7 		
+  8 		this.removeDangerousElements(element);
+  9 		this.validateFontColor(element);
+ 10 		this.validateBackgroundColor(element);
+ 11 		
+ 12 		var content = element.innerHTML;
+ 13 		
+ 14 		try {
+ 15 			content = this.validateStrike(content);
+ 16 			content = this.validateUnderline(content);
+ 17 			
+ 18 			if(fullValidation) content = this.performFullValidation(content);
+ 19 		} catch(ignored) {}
+ 20 		
+ 21 		return content;
+ 22 	},
+ 23 	
+ 24 	invalidate: function(element) {
+ 25 		var rdom = xq.RichDom.createInstance();
+ 26 		rdom.setRoot(element);
+ 27 		
+ 28 		this.invalidateFontColor(element);
+ 29 		this.invalidateBackgroundColor(element);
+ 30 		
+ 31 		// <span class="strike"> -> <strike>
+ 32 		var strikes = rdom.findByAttribute("className", "strike");
+ 33 		for(var i = 0; i < strikes.length; i++) {
+ 34 			if("SPAN" == strikes[i].nodeName) rdom.replaceTag("strike", strikes[i]).removeAttribute("className");
+ 35 		}
+ 36 		
+ 37 		// <em|i class="underline"> -> <u>
+ 38 		var underlines = rdom.findByAttribute("className", "underline");
+ 39 		for(var i = 0; i < underlines.length; i++) {
+ 40 			if(["EM", "I"].include(underlines[i].nodeName)) rdom.replaceTag("u", underlines[i]).removeAttribute("className");
+ 41 		}
+ 42 
+ 43 		var content = rdom.getRoot().innerHTML;
+ 44 
+ 45 		content = this.removeComments(content);
+ 46 		
+ 47 		return content;
+ 48 	},
+ 49 	
+ 50 	performFullValidation: function(content) {
+ 51 		content = this.lowerTagNamesAndUniformizeQuotation(content);
+ 52 		content = this.validateSelfClosingTags(content);
+ 53 		content = this.applyWhitelist(content);
+ 54 		
+ 55 		if(this.urlValidationMode == 'relative') {
+ 56 			content = this.makeUrlsRelative(content);
+ 57 		} else if(this.urlValidationMode == 'host_relative') {
+ 58 			content = this.makeUrlsHostRelative(content);
+ 59 		} else if(this.urlValidationMode == 'absolute') {
+ 60 			// Trident always use absolute URL so we don't need to do anything.
+ 61 			//
+ 62 			// content = this.makeUrlsAbsolute(content);
+ 63 		}
+ 64 		
+ 65 		return content;
+ 66 	},
+ 67 	
+ 68 	validateFontColor: function(element) {
+ 69 		var rdom = xq.RichDom.createInstance();
+ 70 		rdom.setRoot(element);
+ 71 		
+ 72 		// It should be reversed to deal with nested elements
+ 73 		var fonts = $A(element.getElementsByTagName('FONT')).reverse();
+ 74 		for(var i = 0; i < fonts.length; i++) {
+ 75 			var font = fonts[i];
+ 76 			var color = font.getAttribute('color');
+ 77 			
+ 78 			if(color) {
+ 79 				var span = rdom.replaceTag("span", font);
+ 80 				span.removeAttribute('color');
+ 81 				span.style.color = color;
+ 82 			}
+ 83 		}
+ 84 	},
+ 85 
+ 86 	invalidateFontColor: function(element) {
+ 87 		var rdom = xq.RichDom.createInstance();
+ 88 		rdom.setRoot(element);
+ 89 
+ 90 		var spans = $A(element.getElementsByTagName('SPAN')).reverse();
+ 91 		for(var i = 0; i < spans.length; i++) {
+ 92 			var span = spans[i];
+ 93 			var color = span.style.color;
+ 94 			
+ 95 			if(color) {
+ 96 				var font = rdom.replaceTag("font", span);
+ 97 				font.style.color = "";
+ 98 				font.setAttribute('color', color);
+ 99 			}
+100 		}
+101 	},
+102 
+103 	validateBackgroundColor: function(element) {
+104 		var rdom = xq.RichDom.createInstance();
+105 		rdom.setRoot(element);
+106 
+107 		// It should be reversed to deal with nested elements
+108 		var fonts = $A(element.getElementsByTagName('FONT')).reverse();
+109 		for(var i = 0; i < fonts.length; i++) {
+110 			if(fonts[i].style.color || fonts[i].style.backgroundColor) rdom.replaceTag("span", fonts[i]);
+111 		}
+112 	},
+113 
+114 	invalidateBackgroundColor: function(element) {
+115 		var rdom = xq.RichDom.createInstance();
+116 		rdom.setRoot(element);
+117 
+118 		// It should be reversed to deal with nested elements
+119 		var spans = $A(element.getElementsByTagName('SPAN')).reverse();
+120 		for(var i = 0; i < spans.length; i++) {
+121 			if(spans[i].style.color || spans[i].style.backgroundColor) rdom.replaceTag("font", spans[i]);
+122 		}
+123 	},
+124 	
+125 	lowerTagNamesAndUniformizeQuotation: function(content) {
+126 		// Uniformize quotation, turn tag names and attribute names into lower case
+127 		content = content.replace(/<(\/?)(\w+)([^>]*?)>/img, function(str, closingMark, tagName, attrs) {
+128 			return "<" + closingMark + tagName.toLowerCase() + this.correctHtmlAttrQuotation(attrs) + ">";
+129 		}.bind(this));
+130 		
+131 		return content;
+132 	},
+133 	
+134 	correctHtmlAttrQuotation: function(html) {
+135 		html = html.replace(/\s(\w+?)=\s+"([^"]+)"/mg,function (str, name, value) {return " " + name.toLowerCase() + '=' + '"' + value + '"'});
+136 		html = html.replace(/\s(\w+?)=([^ "]+)/mg,function (str, name, value) {return " " + name.toLowerCase() + '=' + '"' + value + '"'});
+137 		return html;
+138 	}
+139 });
+140 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_16.html b/modules/editor/skins/xquared/doc/api/src_16.html new file mode 100644 index 000000000..eacdef1b9 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_16.html @@ -0,0 +1,112 @@ +
  1 /**
+  2  * Validator for W3C Standard Engine
+  3  */
+  4 xq.ValidatorW3 = Class.create(xq.Validator, {
+  5 	validate: function(element, fullValidation) {
+  6 		element = element.cloneNode(true);
+  7 
+  8 		var rdom = xq.RichDom.createInstance();
+  9 		rdom.setRoot(element);
+ 10 		rdom.removePlaceHoldersAndEmptyNodes(element);
+ 11 		this.removeDangerousElements(element);
+ 12 		this.validateFontColor(element);
+ 13 
+ 14 		var content = element.innerHTML;
+ 15 		
+ 16 		try {
+ 17 			content = this.replaceTag(content, "b", "strong");
+ 18 			content = this.replaceTag(content, "i", "em");
+ 19 			
+ 20 			content = this.validateStrike(content);
+ 21 			content = this.validateUnderline(content);
+ 22 			content = this.addNbspToEmptyBlocks(content);
+ 23 			
+ 24 			if(fullValidation) content = this.performFullValidation(content);
+ 25 		} catch(ignored) {}
+ 26 
+ 27 		// insert newline between block-tags
+ 28 		var blocks = rdom.tree.getBlockTags().join("|");
+ 29 		var regex = new RegExp("</(" + blocks + ")>([^\n])", "img");
+ 30 		content = content.replace(regex, '</$1>\n$2');
+ 31 		
+ 32 		return content;
+ 33 	},
+ 34 	invalidate: function(element) {
+ 35 		var rdom = xq.RichDom.createInstance();
+ 36 		rdom.setRoot(element);
+ 37 		
+ 38 		// <span class="strike"> -> <strike>
+ 39 		var strikes = rdom.findByAttribute("class", "strike");
+ 40 		for(var i = 0; i < strikes.length; i++) {
+ 41 			if("SPAN" == strikes[i].nodeName) rdom.replaceTag("strike", strikes[i]).removeAttribute("class");
+ 42 		}
+ 43 		
+ 44 		// <em|i class="underline"> -> <u>
+ 45 		var underlines = rdom.findByAttribute("class", "underline");
+ 46 		for(var i = 0; i < underlines.length; i++) {
+ 47 			if(["EM", "I"].include(underlines[i].nodeName)) rdom.replaceTag("u", underlines[i]).removeAttribute("class");
+ 48 		}
+ 49 		
+ 50 		var content = rdom.getRoot().innerHTML;
+ 51 		
+ 52 		content = this.replaceTag(content, "strong", "b");
+ 53 		content = this.replaceTag(content, "em", "i");
+ 54 		content = this.removeComments(content);
+ 55 		content = this.replaceNbspToBr(content);
+ 56 		
+ 57 		return content;
+ 58 	},
+ 59 	
+ 60 	performFullValidation: function(content) {
+ 61 		content = this.validateSelfClosingTags(content);
+ 62 		content = this.applyWhitelist(content);
+ 63 		
+ 64 		if(this.urlValidationMode == 'relative') {
+ 65 			content = this.makeUrlsRelative(content);
+ 66 		} else if(this.urlValidationMode == 'host_relative') {
+ 67 			content = this.makeUrlsHostRelative(content);
+ 68 		} else if(this.urlValidationMode == 'absolute') {
+ 69 			content = this.makeUrlsAbsolute(content);
+ 70 		}
+ 71 
+ 72 		return content;
+ 73 	},
+ 74 	
+ 75 	validateFontColor: function(element) {
+ 76 		var rdom = xq.RichDom.createInstance();
+ 77 		rdom.setRoot(element);
+ 78 
+ 79 		var fonts = $A(element.getElementsByTagName('FONT')).reverse();
+ 80 		for(var i = 0; i < fonts.length; i++) {
+ 81 			var font = fonts[i];
+ 82 			var color = font.getAttribute('color');
+ 83 			
+ 84 			if(color) {
+ 85 				var span = rdom.replaceTag("span", font);
+ 86 				span.removeAttribute('color');
+ 87 				span.style.color = color;
+ 88 			}
+ 89 		}
+ 90 	},
+ 91 	
+ 92 	addNbspToEmptyBlocks: function(content) {
+ 93 		var blocks = new xq.DomTree().getBlockTags().join("|");
+ 94 		var regex = new RegExp("<(" + blocks + ")>\\s*?</(" + blocks + ")>", "img");
+ 95 		return content.replace(regex, '<$1> </$2>');
+ 96 	},
+ 97 	
+ 98 	replaceNbspToBr: function(content) {
+ 99 		var blocks = new xq.DomTree().getBlockTags().join("|");
+100 		var regex = new RegExp("<(" + blocks + ")>( )?</(" + blocks + ")>", "img");
+101 		var rdom = xq.RichDom.createInstance();
+102 		return content.replace(regex, '<$1>' + rdom.makePlaceHolderString() + '</$3>');
+103 	}
+104 });
+105 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_17.html b/modules/editor/skins/xquared/doc/api/src_17.html new file mode 100644 index 000000000..b07ebbc67 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_17.html @@ -0,0 +1,21 @@ +
  1 /**
+  2  * Validator for Webkit
+  3  */
+  4 xq.ValidatorWebkit = Class.create(xq.ValidatorW3, {
+  5 });
+  6 
+  7 /*
+  8 if(node.nodeName == "SPAN" && node.className == "Apple-style-span" && node.style.fontStyle == "italic") em = true;
+  9 if(node.nodeName == "SPAN" && node.className == "Apple-style-span" && node.style.fontWeight == "bold") strong = true;
+ 10 if(node.nodeName == "SPAN" && node.className == "Apple-style-span" && node.style.textDecoration == "line-through") strike = true;
+ 11 if(node.nodeName == "SPAN" && node.className == "Apple-style-span" && node.style.textDecoration == "underline") underline = true;
+ 12 if(node.nodeName == "SPAN" && node.className == "Apple-style-span" && node.style.cssText.indexOf("vertical-align: super;") != -1) superscription = true;
+ 13 if(node.nodeName == "SPAN" && node.className == "Apple-style-span" && node.style.cssText.indexOf("vertical-align: sub;") != -1) subscription = true;
+ 14 */
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_18.html b/modules/editor/skins/xquared/doc/api/src_18.html new file mode 100644 index 000000000..5fa6b1885 --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_18.html @@ -0,0 +1,157 @@ +
  1 /**
+  2  * Namespace for entire Xquared classes
+  3  */
+  4 var xq = {
+  5 	majorVersion: '0.1',
+  6 	minorVersion: '2007119'
+  7 };
+  8 
+  9 /**
+ 10  * Make given object as event source
+ 11  *
+ 12  * @param {Object} object target object
+ 13  * @param {String} prefix prefix for generated functions
+ 14  * @param {Array} events array of string which contains name of events
+ 15  */
+ 16 xq.asEventSource = function(object, prefix, events) {
+ 17 	object._listeners = []
+ 18 	object._registerEventFirer = function(prefix, name) {
+ 19 		this["_fireOn" + name] = function() {
+ 20 			for(var i = 0; i < this._listeners.length; i++) {
+ 21 				var listener = this._listeners[i];
+ 22 				var func = listener["on" + prefix + name];
+ 23 				if(func) func.apply(listener, $A(arguments));
+ 24 			}
+ 25 		}
+ 26 	}
+ 27 	object.addListener = function(l) {
+ 28 		this._listeners.push(l);
+ 29 	}
+ 30 	
+ 31 	for(var i = 0; i < events.length; i++) {
+ 32 		object._registerEventFirer(prefix, events[i]);
+ 33 	}
+ 34 }
+ 35 
+ 36 /**
+ 37  * Returns the index of given element
+ 38  *
+ 39  * @returns {Number} index or -1
+ 40  */
+ 41 Array.prototype.indexOf = function(n) {
+ 42 	for(var i = 0; i < this.length; i++) {
+ 43 		if(this[i] == n) return i;
+ 44 	}
+ 45 	
+ 46 	return -1;
+ 47 }
+ 48 
+ 49 Date.preset = null;
+ 50 Date.pass = function(msec) {
+ 51 	if(Date.preset == null) return;
+ 52 	Date.preset = new Date(Date.preset.getTime() + msec);
+ 53 }
+ 54 Date.get = function() {
+ 55 	return Date.preset == null ? new Date() : Date.preset;
+ 56 }
+ 57 Date.prototype.elapsed = function(msec) {
+ 58 	return Date.get().getTime() - this.getTime() >= msec;
+ 59 }
+ 60 
+ 61 String.prototype.merge = function(data) {
+ 62 	var newString = this;
+ 63 	for(k in data) {
+ 64 		newString = newString.replace("{" + k + "}", data[k]);
+ 65 	}
+ 66 	return newString;
+ 67 }
+ 68 
+ 69 String.prototype.parseURL = function() {
+ 70 	var m = this.match(/((((\w+):\/\/(((([^@:]+)(:([^@]+))?)@)?([^:\/\?#]+)?(:(\d+))?))?([^\?#]+)?)(\?([^#]+))?)(#(.+))?/);
+ 71 	
+ 72 	var includeAnchor = m[0];
+ 73 	var includeQuery = m[1] || undefined;
+ 74 	var includePath = m[2] || undefined;
+ 75 	var includeHost = m[3] || undefined;
+ 76 	var includeBase = null;
+ 77 	var protocol = m[4] || undefined;
+ 78 	var user = m[8] || undefined;
+ 79 	var password = m[10] || undefined;
+ 80 	var domain = m[11] || undefined;
+ 81 	var port = m[13] || undefined;
+ 82 	var path = m[14] || undefined;
+ 83 	var query = m[16] || undefined;
+ 84 	var anchor = m[18] || undefined;
+ 85 	
+ 86 	if(!path || path == '/') {
+ 87 		includeBase = includeHost + '/';
+ 88 	} else {
+ 89 		var index = path.lastIndexOf('/');
+ 90 		includeBase = includeHost + path.substring(0, index + 1);
+ 91 	}
+ 92 	
+ 93 	return {
+ 94 		includeAnchor: includeAnchor,
+ 95 		includeQuery: includeQuery,
+ 96 		includePath: includePath,
+ 97 		includeBase: includeBase,
+ 98 		includeHost: includeHost,
+ 99 		protocol: protocol,
+100 		user: user,
+101 		password: password,
+102 		domain: domain,
+103 		port: port,
+104 		path: path,
+105 		query: query,
+106 		anchor: anchor
+107 	};
+108 }
+109 
+110 xq.findXquaredScript = function() {
+111     return $A(document.getElementsByTagName("script")).find(function(script) {
+112     	return script.src && script.src.match(/xquared\.js/i);
+113     });
+114 }
+115 xq.shouldLoadOthers = function() {
+116 	var script = xq.findXquaredScript();
+117     return script && !!script.src.match(/xquared\.js\?load_others=1/i);
+118 }
+119 xq.loadScript = function(url) {
+120     document.write('<script type="text/javascript" src="' + url + '"></script>');
+121 }
+122 xq.loadOthers = function() {
+123 	var script = xq.findXquaredScript();
+124 	var basePath = script.src.match(/(.*\/)xquared\.js.*/i)[1];
+125 	var others = [
+126 		'Editor.js',
+127 		'Browser.js',
+128 		'Shortcut.js',
+129 		'DomTree.js',
+130 		'RichDom.js',
+131 		'RichDomW3.js',
+132 		'RichDomGecko.js',
+133 		'RichDomWebkit.js',
+134 		'RichDomTrident.js',
+135 		'RichTable.js',
+136 		'Validator.js',
+137 		'ValidatorW3.js',
+138 		'ValidatorGecko.js',
+139 		'ValidatorWebkit.js',
+140 		'ValidatorTrident.js',
+141 		'EditHistory.js',
+142 		'Controls.js',
+143 		'_ui_templates.js'
+144 	];
+145 	others.each(function(name) {
+146 		xq.loadScript(basePath + name);
+147 	});
+148 }
+149 
+150 if(xq.shouldLoadOthers()) xq.loadOthers();
\ No newline at end of file diff --git a/modules/editor/skins/xquared/doc/api/src_19.html b/modules/editor/skins/xquared/doc/api/src_19.html new file mode 100644 index 000000000..feb2f6a4e --- /dev/null +++ b/modules/editor/skins/xquared/doc/api/src_19.html @@ -0,0 +1,16 @@ +
  1 if(!xq) xq = {};
+  2 if(!xq.ui_templates) xq.ui_templates = {};
+  3 
+  4 xq.ui_templates.basicColorPickerDialog='<form action="#" class="xqFormDialog xqBasicColorPickerDialog">\n		<div>\n			<label>\n				<input type="radio" class="initialFocus" name="color" value="black" checked="checked" />\n				<span style="color: black;">Black</span>\n			</label>\n			<label>\n				<input type="radio" name="color" value="red" />\n				<span style="color: red;">Red</span>\n			</label>\n				<input type="radio" name="color" value="yellow" />\n				<span style="color: yellow;">Yellow</span>\n			</label>\n			</label>\n				<input type="radio" name="color" value="pink" />\n				<span style="color: pink;">Pink</span>\n			</label>\n			<label>\n				<input type="radio" name="color" value="blue" />\n				<span style="color: blue;">Blue</span>\n			</label>\n			<label>\n				<input type="radio" name="color" value="green" />\n				<span style="color: green;">Green</span>\n			</label>\n			\n			<input type="submit" value="Ok" />\n			<input type="button" class="cancel" value="Cancel" />\n		</div>\n	</form>';
+  5 if(!xq) xq = {};
+  6 if(!xq.ui_templates) xq.ui_templates = {};
+  7 
+  8 xq.ui_templates.basicLinkDialog='<form action="#" class="xqFormDialog xqBasicLinkDialog">\n		<h3>Link</h3>\n		<div>\n			<input type="text" class="initialFocus" name="text" value="" />\n			<input type="text" name="url" value="http://" />\n			\n			<input type="submit" value="Ok" />\n			<input type="button" class="cancel" value="Cancel" />\n		</div>\n	</form>';
+  9 
\ No newline at end of file diff --git a/modules/editor/skins/xquared/editor.html b/modules/editor/skins/xquared/editor.html new file mode 100644 index 000000000..f0f705a09 --- /dev/null +++ b/modules/editor/skins/xquared/editor.html @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + + + + + + + + + + + +
+ + diff --git a/modules/editor/skins/xquared/examples/css/xq_contents.css b/modules/editor/skins/xquared/examples/css/xq_contents.css new file mode 100644 index 000000000..551b8f3f3 --- /dev/null +++ b/modules/editor/skins/xquared/examples/css/xq_contents.css @@ -0,0 +1,122 @@ +/* Basic */ +.xed { + color: #494949; + padding: 0.3em; + font-size: 0.833em; + font-family: Arial, dotum, sans-serif; +} +.xed * { + margin: 0; + padding: 0; +} + + + +/* Headings and paragraph */ +.xed h1, +.xed h2, +.xed h3, +.xed h4, +.xed h5, +.xed h6 { + border-bottom: 2px solid #425e89; + line-height: 1em; + padding-bottom: 0.2em; + margin: 0.2em 0; +} + +.xed h1 {font-size: 2.845em;} +.xed h2 {font-size: 2.460em;} +.xed h3 {font-size: 2.153em;} +.xed h4 {font-size: 1.922em;} +.xed h5 {font-size: 1.461em;} +.xed h6 {font-size: 1.230em;} + +.xed p { + margin: 0.614em 0; + line-height: 1.230em; +} + +.xed hr { +} + + + +/* List */ +.xed ul, +.xed ol { + padding-left: 2em; + margin: 0.614em 0; +} +.xed li { + line-height: 1.306em; +} + + + +/* Other block containers */ +.xed ol.code, +.xed div, +.xed blockquote { + margin: 1em 0; + padding-left: 2.460em; + padding-right: 1em; + border-width: 0 3px; + border-style: solid; + line-height: 1.306em; +} +.xed ol.code { + font-family: monospace; + list-style-type: none; + border-color: #ffb781; + background: url(../img/content/code.gif) no-repeat 0 0; +} +.xed div { + border-color: #8ccfff; + background: url(../img/content/div.gif) no-repeat 0 0; +} +.xed blockquote { + border-color: #c9c9c9; + background: url(../img/content/blockquote.gif) no-repeat 0 0; +} + + + +/* Inline elements */ +.xed em.underline { + font-style: normal; + text-decoration: underline; +} +.xed span.strike { + text-decoration: line-through; +} + + + +/* table */ +.xed table.datatable { + width: 100%; + /width: auto; + border-collapse: collapse; + table-layout: fixed; +} +.xed table.datatable th { + font-weight: normal; + padding: 0.25em 0.307em; +} +.xed table.datatable td { + padding: 0.25em 0.307em; +} +.xed table.datatable { + border-left: 1px solid #000; + border-top: 1px solid #000; +} +.xed table.datatable th { + background-color: #e7e7e7; + border-bottom: 1px solid #000; + border-right: 1px solid #000; +} +.xed table.datatable td { + border-bottom: 1px solid #000; + border-right: 1px solid #000; +} \ No newline at end of file diff --git a/modules/editor/skins/xquared/examples/css/xq_ui.css b/modules/editor/skins/xquared/examples/css/xq_ui.css new file mode 100644 index 000000000..3fe714644 --- /dev/null +++ b/modules/editor/skins/xquared/examples/css/xq_ui.css @@ -0,0 +1,233 @@ +/** + * Default Toolbar + */ + .xquared { + border: 1px solid #c2c2c2; + } +.xquared div.toolbar { + position: relative; + background-color: #ebebeb; + background-position: 0 0; + background-repeat: repeat-x; + background-image: url(../img/toolbar/toolbarBg.gif); +} + +.xquared ul.buttons { + margin: 0; + padding: 5px 4px 2px; + list-style: none; + border-top: 1px solid #fff; + border-bottom: 1px solid #fff; + overflow: hidden; + height: 100%; +} +.xquared ul.buttons li { + float: left; + padding-bottom: 3px; + background-position: 0 0; + background-repeat: repeat-x; + background-image: url(../img/toolbar/toolbarButtonBg.gif); +} +.xquared ul.buttons li.xq_separator { + padding-left: 8px; + margin-left: 8px; + background-position: 0 0; + background-repeat: repeat-x; + background-image: url(../img/toolbar/toolbarSeparator.gif); +} +.xquared ul.buttons li a { + display: block; + padding: 2px; + font-size: 0; + border: 1px solid #d2d2d2; + margin-right: -1px; + position: relative; + height: 100%; + _height: auto; + z-index: 0; +} +.xquared ul.buttons li a img { + margin: 0; + padding: 0; + border: none; + font-size: 1.25em; +} + +/* selected */ +.xquared ul.buttons li a.selected { + background-color: #ffea5f; + border: 1px solid #a0a0a0; + position: relative; + z-index: 1; +} + +/* mouseover */ +.xquared ul.buttons li a:hover { + border: 1px solid #000; + background-color: transparent; + position: relative; + z-index: 2; +} + +/* disabled */ +.xquared .disabled ul.buttons li a, +.xquared ul.buttons li .disabled a { + background-color: #fff; + opacity: 0.3; + filter:alpha(opacity=30); + _width: 1px; + _height: 1px; + cursor: default; +} +.xquared .disabled ul.buttons li a, +.xquared ul.buttons li .disabled a:hover { + border: 1px solid #dbdbdb; + position: relative; + z-index: 0; +} +.xquared .disabled ul.buttons li a, +.xquared ul.buttons li .disabled a.selected { + border: 1px solid #dbdbdb; +} + +.xquared .editor { + border: 0 none; + border-top:1px solid #c2c2c2; + height:300px; +} +.xquared .editor textarea, +.xquared .editor iframe { + margin: 0; + padding: 0; + border: 0 none; + height: 100%; + width: 100%; +} + +.xquared .editor textarea { + _height: expression(this.parentNode.clientHeight - 2); /* TODO remove IE6 hack */ +} +*+html .xquared .editor textarea { + height: expression(this.parentNode.clientHeight - 1); /* TODO remove IE7 hack */ +} + +.xquared .source_editor { + padding-left: 0.3em; +} + + +/** + * Context Menu + */ +.xqContextMenu { + position:absolute; + z-index: 902; + font-size: medium; + font-family: arial, "돋움"; + width:auto; + margin:0; + padding:3px; + border:1px solid #aaa; + list-style:none; + background-color: #fff; +} +.xqContextMenu li { + padding:2px 3px 2px 20px; + background-position:left; + background-repeat:no-repeat; + color:#aaa; + display:block; + width:200px; +} +.xqContextMenu a { + color:#000; + text-decoration:none; +} +.xqContextMenu .separator { + margin-top:5px; + padding-bottom:0px; + line-height:0em; + height: 1px; + /height: auto; + border-top: 1px solid #aaa; +} + + + +/** + * Controls - BasicLinkDialog + */ +.xqFormDialog { + position:absolute; + z-index: 902; + border:1px solid #aaa; + background-color: #fff; + padding: 0.5em; + overflow:auto; /* to fix FF caret bug */ +} + +.xqFormDialog h3 { + font-size: 1.2em; + margin: 0; +} + +/** + * Controls - QuickSearchDialog + */ +.xqQuickSearch { + position:absolute; + z-index: 902; + font-size: medium; + font-family: arial, "돋움"; + width:15em; + margin:0; + padding:0; + border:1px solid #aaa; + list-style:none; + background-color: #fff; + overflow:auto; /* to fix FF caret bug */ +} + +.xqQuickSearch h1 { + font-size: medium; + font-weight: bold; + margin: 2px; + padding: 2px; +} + +.xqQuickSearch .input { + border: 1px solid #aaa; + padding: 2px; + margin: 2px; +} + +.xqQuickSearch form { + padding: 0; + margin: 0; +} + +.xqQuickSearch input { + border-width: 0; + margin: 0 2px; + width: 98%; +} + +.xqQuickSearch ol { + border: 1px solid #aaa; + padding: 2px; + margin: 2px; + height: 10em; + overflow: auto; +} + +.xqQuickSearch li { + list-style: none; + padding-bottom: 1px; + border-bottom: 1px solid #ddd; + cursor: pointer; + _cursor: hand; +} + +.xqQuickSearch li.selected { + background-color: #ffd; +} \ No newline at end of file diff --git a/modules/editor/skins/xquared/examples/default.html b/modules/editor/skins/xquared/examples/default.html new file mode 100644 index 000000000..b91d29415 --- /dev/null +++ b/modules/editor/skins/xquared/examples/default.html @@ -0,0 +1,77 @@ + + + + +Xquared example - Default + + + + + + + +
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

This holds true both for individuals as well as whole tagging communities.

+ +

This holds true both for individuals as well as whole tagging communities.

+ +

This holds true both for individuals as well as whole tagging communities.

+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+

This holds true both for individuals as well as whole tagging communities.

+
+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+

This holds true both for individuals as well as whole tagging communities.

+
+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 1

+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 1

+

Heading 2

+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 1

+

Heading 2

+

Heading 3

+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 4

+
Heading 5
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
Heading 6
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
    +
  1. print "Hello World"
  2. +
  3. print "Hello World"
  4. +
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+ + diff --git a/modules/editor/skins/xquared/examples/extensionpoint.html b/modules/editor/skins/xquared/examples/extensionpoint.html new file mode 100644 index 000000000..f9bfe5516 --- /dev/null +++ b/modules/editor/skins/xquared/examples/extensionpoint.html @@ -0,0 +1,308 @@ + + + + +Xquared example - Extension point + + + + + + + +
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

This holds true both for individuals as well as whole tagging communities.

+ +

This holds true both for individuals as well as whole tagging communities.

+ +

This holds true both for individuals as well as whole tagging communities.

+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+

This holds true both for individuals as well as whole tagging communities.

+
+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+

This holds true both for individuals as well as whole tagging communities.

+
+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 1

+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 1

+

Heading 2

+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 1

+

Heading 2

+

Heading 3

+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 4

+
Heading 5
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
Heading 6
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
    +
  1. print "Hello World"
  2. +
  3. print "Hello World"
  4. +
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+ + diff --git a/modules/editor/skins/xquared/examples/form_and_textarea.html b/modules/editor/skins/xquared/examples/form_and_textarea.html new file mode 100644 index 000000000..8423f9355 --- /dev/null +++ b/modules/editor/skins/xquared/examples/form_and_textarea.html @@ -0,0 +1,30 @@ + + + + +Xquared example - Form and Textarea + + + + + + + +
+ + + +
+ + diff --git a/modules/editor/skins/xquared/examples/img/content/blockquote.gif b/modules/editor/skins/xquared/examples/img/content/blockquote.gif new file mode 100644 index 000000000..3efb99fe0 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/content/blockquote.gif differ diff --git a/modules/editor/skins/xquared/examples/img/content/code.gif b/modules/editor/skins/xquared/examples/img/content/code.gif new file mode 100644 index 000000000..151ab68e0 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/content/code.gif differ diff --git a/modules/editor/skins/xquared/examples/img/content/div.gif b/modules/editor/skins/xquared/examples/img/content/div.gif new file mode 100644 index 000000000..78521dca6 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/content/div.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/backgroundColor.gif b/modules/editor/skins/xquared/examples/img/toolbar/backgroundColor.gif new file mode 100644 index 000000000..af46da29d Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/backgroundColor.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/blockquote.gif b/modules/editor/skins/xquared/examples/img/toolbar/blockquote.gif new file mode 100644 index 000000000..541b80168 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/blockquote.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/code.gif b/modules/editor/skins/xquared/examples/img/toolbar/code.gif new file mode 100644 index 000000000..0e8e1e010 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/code.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/division.gif b/modules/editor/skins/xquared/examples/img/toolbar/division.gif new file mode 100644 index 000000000..6c032d194 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/division.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/emphasis.gif b/modules/editor/skins/xquared/examples/img/toolbar/emphasis.gif new file mode 100644 index 000000000..bcc53ea2e Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/emphasis.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/foregroundColor.gif b/modules/editor/skins/xquared/examples/img/toolbar/foregroundColor.gif new file mode 100644 index 000000000..a393d0d29 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/foregroundColor.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/heading1.gif b/modules/editor/skins/xquared/examples/img/toolbar/heading1.gif new file mode 100644 index 000000000..04acad953 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/heading1.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/html.gif b/modules/editor/skins/xquared/examples/img/toolbar/html.gif new file mode 100644 index 000000000..cee19fbbe Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/html.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/indent.gif b/modules/editor/skins/xquared/examples/img/toolbar/indent.gif new file mode 100644 index 000000000..97d6e1012 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/indent.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/justifyBoth.gif b/modules/editor/skins/xquared/examples/img/toolbar/justifyBoth.gif new file mode 100644 index 000000000..d3468654a Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/justifyBoth.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/justifyCenter.gif b/modules/editor/skins/xquared/examples/img/toolbar/justifyCenter.gif new file mode 100644 index 000000000..95d40c957 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/justifyCenter.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/justifyLeft.gif b/modules/editor/skins/xquared/examples/img/toolbar/justifyLeft.gif new file mode 100644 index 000000000..d4a39694c Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/justifyLeft.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/justifyRight.gif b/modules/editor/skins/xquared/examples/img/toolbar/justifyRight.gif new file mode 100644 index 000000000..e56a15811 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/justifyRight.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/link.gif b/modules/editor/skins/xquared/examples/img/toolbar/link.gif new file mode 100644 index 000000000..0049162b7 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/link.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/orderedList.gif b/modules/editor/skins/xquared/examples/img/toolbar/orderedList.gif new file mode 100644 index 000000000..dac8fed96 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/orderedList.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/outdent.gif b/modules/editor/skins/xquared/examples/img/toolbar/outdent.gif new file mode 100644 index 000000000..45d8fed50 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/outdent.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/paragraph.gif b/modules/editor/skins/xquared/examples/img/toolbar/paragraph.gif new file mode 100644 index 000000000..f8fa61e20 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/paragraph.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/redo.gif b/modules/editor/skins/xquared/examples/img/toolbar/redo.gif new file mode 100644 index 000000000..4e06855c3 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/redo.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/removeFormat.gif b/modules/editor/skins/xquared/examples/img/toolbar/removeFormat.gif new file mode 100644 index 000000000..cee1d0bba Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/removeFormat.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/separator.gif b/modules/editor/skins/xquared/examples/img/toolbar/separator.gif new file mode 100644 index 000000000..ac2bf3eb7 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/separator.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/strike.gif b/modules/editor/skins/xquared/examples/img/toolbar/strike.gif new file mode 100644 index 000000000..c87967fb7 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/strike.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/strongEmphasis.gif b/modules/editor/skins/xquared/examples/img/toolbar/strongEmphasis.gif new file mode 100644 index 000000000..776e317d8 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/strongEmphasis.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/subscription.gif b/modules/editor/skins/xquared/examples/img/toolbar/subscription.gif new file mode 100644 index 000000000..5e7f73083 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/subscription.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/superscription.gif b/modules/editor/skins/xquared/examples/img/toolbar/superscription.gif new file mode 100644 index 000000000..18a5994e6 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/superscription.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/table.gif b/modules/editor/skins/xquared/examples/img/toolbar/table.gif new file mode 100644 index 000000000..4a559f82a Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/table.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/toolbarBg.gif b/modules/editor/skins/xquared/examples/img/toolbar/toolbarBg.gif new file mode 100644 index 000000000..1fc3f3fc3 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/toolbarBg.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/toolbarButtonBg.gif b/modules/editor/skins/xquared/examples/img/toolbar/toolbarButtonBg.gif new file mode 100644 index 000000000..75e74c168 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/toolbarButtonBg.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/toolbarSeparator.gif b/modules/editor/skins/xquared/examples/img/toolbar/toolbarSeparator.gif new file mode 100644 index 000000000..571a3e846 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/toolbarSeparator.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/underline.gif b/modules/editor/skins/xquared/examples/img/toolbar/underline.gif new file mode 100644 index 000000000..6b7e812a0 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/underline.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/undo.gif b/modules/editor/skins/xquared/examples/img/toolbar/undo.gif new file mode 100644 index 000000000..19670bca2 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/undo.gif differ diff --git a/modules/editor/skins/xquared/examples/img/toolbar/unorderedList.gif b/modules/editor/skins/xquared/examples/img/toolbar/unorderedList.gif new file mode 100644 index 000000000..47eef67f3 Binary files /dev/null and b/modules/editor/skins/xquared/examples/img/toolbar/unorderedList.gif differ diff --git a/modules/editor/skins/xquared/examples/toolbar_customized1.html b/modules/editor/skins/xquared/examples/toolbar_customized1.html new file mode 100644 index 000000000..648a33392 --- /dev/null +++ b/modules/editor/skins/xquared/examples/toolbar_customized1.html @@ -0,0 +1,43 @@ + + + + +Xquared example - Custom toolbar 1 + + + + + + + +
+ + diff --git a/modules/editor/skins/xquared/examples/toolbar_customized2.html b/modules/editor/skins/xquared/examples/toolbar_customized2.html new file mode 100644 index 000000000..84cdbdf5b --- /dev/null +++ b/modules/editor/skins/xquared/examples/toolbar_customized2.html @@ -0,0 +1,92 @@ + + + + +Xquared example - Custom toolbar 2 + + + + + + + + +
+ +
+
+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

This holds true both for individuals as well as whole tagging communities.

+
    +
  • Item 1
  • +
  • Item 2
  • +
+

This holds true both for individuals as well as whole tagging communities.

+
    +
  • +

    Item 1

    +
      +
    1. Item 1
    2. +
    3. Item 2
    4. +
    +
  • +
  • Item 2
  • +
  • Item 3
  • +
+

This holds true both for individuals as well as whole tagging communities.

+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+

This holds true both for individuals as well as whole tagging communities.

+
+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+

This holds true both for individuals as well as whole tagging communities.

+
+
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 1

+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 1

+

Heading 2

+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 1

+

Heading 2

+

Heading 3

+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+

Heading 4

+
Heading 5
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
Heading 6
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
    +
  1. print "Hello World"
  2. +
  3. print "Hello World"
  4. +
+

Most notably, it has been confirmed in many empirical studies on tagging(see e.g. [Golder:2005, Hotho:2006, Cattuto:2006]), that tag distributions tend follow a power law—a small number of tags is used very often, while a very large number of tags occurs very rarely.

+
+
+ + diff --git a/modules/editor/skins/xquared/examples/toolbar_disabled.html b/modules/editor/skins/xquared/examples/toolbar_disabled.html new file mode 100644 index 000000000..bcc90cc5a --- /dev/null +++ b/modules/editor/skins/xquared/examples/toolbar_disabled.html @@ -0,0 +1,28 @@ + + + + +Xquared example - No toolbar + + + + + + + +
+ + diff --git a/modules/editor/skins/xquared/js/prototype.js b/modules/editor/skins/xquared/js/prototype.js new file mode 100644 index 000000000..fc1ecbc38 --- /dev/null +++ b/modules/editor/skins/xquared/js/prototype.js @@ -0,0 +1,4184 @@ +/* Prototype JavaScript framework, version 1.6.0 + * (c) 2005-2007 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * + *--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.6.0', + + Browser: { + IE: !!(window.attachEvent && !window.opera), + Opera: !!window.opera, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div').__proto__ && + document.createElement('div').__proto__ !== + document.createElement('form').__proto__ + }, + + ScriptFragment: ']*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + +if (Prototype.Browser.WebKit) + Prototype.BrowserFeatures.XPath = false; + +/* Based on Alex Arnell's inheritance implementation. */ +var Class = { + create: function() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + var subclass = function() { }; + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + + return klass; + } +}; + +Class.Methods = { + addMethods: function(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) + properties.push("toString", "valueOf"); + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value, value = Object.extend((function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method), { + valueOf: function() { return method }, + toString: function() { return method.toString() } + }); + } + this.prototype[property] = value; + } + + return this; + } +}; + +var Abstract = { }; + +Object.extend = function(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; +}; + +Object.extend(Object, { + inspect: function(object) { + try { + if (object === undefined) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + toJSON: function(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (Object.isElement(object)) return; + + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (value !== undefined) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + }, + + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + + toHTML: function(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({ }, object); + }, + + isElement: function(object) { + return object && object.nodeType == 1; + }, + + isArray: function(object) { + return object && object.constructor === Array; + }, + + isHash: function(object) { + return object instanceof Hash; + }, + + isFunction: function(object) { + return typeof object == "function"; + }, + + isString: function(object) { + return typeof object == "string"; + }, + + isNumber: function(object) { + return typeof object == "number"; + }, + + isUndefined: function(object) { + return typeof object == "undefined"; + } +}); + +Object.extend(Function.prototype, { + argumentNames: function() { + var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip"); + return names.length == 1 && !names[0] ? [] : names; + }, + + bind: function() { + if (arguments.length < 2 && arguments[0] === undefined) return this; + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } + }, + + bindAsEventListener: function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } + }, + + curry: function() { + if (!arguments.length) return this; + var __method = this, args = $A(arguments); + return function() { + return __method.apply(this, args.concat($A(arguments))); + } + }, + + delay: function() { + var __method = this, args = $A(arguments), timeout = args.shift() * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; + } +}); + +Function.prototype.defer = Function.prototype.delay.curry(0.01); + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + } finally { + this.currentlyExecuting = false; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; + }, + + unescapeHTML: function() { + var div = new Element('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; + }, + + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + }, + + toArray: function() { + return this.split(''); + }, + + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + times: function(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + }, + + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&').replace(//g,'>'); + }, + unescapeHTML: function() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +}; + +String.prototype.parseQuery = String.prototype.toQueryParams; + +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +with (String.prototype.escapeHTML) div.appendChild(text); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return ''; + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/, match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }.bind(this)); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = { + each: function(iterator, context) { + var index = 0; + iterator = iterator.bind(context); + try { + this._each(function(value) { + iterator(value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + }, + + all: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function(iterator, context) { + iterator = iterator.bind(context); + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(filter, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator(value, index)); + }); + return results; + }, + + include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = fillWith === undefined ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator, context) { + iterator = iterator.bind(context); + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator, context) { + iterator = iterator.bind(context); + return this.map(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#'; + } +}; + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + filter: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any +}); +function $A(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +if (Prototype.Browser.WebKit) { + function $A(iterable) { + if (!iterable) return []; + if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && + iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; + } +} + +Array.from = $A; + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(Object.isArray(value) ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + intersect: function(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (value !== undefined) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } +}); + +// use native browser JS 1.6 implementation if available +if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +}; + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +}; + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { + var array = []; + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (Object.isArray(arguments[i])) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + }; +} +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + }, + + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + }, + + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; + } +}); + +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + if (function() { + var i = 0, Test = function(value) { this.key = value }; + Test.prototype.key = 'foo'; + for (var property in new Test('bar')) i++; + return i > 1; + }()) { + function each(iterator) { + var cache = []; + for (var key in this._object) { + var value = this._object[key]; + if (cache.include(key)) continue; + cache.push(key); + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + } + } else { + function each(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + } + } + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + }, + + _each: each, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return values.map(toQueryPair.curry(key)).join('&'); + } + return toQueryPair(key, values); + }).join('&'); + }, + + inspect: function() { + return '#'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; +Hash.from = $H; +var ObjectRange = Class.create(Enumerable, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +}; + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } +}); + +Ajax.Base = Class.create({ + initialize: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + } +}); + +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = xml === undefined ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json'))) + return null; + try { + return this.transport.responseText.evalJSON(options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = options || { }; + var onComplete = options.onComplete; + options.onComplete = (function(response, param) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, param); + }).bind(this); + + $super(url, options); + }, + + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); + } + + if (this.success()) { + if (this.onComplete) this.onComplete.bind(this).defer(); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = { }; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(response) { + if (this.options.decay) { + this.decay = (response.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = response.responseText; + } + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + // DOM level 2 ECMAScript Language Binding + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} + +(function() { + var element = this.Element; + this.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (Prototype.Browser.IE && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(this.Element, element || { }); +}).call(window); + +Element.cache = { }; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + $(element).style.display = 'none'; + return element; + }, + + show: function(element) { + $(element).style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); + return element; + }, + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, t, range; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + t = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + t.insert(element, content); + continue; + } + + content = Object.toHTML(content); + + range = element.ownerDocument.createRange(); + t.initializeRange(element, range); + t.insert(element, range.createContextualFragment(content.stripScripts())); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $A($(element).getElementsByTagName('*')).each(Element.extend); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return expression ? Selector.findElement(ancestors, expression, index) : + ancestors[index || 0]; + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + var descendants = element.descendants(); + return expression ? Selector.findElement(descendants, expression, index) : + descendants[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return expression ? Selector.findElement(previousSiblings, expression, index) : + previousSiblings[index || 0]; + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return expression ? Selector.findElement(nextSiblings, expression, index) : + nextSiblings[index || 0]; + }, + + select: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + adjacent: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = element.readAttribute('id'), self = arguments.callee; + if (id) return id; + do { id = 'anonymous_element_' + self.counter++ } while ($(id)); + element.writeAttribute('id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = value === undefined ? true : value; + + for (var attr in attributes) { + var name = t.names[attr] || attr, value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + if (!element.hasClassName(className)) + element.className += (element.className ? ' ' : '') + className; + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return element[element.hasClassName(className) ? + 'removeClassName' : 'addClassName'](className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (element.sourceIndex && !Prototype.Browser.Opera) { + var e = element.sourceIndex, a = ancestor.sourceIndex, + nextAncestor = ancestor.nextSibling; + if (!nextAncestor) { + do { ancestor = ancestor.parentNode; } + while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); + } + if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex); + } + + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = element.cumulativeOffset(); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = $(element).getStyle('display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + var offsets = element.positionedOffset(); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || element.tagName == 'BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + // find page position of source + source = $(source); + var p = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + + +if (!document.createRange || Prototype.Browser.Opera) { + Element.Methods.insert = function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = { bottom: insertions }; + + var t = Element._insertionTranslations, content, position, pos, tagName; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + pos = t[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + pos.insert(element, content); + continue; + } + + content = Object.toHTML(content); + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + if (t.tags[tagName]) { + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + if (position == 'top' || position == 'after') fragments.reverse(); + fragments.each(pos.insert.curry(element)); + } + else element.insertAdjacentHTML(pos.adjacency, content.stripScripts()); + + content.evalScripts.bind(content).defer(); + } + + return element; + }; +} + +if (Prototype.Browser.Opera) { + Element.Methods._getStyle = Element.Methods.getStyle; + Element.Methods.getStyle = function(element, style) { + switch(style) { + case 'left': + case 'top': + case 'right': + case 'bottom': + if (Element._getStyle(element, 'position') == 'static') return null; + default: return Element._getStyle(element, style); + } + }; + Element.Methods._readAttribute = Element.Methods.readAttribute; + Element.Methods.readAttribute = function(element, attribute) { + if (attribute == 'title') return element.title; + return Element._readAttribute(element, attribute); + }; +} + +else if (Prototype.Browser.IE) { + $w('positionedOffset getOffsetParent viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + var position = element.getStyle('position'); + if (position != 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = { + read: { + names: { + 'class': 'className', + 'for': 'htmlFor' + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: function(element, attribute) { + var attribute = element.getAttribute(attribute); + return attribute ? attribute.toString().slice(23, -2) : null; + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + }; + + Element._attributeTranslations.write = { + names: Object.clone(Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine Position.cumulativeOffset for + // KHTML/WebKit only. + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return Element._returnOffset(valueL, valueT); + }; +} + +if (Prototype.Browser.IE || Prototype.Browser.Opera) { + // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements + Element.Methods.update = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (tagName in Element._insertionTranslations.tags) { + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { element.appendChild(node) }); + } + else element.innerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +if (document.createElement('div').outerHTML) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: { + adjacency: 'beforeBegin', + insert: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + initializeRange: function(element, range) { + range.setStartBefore(element); + } + }, + top: { + adjacency: 'afterBegin', + insert: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + initializeRange: function(element, range) { + range.selectNodeContents(element); + range.collapse(true); + } + }, + bottom: { + adjacency: 'beforeEnd', + insert: function(element, node) { + element.appendChild(node); + } + }, + after: { + adjacency: 'afterEnd', + insert: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + initializeRange: function(element, range) { + range.setStartAfter(element); + } + }, + tags: { + TABLE: ['', '
', 1], + TBODY: ['', '
', 2], + TR: ['', '
', 3], + TD: ['
', '
', 4], + SELECT: ['', 1] + } +}; + +(function() { + this.bottom.initializeRange = this.top.initializeRange; + Object.extend(this.tags, { + THEAD: this.tags.TBODY, + TFOOT: this.tags.TBODY, + TH: this.tags.TD + }); +}).call(Element._insertionTranslations); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return node && node.specified; + } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div').__proto__) { + window.HTMLElement = { }; + window.HTMLElement.prototype = document.createElement('div').__proto__; + Prototype.BrowserFeatures.ElementExtensions = true; +} + +Element.extend = (function() { + if (Prototype.BrowserFeatures.SpecificElementExtensions) + return Prototype.K; + + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || element._extendedByPrototype || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName, property, value; + + // extend methods for specific tags + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + for (property in methods) { + value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + // extend methods for all tags (Safari doesn't need this) + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = { }; + window[klass].prototype = document.createElement(tagName).__proto__; + return window[klass]; + } + + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + +document.viewport = { + getDimensions: function() { + var dimensions = { }; + $w('width height').each(function(d) { + var D = d.capitalize(); + dimensions[d] = self['inner' + D] || + (document.documentElement['client' + D] || document.body['client' + D]); + }); + return dimensions; + }, + + getWidth: function() { + return this.getDimensions().width; + }, + + getHeight: function() { + return this.getDimensions().height; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); + } +}; +/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + this.compileMatcher(); + }, + + compileMatcher: function() { + // Selectors with namespaced attributes can't use the XPath version + if (Prototype.BrowserFeatures.XPath && !(/(\[[\w-]*?:|:checked)/).test(this.expression)) + return this.compileXPathMatcher(); + + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + if (this.xpath) return document._getElementsByXPath(this.xpath, root); + return this.matcher(root); + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#"; + } +}); + +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: "[@#{1}]", + attr: function(m) { + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]", + 'checked': "[@checked]", + 'disabled': "[@disabled]", + 'enabled': "[not(@disabled)]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, m, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/, + attrPresence: /^\[([\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + // for Selector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return Selector.operators[matches[2]](nodeValue, matches[3]); + } + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._counted = true; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._counted) { + n._counted = true; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, children = [], child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + tagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() == tagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!targetNode) return []; + if (!nodes && root == document) return [targetNode]; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._counted) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._counted) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled) results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv.startsWith(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } + }, + + matchElements: function(elements, expression) { + var matches = new Selector(expression).findElements(), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._counted) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + var exprs = expressions.join(','), expressions = []; + exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (options.hash === undefined) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + // a key is already present; construct an array of values + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return options.hash ? data : Object.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); + }, + + getElements: function(form) { + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) { } + return element; + }, + + disable: function(element) { + element = $(element); + element.blur(); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (value === undefined) return element.checked ? element.value : null; + else element.checked = !!value; + }, + + textarea: function(element, value) { + if (value === undefined) return element.value; + else element.value = value; + }, + + select: function(element, index) { + if (index === undefined) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, value, single = !Object.isArray(index); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + value = this.optionValue(opt); + if (single) { + if (value == index) { + opt.selected = true; + return; + } + } + else opt.selected = index.include(value); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) var Event = { }; + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: { }, + + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); + +Event.Methods = (function() { + var isButton; + + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + var node = Event.extend(event).target; + return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + return element.match(expression) ? element : element.up(expression); + }, + + pointer: function(event) { + return { + x: event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)), + y: event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + event.stopped = true; + } + }; +})(); + +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; + + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._eventID) return element._eventID; + arguments.callee.id = arguments.callee.id || 1; + return element._eventID = ++arguments.callee.id; + } + + function getDOMEventName(eventName) { + if (eventName && eventName.include(':')) return "dataavailable"; + return eventName; + } + + function getCacheForID(id) { + return cache[id] = cache[id] || { }; + } + + function getWrappersForEventName(id, eventName) { + var c = getCacheForID(id); + return c[eventName] = c[eventName] || []; + } + + function createWrapper(element, eventName, handler) { + var id = getEventID(element); + var c = getWrappersForEventName(id, eventName); + if (c.pluck("handler").include(handler)) return false; + + var wrapper = function(event) { + if (!Event || !Event.extend || + (event.eventName && event.eventName != eventName)) + return false; + + Event.extend(event); + handler.call(element, event) + }; + + wrapper.handler = handler; + c.push(wrapper); + return wrapper; + } + + function findWrapper(id, eventName, handler) { + var c = getWrappersForEventName(id, eventName); + return c.find(function(wrapper) { return wrapper.handler == handler }); + } + + function destroyWrapper(id, eventName, handler) { + var c = getCacheForID(id); + if (!c[eventName]) return false; + c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); + } + + function destroyCache() { + for (var id in cache) + for (var eventName in cache[id]) + cache[id][eventName] = null; + } + + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } + + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + if (document.createEvent) { + var event = document.createEvent("HTMLEvents"); + event.initEvent("dataavailable", true, true); + } else { + var event = document.createEventObject(); + event.eventType = "ondataavailable"; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent(event.eventType, event); + } + + return event; + } + }; +})()); + +Object.extend(Event, Event.Methods); + +Element.addMethods({ + fire: Event.fire, + observe: Event.observe, + stopObserving: Event.stopObserving +}); + +Object.extend(document, { + fire: Element.Methods.fire.methodize(), + observe: Element.Methods.observe.methodize(), + stopObserving: Element.Methods.stopObserving.methodize() +}); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards and John Resig. */ + + var timer, fired = false; + + function fireContentLoadedEvent() { + if (fired) return; + if (timer) window.clearInterval(timer); + document.fire("dom:loaded"); + fired = true; + } + + if (document.addEventListener) { + if (Prototype.Browser.WebKit) { + timer = window.setInterval(function() { + if (/loaded|complete/.test(document.readyState)) + fireContentLoadedEvent(); + }, 0); + + Event.observe(window, "load", fireContentLoadedEvent); + + } else { + document.addEventListener("DOMContentLoaded", + fireContentLoadedEvent, false); + } + + } else { + document.write("") +}; +xq.loadOthers=function(){var A=xq.findXquaredScript(); +var C=A.src.match(/(.*\/)xquared\.js.*/i)[1]; +var B=["Editor.js","Browser.js","Shortcut.js","DomTree.js","RichDom.js","RichDomW3.js","RichDomGecko.js","RichDomWebkit.js","RichDomTrident.js","RichTable.js","Validator.js","ValidatorW3.js","ValidatorGecko.js","ValidatorWebkit.js","ValidatorTrident.js","EditHistory.js","Controls.js","_ui_templates.js"]; +B.each(function(D){xq.loadScript(C+D) +}) +}; +if(xq.shouldLoadOthers()){xq.loadOthers() +}xq.Editor=Class.create({initialize:function(B,A){if(!B){throw"[contentElement] is null" +}if(B.nodeType!=1){throw"[contentElement] is not an element" +}xq.asEventSource(this,"Editor",["ElementChanged","BeforeEvent","AfterEvent","CurrentContentChanged","StaticContentChanged","CurrentEditModeChanged"]); +this.config={}; +this.config.enableLinkClick=false; +this.config.changeCursorOnLink=false; +this.config.generateDefaultToolbar=true; +this.config.defaultToolbarButtonMap=[[{className:"foregroundColor",title:"Foreground color",handler:"xed.handleForegroundColor()"},{className:"backgroundColor",title:"Background color",handler:"xed.handleBackgroundColor()"}],[{className:"link",title:"Link",handler:"xed.handleLink()"},{className:"strongEmphasis",title:"Strong emphasis",handler:"xed.handleStrongEmphasis()"},{className:"emphasis",title:"Emphasis",handler:"xed.handleEmphasis()"},{className:"underline",title:"Underline",handler:"xed.handleUnderline()"},{className:"strike",title:"Strike",handler:"xed.handleStrike()"},{className:"superscription",title:"Superscription",handler:"xed.handleSuperscription()"},{className:"subscription",title:"Subscription",handler:"xed.handleSubscription()"}],[{className:"removeFormat",title:"Remove format",handler:"xed.handleRemoveFormat()"}],[{className:"justifyLeft",title:"Justify left",handler:"xed.handleJustify('left')"},{className:"justifyCenter",title:"Justify center",handler:"xed.handleJustify('center')"},{className:"justifyRight",title:"Justify right",handler:"xed.handleJustify('right')"},{className:"justifyBoth",title:"Justify both",handler:"xed.handleJustify('both')"}],[{className:"indent",title:"Indent",handler:"xed.handleIndent()"},{className:"outdent",title:"Outdent",handler:"xed.handleOutdent()"}],[{className:"unorderedList",title:"Unordered list",handler:"xed.handleList('UL')"},{className:"orderedList",title:"Ordered list",handler:"xed.handleList('OL')"}],[{className:"paragraph",title:"Paragraph",handler:"xed.handleApplyBlock('P')"},{className:"heading1",title:"Heading 1",handler:"xed.handleApplyBlock('H1')"},{className:"blockquote",title:"Blockquote",handler:"xed.handleApplyBlock('BLOCKQUOTE')"},{className:"code",title:"Code",handler:"xed.handleList('CODE')"},{className:"division",title:"Division",handler:"xed.handleApplyBlock('DIV')"}],[{className:"table",title:"Table",handler:"xed.handleTable(3,3,'tl')"},{className:"separator",title:"Separator",handler:"xed.handleSeparator()"}],[{className:"html",title:"Edit source",handler:"xed.toggleSourceAndWysiwygMode()"}],[{className:"undo",title:"Undo",handler:"xed.handleUndo()"},{className:"redo",title:"Redo",handler:"xed.handleRedo()"}]]; +this.config.imagePathForDefaultToobar=request_uri+editor_path+'examples/img/toolbar/'; +this.config.urlValidationMode="absolute"; +this.config.automaticallyHookSubmitEvent=true; +this.config.allowedTags=["a","abbr","acronym","address","blockquote","br","caption","cite","code","dd","dfn","div","dl","dt","em","h1","h2","h3","h4","h5","h6","hr","img","kbd","li","ol","p","pre","q","samp","span","sup","sub","strong","table","thead","tbody","td","th","tr","ul","var"]; +this.config.allowedAttributes=["alt","cite","class","datetime","height","href","id","rel","rev","src","style","title","width"]; +this.config.shortcuts={}; +this.config.autocorrections={}; +this.config.autocompletions={}; +this.config.templateProcessors={}; +this.config.contextMenuHandlers={}; +this.contentElement=B; +this.doc=this.contentElement.ownerDocument; +this.body=this.doc.body; +this.currentEditMode="readonly"; +this.rdom=xq.RichDom.createInstance(); +this.validator=null; +this.outmostWrapper=null; +this.sourceEditorDiv=null; +this.sourceEditorTextarea=null; +this.wysiwygEditorDiv=null; +this.editorFrame=null; +this.editorWin=null; +this.editorDoc=null; +this.editorBody=null; +this.toolbarContainer=A; +this.toolbarButtons=null; +this.editHistory=null; +this._contextMenuContainer=null; +this._contextMenuItems=null; +this._validContentCache=null; +this._lastModified=null; +this.addShortcuts(this._getDefaultShortcuts()); +this.addTemplateProcessors(this._getDefaultTemplateProcessors()); +this.addListener({onEditorCurrentContentChanged:function(D){var C=D.rdom.getCurrentElement(); +if(!C){return +}if(D._lastFocusElement!=C){if(!D.rdom.tree.isBlockOnlyContainer(D._lastFocusElement)&&D.rdom.tree.isBlock(D._lastFocusElement)){D.rdom.removeTrailingWhitespace(D._lastFocusElement) +}D._fireOnElementChanged(D._lastFocusElement,C); +D._lastFocusElement=C +}D.updateAllToolbarButtonsStatus(C) +}}) +},_getDefaultShortcuts:function(){if(xq.Browser.isMac){return[{event:"Ctrl+Shift+SPACE",handler:"this.handleAutocompletion(); stop = true;"},{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:"Meta+Z",handler:"this.handleUndo()"},{event:"Meta+Shift+Z",handler:"this.handleRedo()"},{event:"Meta+Y",handler:"this.handleRedo()"}] +}else{if(xq.Browser.isUbuntu){return[{event:"Ctrl+SPACE",handler:"this.handleAutocompletion(); stop = true;"},{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+Y",handler:"this.handleRedo()"}] +}else{return[{event:"Ctrl+SPACE",handler:"this.handleAutocompletion(); stop = true;"},{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+Y",handler:"this.handleRedo()"}] +}}},_getDefaultTemplateProcessors:function(){return[{id:"predefinedKeywordProcessor",handler:function(C){var A=Date.get(); +var B={year:A.getFullYear(),month:A.getMonth()+1,date:A.getDate(),hour:A.getHours(),min:A.getMinutes(),sec:A.getSeconds()}; +return C.replace(/\{xq:(year|month|date|hour|min|sec)\}/img,function(E,D){return B[D]||D +}) +}}] +},addShortcut:function(A,B){this.config.shortcuts[A]={"event":new xq.Shortcut(A),"handler":B} +},addShortcuts:function(A){A.each(function(B){this.addShortcut(B.event,B.handler) +}.bind(this)) +},getShortcut:function(A){return this.config.shortcuts[A] +},getShortcuts:function(){return this.config.shortcuts +},removeShortcut:function(A){delete this.config.shortcuts[A] +},addAutocorrection:function(D,C,A){if(C.exec){var B=C; +C=function(E){return E.match(B) +}}this.config.autocorrections[D]={"criteria":C,"handler":A} +},addAutocorrections:function(A){A.each(function(B){this.addAutocorrection(B.id,B.criteria,B.handler) +}.bind(this)) +},getAutocorrection:function(A){return this.config.autocorrection[A] +},getAutocorrections:function(){return this.config.autocorrections +},removeAutocorrection:function(A){delete this.config.autocorrections[A] +},addAutocompletion:function(D,C,A){if(C.exec){var B=C; +C=function(F){var E=B.exec(F); +return E?E.index:-1 +}}this.config.autocompletions[D]={"criteria":C,"handler":A} +},addAutocompletions:function(A){A.each(function(B){this.addAutocompletion(B.id,B.criteria,B.handler) +}.bind(this)) +},getAutocompletion:function(A){return this.config.autocompletions[A] +},getAutocompletions:function(){return this.config.autocompletions +},removeAutocompletion:function(A){delete this.config.autocompletions[A] +},addTemplateProcessor:function(B,A){this.config.templateProcessors[B]={"handler":A} +},addTemplateProcessors:function(A){A.each(function(B){this.addTemplateProcessor(B.id,B.handler) +}.bind(this)) +},getTemplateProcessor:function(A){return this.config.templateProcessors[A] +},getTemplateProcessors:function(){return this.config.templateProcessors +},removeTemplateProcessor:function(A){delete this.config.templateProcessors[A] +},addContextMenuHandler:function(B,A){this.config.contextMenuHandlers[B]={"handler":A} +},addContextMenuHandlers:function(A){A.each(function(B){this.addContextMenuHandler(B.id,B.handler) +}.bind(this)) +},getContextMenuHandler:function(A){return this.config.contextMenuHandlers[A] +},getContextMenuHandlers:function(){return this.config.contextMenuHandlers +},removeContextMenuHandler:function(A){delete this.config.contextMenuHandlers[A] +},getCurrentEditMode:function(){return this.currentEditMode +},toggleSourceAndWysiwygMode:function(){var A=this.getCurrentEditMode(); +if(A=="readonly"){return +}this.setEditMode(A=="wysiwyg"?"source":"wysiwyg"); +return true +},setEditMode:function(B){if(this.currentEditMode==B){return +}var A=B!=false&&B!="readonly"&&!this.outmostWrapper; +if(A){this._createEditorFrame(); +this._registerEventHandlers(); +this.loadCurrentContentFromStaticContent(); +this.editHistory=new xq.EditHistory(this.rdom) +}if(B=="wysiwyg"){if(this.currentEditMode=="source"){this.setStaticContent(this.getSourceContent()) +}this.loadCurrentContentFromStaticContent(); +this.contentElement.style.display="none"; +this.sourceEditorDiv.style.display="none"; +this.wysiwygEditorDiv.style.display="block"; +this.outmostWrapper.style.display="block"; +this.currentEditMode=B; +if(!xq.Browser.isTrident){window.setTimeout(function(){if(this.getDoc().designMode=="On"){return +}this.getDoc().designMode="On"; +try{this.getDoc().execCommand("enableInlineTableEditing",false,"false") +}catch(C){}}.bind(this),0) +}this.enableToolbarButtons(); +if(!A){this.focus() +}}else{if(B=="source"){if(this.currentEditMode=="wysiwyg"){this.setStaticContent(this.getWysiwygContent()) +}this.loadCurrentContentFromStaticContent(); +this.contentElement.style.display="none"; +this.sourceEditorDiv.style.display="block"; +this.wysiwygEditorDiv.style.display="none"; +this.outmostWrapper.style.display="block"; +this.currentEditMode=B; +this.disableToolbarButtons(["html"]); +if(!A){this.focus() +}}else{this.setStaticContent(this.getCurrentContent()); +this.loadCurrentContentFromStaticContent(); +this.outmostWrapper.style.display="none"; +this.contentElement.style.display="block"; +this.currentEditMode=B +}}this._fireOnCurrentEditModeChanged(this,B) +},loadStylesheet:function(C){var A=this.editorDoc.getElementsByTagName("HEAD")[0]; +var B=this.editorDoc.createElement("LINK"); +B.rel="Stylesheet"; +B.type="text/css"; +B.href=C; +A.appendChild(B) +},loadCurrentContentFromStaticContent:function(){var A=this.validator.invalidate(this.getStaticContentAsDOM()); +A=this.removeUnnecessarySpaces(A); +if(A.blank()){this.rdom.clearRoot() +}else{this.rdom.getRoot().innerHTML=A +}this.rdom.wrapAllInlineOrTextNodesAs("P",this.rdom.getRoot(),true); +var B=this.getWysiwygContent(true,true); +this.sourceEditorTextarea.value=B; +if(xq.Browser.isWebkit){this.sourceEditorTextarea.innerHTML=B +}this._fireOnCurrentContentChanged(this) +},enableToolbarButtons:function(A){if(!this.toolbarContainer){return +}this._execForAllToolbarButtons(A,function(B,C){B.firstChild.className=!C?"":"disabled" +}); +if(xq.Browser.isIE6){this.toolbarContainer.style.display="none"; +setTimeout(function(){this.toolbarContainer.style.display="block" +}.bind(this),0) +}},disableToolbarButtons:function(A){this._execForAllToolbarButtons(A,function(B,C){B.firstChild.className=C?"":"disabled" +}) +},_execForAllToolbarButtons:function(B,A){if(!this.toolbarContainer){return +}B=B||[]; +$(this.toolbarContainer).select("li").each(function(C){var E=C.classNames().find(function(F){return F!="xq_separator" +}); +var D=B.include(E); +A(C,D) +}) +},_updateToolbarButtonStatus:function(C,B){var A=this.toolbarButtons.get(C); +if(A){A.firstChild.firstChild.className=B?"selected":"" +}},updateAllToolbarButtonsStatus:function(A){if(!this.toolbarContainer){return +}if(!this.toolbarButtons){var D=["emphasis","strongEmphasis","underline","strike","superscription","subscription","justifyLeft","justifyCenter","justifyRight","justifyBoth","unorderedList","orderedList","code","paragraph","heading1","heading2","heading3","heading4","heading5","heading6"]; +this.toolbarButtons=$H({}); +D.each(function(F){var G=$(this.toolbarContainer).getElementsBySelector("."+F); +var E=G&&G.length>0?G[0]:null; +if(E){this.toolbarButtons.set(F,E) +}}.bind(this)) +}var B=this.toolbarButtons; +var C=this.rdom.collectStructureAndStyle(A); +this._updateToolbarButtonStatus("emphasis",C.em); +this._updateToolbarButtonStatus("strongEmphasis",C.strong); +this._updateToolbarButtonStatus("underline",C.underline); +this._updateToolbarButtonStatus("strike",C.strike); +this._updateToolbarButtonStatus("superscription",C.superscription); +this._updateToolbarButtonStatus("subscription",C.subscription); +this._updateToolbarButtonStatus("justifyLeft",C.justification=="left"); +this._updateToolbarButtonStatus("justifyCenter",C.justification=="center"); +this._updateToolbarButtonStatus("justifyRight",C.justification=="right"); +this._updateToolbarButtonStatus("justifyBoth",C.justification=="justify"); +this._updateToolbarButtonStatus("orderedList",C.list=="OL"); +this._updateToolbarButtonStatus("unorderedList",C.list=="UL"); +this._updateToolbarButtonStatus("code",C.list=="CODE"); +this._updateToolbarButtonStatus("paragraph",C.block=="P"); +this._updateToolbarButtonStatus("heading1",C.block=="H1"); +this._updateToolbarButtonStatus("heading2",C.block=="H2"); +this._updateToolbarButtonStatus("heading3",C.block=="H3"); +this._updateToolbarButtonStatus("heading4",C.block=="H4"); +this._updateToolbarButtonStatus("heading5",C.block=="H5"); +this._updateToolbarButtonStatus("heading6",C.block=="H6") +},removeUnnecessarySpaces:function(A){var C=this.rdom.tree.getBlockTags().join("|"); +var B=new RegExp("\\s*<(/?)("+C+")>\\s*","img"); +return A.replace(B,"<$1$2>") +},getCurrentContent:function(A){if(this.getCurrentEditMode()=="source"){return this.getSourceContent(A) +}else{return this.getWysiwygContent(A) +}},getWysiwygContent:function(A,B){if(B||!A){return this.validator.validate(this.rdom.getRoot(),A) +}var C=this.editHistory.getLastModifiedDate(); +if(this._lastModified!=C){this._validContentCache=this.validator.validate(this.rdom.getRoot(),A); +this._lastModified=C +}return this._validContentCache +},getSourceContent:function(C){var B=this.sourceEditorTextarea[xq.Browser.isWebkit?"innerHTML":"value"]; +var A=document.createElement("div"); +A.innerHTML=this.removeUnnecessarySpaces(B); +var D=xq.RichDom.createInstance(); +D.setRoot(document.body); +D.wrapAllInlineOrTextNodesAs("P",A,true); +return this.validator.validate(A,C) +},setStaticContent:function(A){if(this.contentElement.nodeName=="TEXTAREA"){this.contentElement.value=A; +if(xq.Browser.isWebkit){this.contentElement.innerHTML=A +}}else{this.contentElement.innerHTML=A +}this._fireOnStaticContentChanged(this,A) +},getStaticContent:function(){var A; +if(this.contentElement.nodeName=="TEXTAREA"){A=this.contentElement[xq.Browser.isWebkit?"innerHTML":"value"] +}else{A=this.contentElement.innerHTML +}return A +},getStaticContentAsDOM:function(){if(this.contentElement.nodeName=="TEXTAREA"){var A=this.doc.createElement("DIV"); +A.innerHTML=this.contentElement[xq.Browser.isWebkit?"innerHTML":"value"]; +return A +}else{return this.contentElement +}},focus:function(){if(this.getCurrentEditMode()=="wysiwyg"){this.rdom.focus(); +window.setTimeout(function(){this.updateAllToolbarButtonsStatus(this.rdom.getCurrentElement()) +}.bind(this),0) +}else{if(this.getCurrentEditMode()=="source"){this.sourceEditorTextarea.focus() +}}},getFrame:function(){return this.editorFrame +},getWin:function(){return this.editorWin +},getDoc:function(){return this.editorDoc +},getOutmostWrapper:function(){return this.outmostWrapper +},getBody:function(){return this.editorBody +},_createEditorFrame:function(){this.outmostWrapper=this.doc.createElement("div"); +this.outmostWrapper.className="xquared"; +this.contentElement.parentNode.insertBefore(this.outmostWrapper,this.contentElement); +if(!this.toolbarContainer&&this.config.generateDefaultToolbar){this.toolbarContainer=this._generateDefaultToolbar(); +this.outmostWrapper.appendChild(this.toolbarContainer) +}this.sourceEditorDiv=this.doc.createElement("div"); +this.sourceEditorDiv.className="editor source_editor"; +this.sourceEditorDiv.style.display="none"; +this.outmostWrapper.appendChild(this.sourceEditorDiv); +this.sourceEditorTextarea=this.doc.createElement("textarea"); +this.sourceEditorDiv.appendChild(this.sourceEditorTextarea); +this.wysiwygEditorDiv=this.doc.createElement("div"); +this.wysiwygEditorDiv.className="editor wysiwyg_editor"; +this.wysiwygEditorDiv.style.display="none"; +this.outmostWrapper.appendChild(this.wysiwygEditorDiv); +this.editorFrame=this.doc.createElement("iframe"); +this.rdom.setAttributes(this.editorFrame,{"frameBorder":"0","marginWidth":"0","marginHeight":"0","leftMargin":"0","topMargin":"0","allowTransparency":"true"}); +this.wysiwygEditorDiv.appendChild(this.editorFrame); +var B=this.editorFrame.contentWindow.document; +if(xq.Browser.isTrident){B.designMode="On" +}B.open(); +B.write(""); +B.write(""); +B.write(""); +if(!xq.Browser.isTrident){B.write("") +}B.write(""); +B.write("XQuared"); +if(this.config.changeCursorOnLink){B.write("") +}B.write(""); +B.write("

"+this.rdom.makePlaceHolderString()+"

"); +B.write(""); +B.close(); +this.editorWin=this.editorFrame.contentWindow; +this.editorDoc=this.editorWin.document; +this.editorBody=this.editorDoc.body; +this.editorBody.className="xed"; +if(xq.Browser.isIE6){this.editorDoc.documentElement.style.overflowY="auto"; +this.editorDoc.documentElement.style.overflowX="hidden" +}this.rdom.setWin(this.editorWin); +this.rdom.setRoot(this.editorBody); +this.validator=xq.Validator.createInstance(this.doc.location.href,this.config.urlValidationMode,this.config.allowedTags,this.config.allowedAttributes); +if(this.config.automaticallyHookSubmitEvent&&this.contentElement.nodeName=="TEXTAREA"&&this.contentElement.form){var A=this.contentElement.form.onsubmit; +this.contentElement.form.onsubmit=function(){this.contentElement.value=this.getCurrentContent(true); +if(A){return A() +}else{return true +}}.bind(this) +}},_addStyleRule:function(A,C){if(!this.dynamicStyle){if(xq.Browser.isTrident){this.dynamicStyle=this.doc.createStyleSheet() +}else{var B=this.doc.createElement("style"); +this.doc.body.appendChild(B); +this.dynamicStyle=$A(this.doc.styleSheets).last() +}}if(xq.Browser.isTrident){this.dynamicStyle.addRule(A,C) +}else{this.dynamicStyle.insertRule(A+" {"+C+"}",this.dynamicStyle.cssRules.length) +}},_generateDefaultToolbar:function(){this._addStyleRule(".xquared div.toolbar","background-image: url("+this.config.imagePathForDefaultToobar+"toolbarBg.gif)"); +this._addStyleRule(".xquared ul.buttons li","background-image: url("+this.config.imagePathForDefaultToobar+"toolbarButtonBg.gif)"); +this._addStyleRule(".xquared ul.buttons li.xq_separator","background-image: url("+this.config.imagePathForDefaultToobar+"toolbarSeparator.gif)"); +var container=this.doc.createElement("div"); +container.className="toolbar"; +var buttons=this.doc.createElement("ul"); +buttons.className="buttons"; +container.appendChild(buttons); +var cancelMousedown=function(e){Event.stop(e); +return false +}; +var map=this.config.defaultToolbarButtonMap; +for(var i=0; +i0){this._contextMenuContainer.removeChild(this._contextMenuContainer.childNodes[0]) +}}for(var B=0; +B"+(B.title.toString().escapeHTML())+"" +}else{A.innerHTML=(B.title.toString().escapeHTML()) +}}if(B.className){A.className=B.className +}this._contextMenuContainer.appendChild(A); +return A +},_contextMenuClicked:function(e){this.hideContextMenu(); +if(!this._contextMenuContainer){return +}var node=Event.findElement(e,"LI"); +if(!node||!this.rdom.tree.isDescendantOf(this._contextMenuContainer,node)){return +}for(var i=0; +i-1,isGecko:navigator.userAgent.indexOf("Gecko")>-1&&navigator.userAgent.indexOf("KHTML")==-1,isKHTML:navigator.userAgent.indexOf("KHTML")!=-1,isPresto:navigator.appName=="Opera",isMac:navigator.userAgent.indexOf("Macintosh")!=-1,isUbuntu:navigator.userAgent.indexOf("Ubuntu")!=-1,isIE:navigator.appName=="Microsoft Internet Explorer",isIE6:navigator.userAgent.indexOf("MSIE 6")!=-1,isIE7:navigator.userAgent.indexOf("MSIE 7")!=-1}; +xq.Shortcut=Class.create({initialize:function(A){this.keymap=(typeof A=="string")?xq.Shortcut.interprete(A).keymap:A +},matches:function(A){var B=xq.Browser.isGecko&&xq.Browser.isMac?(A.keyCode+"_"+A.charCode):A.keyCode; +var D=(this.keymap.which==B)||(this.keymap.which==32&&B==25); +if(typeof A.metaKey=="undefined"){A.metaKey=false +}var C=(typeof this.keymap.shiftKey=="undefined"||this.keymap.shiftKey==A.shiftKey)&&(typeof this.keymap.altKey=="undefined"||this.keymap.altKey==A.altKey)&&(typeof this.keymap.ctrlKey=="undefined"||this.keymap.ctrlKey==A.ctrlKey)&&(typeof this.keymap.metaKey=="undefined"||this.keymap.metaKey==A.metaKey); +return C&&D +}}); +xq.Shortcut.interprete=function(G){G=G.toUpperCase(); +var F=xq.Shortcut._interpreteWhich(G.split("+").pop()); +var E=xq.Shortcut._interpreteModifier(G,"CTRL"); +var C=xq.Shortcut._interpreteModifier(G,"ALT"); +var B=xq.Shortcut._interpreteModifier(G,"SHIFT"); +var D=xq.Shortcut._interpreteModifier(G,"META"); +var A={}; +A.which=F; +if(typeof E!="undefined"){A.ctrlKey=E +}if(typeof C!="undefined"){A.altKey=C +}if(typeof B!="undefined"){A.shiftKey=B +}if(typeof D!="undefined"){A.metaKey=D +}return new xq.Shortcut(A) +}; +xq.Shortcut._interpreteModifier=function(A,B){return A.match("\\("+B+"\\)")?undefined:A.match(B)?true:false +}; +xq.Shortcut._interpreteWhich=function(A){var B=A.length==1?((xq.Browser.isMac&&xq.Browser.isGecko)?"0_"+A.toLowerCase().charCodeAt(0):A.charCodeAt(0)):xq.Shortcut._keyNames[A]; +if(typeof B=="undefined"){throw"Unknown special key name: ["+A+"]" +}return B +}; +xq.Shortcut._keyNames=xq.Browser.isMac&&xq.Browser.isGecko?{BACKSPACE:"8_0",TAB:"9_0",RETURN:"13_0",ENTER:"13_0",ESC:"27_0",SPACE:"0_32",LEFT:"37_0",UP:"38_0",RIGHT:"39_0",DOWN:"40_0",DELETE:"46_0",HOME:"36_0",END:"35_0",PAGEUP:"33_0",PAGEDOWN:"34_0",COMMA:"0_44",HYPHEN:"0_45",EQUAL:"0_61",PERIOD:"0_46",SLASH:"0_47",F1:"112_0",F2:"113_0",F3:"114_0",F4:"115_0",F5:"116_0",F6:"117_0",F7:"118_0",F8:"119_0"}:{BACKSPACE:8,TAB:9,RETURN:13,ENTER:13,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46,HOME:36,END:35,PAGEUP:33,PAGEDOWN:34,COMMA:188,HYPHEN:xq.Browser.isTrident?189:109,EQUAL:xq.Browser.isTrident?187:61,PERIOD:190,SLASH:191,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123}; +xq.DomTree=Class.create({initialize:function(){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","BR","HR"] +},getBlockTags:function(){return this._blockTags +},findCommonAncestorAndImmediateChildrenOf:function(E,C){if(E.parentNode==C.parentNode){return{left:E,right:C,parent:E.parentNode} +}else{var D=this.collectParentsOf(E,true); +var G=this.collectParentsOf(C,true); +var B=this.getCommonAncestor(D,G); +var F=D.find(function(H){return H.parentNode==B +}); +var A=G.find(function(H){return H.parentNode==B +}); +return{left:F,right:A,parent:B} +}},getLeavesAtEdge:function(C){if(!C.hasChildNodes()){return[null,null] +}var D=function(G){for(var F=0; +F0){for(var A=0; +AQ[L].length){C-=Q[L].length +}else{M=L; +break +}}if(R){return{text:P,textIndex:N,nodeIndex:M,breakPoint:C} +}if(C!=0){var I=B[M].splitText(C); +M++; +B.splice(M,0,I) +}var A=B[M]||H.firstChild; +var O=this.tree.findCommonAncestorAndImmediateChildrenOf(A,G); +var K=O.parent; +if(K){if(A.parentNode!=K){A=this.splitElementUpto(A,K,true) +}if(G.parentNode!=K){G=this.splitElementUpto(G,K,true) +}var D=A.previousSibling; +var J=G.nextSibling; +if(D&&D.nodeType==1&&this.isEmptyBlock(D)){this.deleteNode(D) +}if(J&&J.nodeType==1&&this.isEmptyBlock(J)){this.deleteNode(J) +}var E=this.insertNodeAt(this.createElement(S),A,"before"); +while(E.nextSibling!=G){E.appendChild(E.nextSibling) +}return E +}else{var E=this.insertNodeAt(this.createElement(S),G,"before"); +return E +}},wrapAllInlineOrTextNodesAs:function(A,B,E){var D=[]; +if(!E&&!this.tree.hasMixedContents(B)){return D +}var C=B.firstChild; +while(C){if(this.tree.isTextOrInlineNode(C)){var F=this.wrapInlineOrTextNodesAs(A,C); +D.push(F); +C=F.nextSibling +}else{C=C.nextSibling +}}return D +},wrapInlineOrTextNodesAs:function(A,B){var D=this.createElement(A); +var C=B; +C.parentNode.replaceChild(D,C); +D.appendChild(C); +while(D.nextSibling&&this.tree.isTextOrInlineNode(D.nextSibling)){D.appendChild(D.nextSibling) +}return D +},turnElementIntoListItem:function(C,D){D=D.toUpperCase(); +var B=this.createElement(D=="UL"?"UL":"OL"); +if(D=="CODE"){B.className="code" +}if(this.tree.isTableCell(C)){var E=this.wrapAllInlineOrTextNodesAs("P",C,true)[0]; +B=this.insertNodeAt(B,C,"start"); +var A=this.insertNodeAt(this.createElement("LI"),B,"start"); +A.appendChild(E) +}else{B=this.insertNodeAt(B,C,"after"); +var A=this.insertNodeAt(this.createElement("LI"),B,"start"); +A.appendChild(C) +}this.unwrapUnnecessaryParagraph(A); +this.mergeAdjustLists(B); +return A +},extractOutElementFromParent:function(B){if(B==this.root||this.root==B.parentNode||!B.offsetParent){return null +}if(B.nodeName=="LI"){this.wrapAllInlineOrTextNodesAs("P",B,true); +B=B.firstChild +}var A=B.parentNode; +var D=null; +if(A.nodeName=="LI"&&A.parentNode.parentNode.nodeName=="LI"){if(B.previousSibling){this.splitContainerOf(B,true); +this.correctEmptyElement(B) +}this.outdentListItem(B); +D=B +}else{if(A.nodeName=="LI"){if(this.tree.isListContainer(B.nextSibling)){var E=A.parentNode; +this.splitContainerOf(A,true); +this.correctEmptyElement(B); +D=A.firstChild; +while(A.firstChild){this.insertNodeAt(A.firstChild,E,"before") +}var C=E.previousSibling; +this.deleteNode(E); +if(C&&this.tree.isListContainer(C)){this.mergeAdjustLists(C) +}}else{this.splitContainerOf(B,true); +this.correctEmptyElement(B); +var E=this.splitContainerOf(A); +this.insertNodeAt(B,E.parentNode,"before"); +this.deleteNode(E.parentNode); +D=B +}}else{if(this.tree.isTableCell(A)||this.tree.isTableCell(B)){}else{this.splitContainerOf(B,true); +this.correctEmptyElement(B); +D=this.insertNodeAt(B,A,"before"); +this.deleteNode(A) +}}}return D +},insertNewBlockAround:function(E,D,B){var C=E.nodeName=="LI"||E.parentNode.nodeName=="LI"; +this.removeTrailingWhitespace(E); +if(this.isFirstLiWithNestedList(E)&&!B&&D){var A=this.getParentElementOf(E,["LI"]); +var F=this._insertNewBlockAround(A,D); +return F +}else{if(C&&!B){var A=this.getParentElementOf(E,["LI"]); +var F=this._insertNewBlockAround(E,D); +if(A!=E){F=this.splitContainerOf(F,false,"prev") +}return F +}else{if(this.tree.isBlockContainer(E)){this.wrapAllInlineOrTextNodesAs("P",E,true); +return this._insertNewBlockAround(E.firstChild,D,B) +}else{return this._insertNewBlockAround(E,D,this.tree.isHeading(E)?"P":B) +}}}},_insertNewBlockAround:function(B,C,A){var D=this.createElement(A||B.nodeName); +this.copyAttributes(B,D,false); +this.correctEmptyElement(D); +D=this.insertNodeAt(D,B,C?"before":"after"); +return D +},applyTagIntoElement:function(A,B){if(this.tree.isBlockOnlyContainer(A)){return this.wrapBlock(A,B) +}else{if(this.tree.isBlockContainer(B)){var C=this.createElement(A); +this.moveChildNodes(B,C); +return this.insertNodeAt(C,B,"start") +}else{if(this.tree.isBlockContainer(A)&&this.hasImportantAttributes(B)){return this.wrapBlock(A,B) +}else{return this.replaceTag(A,B) +}}}throw"IllegalArgumentException - ["+A+", "+B+"]" +},applyTagIntoElements:function(C,I,J){var E=[]; +if(this.tree.isBlockContainer(C)){var G=this.tree.findCommonAncestorAndImmediateChildrenOf(I,J); +var D=G.left; +var B=this.insertNodeAt(this.createElement(C),D,"before"); +var K=G.parent.nodeName=="LI"&&G.parent.parentNode.childNodes.length==1&&!G.left.previousSilbing&&!G.right.nextSibling; +if(K){var H=D.parentNode.parentNode; +this.insertNodeAt(B,H,"before"); +B.appendChild(H) +}else{while(D!=G.right){next=D.nextSibling; +B.appendChild(D); +D=next +}B.appendChild(G.right) +}E.push(B) +}else{var A=this.getBlockElementsBetween(I,J); +for(var F=0; +F=2&&this.isMarker(D.lastChild.previousSibling)&&D.lastChild.nodeType==3&&D.lastChild.nodeValue.length==1&&D.lastChild.nodeValue.charCodeAt(0)==160){this.deleteNode(D.lastChild) +}this.removePlaceHoldersAndEmptyNodes(D); +if(this.isEmptyBlock(D)){if(this.tree.isAtomic(D)){D=this.replaceTag("P",D) +}D=this.replaceTag(F.nodeName,D)||D; +D.innerHTML="" +}else{if(D.firstChild==D.lastChild&&this.isMarker(D.firstChild)){D=this.replaceTag(F.nodeName,D)||D +}}if(this.isEmptyBlock(F)){if(this.tree.isAtomic(F)){F=this.replaceTag("P",F) +}F.innerHTML="" +}this.moveChildNodes(F,D); +this.deleteNode(F); +return D +}finally{if(H&&this.isEmptyBlock(H)){this.deleteNode(H,true) +}if(G&&this.isEmptyBlock(G)){this.deleteNode(G,true) +}if(H){this.unwrapUnnecessaryParagraph(H) +}if(G){this.unwrapUnnecessaryParagraph(G) +}}},mergeAdjustLists:function(A,G,D){var F=A.previousSibling; +var C=F&&(F.nodeName==A.nodeName&&F.className==A.className); +if((!D||D.toLowerCase()=="prev")&&(C||(G&&this.tree.isListContainer(F)))){while(F.lastChild){this.insertNodeAt(F.lastChild,A,"start") +}this.deleteNode(F) +}var E=A.nextSibling; +var B=E&&(E.nodeName==A.nodeName&&E.className==A.className); +if((!D||D.toLowerCase()=="next")&&(B||(G&&this.tree.isListContainer(E)))){while(E.firstChild){this.insertNodeAt(E.firstChild,A,"end") +}this.deleteNode(E) +}},moveChildNodes:function(B,A){if(this.tree.isDescendantOf(B,A)||["HTML","HEAD"].include(A.nodeName)){throw"Illegal argument. Cannot move children of element["+B.nodeName+"] to element["+A.nodeName+"]" +}if(B==A){return +}while(B.firstChild){A.appendChild(B.firstChild) +}},copyAttributes:function(E,D,B){var A=E.attributes; +if(!A){return +}for(var C=0; +C0?D:E +},outdentElementsCode:function(A){if(A.tagName=="LI"){A=A.parentNode +}if(A.tagName=="OL"&&A.className=="code"){return true +}return false +},_outdentElements:function(C,F,E){for(var B=0; +B0; +var D=O.length>0; +var L=null; +if(E){L=H +}else{if(D){L=O +}else{L=B +}}for(var F=0; +F" +},makeEmptyParagraph:function(){return this.createElementFromHtml("


") +},isPlaceHolder:function(B){if(B.nodeType!=1){return false +}var A=B.nodeName=="BR"&&B.getAttribute("type")=="_moz"; +if(A){return true +}var C=B.nodeName=="BR"&&!this.getNextSibling(B); +if(C){return true +}return false +},selectElement:function(B,C){if(!B){throw"[element] is null" +}if(B.nodeType!=1){throw"[element] is not an element" +}try{if(!xq.Browser.isMac){this.doc.execCommand("SelectAll",false,null) +}}catch(A){}if(C){this.rng().selectNode(B) +}else{this.rng().selectNodeContents(B) +}}}); +xq.RichDomWebkit=Class.create(xq.RichDomW3,{makePlaceHolder:function(){var A=this.createElement("BR"); +A.className="webkit-block-placeholder"; +return A +},makePlaceHolderString:function(){return"
" +},makeEmptyParagraph:function(){return this.createElementFromHtml("


") +},isPlaceHolder:function(A){return A.nodeName=="BR"&&A.className=="webkit-block-placeholder" +},rng:function(){var B=this.sel(); +var A=this.doc.createRange(); +if(!this._rng||this._anchorNode!=B.anchorNode||this._anchorOffset!=B.anchorOffset||this._focusNode!=B.focusNode||this._focusOffset!=B.focusOffset){if(B.type!="None"){A.setStart(B.anchorNode,B.anchorOffset); +A.setEnd(B.focusNode,B.focusOffset) +}this._anchorNode=B.anchorNode; +this._anchorOffset=B.anchorOffset; +this._focusNode=B.focusNode; +this._focusOffset=B.focusOffset; +this._rng=A +}return this._rng +},selectElement:function(B,C){if(!B){throw"[element] is null" +}if(B.nodeType!=1){throw"[element] is not an element" +}var A=this.rng(); +if(C){A.selectNode(B) +}else{A.selectNodeContents(B) +}this._setSelectionByRange(A) +},deleteSelection:function(){this.rng().deleteContents() +},collapseSelection:function(B){var A=this.rng(); +A.collapse(B); +this._setSelectionByRange(A) +},getSelectionAsHtml:function(){var B=this.createElement("div"); +var A=this.rng(); +var C=this.rng().cloneContents(); +if(C){B.appendChild(C) +}return B.innerHTML +},_setSelectionByRange:function(A){var B=this.sel(); +B.setBaseAndExtent(A.startContainer,A.startOffset,A.endContainer,A.endOffset); +this._anchorNode=B.anchorNode; +this._anchorOffset=B.anchorOffset; +this._focusNode=B.focusNode; +this._focusOffset=B.focusOffset +}}); +xq.RichDomTrident=Class.create(xq.RichDom,{makePlaceHolder:function(){return this.createTextNode(" ") +},makePlaceHolderString:function(){return" " +},makeEmptyParagraph:function(){return this.createElementFromHtml("

 

") +},isPlaceHolder:function(A){return false +},getOuterHTML:function(A){return A.outerHTML +},insertNode:function(B){if(this.hasSelection()){this.collapseSelection(true) +}this.rng().pasteHTML(""); +var A=this.$("xquared_temp"); +if(B.id=="xquared_temp"){return A +}A.replaceNode(B); +return B +},removeTrailingWhitespace:function(E){if(!E){return +}if(this.tree.isBlockContainer(E)){return +}if(this.isEmptyBlock(E)){return +}var D=E.innerText; +var B=D.charCodeAt(D.length-1); +if(D.length<=1||![32,160].include(B)){return +}var C=E; +while(C&&C.nodeType!=3){C=C.lastChild +}if(!C){return +}var A=C.nodeValue; +if(A.length<=1){this.deleteNode(C,true) +}else{C.nodeValue=A.substring(0,A.length-1) +}},correctEmptyElement:function(A){if(!A||A.nodeType!=1||this.tree.isAtomic(A)){return +}if(A.firstChild){this.correctEmptyElement(A.firstChild) +}else{A.innerHTML=" " +}},copyAttributes:function(C,B,A){B.mergeAttributes(C,!A) +},correctParagraph:function(){if(!this.hasFocus()){return false +}if(this.hasSelection()){return false +}var D=this.getCurrentElement(); +if(D.nodeName=="BODY"){D=this.insertNode(this.makeEmptyParagraph()); +var B=D.nextSibling; +if(this.tree.isAtomic(B)){D=this.insertNodeAt(D,B,"after"); +this.placeCaretAtStartOf(D); +var C=this.tree.findForward(D,function(E){return this.tree.isBlock(E)&&!this.tree.isBlockOnlyContainer(E) +}.bind(this)); +if(C){this.deleteNode(D); +this.placeCaretAtStartOf(C) +}return true +}else{var C=this.tree.findForward(D,function(E){return this.tree.isBlock(E)&&!this.tree.isBlockOnlyContainer(E) +}.bind(this)); +if(C){this.deleteNode(D); +this.placeCaretAtStartOf(C) +}return true +}}else{D=this.getCurrentBlockElement(); +if(D.nodeType==3){D=D.parentNode +}if(this.tree.hasMixedContents(D)){var A=this.pushMarker(); +this.wrapAllInlineOrTextNodesAs("P",D,true); +this.popMarker(true); +return true +}else{if((this.tree.isTextOrInlineNode(D.previousSibling)||this.tree.isTextOrInlineNode(D.nextSibling))&&this.tree.hasMixedContents(D.parentNode)){this.wrapAllInlineOrTextNodesAs("P",D.parentNode,true); +return true +}else{return false +}}}},execCommand:function(A,B){return this.doc.execCommand(A,false,B) +},applyBackgroundColor:function(A){this.execCommand("BackColor",A) +},applyEmphasis:function(){this.execCommand("Italic") +},applyStrongEmphasis:function(){this.execCommand("Bold") +},applyStrike:function(){this.execCommand("strikethrough") +},applyUnderline:function(){this.execCommand("underline") +},applyRemoveFormat:function(){this.execCommand("RemoveFormat"); +this.execCommand("Unlink") +},execHeading:function(A){this.execCommand("FormatBlock","") +},focus:function(){this.win.focus(); +if(!this._focusedBefore){this.correctParagraph(); +this.placeCaretAtStartOf(this.getCurrentBlockElement()); +this._focusedBefore=true +}},sel:function(){return this.doc.selection +},rng:function(){try{var B=this.sel(); +return(B==null)?null:B.createRange() +}catch(A){return null +}},hasSelection:function(){var A=this.sel().type.toLowerCase(); +if("none"==A){return false +}if("text"==A&&this.getSelectionAsHtml().length==0){return false +}return true +},deleteSelection:function(){if(this.getSelectionAsText()!=""){this.sel().clear() +}},placeCaretAtStartOf:function(A){var B=this.insertNodeAt(this.createElement("SPAN"),A,"start"); +this.selectElement(B); +this.collapseSelection(false); +this.deleteNode(B) +},selectElement:function(B,C){if(!B){throw"[element] is null" +}if(B.nodeType!=1){throw"[element] is not an element" +}var A=this.rng(); +A.moveToElementText(B); +A.select() +},selectBlocksBetween:function(D,B){var A=this.rng(); +var C=this.rng(); +C.moveToElementText(D); +A.setEndPoint("StartToStart",C); +C.moveToElementText(B); +A.setEndPoint("EndToEnd",C); +A.select() +},collapseSelection:function(B){var A=this.rng(); +A.collapse(B); +A.select() +},getSelectionAsHtml:function(){var A=this.rng(); +return A&&A.htmlText?A.htmlText:"" +},getSelectionAsText:function(){var A=this.rng(); +return A&&A.text?A.text:"" +},hasImportantAttributes:function(A){return !!(A.id||A.className||A.style.cssText) +},isEmptyBlock:function(A){if(!A.hasChildNodes()){return true +}if(A.nodeType==3&&!A.nodeValue){return true +}if([" "," ",""].include(A.innerHTML)){return true +}return false +},getLastChild:function(C){if(!C||!C.hasChildNodes()){return null +}var A=$A(C.childNodes).reverse(); +for(var B=0; +BA)?B.cells[A]:null +},getRowAt:function(A){if(this.hasHeadingAtTop()){return A==0?this.table.tHead.rows[0]:this.table.tBodies[0].rows[A-1] +}else{var B=this.table.tBodies[0].rows; +return(B.length>A)?B[A]: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 A=$A(this.table.getElementsByTagName("TH")); +A.push($A(this.table.getElementsByTagName("TD"))); +A=A.flatten(); +for(var B=0; +B"); +if(I){F.push(""); +for(var D=0; +D") +}F.push(""); +J-=1 +}F.push(""); +for(var D=0; +D"); +for(var B=0; +B") +}else{F.push("") +}}F.push("") +}F.push(""); +F.push(""); +var A=E.createElement("div"); +A.innerHTML=F.join(""); +var H=new xq.RichTable(E,A.firstChild); +H.correctEmptyCells(); +return H +}; +xq.Validator=Class.create({initialize:function(C,A,D,B){this.allowedTags=(D||["a","abbr","acronym","address","blockquote","br","caption","cite","code","dd","dfn","div","dl","dt","em","h1","h2","h3","h4","h5","h6","hr","img","kbd","li","ol","p","pre","q","samp","span","sup","sub","strong","table","thead","tbody","td","th","tr","ul","var"]).join(" ")+" "; +this.allowedAttrs=(B||["alt","cite","class","datetime","height","href","id","rel","rev","src","style","title","width"]).join(" ")+" "; +this.curUrl=C; +this.curUrlParts=C?C.parseURL():null; +this.urlValidationMode=A +},validate:function(B,A){throw"Not implemented" +},invalidate:function(A){throw"Not implemented" +},validateStrike:function(A){A=A.replace(/|\s+[^>]*>)/ig,"/ig,""); +return A +},validateUnderline:function(A){A=A.replace(/|\s+[^>]*>)/ig,"/ig,""); +return A +},replaceTag:function(A,C,B){return A.replace(new RegExp("(|\\s+[^>]*>)","ig"),"$1"+B+"$2") +},validateSelfClosingTags:function(A){return A.replace(/<(br|hr|img)([^>]*?)>/img,function(D,B,C){return"<"+B+C+" />" +}) +},removeComments:function(A){return A.replace(//img,"") +},removeDangerousElements:function(C){var A=$A(C.getElementsByTagName("SCRIPT")).reverse(); +for(var B=0; +B]+?)(>|\\s+([^>]*?)(\\s?/?)>)","g"),function(H,K,M,G,L,I){if(C.indexOf(M)==-1){return"" +}if(L){L=L.replace(/(^|\s")([^"=]+)(\s|$)/g,"$1$2=\"$2\"$3"); +var J=[]; +var E=L.match(/([^=]+)="[^"]*?"/g); +for(var F=0; +F" +}else{return H +}}) +},makeUrlsRelative:function(A){var B=this.curUrl; +var C=this.curUrlParts; +return A.replace(/(<\w+\s+)(\/|([^>]+?)(\/?))>/g,function(H,G,D,F,E){if(F){F=F.replace(/(href|src)="([^"]+)"/g,function(M,L,K){var J=null; +if(K.charAt(0)=="#"){J=C.includeQuery+K +}else{if(K.charAt(0)=="?"){J=C.includePath+K +}else{if(K.charAt(0)=="/"){J=C.includeHost+K +}else{if(K.match(/^\w+:\/\//)){J=K +}else{J=C.includeBase+K +}}}}var I=J; +if(J.indexOf(C.includeQuery)==0){I=J.substring(C.includeQuery.length) +}else{if(J.indexOf(C.includePath)==0){I=J.substring(C.includePath.length) +}else{if(J.indexOf(C.includeBase)==0){I=J.substring(C.includeBase.length) +}else{if(J.indexOf(C.includeHost)==0){I=J.substring(C.includeHost.length) +}}}}if(I==""){I="#" +}return L+"=\""+I+"\"" +}); +return G+F+E+">" +}else{return H +}}); +return A +},makeUrlsHostRelative:function(A){var B=this.curUrl; +var C=this.curUrlParts; +return A.replace(/(<\w+\s+)(\/|([^>]+?)(\/?))>/g,function(H,G,D,F,E){if(F){F=F.replace(/(href|src)="([^"]+)"/g,function(M,L,K){var J=null; +if(K.charAt(0)=="#"){J=C.includeQuery+K +}else{if(K.charAt(0)=="?"){J=C.includePath+K +}else{if(K.charAt(0)=="/"){J=C.includeHost+K +}else{if(K.match(/^\w+:\/\//)){J=K +}else{J=C.includeBase+K +}}}}var I=J; +if(J.indexOf(C.includeHost)==0){I=J.substring(C.includeHost.length) +}if(I==""){I="#" +}return L+"=\""+I+"\"" +}); +return G+F+E+">" +}else{return H +}}); +return A +},makeUrlsAbsolute:function(A){var B=this.curUrl; +var C=this.curUrlParts; +return A.replace(/(<\w+\s+)(\/|([^>]+?)(\/?))>/g,function(H,G,D,F,E){if(F){F=F.replace(/(href|src)="([^"]+)"/g,function(L,K,J){var I=null; +if(J.charAt(0)=="#"){I=C.includeQuery+J +}else{if(J.charAt(0)=="?"){I=C.includePath+J +}else{if(J.charAt(0)=="/"){I=C.includeHost+J +}else{if(J.match(/^\w+:\/\//)){I=J +}else{I=C.includeBase+J +}}}}return K+"=\""+I+"\"" +}); +return G+F+E+">" +}else{return H +}}) +}}); +xq.Validator.createInstance=function(C,A,D,B){if(xq.Browser.isTrident){return new xq.ValidatorTrident(C,A,D,B) +}else{if(xq.Browser.isWebkit){return new xq.ValidatorWebkit(C,A,D,B) +}else{return new xq.ValidatorGecko(C,A,D,B) +}}}; +xq.ValidatorW3=Class.create(xq.Validator,{validate:function(C,B){C=C.cloneNode(true); +var F=xq.RichDom.createInstance(); +F.setRoot(C); +F.removePlaceHoldersAndEmptyNodes(C); +this.removeDangerousElements(C); +this.validateFontColor(C); +var E=C.innerHTML; +try{E=this.replaceTag(E,"b","strong"); +E=this.replaceTag(E,"i","em"); +E=this.validateStrike(E); +E=this.validateUnderline(E); +E=this.addNbspToEmptyBlocks(E); +if(B){E=this.performFullValidation(E) +}}catch(A){}var G=F.tree.getBlockTags().join("|"); +var D=new RegExp("([^\n])","img"); +E=E.replace(D,"\n$2"); +return E +},invalidate:function(C){var F=xq.RichDom.createInstance(); +F.setRoot(C); +var E=F.findByAttribute("class","strike"); +for(var B=0; +B\\s*?","img"); +return B.replace(A,"<$1> ") +},replaceNbspToBr:function(B){var D=new xq.DomTree().getBlockTags().join("|"); +var A=new RegExp("<("+D+")>( )?","img"); +var C=xq.RichDom.createInstance(); +return B.replace(A,"<$1>"+C.makePlaceHolderString()+"") +}}); +xq.ValidatorGecko=Class.create(xq.ValidatorW3,{}); +xq.ValidatorWebkit=Class.create(xq.ValidatorW3,{}); +xq.ValidatorTrident=Class.create(xq.Validator,{validate:function(C,B){C=C.cloneNode(true); +this.removeDangerousElements(C); +this.validateFontColor(C); +this.validateBackgroundColor(C); +var D=C.innerHTML; +try{D=this.validateStrike(D); +D=this.validateUnderline(D); +if(B){D=this.performFullValidation(D) +}}catch(A){}return D +},invalidate:function(C){var F=xq.RichDom.createInstance(); +F.setRoot(C); +this.invalidateFontColor(C); +this.invalidateBackgroundColor(C); +var E=F.findByAttribute("className","strike"); +for(var B=0; +B]*?)>/img,function(E,B,D,C){return"<"+B+D.toLowerCase()+this.correctHtmlAttrQuotation(C)+">" +}.bind(this)); +return A +},correctHtmlAttrQuotation:function(A){A=A.replace(/\s(\w+?)=\s+"([^"]+)"/mg,function(D,B,C){return" "+B.toLowerCase()+"=\""+C+"\"" +}); +A=A.replace(/\s(\w+?)=([^ "]+)/mg,function(D,B,C){return" "+B.toLowerCase()+"=\""+C+"\"" +}); +return A +}}); +xq.EditHistory=Class.create({initialize:function(B,A){if(!B){throw"IllegalArgumentException" +}this.disabled=false; +this.max=A||100; +this.rdom=B; +this.root=B.getRoot(); +this.clear(); +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.index0){var A=B.html.substring(0,B.caret)+""+B.html.substring(B.caret); +this.root.innerHTML=A +}else{this.root.innerHTML=B.html +}this.restoreCaret() +},pushContent:function(B){if(xq.Browser.isTrident&&!B&&!this.rdom.hasFocus()){return false +}if(!this.rdom.getCurrentElement()){return false +}var A=this.root.innerHTML; +if(A==(this.queue[this.index]?this.queue[this.index].html:null)){return false +}var C=B?-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:A,caret:C}); +return true +},clear:function(){this.index=-1; +this.queue=[]; +this.pushContent(true) +},saveCaret:function(){if(this.rdom.hasSelection()){return null +}var A=this.rdom.pushMarker(); +var C=xq.Browser.isTrident?"0){D[0].focus() +}if(C.cancelOnEsc){Event.observe(this.form,"keydown",function(F){if(F.keyCode==27){this.onCloseHandler(); +this.close() +}return false +}.bind(this)) +}this.onLoadHandler(this) +},close:function(){this.form.parentNode.removeChild(this.form) +},setPosition:function(G){var D; +if(G=="centerOfWindow"){D=document.documentElement +}else{if(G=="centerOfEditor"){D=this.xed.getDoc()[xq.Browser.isTrident?"body":"documentElement"] +}else{if(G=="nearbyCaret"){throw"Not implemented yet" +}else{throw"Invalid argument: "+G +}}}var E=D.clientWidth; +var B=D.clientHeight; +var F=this.form.clientWidth; +var C=this.form.clientHeight; +var A=parseInt((E-F)/2); +var H=parseInt((B-C)/2); +this.form.style.left=A+"px"; +this.form.style.top=H+"px" +}}); +xq.controls.QuickSearchDialog=Class.create({initialize:function(A,B){this.xed=A; +this.rdom=xq.RichDom.createInstance(); +this.rdom.setRoot(document.body); +this.param=B; +if(!this.param.renderItem){this.param.renderItem=function(C){return this.rdom.getInnerText(C) +}.bind(this) +}this.container=null +},getQuery:function(){if(!this.container){return"" +}return this._getInputField().value +},onSubmit:function(A){if(this.matchCount()>0){this.param.onSelect(this.xed,this.list[this._getSelectedIndex()]) +}this.close(); +Event.stop(A); +return false +},onCancel:function(A){if(this.param.onCancel){this.param.onCancel(this.xed) +}this.close() +},onBlur:function(A){setTimeout(function(){this.onCancel(A) +}.bind(this),400) +},onKey:function(C){var B=new xq.Shortcut("ESC"); +var D=new xq.Shortcut("ENTER"); +var A=new xq.Shortcut("UP"); +var E=new xq.Shortcut("DOWN"); +if(B.matches(C)){this.onCancel(C) +}else{if(D.matches(C)){this.onSubmit(C) +}else{if(A.matches(C)){this._moveSelectionUp() +}else{if(E.matches(C)){this._moveSelectionDown() +}else{this.updateList() +}}}}},onClick:function(C){var B=C.srcElement||C.target; +if(B.nodeName=="LI"){var A=this._getIndexOfLI(B); +this.param.onSelect(this.xed,this.list[A]) +}},onList:function(A){this.list=A; +this.renderList(A) +},updateList:function(){window.setTimeout(function(){this.param.listProvider(this.getQuery(),this.xed,this.onList.bind(this)) +}.bind(this),0) +},renderList:function(D){var B=this._getListContainer(); +B.innerHTML=""; +for(var C=0; +C=C){B=0 +}A.childNodes[B].className="selected" +}}); +if(!xq){xq={} +}if(!xq.ui_templates){xq.ui_templates={} +}xq.ui_templates.basicColorPickerDialog="
\n\t\t
\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\tYellow\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\tPink\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t
\n\t
"; +if(!xq){xq={} +}if(!xq.ui_templates){xq.ui_templates={} +}xq.ui_templates.basicLinkDialog="
\n\t\t

Link

\n\t\t
\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t
\n\t
" diff --git a/modules/editor/skins/xquared/js/xquared.js b/modules/editor/skins/xquared/js/xquared.js new file mode 100644 index 000000000..1e010f684 --- /dev/null +++ b/modules/editor/skins/xquared/js/xquared.js @@ -0,0 +1,7139 @@ +/** + * Namespace for entire Xquared classes + */ +var xq = { + majorVersion: '0.1', + minorVersion: '2007119' +}; + +/** + * Make given object as event source + * + * @param {Object} object target object + * @param {String} prefix prefix for generated functions + * @param {Array} events array of string which contains name of events + */ +xq.asEventSource = function(object, prefix, events) { + object._listeners = [] + object._registerEventFirer = function(prefix, name) { + this["_fireOn" + name] = function() { + for(var i = 0; i < this._listeners.length; i++) { + var listener = this._listeners[i]; + var func = listener["on" + prefix + name]; + if(func) func.apply(listener, $A(arguments)); + } + } + } + object.addListener = function(l) { + this._listeners.push(l); + } + + for(var i = 0; i < events.length; i++) { + object._registerEventFirer(prefix, events[i]); + } +} + +/** + * Returns the index of given element + * + * @returns {Number} index or -1 + */ +Array.prototype.indexOf = function(n) { + for(var i = 0; i < this.length; i++) { + if(this[i] == n) return i; + } + + return -1; +} + +Date.preset = null; +Date.pass = function(msec) { + if(Date.preset == null) return; + Date.preset = new Date(Date.preset.getTime() + msec); +} +Date.get = function() { + return Date.preset == null ? new Date() : Date.preset; +} +Date.prototype.elapsed = function(msec) { + return Date.get().getTime() - this.getTime() >= msec; +} + +String.prototype.merge = function(data) { + var newString = this; + for(k in data) { + newString = newString.replace("{" + k + "}", data[k]); + } + return newString; +} + +String.prototype.parseURL = function() { + var m = this.match(/((((\w+):\/\/(((([^@:]+)(:([^@]+))?)@)?([^:\/\?#]+)?(:(\d+))?))?([^\?#]+)?)(\?([^#]+))?)(#(.+))?/); + + var includeAnchor = m[0]; + var includeQuery = m[1] || undefined; + var includePath = m[2] || undefined; + var includeHost = m[3] || undefined; + var includeBase = null; + var protocol = m[4] || undefined; + var user = m[8] || undefined; + var password = m[10] || undefined; + var domain = m[11] || undefined; + var port = m[13] || undefined; + var path = m[14] || undefined; + var query = m[16] || undefined; + var anchor = m[18] || undefined; + + if(!path || path == '/') { + includeBase = includeHost + '/'; + } else { + var index = path.lastIndexOf('/'); + includeBase = includeHost + path.substring(0, index + 1); + } + + return { + includeAnchor: includeAnchor, + includeQuery: includeQuery, + includePath: includePath, + includeBase: includeBase, + includeHost: includeHost, + protocol: protocol, + user: user, + password: password, + domain: domain, + port: port, + path: path, + query: query, + anchor: anchor + }; +} + +xq.findXquaredScript = function() { + return $A(document.getElementsByTagName("script")).find(function(script) { + return script.src && script.src.match(/xquared\.js/i); + }); +} +xq.shouldLoadOthers = function() { + var script = xq.findXquaredScript(); + return script && !!script.src.match(/xquared\.js\?load_others=1/i); +} +xq.loadScript = function(url) { + document.write(''); +} +xq.loadOthers = function() { + var script = xq.findXquaredScript(); + var basePath = script.src.match(/(.*\/)xquared\.js.*/i)[1]; + var others = [ + 'Editor.js', + 'Browser.js', + 'Shortcut.js', + 'DomTree.js', + 'RichDom.js', + 'RichDomW3.js', + 'RichDomGecko.js', + 'RichDomWebkit.js', + 'RichDomTrident.js', + 'RichTable.js', + 'Validator.js', + 'ValidatorW3.js', + 'ValidatorGecko.js', + 'ValidatorWebkit.js', + 'ValidatorTrident.js', + 'EditHistory.js', + 'Controls.js', + '_ui_templates.js' + ]; + others.each(function(name) { + xq.loadScript(basePath + name); + }); +} + +if(xq.shouldLoadOthers()) xq.loadOthers(); +/** + * @fileOverview xq.Editor manages configurations such as autocompletion and autocorrection, edit mode/normal mode switching, handles editing commands, keyboard shortcuts and other events. + */ +xq.Editor = Class.create({ + /** + * Initialize editor but it doesn't automatically start designMode. setEditMode should be called after initialization. + * + * @constructor + * @param {Element} contentElement HTML element(TEXTAREA or normal block element such as DIV) to be replaced with editable area + * @param {Element} toolbarContainer HTML element which contains toolbar icons + */ + initialize: function(contentElement, toolbarContainer) { + if(!contentElement) throw "[contentElement] is null"; + if(contentElement.nodeType != 1) throw "[contentElement] is not an element"; + + xq.asEventSource(this, "Editor", ["ElementChanged", "BeforeEvent", "AfterEvent", "CurrentContentChanged", "StaticContentChanged", "CurrentEditModeChanged"]); + + /** + * Editor's configuration + * @type object + */ + this.config = {}; + this.config.enableLinkClick = false; + this.config.changeCursorOnLink = false; + this.config.generateDefaultToolbar = true; + this.config.defaultToolbarButtonMap = [ + [ + {className:"foregroundColor", title:"Foreground color", handler:"xed.handleForegroundColor()"}, + {className:"backgroundColor", title:"Background color", handler:"xed.handleBackgroundColor()"} + ], + [ + {className:"link", title:"Link", handler:"xed.handleLink()"}, + {className:"strongEmphasis", title:"Strong emphasis", handler:"xed.handleStrongEmphasis()"}, + {className:"emphasis", title:"Emphasis", handler:"xed.handleEmphasis()"}, + {className:"underline", title:"Underline", handler:"xed.handleUnderline()"}, + {className:"strike", title:"Strike", handler:"xed.handleStrike()"}, + {className:"superscription", title:"Superscription", handler:"xed.handleSuperscription()"}, + {className:"subscription", title:"Subscription", handler:"xed.handleSubscription()"} + ], + [ + {className:"removeFormat", title:"Remove format", handler:"xed.handleRemoveFormat()"} + ], + [ + {className:"justifyLeft", title:"Justify left", handler:"xed.handleJustify('left')"}, + {className:"justifyCenter", title:"Justify center", handler:"xed.handleJustify('center')"}, + {className:"justifyRight", title:"Justify right", handler:"xed.handleJustify('right')"}, + {className:"justifyBoth", title:"Justify both", handler:"xed.handleJustify('both')"} + ], + [ + {className:"indent", title:"Indent", handler:"xed.handleIndent()"}, + {className:"outdent", title:"Outdent", handler:"xed.handleOutdent()"} + ], + [ + {className:"unorderedList", title:"Unordered list", handler:"xed.handleList('UL')"}, + {className:"orderedList", title:"Ordered list", handler:"xed.handleList('OL')"} + ], + [ + {className:"paragraph", title:"Paragraph", handler:"xed.handleApplyBlock('P')"}, + {className:"heading1", title:"Heading 1", handler:"xed.handleApplyBlock('H1')"}, + {className:"blockquote", title:"Blockquote", handler:"xed.handleApplyBlock('BLOCKQUOTE')"}, + {className:"code", title:"Code", handler:"xed.handleList('CODE')"}, + {className:"division", title:"Division", handler:"xed.handleApplyBlock('DIV')"} + ], + [ + {className:"table", title:"Table", handler:"xed.handleTable(3,3,'tl')"}, + {className:"separator", title:"Separator", handler:"xed.handleSeparator()"} + ], + [ + {className:"html", title:"Edit source", handler:"xed.toggleSourceAndWysiwygMode()"} + ], + [ + {className:"undo", title:"Undo", handler:"xed.handleUndo()"}, + {className:"redo", title:"Redo", handler:"xed.handleRedo()"} + ] + ]; + + this.config.imagePathForDefaultToobar = request_uri+editor_path.substring(1)+'examples/img/toolbar/'; + + // relative | host_relative | absolute | browser_default + this.config.urlValidationMode = 'absolute'; + + this.config.automaticallyHookSubmitEvent = true; + + this.config.allowedTags = ['a', 'abbr', 'acronym', 'address', 'blockquote', 'br', 'caption', 'cite', 'code', 'dd', 'dfn', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'span', 'sup', 'sub', 'strong', 'table', 'thead', 'tbody', 'td', 'th', 'tr', 'ul', 'var']; + this.config.allowedAttributes = ['alt', 'cite', 'class', 'datetime', 'height', 'href', 'id', 'rel', 'rev', 'src', 'style', 'title', 'width']; + + this.config.shortcuts = {}; + this.config.autocorrections = {}; + this.config.autocompletions = {}; + this.config.templateProcessors = {}; + 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 'readonly' means read-only mode, true or 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode. + * @type Object + */ + this.currentEditMode = 'readonly'; + + /** + * RichDom instance + * @type xq.RichDom + */ + this.rdom = xq.RichDom.createInstance(); + + /** + * Validator instance + * @type xq.Validator + */ + 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; + + /** + * Design mode iframe + * @type IFrame + */ + this.editorFrame = null; + + /** + * Window that contains design mode iframe + * @type Window + */ + this.editorWin = null; + + /** + * Document that contained by design mode iframe + * @type Document + */ + this.editorDoc = null; + + /** + * Body that contained by design mode iframe + * @type Element + */ + this.editorBody = null; + + /** + * Toolbar container + * @type Element + */ + this.toolbarContainer = toolbarContainer; + + /** + * Toolbar buttons + * @type Array + */ + this.toolbarButtons = null; + + /** + * Undo/redo manager + * @type xq.EditHistory + */ + this.editHistory = null; + + this._contextMenuContainer = null; + this._contextMenuItems = null; + + this._validContentCache = null; + this._lastModified = null; + + this.addShortcuts(this._getDefaultShortcuts()); + this.addTemplateProcessors(this._getDefaultTemplateProcessors()); + + this.addListener({ + onEditorCurrentContentChanged: function(xed) { + var curFocusElement = xed.rdom.getCurrentElement(); + if(!curFocusElement) 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._lastFocusElement, curFocusElement); + xed._lastFocusElement = curFocusElement; + } + + xed.updateAllToolbarButtonsStatus(curFocusElement); + } + }); + }, + + + + ///////////////////////////////////////////// + // Configuration Management + + _getDefaultShortcuts: function() { + if(xq.Browser.isMac) { + // Mac FF & Safari + return [ + {event:"Ctrl+Shift+SPACE", handler:"this.handleAutocompletion(); stop = true;"}, + {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:"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:"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+Y", handler:"this.handleRedo()"} + ]; + } else { + // Win IE & FF + return [ + {event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"}, + {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+Y", handler:"this.handleRedo()"} + ]; + } + }, + + _getDefaultTemplateProcessors: function() { + return [ + { + id:"predefinedKeywordProcessor", + handler:function(html) { + var today = Date.get(); + var keywords = { + year: today.getFullYear(), + month: today.getMonth() + 1, + date: today.getDate(), + hour: today.getHours(), + min: today.getMinutes(), + sec: today.getSeconds() + }; + + return html.replace(/\{xq:(year|month|date|hour|min|sec)\}/img, function(text, keyword) { + return keywords[keyword] || keyword; + }); + } + } + ]; + }, + + /** + * 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) { + list.each(function(shortcut) { + this.addShortcut(shortcut.event, shortcut.handler); + }.bind(this)); + }, + + /** + * 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) { + list.each(function(ac) { + this.addAutocorrection(ac.id, ac.criteria, ac.handler); + }.bind(this)); + }, + + /** + * 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) { + list.each(function(ac) { + this.addAutocompletion(ac.id, ac.criteria, ac.handler); + }.bind(this)); + }, + + /** + * 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) { + list.each(function(tp) { + this.addTemplateProcessor(tp.id, tp.handler); + }.bind(this)); + }, + + /** + * 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) { + list.each(function(mh) { + this.addContextMenuHandler(mh.id, mh.handler); + }.bind(this)); + }, + + /** + * 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];}, + + + + ///////////////////////////////////////////// + // Edit mode management + + /** + * Returns current edit mode - readonly, wysiwyg, source + */ + getCurrentEditMode: function() { + return this.currentEditMode; + }, + + toggleSourceAndWysiwygMode: function() { + var mode = this.getCurrentEditMode(); + if(mode == 'readonly') return; + this.setEditMode(mode == 'wysiwyg' ? 'source' : 'wysiwyg'); + + return true; + }, + + /** + * Switches between edit-mode/normal mode. + * + * @param {Object} mode false or 'readonly' means read-only mode, true or 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode. + */ + setEditMode: function(mode) { + if(this.currentEditMode == mode) return; + + var firstCall = mode != false && mode != 'readonly' && !this.outmostWrapper; + if(firstCall) { + // Create editor element if needed + this._createEditorFrame(); + this._registerEventHandlers(); + + this.loadCurrentContentFromStaticContent(); + this.editHistory = new xq.EditHistory(this.rdom); + } + + if(mode == 'wysiwyg') { + // Update contents + if(this.currentEditMode == 'source') this.setStaticContent(this.getSourceContent()); + this.loadCurrentContentFromStaticContent(); + + // Make static content invisible + this.contentElement.style.display = "none"; + + // Make WYSIWYG editor visible + this.sourceEditorDiv.style.display = "none"; + this.wysiwygEditorDiv.style.display = "block"; + this.outmostWrapper.style.display = "block"; + + this.currentEditMode = mode; + + if(!xq.Browser.isTrident) { + window.setTimeout(function() { + if(this.getDoc().designMode == 'On') return; + + // Without it, Firefox doesn't display embedded SWF + this.getDoc().designMode = 'On'; + + // turn off Firefox's table editing feature + try {this.getDoc().execCommand("enableInlineTableEditing", false, "false")} catch(ignored) {} + }.bind(this), 0); + } + + this.enableToolbarButtons(); + if(!firstCall) this.focus(); + } else if(mode == 'source') { + // Update contents + if(this.currentEditMode == 'wysiwyg') this.setStaticContent(this.getWysiwygContent()); + this.loadCurrentContentFromStaticContent(); + + // Make static content invisible + this.contentElement.style.display = "none"; + + // Make source editor visible + this.sourceEditorDiv.style.display = "block"; + this.wysiwygEditorDiv.style.display = "none"; + this.outmostWrapper.style.display = "block"; + + this.currentEditMode = mode; + + this.disableToolbarButtons(['html']); + if(!firstCall) this.focus(); + } else { + // Update contents + this.setStaticContent(this.getCurrentContent()); + this.loadCurrentContentFromStaticContent(); + + // Make editor and toolbar invisible + this.outmostWrapper.style.display = "none"; + + // Make static content visible + this.contentElement.style.display = "block"; + + this.currentEditMode = mode; + } + + this._fireOnCurrentEditModeChanged(this, mode); + }, + + /** + * Load CSS into editing-mode document + * + * @param {string} path URL + */ + loadStylesheet: function(path) { + var head = this.editorDoc.getElementsByTagName("HEAD")[0]; + var link = this.editorDoc.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() { + // update WYSIWYG editor + var html = this.validator.invalidate(this.getStaticContentAsDOM()); + html = this.removeUnnecessarySpaces(html); + + if(html.blank()) { + this.rdom.clearRoot(); + } else { + this.rdom.getRoot().innerHTML = html; + } + this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true); + + // update source editor + var source = this.getWysiwygContent(true, true); + + this.sourceEditorTextarea.value = source; + if(xq.Browser.isWebkit) { + this.sourceEditorTextarea.innerHTML = source; + } + + this._fireOnCurrentContentChanged(this); + }, + + /** + * Enables all toolbar buttons + * + * @param {Array} [exceptions] array of string containing classnames to exclude + */ + enableToolbarButtons: function(exceptions) { + if(!this.toolbarContainer) return; + + this._execForAllToolbarButtons(exceptions, function(li, exception) { + li.firstChild.className = !exception ? '' : 'disabled'; + }); + + // Toolbar image icon disappears without following code: + if(xq.Browser.isIE6) { + this.toolbarContainer.style.display = 'none'; + setTimeout(function() {this.toolbarContainer.style.display = 'block';}.bind(this), 0); + } + }, + + /** + * Disables all toolbar buttons + * + * @param {Array} [exceptions] array of string containing classnames to exclude + */ + disableToolbarButtons: function(exceptions) { + this._execForAllToolbarButtons(exceptions, function(li, exception) { + li.firstChild.className = exception ? '' : 'disabled'; + }); + }, + + _execForAllToolbarButtons: function(exceptions, exec) { + if(!this.toolbarContainer) return; + exceptions = exceptions || []; + + $(this.toolbarContainer).select('li').each(function(li) { + var buttonsClassName = li.classNames().find(function(name) {return name != 'xq_separator'}); + var exception = exceptions.include(buttonsClassName); + exec(li, exception); + }); + }, + + _updateToolbarButtonStatus: function(buttonClassName, selected) { + var button = this.toolbarButtons.get(buttonClassName); + if(button) button.firstChild.firstChild.className = selected ? 'selected' : ''; + }, + + updateAllToolbarButtonsStatus: function(element) { + if(!this.toolbarContainer) return; + if(!this.toolbarButtons) { + var classNames = [ + "emphasis", "strongEmphasis", "underline", "strike", "superscription", "subscription", + "justifyLeft", "justifyCenter", "justifyRight", "justifyBoth", + "unorderedList", "orderedList", "code", + "paragraph", "heading1", "heading2", "heading3", "heading4", "heading5", "heading6" + ]; + + this.toolbarButtons = $H({}); + + classNames.each(function(className) { + var found = $(this.toolbarContainer).getElementsBySelector("." + className); + var button = found && found.length > 0 ? found[0] : null; + if(button) this.toolbarButtons.set(className, button); + }.bind(this)); + } + + var buttons = this.toolbarButtons; + + var info = this.rdom.collectStructureAndStyle(element); + + this._updateToolbarButtonStatus('emphasis', info.em); + this._updateToolbarButtonStatus('strongEmphasis', info.strong); + this._updateToolbarButtonStatus('underline', info.underline); + this._updateToolbarButtonStatus('strike', info.strike); + this._updateToolbarButtonStatus('superscription', info.superscription); + this._updateToolbarButtonStatus('subscription', info.subscription); + + this._updateToolbarButtonStatus('justifyLeft', info.justification == 'left'); + this._updateToolbarButtonStatus('justifyCenter', info.justification == 'center'); + this._updateToolbarButtonStatus('justifyRight', info.justification == 'right'); + this._updateToolbarButtonStatus('justifyBoth', info.justification == 'justify'); + + this._updateToolbarButtonStatus('orderedList', info.list == 'OL'); + this._updateToolbarButtonStatus('unorderedList', info.list == 'UL'); + this._updateToolbarButtonStatus('code', info.list == 'CODE'); + + this._updateToolbarButtonStatus('paragraph', info.block == 'P'); + this._updateToolbarButtonStatus('heading1', info.block == 'H1'); + this._updateToolbarButtonStatus('heading2', info.block == 'H2'); + this._updateToolbarButtonStatus('heading3', info.block == 'H3'); + this._updateToolbarButtonStatus('heading4', info.block == 'H4'); + this._updateToolbarButtonStatus('heading5', info.block == 'H5'); + this._updateToolbarButtonStatus('heading6', info.block == 'H6'); + }, + + 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(performFullValidation) { + if(this.getCurrentEditMode() == 'source') { + return this.getSourceContent(performFullValidation); + } else { + return this.getWysiwygContent(performFullValidation); + } + }, + + /** + * Gets editor's dynamic content from WYSIWYG editor + * + * @return {Object} HTML String + */ + getWysiwygContent: function(performFullValidation, dontUseCache) { + if(dontUseCache || !performFullValidation) return this.validator.validate(this.rdom.getRoot(), performFullValidation); + + var lastModified = this.editHistory.getLastModifiedDate(); + if(this._lastModified != lastModified) { + this._validContentCache = this.validator.validate(this.rdom.getRoot(), performFullValidation); + this._lastModified = lastModified; + } + return this._validContentCache; + }, + + /** + * Gets editor's dynamic content from source editor + * + * @return {Object} HTML String + */ + getSourceContent: function(performFullValidation) { + var raw = this.sourceEditorTextarea[xq.Browser.isWebkit ? 'innerHTML' : 'value']; + var tempDiv = document.createElement('div'); + tempDiv.innerHTML = this.removeUnnecessarySpaces(raw); + + var rdom = xq.RichDom.createInstance(); + rdom.setRoot(document.body); + rdom.wrapAllInlineOrTextNodesAs("P", tempDiv, true); + + return this.validator.validate(tempDiv, performFullValidation); + }, + + /** + * Sets editor's original content + * + * @param {Object} content HTML String + */ + setStaticContent: function(content) { + if(this.contentElement.nodeName == 'TEXTAREA') { + this.contentElement.value = content; + if(xq.Browser.isWebkit) { + this.contentElement.innerHTML = content; + } + } else { + this.contentElement.innerHTML = content; + } + this._fireOnStaticContentChanged(this, content); + }, + + /** + * Gets editor's original content + * + * @return {Object} HTML String + */ + getStaticContent: function() { + var content; + if(this.contentElement.nodeName == 'TEXTAREA') { + content = this.contentElement[xq.Browser.isWebkit ? 'innerHTML' : 'value']; + } else { + content = this.contentElement.innerHTML; + } + return content; + }, + + /** + * Gets editor's original content as DOM node + * + * @return {Object} HTML String + */ + getStaticContentAsDOM: function() { + if(this.contentElement.nodeName == 'TEXTAREA') { + var div = this.doc.createElement('DIV'); + div.innerHTML = this.contentElement[xq.Browser.isWebkit ? 'innerHTML' : 'value']; + return div; + } else { + return this.contentElement; + } + }, + + /** + * Gives focus to editor + */ + focus: function() { + if(this.getCurrentEditMode() == 'wysiwyg') { + this.rdom.focus(); + window.setTimeout(function() { + this.updateAllToolbarButtonsStatus(this.rdom.getCurrentElement()); + }.bind(this), 0); + } else if(this.getCurrentEditMode() == 'source') { + this.sourceEditorTextarea.focus(); + } + }, + + /** + * Returns designmode iframe object + */ + getFrame: function() { + return this.editorFrame; + }, + + /** + * Returns designmode window object + */ + getWin: function() { + return this.editorWin; + }, + + /** + * Returns designmode document object + */ + getDoc: function() { + return this.editorDoc; + }, + + /** + * Returns outmost wrapper element + */ + getOutmostWrapper: function() { + return this.outmostWrapper; + }, + + /** + * Returns designmode body object + */ + getBody: function() { + return this.editorBody; + }, + + _createEditorFrame: function() { + // create outer DIV + this.outmostWrapper = this.doc.createElement('div'); + this.outmostWrapper.className = "xquared"; + + this.contentElement.parentNode.insertBefore(this.outmostWrapper, this.contentElement); + + // create toolbar is needed + if(!this.toolbarContainer && this.config.generateDefaultToolbar) { + this.toolbarContainer = this._generateDefaultToolbar(); + this.outmostWrapper.appendChild(this.toolbarContainer); + } + + // 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.wysiwygEditorDiv.style.display = "none"; + this.outmostWrapper.appendChild(this.wysiwygEditorDiv); + + // create designmode iframe for WYSIWYG editor + this.editorFrame = this.doc.createElement('iframe'); + this.rdom.setAttributes(this.editorFrame, { + "frameBorder": "0", + "marginWidth": "0", + "marginHeight": "0", + "leftMargin": "0", + "topMargin": "0", + "allowTransparency": "true" + }); + this.wysiwygEditorDiv.appendChild(this.editorFrame); + + var doc = this.editorFrame.contentWindow.document; + if(xq.Browser.isTrident) doc.designMode = 'On'; + + doc.open(); + doc.write(''); + doc.write(''); + doc.write(''); + + // it is needed to force href of pasted content to be an absolute url + if(!xq.Browser.isTrident) doc.write(''); + + doc.write(''); + doc.write('XQuared'); + if(this.config.changeCursorOnLink) doc.write(''); + doc.write(''); + doc.write('

' + this.rdom.makePlaceHolderString() + '

'); + doc.write(''); + doc.close(); + + this.editorWin = this.editorFrame.contentWindow; + this.editorDoc = this.editorWin.document; + this.editorBody = this.editorDoc.body; + this.editorBody.className = "xed"; + + // it is needed to fix IE6 horizontal scrollbar problem + if(xq.Browser.isIE6) { + this.editorDoc.documentElement.style.overflowY='auto'; + this.editorDoc.documentElement.style.overflowX='hidden'; + } + + this.rdom.setWin(this.editorWin); + this.rdom.setRoot(this.editorBody); + this.validator = xq.Validator.createInstance(this.doc.location.href, this.config.urlValidationMode, this.config.allowedTags, this.config.allowedAttributes); + + // hook onsubmit of form + if(this.config.automaticallyHookSubmitEvent && this.contentElement.nodeName == 'TEXTAREA' && this.contentElement.form) { + var original = this.contentElement.form.onsubmit; + + this.contentElement.form.onsubmit = function() { + this.contentElement.value = this.getCurrentContent(true); + if(original) { + return original(); + } else { + return true; + } + }.bind(this); + } + }, + + _addStyleRule: function(selector, rule) { + if(!this.dynamicStyle) { + if(xq.Browser.isTrident) { + this.dynamicStyle = this.doc.createStyleSheet(); + } else { + var style = this.doc.createElement('style'); + this.doc.body.appendChild(style); + this.dynamicStyle = $A(this.doc.styleSheets).last(); + } + } + + if(xq.Browser.isTrident) { + this.dynamicStyle.addRule(selector, rule); + } else { + this.dynamicStyle.insertRule(selector + " {" + rule + "}", this.dynamicStyle.cssRules.length); + } + }, + + _generateDefaultToolbar: function() { + // override image path + this._addStyleRule(".xquared div.toolbar", "background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarBg.gif)"); + this._addStyleRule(".xquared ul.buttons li", "background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarButtonBg.gif)"); + this._addStyleRule(".xquared ul.buttons li.xq_separator", "background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarSeparator.gif)"); + + // outmost container + var container = this.doc.createElement('div'); + container.className = 'toolbar'; + + // button container + var buttons = this.doc.createElement('ul'); + buttons.className = 'buttons'; + container.appendChild(buttons); + + // Generate buttons from map and append it to button container + var cancelMousedown = function(e) {Event.stop(e); return false}; + var map = this.config.defaultToolbarButtonMap; + for(var i = 0; i < map.length; i++) { + for(var j = 0; j < map[i].length; j++) { + var buttonConfig = map[i][j]; + + var li = this.doc.createElement('li'); + buttons.appendChild(li); + li.className = buttonConfig.className; + + var span = this.doc.createElement('span'); + li.appendChild(span); + + var a = this.doc.createElement('a'); + span.appendChild(a); + a.href = '#'; + a.title = buttonConfig.title; + a.handler = buttonConfig.handler; + a.xed = this; + Event.observe(a, 'mousedown', cancelMousedown); + Event.observe(a, 'click', function(e) { + var xed = this.xed; + + if($(this.parentNode).hasClassName('disabled') || xed.toolbarContainer.hasClassName('disabled')) { + Event.stop(e); + return false; + } + + if(xq.Browser.isTrident) xed.focus(); + + var handler = this.handler; + var stop = (typeof handler == "function") ? handler(xed) : eval(handler); + if(stop) { + Event.stop(e); + return false; + } else { + return true; + } + }.bind(a)); + + var img = this.doc.createElement('img'); + a.appendChild(img); + img.src = this.config.imagePathForDefaultToobar + buttonConfig.className + '.gif'; + + if(j == 0 && i != 0) li.className += ' xq_separator'; + } + } + + return container; + }, + + + + ///////////////////////////////////////////// + // Event Management + + _registerEventHandlers: function() { + var events = ['keydown', 'click', 'keyup', 'mouseup', 'contextmenu', 'scroll']; + + if(xq.Browser.isTrident && this.config.changeCursorOnLink) events.push('mousemove'); + if(xq.Browser.isMac && xq.Browser.isGecko) events.push('keypress'); + + for(var i = 0; i < events.length; i++) { + Event.observe(this.getDoc(), events[i], this._handleEvent.bindAsEventListener(this)); + } + }, + + _handleEvent: function(e) { + this._fireOnBeforeEvent(this, e); + + var stop = false; + + var modifiedByCorrection = false; + + if(e.type == 'mousemove' && this.config.changeCursorOnLink) { + // Trident only + var link = !!this.rdom.getParentElementOf(e.srcElement, ["A"]); + if(this.editorBody.contentEditable != link && !this.rdom.hasSelection()) this.editorBody.contentEditable = !link; + } 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(e.type == (xq.Browser.isMac && xq.Browser.isGecko ? "keypress" : "keydown")) { + 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(["mouseup", "keyup"].include(e.type)) { + modifiedByCorrection = this.rdom.correctParagraph(); + } else if(["contextmenu"].include(e.type)) { + this._handleContextMenu(e); + } + + if(stop) Event.stop(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 = $H(this.config.autocompletions); + if(acs.size() == 0) 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 = + acs.map(function(pair) { + return [pair.key, pair.value.criteria(text)]; + }.bind(this)).findAll(function(elem) { + return elem[1] != -1; + }).sortBy(function(elem) { + return elem[1]; + }); + + if(filtered.length == 0) { + this.rdom.popMarker(true); + return; + } + var ac = acs.get(filtered[0][0]); + + this.editHistory.disable(); + } else { + var marker = this.rdom.pushMarker(); + + var filtered = + acs.map(function(pair) { + return [pair.key, this.rdom.testSmartWrap(marker, pair.value.criteria).textIndex]; + }.bind(this)).findAll(function(elem) { + return elem[1] != -1; + }).sortBy(function(elem) { + return elem[1]; + }); + + if(filtered.length == 0) { + this.rdom.popMarker(true); + return; + } + + var ac = acs.get(filtered[0][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 + */ + handleLink: function() { + var text = this.rdom.getSelectionAsText() || ''; + var dialog = new xq.controls.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 + * + * @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; + }, + + /** + * Called when enter key pressed. + * + * @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; + + // Perform autocorrection + if(!skipAutocorrection && this.handleAutocorrection()) return true; + + 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 block = this.rdom.getCurrentBlockElement(); + 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(); + } else { + this._handleEnterAtEdge(atStart, forceInsertParagraph); + } + + 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); + block.scrollIntoView(false); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } + return true; + }, + + /** + * Called when tab key pressed + */ + 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 + */ + 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 + */ + handleInsertTab: function() { + this.rdom.insertHtml(' '); + this.rdom.insertHtml(' '); + this.rdom.insertHtml(' '); + + return true; + }, + + /** + * Called when delete key pressed + */ + handleDelete: function() { + if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockEnd()) return false; + return this._handleMerge(true); + }, + + /** + * Called when backspace key pressed + */ + handleBackspace: function() { + if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockStart()) return false; + return this._handleMerge(false); + }, + + _handleMerge: function(withNext) { + var block = this.rdom.getCurrentBlockElement(); + + // 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 + */ + handleMoveToNextCell: function() { + this._handleMoveToCell("next"); + }, + + /** + * (in table) Moves caret to the previous cell + */ + handleMoveToPreviousCell: function() { + this._handleMoveToCell("prev"); + }, + + /** + * (in table) Moves caret to the above cell + */ + handleMoveToAboveCell: function() { + this._handleMoveToCell("above"); + }, + + /** + * (in table) Moves caret to the below cell + */ + 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"].include(dir)) { + 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'].include(node.nodeName) && 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 + */ + handleStrongEmphasis: function() { + this.rdom.applyStrongEmphasis(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies EM tag + */ + handleEmphasis: function() { + this.rdom.applyEmphasis(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies EM.underline tag + */ + handleUnderline: function() { + this.rdom.applyUnderline(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies SPAN.strike tag + */ + handleStrike: function() { + this.rdom.applyStrike(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Removes all style + */ + handleRemoveFormat: function() { + this.rdom.applyRemoveFormat(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Inserts table + * + * @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; + }, + 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; + }, + + 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; + }, + + 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); + + return true; + }, + + /** + * Performs block indentation + */ + 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 + */ + 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. + * + * @param {String} type "UL" or "OL" or "CODE". CODE generates OL.code + */ + handleList: function(type) { + 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); + } else { + blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type); + } + this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); + } else { + var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type); + this.rdom.placeCaretAtStartOf(block); + } + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies justification + * + * @param {String} dir "left", "center", "right" or "both" + */ + handleJustify: function(dir) { + var block = this.rdom.getCurrentBlockElement(); + var dir = (dir == "left" || dir == "both") && (block.style.textAlign == "left" || block.style.textAlign == "") ? "both" : dir; + + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getSelectedBlockElements(); + this.rdom.justifyBlocks(blocks, dir); + this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); + } else { + this.rdom.justifyBlock(block, dir); + } + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Removes current block element + */ + handleRemoveBlock: function() { + var block = this.rdom.getCurrentBlockElement(); + var blockToMove = this.rdom.removeBlock(block); + this.rdom.placeCaretAtStartOf(blockToMove); + blockToMove.scrollIntoView(false); + }, + + /** + * Applies background color + * + * @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.controls.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 + * + * @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.controls.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 superscription + */ + handleSuperscription: function() { + this.rdom.applySuperscription(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies subscription + */ + handleSubscription: function() { + this.rdom.applySubscription(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Change of wrap current block's tag + */ + handleApplyBlock: function(tagName) { + 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()); + this.rdom.selectBlocksBetween(applied.first(), applied.last()); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } + } + + var block = this.rdom.getCurrentBlockElement(); + this.rdom.pushMarker(); + var applied = + this.rdom.applyTagIntoElement(tagName, block) || + 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) + */ + 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 + */ + 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 + */ + 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 || Event.isLeftClick(e)) return false; + } else if (e.shiftKey || e.ctrlKey || e.altKey) { + return false; + } + + var x=Event.pointerX(e); + var y=Event.pointerY(e); + var pos=Position.cumulativeOffset(this.getFrame()); + x+=pos[0]; + y+=pos[1]; + this._contextMenuTargetElement = e.target || e.srcElement; + + //TODO: Safari on Windows doesn't work with context key(app key) + if (!x || !y || xq.Browser.isTrident) { + var pos = Position.cumulativeOffset(this._contextMenuTargetElement); + var posFrame = Position.cumulativeOffset(this.getFrame()); + x = pos[0] + posFrame[0] - this.getDoc().documentElement.scrollLeft; + y = pos[1] + posFrame[1] - this.getDoc().documentElement.scrollTop; + } + + if (!xq.Browser.isTrident) { + var doc = this.getDoc(); + var body = this.getBody(); + + x -= doc.documentElement.scrollLeft; + y -= doc.documentElement.scrollTop; + + if (doc != body) { + 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) { + Event.stop(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'; + + Event.observe(this.doc, 'click', this._contextMenuClicked.bindAsEventListener(this)); + Event.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 = Event.findElement(e, 'LI'); + 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 + * + * @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. + * + * @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 = $H(this.getTemplateProcessors()).values(); + for(var i = 0; i < tps.length; i++) { + html = tps[i].handler(html); + } + + // remove all whitespace characters between block tags + return html = 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); + } +}); +xq.Browser = { + // By Layout Engines + isTrident: navigator.appName == "Microsoft Internet Explorer", + isWebkit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + isGecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1, + isKHTML: navigator.userAgent.indexOf('KHTML') != -1, + isPresto: navigator.appName == "Opera", + + // By Platforms + isMac: navigator.userAgent.indexOf("Macintosh") != -1, + isUbuntu: navigator.userAgent.indexOf('Ubuntu') != -1, + + // By Browsers + isIE: navigator.appName == "Microsoft Internet Explorer", + isIE6: navigator.userAgent.indexOf('MSIE 6') != -1, + isIE7: navigator.userAgent.indexOf('MSIE 7') != -1 +}; +xq.Shortcut = Class.create({ + initialize: function(keymapOrExpression) { + this.keymap = (typeof keymapOrExpression == "string") ? + xq.Shortcut.interprete(keymapOrExpression).keymap : + keymapOrExpression; + }, + matches: function(e) { + var which = xq.Browser.isGecko && xq.Browser.isMac ? (e.keyCode + "_" + e.charCode) : e.keyCode; + + var keyMatches = + (this.keymap.which == which) || + (this.keymap.which == 32 && which == 25); // 25 is SPACE in Type-3 keyboard. + + if(typeof e.metaKey == "undefined") e.metaKey = false; + + var modifierMatches = + (typeof this.keymap.shiftKey == "undefined" || this.keymap.shiftKey == e.shiftKey) && + (typeof this.keymap.altKey == "undefined" || this.keymap.altKey == e.altKey) && + (typeof this.keymap.ctrlKey == "undefined" || this.keymap.ctrlKey == e.ctrlKey) && + (typeof this.keymap.metaKey == "undefined" || this.keymap.metaKey == e.metaKey) + + return modifierMatches && keyMatches; + } +}); + +xq.Shortcut.interprete = function(expression) { + expression = expression.toUpperCase(); + + var which = xq.Shortcut._interpreteWhich(expression.split("+").pop()); + var ctrlKey = xq.Shortcut._interpreteModifier(expression, "CTRL"); + var altKey = xq.Shortcut._interpreteModifier(expression, "ALT"); + var shiftKey = xq.Shortcut._interpreteModifier(expression, "SHIFT"); + var metaKey = xq.Shortcut._interpreteModifier(expression, "META"); + + var keymap = {}; + + keymap.which = which; + if(typeof ctrlKey != "undefined") keymap.ctrlKey = ctrlKey; + if(typeof altKey != "undefined") keymap.altKey = altKey; + if(typeof shiftKey != "undefined") keymap.shiftKey = shiftKey; + if(typeof metaKey != "undefined") keymap.metaKey = metaKey; + + return new xq.Shortcut(keymap); +} + +xq.Shortcut._interpreteModifier = function(expression, modifierName) { + return expression.match("\\(" + modifierName + "\\)") ? + undefined : + expression.match(modifierName) ? + true : false; +} +xq.Shortcut._interpreteWhich = function(keyName) { + var which = keyName.length == 1 ? + ((xq.Browser.isMac && xq.Browser.isGecko) ? "0_" + keyName.toLowerCase().charCodeAt(0) : keyName.charCodeAt(0)) : + xq.Shortcut._keyNames[keyName]; + + if(typeof which == "undefined") throw "Unknown special key name: [" + keyName + "]" + + return which; +} +xq.Shortcut._keyNames = + xq.Browser.isMac && xq.Browser.isGecko ? + { + BACKSPACE: "8_0", + TAB: "9_0", + RETURN: "13_0", + ENTER: "13_0", + ESC: "27_0", + SPACE: "0_32", + LEFT: "37_0", + UP: "38_0", + RIGHT: "39_0", + DOWN: "40_0", + DELETE: "46_0", + HOME: "36_0", + END: "35_0", + PAGEUP: "33_0", + PAGEDOWN: "34_0", + COMMA: "0_44", + HYPHEN: "0_45", + EQUAL: "0_61", + PERIOD: "0_46", + SLASH: "0_47", + F1: "112_0", + F2: "113_0", + F3: "114_0", + F4: "115_0", + F5: "116_0", + F6: "117_0", + F7: "118_0", + F8: "119_0" + } + : + { + BACKSPACE: 8, + TAB: 9, + RETURN: 13, + ENTER: 13, + ESC: 27, + SPACE: 32, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + DELETE: 46, + HOME: 36, + END: 35, + PAGEUP: 33, + PAGEDOWN: 34, + COMMA: 188, + HYPHEN: xq.Browser.isTrident ? 189 : 109, + EQUAL: xq.Browser.isTrident ? 187 : 61, + PERIOD: 190, + SLASH: 191, + F1:112, + F2:113, + F3:114, + F4:115, + F5:116, + F6:117, + F7:118, + F8:119, + F9:120, + F10:121, + F11:122, + F12:123 + } +/** + * Provide various tree operations. + * + * TODO: Add specs + */ +xq.DomTree = Class.create({ + initialize: function() { + 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", "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) { + if(node.hasChildNodes()) return node.firstChild; + if(node.nextSibling) return node.nextSibling; + + while(node = node.parentNode) { + if(node.nextSibling) return node.nextSibling; + } + + 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); + }, + + /** @private */ + _check: function(start, direction, target) { + if(start == target) return false; + + while(start = direction(start)) { + if(start == target) return true; + } + return false; + }, + + /** @private */ + _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 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.include(typeof element == 'string' ? element : element.nodeName); + }, + + isTableCell: function(element) { + if(!element) return false; + return this._tableCellTags.include(typeof element == 'string' ? element : element.nodeName); + }, + + isBlockContainer: function(element) { + if(!element) return false; + return this._blockContainerTags.include(typeof element == 'string' ? element : element.nodeName); + }, + + 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.include(typeof element == 'string' ? element : element.nodeName); + }, + + isAtomic: function(element) { + if(!element) return false; + return this._atomicTags.include(typeof element == 'string' ? element : element.nodeName); + }, + + isListContainer: function(element) { + if(!element) return false; + return this._listContainerTags.include(typeof element == 'string' ? element : element.nodeName); + }, + + isTextOrInlineNode: function(node) { + return node && (node.nodeType == 3 || !this.isBlock(node)); + } +}); +/** + * Encapsulates browser incompatibility problem and provides rich set of DOM manipulation API. + * + * RichDom provides basic CRUD + Advanced DOM manipulation API, various query methods and caret/selection management API + */ +xq.RichDom = Class.create({ + /** + * Initialize RichDom. Target window and root element should be set after initialization. See setWin and setRoot. + * + * @constructor + */ + initialize: function() { + /** + * {xq.DomTree} instance of DomTree + */ + this.tree = new xq.DomTree(); + + this._lastMarkerId = 0; + }, + + + + /** + * @param {Window} win Browser's window object + */ + setWin: function(win) { + if(!win) throw "[win] is null"; + this.win = win; + }, + + /** + * @param {Element} root Root element + */ + setRoot: function(root) { + if(!root) throw "[root] is null"; + if(this.win && (root.ownerDocument != this.win.document)) throw "root.ownerDocument != this.win.document"; + this.root = root; + this.doc = this.root.ownerDocument; + }, + + /** + * @returns Browser's window object. + */ + getWin: function() {return this.win}, + + /** + * @returns Document object of root element. + */ + getDoc: function() {return this.doc}, + + /** + * @returns Root element. + */ + getRoot: function() {return this.root}, + + + + ///////////////////////////////////////////// + // CRUDs + + clearRoot: function() { + this.root.innerHTML = ""; + this.root.appendChild(this.makeEmptyParagraph()); + }, + + /** + * Removes place holders and empty text nodes of given element. + * + * @param {Element} element target element + */ + removePlaceHoldersAndEmptyNodes: function(element) { + var children = element.childNodes; + if(!children) return; + var stopAt = this.getBottommostLastChild(element); + if(!stopAt) return; + stopAt = this.tree.walkForward(stopAt); + + while(true) { + if(!element || element == stopAt) break; + + if( + this.isPlaceHolder(element) || + (element.nodeType == 3 && element.nodeValue == "") || + (!this.getNextSibling(element) && element.nodeType == 3 && element.nodeValue.strip() == "") + ) { + var deleteTarget = element; + element = this.tree.walkForward(element); + + this.deleteNode(deleteTarget); + } else { + element = this.tree.walkForward(element); + } + } + }, + + /** + * Sets multiple attributes into element at once + * + * @param {Element} element target element + * @param {Object} map key-value pairs + */ + setAttributes: function(element, map) { + for(key in map) element.setAttribute(key, map[key]); + }, + + /** + * Creates textnode by given node value. + * + * @param {String} value value of textnode + * @returns {Node} Created text node + */ + createTextNode: function(value) {return this.doc.createTextNode(value);}, + + /** + * Creates empty element by given tag name. + * + * @param {String} tagName name of tag + * @returns {Element} Created element + */ + createElement: function(tagName) {return this.doc.createElement(tagName);}, + + /** + * Creates element from HTML string + * + * @param {String} html HTML string + * @returns {Element} Created element + */ + createElementFromHtml: function(html) { + var node = this.createElement("div"); + node.innerHTML = html; + if(node.childNodes.length != 1) { + throw "Illegal HTML fragment"; + } + return this.getFirstChild(node); + }, + + /** + * Deletes node from DOM tree. + * + * @param {Node} node Target node which should be deleted + * @param {boolean} deleteEmptyParentsRecursively Recursively delete empty parent elements + * @param {boolean} correctEmptyParent Call #correctEmptyElement on empty parent element after deletion + */ + deleteNode: function(node, deleteEmptyParentsRecursively, correctEmptyParent) { + if(!node || !node.parentNode) return; + + var parent = node.parentNode; + parent.removeChild(node); + + if(deleteEmptyParentsRecursively) { + while(!parent.hasChildNodes()) { + node = parent; + parent = node.parentNode; + if(!parent || this.getRoot() == node) break; + parent.removeChild(node); + } + } + + if(correctEmptyParent && this.isEmptyBlock(parent)) { + parent.innerHTML = ""; + this.correctEmptyElement(parent); + } + }, + + /** + * Inserts given node into current caret position + * + * @param {Node} node Target node + * @returns {Node} Inserted node. It could be different with given node. + */ + insertNode: function(node) {throw "Not implemented"}, + + /** + * Inserts given html into current caret position + * + * @param {String} html HTML string + * @returns {Node} Inserted node. It could be different with given node. + */ + insertHtml: function(html) { + return this.insertNode(this.createElementFromHtml(html)); + }, + + /** + * Creates textnode from given text and inserts it into current caret position + * + * @param {String} text Value of textnode + * @returns {Node} Inserted node + */ + insertText: function(text) { + this.insertNode(this.createTextNode(text)); + }, + + /** + * Places given node nearby target. + * + * @param {Node} node Node to be inserted. + * @param {Node} target Target node. + * @param {String} where Possible values: "before", "start", "end", "after" + * @param {boolean} performValidation Validate node if needed. For example when P placed into UL, its tag name automatically replaced with LI + * + * @returns {Node} Inserted node. It could be different with given node. + */ + insertNodeAt: function(node, target, where, performValidation) { + if( + ["HTML", "HEAD"].include(target.nodeName) || + ["BODY"].include(target.nodeName) && ["before", "after"].include(where) + ) throw "Illegal argument. Cannot move node[" + node.nodeName + "] to '" + where + "' of target[" + target.nodeName + "]" + + var object; + var message; + var secondParam; + + switch(where.toLowerCase()) { + case "before": + object = target.parentNode; + message = 'insertBefore'; + secondParam = target; + break + case "start": + if(target.firstChild) { + object = target; + message = 'insertBefore'; + secondParam = target.firstChild; + } else { + object = target; + message = 'appendChild'; + } + break + case "end": + object = target; + message = 'appendChild'; + break + case "after": + if(target.nextSibling) { + object = target.parentNode; + message = 'insertBefore'; + secondParam = target.nextSibling; + } else { + object = target.parentNode; + message = 'appendChild'; + } + break + } + + if(performValidation && this.tree.isListContainer(object) && node.nodeName != "LI") { + var li = this.createElement("LI"); + li.appendChild(node); + node = li; + object[message](node, secondParam); + } else if(performValidation && !this.tree.isListContainer(object) && node.nodeName == "LI") { + this.wrapAllInlineOrTextNodesAs("P", node, true); + var div = this.createElement("DIV"); + this.moveChildNodes(node, div); + this.deleteNode(node); + object[message](div, secondParam); + node = this.unwrapElement(div, true); + } else { + object[message](node, secondParam); + } + + return node; + }, + + /** + * Creates textnode from given text and places given node nearby target. + * + * @param {String} text Text to be inserted. + * @param {Node} target Target node. + * @param {String} where Possible values: "before", "start", "end", "after" + * + * @returns {Node} Inserted node. + */ + insertTextAt: function(text, target, where) { + return this.insertNodeAt(this.createTextNode(text), target, where); + }, + + /** + * Creates element from given HTML string and places given it nearby target. + * + * @param {String} html HTML to be inserted. + * @param {Node} target Target node. + * @param {String} where Possible values: "before", "start", "end", "after" + * + * @returns {Node} Inserted node. + */ + insertHtmlAt: function(html, target, where) { + return this.insertNodeAt(this.createElementFromHtml(html), target, where); + }, + + /** + * Replaces element's tag by removing current element and creating new element by given tag name. + * + * @param {String} tag New tag name + * @param {Element} element Target element + * + * @returns {Element} Replaced element + */ + replaceTag: function(tag, element) { + if(element.nodeName == tag) return null; + if(this.tree.isTableCell(element)) return null; + + var newElement = this.createElement(tag); + this.moveChildNodes(element, newElement); + this.copyAttributes(element, newElement, true); + element.parentNode.replaceChild(newElement, element); + + if(!newElement.hasChildNodes()) this.correctEmptyElement(newElement); + + return newElement; + }, + + /** + * Unwraps unnecessary paragraph. + * + * Unnecessary paragraph is P which is the only child of given container element. + * For example, P which is contained by LI and is the only child is the unnecessary paragraph. + * But if given container element is a block-only-container(BLOCKQUOTE, BODY), this method does nothing. + * + * @param {Element} element Container element + * @returns {boolean} True if unwrap performed. + */ + unwrapUnnecessaryParagraph: function(element) { + if(!element) return false; + + if(!this.tree.isBlockOnlyContainer(element) && element.childNodes.length == 1 && element.firstChild.nodeName == "P" && !this.hasImportantAttributes(element.firstChild)) { + var p = element.firstChild; + this.moveChildNodes(p, element); + this.deleteNode(p); + return true; + } + return false; + }, + + /** + * Unwraps element by extracting all children out and removing the element. + * + * @param {Element} element Target element + * @param {boolean} wrapInlineAndTextNodes Wrap all inline and text nodes with P before unwrap + * @returns {Node} First child of unwrapped element + */ + unwrapElement: function(element, wrapInlineAndTextNodes) { + if(wrapInlineAndTextNodes) this.wrapAllInlineOrTextNodesAs("P", element); + + var nodeToReturn = element.firstChild; + + while(element.firstChild) this.insertNodeAt(element.firstChild, element, "before"); + this.deleteNode(element); + + return nodeToReturn; + }, + + /** + * Wraps element by given tag + * + * @param {String} tag tag name + * @param {Element} element target element to wrap + * @returns {Element} wrapper + */ + wrapElement: function(tag, element) { + var wrapper = this.insertNodeAt(this.createElement(tag), element, "before"); + wrapper.appendChild(element); + return wrapper; + }, + + /** + * Tests #smartWrap with given criteria but doesn't change anything + */ + testSmartWrap: function(endElement, criteria) { + return this.smartWrap(endElement, null, criteria, true); + }, + + /** + * Create inline element with given tag name and wraps nodes nearby endElement by given criteria + * + * @param {Element} endElement Boundary(end point, exclusive) of wrapper. + * @param {String} tag Tag name of wrapper. + * @param {Object} function which returns text index of start boundary. + * @param {boolean} testOnly just test boundary and do not perform actual wrapping. + * + * @returns {Element} wrapper + */ + smartWrap: function(endElement, tag, criteria, testOnly) { + var block = this.getParentBlockElementOf(endElement); + + tag = tag || "SPAN"; + criteria = criteria || function(text) {return -1}; + + // check for empty wrapper + if(!testOnly && (!endElement.previousSibling || this.isEmptyBlock(block))) { + var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before"); + return wrapper; + } + + // collect all textnodes + var textNodes = this.tree.collectForward(block, function(node) {return node == endElement}, function(node) {return node.nodeType == 3}); + + // find textnode and break-point + var nodeIndex = 0; + var nodeValues = textNodes.pluck("nodeValue"); + var textToWrap = nodeValues.join(""); + var textIndex = criteria(textToWrap) + var breakPoint = textIndex; + + if(breakPoint == -1) { + breakPoint = 0; + } else { + textToWrap = textToWrap.substring(breakPoint); + } + + for(var i = 0; i < textNodes.length; i++) { + if(breakPoint > nodeValues[i].length) { + breakPoint -= nodeValues[i].length; + } else { + nodeIndex = i; + break; + } + } + + if(testOnly) return {text:textToWrap, textIndex:textIndex, nodeIndex:nodeIndex, breakPoint:breakPoint}; + + // break textnode if necessary + if(breakPoint != 0) { + var splitted = textNodes[nodeIndex].splitText(breakPoint); + nodeIndex++; + textNodes.splice(nodeIndex, 0, splitted); + } + var startElement = textNodes[nodeIndex] || block.firstChild; + + // split inline elements up to parent block if necessary + var family = this.tree.findCommonAncestorAndImmediateChildrenOf(startElement, endElement); + var ca = family.parent; + if(ca) { + if(startElement.parentNode != ca) startElement = this.splitElementUpto(startElement, ca, true); + if(endElement.parentNode != ca) endElement = this.splitElementUpto(endElement, ca, true); + + var prevStart = startElement.previousSibling; + var nextEnd = endElement.nextSibling; + + // remove empty inline elements + if(prevStart && prevStart.nodeType == 1 && this.isEmptyBlock(prevStart)) this.deleteNode(prevStart); + if(nextEnd && nextEnd.nodeType == 1 && this.isEmptyBlock(nextEnd)) this.deleteNode(nextEnd); + + // wrap + var wrapper = this.insertNodeAt(this.createElement(tag), startElement, "before"); + while(wrapper.nextSibling != endElement) wrapper.appendChild(wrapper.nextSibling); + return wrapper; + } else { + // wrap + var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before"); + return wrapper; + } + }, + + /** + * Wraps all adjust inline elements and text nodes into block element. + * + * TODO: empty element should return empty array when it is not forced and (at least) single item array when forced + * + * @param {String} tag Tag name of wrapper + * @param {Element} element Target element + * @param {boolean} force Force wrapping. If it is set to false, this method do not makes unnecessary wrapper. + * + * @returns {Array} Array of wrappers. If nothing performed it returns empty array + */ + wrapAllInlineOrTextNodesAs: function(tag, element, force) { + var wrappers = []; + + if(!force && !this.tree.hasMixedContents(element)) return wrappers; + + var node = element.firstChild; + while(node) { + if(this.tree.isTextOrInlineNode(node)) { + var wrapper = this.wrapInlineOrTextNodesAs(tag, node); + wrappers.push(wrapper); + node = wrapper.nextSibling; + } else { + node = node.nextSibling; + } + } + + return wrappers; + }, + + /** + * Wraps node and its adjust next siblings into an element + */ + wrapInlineOrTextNodesAs: function(tag, node) { + var wrapper = this.createElement(tag); + var from = node; + + from.parentNode.replaceChild(wrapper, from); + wrapper.appendChild(from); + + // move nodes into wrapper + while(wrapper.nextSibling && this.tree.isTextOrInlineNode(wrapper.nextSibling)) wrapper.appendChild(wrapper.nextSibling); + + return wrapper; + }, + + /** + * Turns block element into list item + * + * @param {Element} element Target element + * @param {String} type One of "UL", "OL", "CODE". "CODE" is same with "OL" but it gives "OL" a class name "code" + * + * @return {Element} LI element + */ + turnElementIntoListItem: function(element, type) { + type = type.toUpperCase(); + + var container = this.createElement(type == "UL" ? "UL" : "OL"); + if(type == "CODE") container.className = "code"; + + if(this.tree.isTableCell(element)) { + var p = this.wrapAllInlineOrTextNodesAs("P", element, true)[0]; + container = this.insertNodeAt(container, element, "start"); + var li = this.insertNodeAt(this.createElement("LI"), container, "start"); + li.appendChild(p); + } else { + container = this.insertNodeAt(container, element, "after"); + var li = this.insertNodeAt(this.createElement("LI"), container, "start"); + li.appendChild(element); + } + + this.unwrapUnnecessaryParagraph(li); + this.mergeAdjustLists(container); + + return li; + }, + + /** + * Extracts given element out from its parent element. + * + * @param {Element} element Target element + */ + extractOutElementFromParent: function(element) { + if(element == this.root || this.root == element.parentNode || !element.offsetParent) return null; + + if(element.nodeName == "LI") { + this.wrapAllInlineOrTextNodesAs("P", element, true); + element = element.firstChild; + } + + var container = element.parentNode; + var nodeToReturn = null; + + if(container.nodeName == "LI" && container.parentNode.parentNode.nodeName == "LI") { + // nested list item + if(element.previousSibling) { + this.splitContainerOf(element, true); + this.correctEmptyElement(element); + } + + this.outdentListItem(element); + nodeToReturn = element; + } else if(container.nodeName == "LI") { + // not-nested list item + + if(this.tree.isListContainer(element.nextSibling)) { + // 1. split listContainer + var listContainer = container.parentNode; + this.splitContainerOf(container, true); + this.correctEmptyElement(element); + + // 2. extract out LI's children + nodeToReturn = container.firstChild; + while(container.firstChild) { + this.insertNodeAt(container.firstChild, listContainer, "before"); + } + + // 3. remove listContainer and merge adjust lists + var prevContainer = listContainer.previousSibling; + this.deleteNode(listContainer); + if(prevContainer && this.tree.isListContainer(prevContainer)) this.mergeAdjustLists(prevContainer); + } else { + // 1. split LI + this.splitContainerOf(element, true); + this.correctEmptyElement(element); + + // 2. split list container + var listContainer = this.splitContainerOf(container); + + // 3. extract out + this.insertNodeAt(element, listContainer.parentNode, "before"); + this.deleteNode(listContainer.parentNode); + + nodeToReturn = element; + } + } else if(this.tree.isTableCell(container) || this.tree.isTableCell(element)) { + // do nothing + } else { + // normal block + this.splitContainerOf(element, true); + this.correctEmptyElement(element); + nodeToReturn = this.insertNodeAt(element, container, "before"); + + this.deleteNode(container); + } + + return nodeToReturn; + }, + + /** + * Insert new block above or below given element. + * + * @param {Element} block Target block + * @param {boolean} before Insert new block above(before) target block + * @param {String} forceTag New block's tag name. If omitted, target block's tag name will be used. + * + * @returns {Element} Inserted block + */ + insertNewBlockAround: function(block, before, forceTag) { + var isListItem = block.nodeName == "LI" || block.parentNode.nodeName == "LI"; + + this.removeTrailingWhitespace(block); + if(this.isFirstLiWithNestedList(block) && !forceTag && before) { + var li = this.getParentElementOf(block, ["LI"]); + var newBlock = this._insertNewBlockAround(li, before); + return newBlock; + } else if(isListItem && !forceTag) { + var li = this.getParentElementOf(block, ["LI"]); + var newBlock = this._insertNewBlockAround(block, before); + if(li != block) newBlock = this.splitContainerOf(newBlock, false, "prev"); + return newBlock; + } else if(this.tree.isBlockContainer(block)) { + this.wrapAllInlineOrTextNodesAs("P", block, true); + return this._insertNewBlockAround(block.firstChild, before, forceTag); + } else { + return this._insertNewBlockAround(block, before, this.tree.isHeading(block) ? "P" : forceTag); + } + }, + + /** + * @private + * + * TODO: Rename + */ + _insertNewBlockAround: function(element, before, tagName) { + var newElement = this.createElement(tagName || element.nodeName); + this.copyAttributes(element, newElement, false); + this.correctEmptyElement(newElement); + newElement = this.insertNodeAt(newElement, element, before ? "before" : "after"); + return newElement; + }, + + /** + * Wrap or replace element with given tag name. + * + * @param {String} tag Tag name + * @param {Element} element Target element + * + * @return {Element} wrapper element or replaced element. + */ + applyTagIntoElement: function(tag, element) { + if(this.tree.isBlockOnlyContainer(tag)) { + return this.wrapBlock(tag, element); + } else if(this.tree.isBlockContainer(element)) { + var wrapper = this.createElement(tag); + this.moveChildNodes(element, wrapper); + return this.insertNodeAt(wrapper, element, "start"); + } else { + if(this.tree.isBlockContainer(tag) && this.hasImportantAttributes(element)) { + return this.wrapBlock(tag, element); + } else { + return this.replaceTag(tag, element); + } + } + + throw "IllegalArgumentException - [" + tag + ", " + element + "]"; + }, + + /** + * Wrap or replace elements with given tag name. + * + * @param {String} tag Tag name + * @param {Element} from Start boundary (inclusive) + * @param {Element} to End boundary (inclusive) + * + * @returns {Array} Array of wrappers or replaced elements + */ + applyTagIntoElements: function(tagName, from, to) { + var applied = []; + + if(this.tree.isBlockContainer(tagName)) { + var family = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to); + var node = family.left; + var wrapper = this.insertNodeAt(this.createElement(tagName), node, "before"); + + var coveringWholeList = + family.parent.nodeName == "LI" && + family.parent.parentNode.childNodes.length == 1 && + !family.left.previousSilbing && + !family.right.nextSibling; + + if(coveringWholeList) { + var ul = node.parentNode.parentNode; + this.insertNodeAt(wrapper, ul, "before"); + wrapper.appendChild(ul); + } else { + while(node != family.right) { + next = node.nextSibling; + wrapper.appendChild(node); + node = next; + } + wrapper.appendChild(family.right); + } + applied.push(wrapper); + } else { + // is normal tagName + var elements = this.getBlockElementsBetween(from, to); + for(var i = 0; i < elements.length; i++) { + if(this.tree.isBlockContainer(elements[i])) { + applied.push(this.wrapAllInlineOrTextNodesAs(tagName, elements[i], true)); + } else { + applied.push(this.replaceTag(tagName, elements[i])); + } + } + } + return applied.flatten(); + }, + + /** + * Moves block up or down + * + * @param {Element} block Target block + * @param {boolean} up Move up if true + * + * @returns {Element} Moved block. It could be different with given block. + */ + moveBlock: function(block, up) { + // if block is table cell or contained by table cell, select its row as mover + block = this.getParentElementOf(block, ["TR"]) || block; + + // if block is only child, select its parent as mover + while(block.nodeName != "TR" && block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) { + block = block.parentNode; + } + + // find target and where + var target, where; + if (up) { + target = block.previousSibling; + + if(target) { + var singleNodeLi = target.nodeName == 'LI' && ((target.childNodes.length == 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target)); + var table = ['TABLE', 'TR'].include(target.nodeName); + + where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "end" : "before"; + } else if(block.parentNode != this.getRoot()) { + target = block.parentNode; + where = "before"; + } + } else { + target = block.nextSibling; + + if(target) { + var singleNodeLi = target.nodeName == 'LI' && ((target.childNodes.length == 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target)); + var table = ['TABLE', 'TR'].include(target.nodeName); + + where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "start" : "after"; + } else if(block.parentNode != this.getRoot()) { + target = block.parentNode; + where = "after"; + } + } + + + // no way to go? + if(!target) return null; + if(["TBODY", "THEAD"].include(target.nodeName)) return null; + + // normalize + this.wrapAllInlineOrTextNodesAs("P", target, true); + + // make placeholder if needed + if(this.isFirstLiWithNestedList(block)) { + this.insertNewBlockAround(block, false, "P"); + } + + // perform move + var parent = block.parentNode; + var moved = this.insertNodeAt(block, target, where, true); + + // cleanup + if(!parent.hasChildNodes()) this.deleteNode(parent, true); + this.unwrapUnnecessaryParagraph(moved); + this.unwrapUnnecessaryParagraph(target); + + // remove placeholder + if(up) { + if(moved.previousSibling && this.isEmptyBlock(moved.previousSibling) && !moved.previousSibling.previousSibling && moved.parentNode.nodeName == "LI" && this.tree.isListContainer(moved.nextSibling)) { + this.deleteNode(moved.previousSibling); + } + } else { + if(moved.nextSibling && this.isEmptyBlock(moved.nextSibling) && !moved.previousSibling && moved.parentNode.nodeName == "LI" && this.tree.isListContainer(moved.nextSibling.nextSibling)) { + this.deleteNode(moved.nextSibling); + } + } + + return moved; + }, + + /** + * Remove given block + * + * @param {Element} block Target block + * @returns {Element} Nearest block of remove element + */ + removeBlock: function(block) { + var blockToMove; + + // if block is only child, select its parent as mover + while(block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) { + block = block.parentNode; + } + + var finder = function(node) {return this.tree.isBlock(node) && !this.tree.isAtomic(node) && !this.tree.isDescendantOf(block, node) && !this.tree.hasBlocks(node);}.bind(this); + var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this); + + if(this.isFirstLiWithNestedList(block)) { + blockToMove = this.outdentListItem(block.nextSibling.firstChild); + this.deleteNode(blockToMove.previousSibling, true); + } else if(this.tree.isTableCell(block)) { + var rtable = new xq.RichTable(this, this.getParentElementOf(block, ["TABLE"])); + blockToMove = rtable.getBelowCellOf(block); + + // should not delete row when there's thead and the row is the only child of tbody + if( + block.parentNode.parentNode.nodeName == "TBODY" && + rtable.hasHeadingAtTop() && + rtable.getDom().tBodies[0].rows.length == 1) return blockToMove; + + blockToMove = blockToMove || + this.tree.findForward(block, finder, exitCondition) || + this.tree.findBackward(block, finder, exitCondition); + + this.deleteNode(block.parentNode, true); + } else { + blockToMove = blockToMove || + this.tree.findForward(block, finder, exitCondition) || + this.tree.findBackward(block, finder, exitCondition); + + if(!blockToMove) blockToMove = this.insertNodeAt(this.makeEmptyParagraph(), block, "after"); + + this.deleteNode(block, true); + } + if(!this.getRoot().hasChildNodes()) { + blockToMove = this.createElement("P"); + this.getRoot().appendChild(blockToMove); + this.correctEmptyElement(blockToMove); + } + + return blockToMove; + }, + + /** + * Removes trailing whitespaces of given block + * + * @param {Element} block Target block + */ + removeTrailingWhitespace: function(block) {throw "Not implemented"}, + + /** + * Extract given list item out and change its container's tag + * + * @param {Element} element LI or P which is a child of LI + * @param {String} type "OL", "UL", or "CODE" + * + * @returns {Element} changed element + */ + changeListTypeTo: function(element, type) { + type = type.toUpperCase(); + + var li = this.getParentElementOf(element, ["LI"]); + if(!li) throw "IllegalArgumentException"; + + var container = li.parentNode; + + this.splitContainerOf(li); + + var newContainer = this.insertNodeAt(this.createElement(type == "UL" ? "UL" : "OL"), container, "before"); + if(type == "CODE") newContainer.className = "code"; + + this.insertNodeAt(li, newContainer, "start"); + this.deleteNode(container); + + this.mergeAdjustLists(newContainer); + + return element; + }, + + /** + * Split container of element into (maxium) three pieces. + */ + splitContainerOf: function(element, preserveElementItself, dir) { + if([element, element.parentNode].include(this.getRoot())) return element; + + var container = element.parentNode; + if(element.previousSibling && (!dir || dir.toLowerCase() == "prev")) { + var prev = this.createElement(container.nodeName); + this.copyAttributes(container, prev); + while(container.firstChild != element) { + prev.appendChild(container.firstChild); + } + this.insertNodeAt(prev, container, "before"); + this.unwrapUnnecessaryParagraph(prev); + } + + if(element.nextSibling && (!dir || dir.toLowerCase() == "next")) { + var next = this.createElement(container.nodeName); + this.copyAttributes(container, next); + while(container.lastChild != element) { + this.insertNodeAt(container.lastChild, next, "start"); + } + this.insertNodeAt(next, container, "after"); + this.unwrapUnnecessaryParagraph(next); + } + + if(!preserveElementItself) element = this.unwrapUnnecessaryParagraph(container) ? container : element; + return element; + }, + + /** + * TODO: Add specs + */ + splitParentElement: function(seperator) { + var parent = seperator.parentNode; + if(["HTML", "HEAD", "BODY"].include(parent.nodeName)) throw "Illegal argument. Cannot seperate element[" + parent.nodeName + "]"; + + var previousSibling = seperator.previousSibling; + var nextSibling = seperator.nextSibling; + + var newElement = this.insertNodeAt(this.createElement(parent.nodeName), parent, "after"); + + var next; + while(next = seperator.nextSibling) newElement.appendChild(next); + + this.insertNodeAt(seperator, newElement, "start"); + this.copyAttributes(parent, newElement); + + return newElement; + }, + + /** + * TODO: Add specs + */ + splitElementUpto: function(seperator, element, excludeElement) { + while(seperator.previousSibling != element) { + if(excludeElement && seperator.parentNode == element) break; + seperator = this.splitParentElement(seperator); + } + return seperator; + }, + + /** + * Merges two adjust elements + * + * @param {Element} element base element + * @param {boolean} withNext merge base element with next sibling + * @param {boolean} skip skip merge steps + */ + mergeElement: function(element, withNext, skip) { + this.wrapAllInlineOrTextNodesAs("P", element.parentNode, true); + + // find two block + if(withNext) { + var prev = element; + var next = this.tree.findForward( + element, + function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node != element.parentNode}.bind(this) + ); + } else { + var next = element; + var prev = this.tree.findBackward( + element, + function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node != element.parentNode}.bind(this) + ); + } + + // normalize next block + if(next && this.tree.isDescendantOf(this.getRoot(), next)) { + var nextContainer = next.parentNode; + if(this.tree.isBlockContainer(next)) { + nextContainer = next; + this.wrapAllInlineOrTextNodesAs("P", nextContainer, true); + next = nextContainer.firstChild; + } + } else { + next = null; + } + + // normalize prev block + if(prev && this.tree.isDescendantOf(this.getRoot(), prev)) { + var prevContainer = prev.parentNode; + if(this.tree.isBlockContainer(prev)) { + prevContainer = prev; + this.wrapAllInlineOrTextNodesAs("P", prevContainer, true); + prev = prevContainer.lastChild; + } + } else { + prev = null; + } + + try { + var containersAreTableCell = + prevContainer && (this.tree.isTableCell(prevContainer) || ['TR', 'THEAD', 'TBODY'].include(prevContainer.nodeName)) && + nextContainer && (this.tree.isTableCell(nextContainer) || ['TR', 'THEAD', 'TBODY'].include(nextContainer.nodeName)); + + if(containersAreTableCell && prevContainer != nextContainer) return null; + + // if next has margin, perform outdent + if((!skip || !prev) && next && this.outdentElement(next)) return element; + + // nextContainer is first li and next of it is list container + if(nextContainer && nextContainer.nodeName == 'LI' && this.tree.isListContainer(next.nextSibling)) { + this.extractOutElementFromParent(nextContainer); + return prev; + } + + // merge two list containers + if(nextContainer && nextContainer.nodeName == 'LI' && this.tree.isListContainer(nextContainer.parentNode.previousSibling)) { + this.mergeAdjustLists(nextContainer.parentNode.previousSibling, true, "next"); + return prev; + } + + if(next && !containersAreTableCell && prevContainer && prevContainer.nodeName == 'LI' && nextContainer && nextContainer.nodeName == 'LI' && prevContainer.parentNode.nextSibling == nextContainer.parentNode) { + var nextContainerContainer = nextContainer.parentNode; + this.moveChildNodes(nextContainer.parentNode, prevContainer.parentNode); + this.deleteNode(nextContainerContainer); + return prev; + } + + // merge two containers + if(next && !containersAreTableCell && prevContainer && prevContainer.nextSibling == nextContainer && ((skip && prevContainer.nodeName != "LI") || (!skip && prevContainer.nodeName == "LI"))) { + this.moveChildNodes(nextContainer, prevContainer); + return prev; + } + + // unwrap container + if(nextContainer && nextContainer.nodeName != "LI" && !this.getParentElementOf(nextContainer, ["TABLE"]) && !this.tree.isListContainer(nextContainer) && nextContainer != this.getRoot() && !next.previousSibling) { + return this.unwrapElement(nextContainer, true); + } + + // delete table + if(withNext && nextContainer && nextContainer.nodeName == "TABLE") { + this.deleteNode(nextContainer, true); + return prev; + } else if(!withNext && prevContainer && this.tree.isTableCell(prevContainer) && !this.tree.isTableCell(nextContainer)) { + this.deleteNode(this.getParentElementOf(prevContainer, ["TABLE"]), true); + return next; + } + + // if prev is same with next, do nothing + if(prev == next) return null; + + // if there is a null block, do nothing + if(!prev || !next || !prevContainer || !nextContainer) return null; + + // if two blocks are not in the same table cell, do nothing + if(this.getParentElementOf(prev, ["TD", "TH"]) != this.getParentElementOf(next, ["TD", "TH"])) return null; + + var prevIsEmpty = false; + + // cleanup empty block before merge + + // 1. cleanup prev node which ends with marker +   + if( + xq.Browser.isTrident && + prev.childNodes.length >= 2 && + this.isMarker(prev.lastChild.previousSibling) && + prev.lastChild.nodeType == 3 && + prev.lastChild.nodeValue.length == 1 && + prev.lastChild.nodeValue.charCodeAt(0) == 160 + ) { + this.deleteNode(prev.lastChild); + } + + // 2. cleanup prev node (if prev is empty, then replace prev's tag with next's) + this.removePlaceHoldersAndEmptyNodes(prev); + if(this.isEmptyBlock(prev)) { + // replace atomic block with normal block so that following code don't need to care about atomic block + if(this.tree.isAtomic(prev)) prev = this.replaceTag("P", prev); + + prev = this.replaceTag(next.nodeName, prev) || prev; + prev.innerHTML = ""; + } else if(prev.firstChild == prev.lastChild && this.isMarker(prev.firstChild)) { + prev = this.replaceTag(next.nodeName, prev) || prev; + } + + // 3. cleanup next node + if(this.isEmptyBlock(next)) { + // replace atomic block with normal block so that following code don't need to care about atomic block + if(this.tree.isAtomic(next)) next = this.replaceTag("P", next); + + next.innerHTML = ""; + } + + // perform merge + this.moveChildNodes(next, prev); + this.deleteNode(next); + return prev; + } finally { + // cleanup + if(prevContainer && this.isEmptyBlock(prevContainer)) this.deleteNode(prevContainer, true); + if(nextContainer && this.isEmptyBlock(nextContainer)) this.deleteNode(nextContainer, true); + + if(prevContainer) this.unwrapUnnecessaryParagraph(prevContainer); + if(nextContainer) this.unwrapUnnecessaryParagraph(nextContainer); + } + }, + + /** + * Merges adjust list containers which has same tag name + * + * @param {Element} container target list container + * @param {boolean} force force adjust list container even if they have different list type + * @param {String} dir Specify merge direction: PREV or NEXT. If not supplied it will be merged with both direction. + */ + mergeAdjustLists: function(container, force, dir) { + var prev = container.previousSibling; + var isPrevSame = prev && (prev.nodeName == container.nodeName && prev.className == container.className); + if((!dir || dir.toLowerCase() == 'prev') && (isPrevSame || (force && this.tree.isListContainer(prev)))) { + while(prev.lastChild) { + this.insertNodeAt(prev.lastChild, container, "start"); + } + this.deleteNode(prev); + } + + var next = container.nextSibling; + var isNextSame = next && (next.nodeName == container.nodeName && next.className == container.className); + if((!dir || dir.toLowerCase() == 'next') && (isNextSame || (force && this.tree.isListContainer(next)))) { + while(next.firstChild) { + this.insertNodeAt(next.firstChild, container, "end"); + } + this.deleteNode(next); + } + }, + + /** + * Moves child nodes from one element into another. + * + * @param {Elemet} from source element + * @param {Elemet} to target element + */ + moveChildNodes: function(from, to) { + if(this.tree.isDescendantOf(from, to) || ["HTML", "HEAD"].include(to.nodeName)) + throw "Illegal argument. Cannot move children of element[" + from.nodeName + "] to element[" + to.nodeName + "]"; + + if(from == to) return; + + while(from.firstChild) to.appendChild(from.firstChild); + }, + + /** + * Copies attributes from one element into another. + * + * @param {Element} from source element + * @param {Element} to target element + * @param {boolean} copyId copy ID attribute of source element + */ + copyAttributes: function(from, to, copyId) { + // IE overrides this + + var attrs = from.attributes; + if(!attrs) return; + + for(var i = 0; i < attrs.length; i++) { + if(attrs[i].nodeName == "class" && attrs[i].nodeValue) { + to.className = attrs[i].nodeValue; + } else if((copyId || !["id"].include(attrs[i].nodeName)) && attrs[i].nodeValue) { + to.setAttribute(attrs[i].nodeName, attrs[i].nodeValue); + } + } + }, + + _indentElements: function(node, blocks, affect) { + for (var i=0; i < affect.length; i++) { + if (affect[i] == node || this.tree.isDescendantOf(affect[i], node)) + return; + } + leaves = this.tree.getLeavesAtEdge(node); + + if (blocks.include(leaves[0])) { + var affected = this.indentElement(node, true); + if (affected) { + affect.push(affected); + return; + } + } + + if (blocks.include(node)) { + var affected = this.indentElement(node, true); + if (affected) { + affect.push(affected); + return; + } + } + + var children=$A(node.childNodes); + for (var i=0; i < children.length; i++) + this._indentElements(children[i], blocks, affect); + return; + }, + + indentElements: function(from, to) { + var blocks = this.getBlockElementsBetween(from, to); + var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to); + + var affect = []; + + leaves = this.tree.getLeavesAtEdge(top.parent); + if (blocks.include(leaves[0])) { + var affected = this.indentElement(top.parent); + if (affected) + return [affected]; + } + + var children = $A(top.parent.childNodes); + for (var i=0; i < children.length; i++) { + this._indentElements(children[i], blocks, affect); + } + + affect = affect.flatten() + return affect.length > 0 ? affect : blocks; + }, + + outdentElementsCode: function(node) { + if (node.tagName == 'LI') + node = node.parentNode; + if (node.tagName == 'OL' && node.className == 'code') + return true; + return false; + }, + + _outdentElements: function(node, blocks, affect) { + for (var i=0; i < affect.length; i++) { + if (affect[i] == node || this.tree.isDescendantOf(affect[i], node)) + return; + } + leaves = this.tree.getLeavesAtEdge(node); + + if (blocks.include(leaves[0]) && !this.outdentElementsCode(leaves[0])) { + var affected = this.outdentElement(node, true); + if (affected) { + affect.push(affected); + return; + } + } + + if (blocks.include(node)) { + var children = $A(node.parentNode.childNodes); + var isCode = this.outdentElementsCode(node); + var affected = this.outdentElement(node, true, isCode); + if (affected) { + if (children.include(affected) && this.tree.isListContainer(node.parentNode) && !isCode) { + for (var i=0; i < children.length; i++) { + if (blocks.include(children[i]) && !affect.include(children[i])) + affect.push(children[i]); + } + }else + affect.push(affected); + return; + } + } + + var children=$A(node.childNodes); + for (var i=0; i < children.length; i++) + this._outdentElements(children[i], blocks, affect); + return; + }, + + outdentElements: function(from, to) { + var start, end; + + if (from.parentNode.tagName == 'LI') start=from.parentNode; + if (to.parentNode.tagName == 'LI') end=to.parentNode; + + var blocks = this.getBlockElementsBetween(from, to); + var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to); + + var affect = []; + + leaves = this.tree.getLeavesAtEdge(top.parent); + if (blocks.include(leaves[0]) && !this.outdentElementsCode(top.parent)) { + var affected = this.outdentElement(top.parent); + if (affected) + return [affected]; + } + + var children = $A(top.parent.childNodes); + for (var i=0; i < children.length; i++) { + this._outdentElements(children[i], blocks, affect); + } + + if (from.offsetParent && to.offsetParent) { + start = from; + end = to; + }else if (blocks.first().offsetParent && blocks.last().offsetParent) { + start = blocks.first(); + end = blocks.last(); + } + + affect = affect.flatten() + if (!start || !start.offsetParent) + start = affect.first(); + if (!end || !end.offsetParent) + end = affect.last(); + + return this.getBlockElementsBetween(start, end); + }, + + /** + * Performs indent by increasing element's margin-left + */ + indentElement: function(element, noParent, forceMargin) { + if( + !forceMargin && + (element.nodeName == "LI" || (!this.tree.isListContainer(element) && !element.previousSibling && element.parentNode.nodeName == "LI")) + ) return this.indentListItem(element, noParent); + + var root = this.getRoot(); + if(!element || element == root) return null; + + if (element.parentNode != root && !element.previousSibling && !noParent) element=element.parentNode; + + var margin = element.style.marginLeft; + var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"}; + + cssValue.value += 2; + element.style.marginLeft = cssValue.value + cssValue.unit; + + return element; + }, + + /** + * Performs outdent by decreasing element's margin-left + */ + outdentElement: function(element, noParent, forceMargin) { + if(!forceMargin && element.nodeName == "LI") return this.outdentListItem(element, noParent); + + var root = this.getRoot(); + if(!element || element == root) return null; + + var margin = element.style.marginLeft; + + var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"}; + if(cssValue.value == 0) { + return element.previousSibling || forceMargin ? + null : + this.outdentElement(element.parentNode, noParent); + } + + cssValue.value -= 2; + element.style.marginLeft = cssValue.value <= 0 ? "" : cssValue.value + cssValue.unit; + if(element.style.cssText == "") element.removeAttribute("style"); + + return element; + }, + + /** + * Performs indent for list item + */ + indentListItem: function(element, treatListAsNormalBlock) { + var li = this.getParentElementOf(element, ["LI"]); + var container = li.parentNode; + var prev = li.previousSibling; + if(!li.previousSibling) return this.indentElement(container); + + if(li.parentNode.nodeName == "OL" && li.parentNode.className == "code") return this.indentElement(li, treatListAsNormalBlock, true); + + if(!prev.lastChild) prev.appendChild(this.makePlaceHolder()); + + var targetContainer = + this.tree.isListContainer(prev.lastChild) ? + // if there's existing list container, select it as target container + prev.lastChild : + // if there's nothing, create new one + this.insertNodeAt(this.createElement(container.nodeName), prev, "end"); + + this.wrapAllInlineOrTextNodesAs("P", prev, true); + + // perform move + targetContainer.appendChild(li); + + // flatten nested list + if(!treatListAsNormalBlock && li.lastChild && this.tree.isListContainer(li.lastChild)) { + var childrenContainer = li.lastChild; + var child; + while(child = childrenContainer.lastChild) { + this.insertNodeAt(child, li, "after"); + } + this.deleteNode(childrenContainer); + } + + this.unwrapUnnecessaryParagraph(li); + + return li; + }, + + /** + * Performs outdent for list item + * + * @return {Element} outdented list item or null if no outdent performed + */ + outdentListItem: function(element, treatListAsNormalBlock) { + var li = this.getParentElementOf(element, ["LI"]); + var container = li.parentNode; + + if(!li.previousSibling) { + var performed = this.outdentElement(container); + if(performed) return performed; + } + + if(li.parentNode.nodeName == "OL" && li.parentNode.className == "code") return this.outdentElement(li, treatListAsNormalBlock, true); + + var parentLi = container.parentNode; + if(parentLi.nodeName != "LI") return null; + + if(treatListAsNormalBlock) { + while(container.lastChild != li) { + this.insertNodeAt(container.lastChild, parentLi, "after"); + } + } else { + // make next siblings as children + if(li.nextSibling) { + var targetContainer = + li.lastChild && this.tree.isListContainer(li.lastChild) ? + // if there's existing list container, select it as target container + li.lastChild : + // if there's nothing, create new one + this.insertNodeAt(this.createElement(container.nodeName), li, "end"); + + this.copyAttributes(container, targetContainer); + + var sibling; + while(sibling = li.nextSibling) { + targetContainer.appendChild(sibling); + } + } + } + + // move current LI into parent LI's next sibling + li = this.insertNodeAt(li, parentLi, "after"); + + // remove empty container + if(container.childNodes.length == 0) this.deleteNode(container); + + if(li.firstChild && this.tree.isListContainer(li.firstChild)) { + this.insertNodeAt(this.makePlaceHolder(), li, "start"); + } + + this.wrapAllInlineOrTextNodesAs("P", li); + this.unwrapUnnecessaryParagraph(parentLi); + + return li; + }, + + /** + * Performs justification + * + * @param {Element} block target element + * @param {String} dir one of "LEFT", "CENTER", "RIGHT", "BOTH" + */ + justifyBlock: function(block, dir) { + // if block is only child, select its parent as mover + while(block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) { + block = block.parentNode; + } + + var styleValue = dir.toLowerCase() == "both" ? "justify" : dir; + if(styleValue == "left") { + block.style.textAlign = ""; + if(block.style.cssText == "") block.removeAttribute("style"); + } else { + block.style.textAlign = styleValue; + } + return block; + }, + + justifyBlocks: function(blocks, dir) { + blocks.each(function(block) { + this.justifyBlock(block, dir); + }.bind(this)); + + return blocks; + }, + + /** + * Turn given element into list. If the element is a list already, it will be reversed into normal element. + * + * @param {Element} element target element + * @param {String} type one of "UL", "OL" + * @returns {Element} affected element + */ + applyList: function(element, type) { + type = type.toUpperCase(); + var containerTag = type == "UL" ? "UL" : "OL"; + + if(element.nodeName == "LI" || (element.parentNode.nodeName == "LI" && !element.previousSibling)) { + var element = this.getParentElementOf(element, ["LI"]); + var container = element.parentNode; + if(container.nodeName == containerTag) { + return this.extractOutElementFromParent(element); + } else { + return this.changeListTypeTo(element, type); + } + } else { + return this.turnElementIntoListItem(element, type); + } + }, + + applyLists: function(from, to, type) { + type = type.toUpperCase(); + var containerTag = type == "UL" ? "UL" : "OL"; + var blocks = this.getBlockElementsBetween(from, to); + + // LIs or Non-containing blocks + var whole = blocks.findAll(function(e) { + return e.nodeName == "LI" || !this.tree.isBlockContainer(e); + }.bind(this)); + + // LIs + var listItems = whole.findAll(function(e) {return e.nodeName == "LI"}.bind(this)); + + // Non-containing blocks which is not a descendant of any LIs selected above(listItems). + var normalBlocks = whole.findAll(function(e) { + return e.nodeName != "LI" && + !(e.parentNode.nodeName == "LI" && !e.previousSibling && !e.nextSibling) && + !this.tree.isDescendantOf(listItems, e) + }.bind(this)); + + var diffListItems = listItems.findAll(function(e) { + return e.parentNode.nodeName != containerTag; + }.bind(this)); + + // Conditions needed to determine mode + var hasNormalBlocks = normalBlocks.length > 0; + var hasDifferentListStyle = diffListItems.length > 0; + + var blockToHandle = null; + + if(hasNormalBlocks) { + blockToHandle = normalBlocks; + } else if(hasDifferentListStyle) { + blockToHandle = diffListItems; + } else { + blockToHandle = listItems; + } + + // perform operation + for(var i = 0; i < blockToHandle.length; i++) { + var block = blockToHandle[i]; + + // preserve original index to restore selection + var originalIndex = blocks.indexOf(block); + blocks[originalIndex] = this.applyList(block, type); + } + + return blocks; + }, + + /** + * Insert place-holder for given empty element. Empty element does not displayed and causes many editing problems. + * + * @param {Element} element empty element + */ + correctEmptyElement: function(element) {throw "Not implemented"}, + + /** + * Corrects current block-only-container to do not take any non-block element or node. + */ + correctParagraph: function() {throw "Not implemented"}, + + /** + * Makes place-holder for empty element. + * + * @returns {Node} Platform specific place holder + */ + makePlaceHolder: function() {throw "Not implemented"}, + + /** + * Makes place-holder string. + * + * @returns {String} Platform specific place holder string + */ + makePlaceHolderString: function() {throw "Not implemented"}, + + /** + * Makes empty paragraph which contains only one place-holder + */ + makeEmptyParagraph: function() {throw "Not implemented"}, + + /** + * Applies background color to selected area + * + * @param {Object} color valid CSS color value + */ + applyBackgroundColor: function(color) {throw "Not implemented";}, + + /** + * Applies foreground color to selected area + * + * @param {Object} color valid CSS color value + */ + applyForegroundColor: function(color) { + this.execCommand("forecolor", color); + }, + + execCommand: function(commandId, param) {throw "Not implemented";}, + + applyRemoveFormat: function() {throw "Not implemented";}, + applyEmphasis: function() {throw "Not implemented";}, + applyStrongEmphasis: function() {throw "Not implemented";}, + applyStrike: function() {throw "Not implemented";}, + applyUnderline: function() {throw "Not implemented";}, + applySuperscription: function() { + this.execCommand("superscript"); + }, + applySubscription: function() { + this.execCommand("subscript"); + }, + indentBlock: function(element, treatListAsNormalBlock) { + return (!element.previousSibling && element.parentNode.nodeName == "LI") ? + this.indentListItem(element, treatListAsNormalBlock) : + this.indentElement(element); + }, + outdentBlock: function(element, treatListAsNormalBlock) { + while(true) { + if(!element.previousSibling && element.parentNode.nodeName == "LI") { + element = this.outdentListItem(element, treatListAsNormalBlock); + return element; + } else { + var performed = this.outdentElement(element); + if(performed) return performed; + + // first-child can outdent container + if(!element.previousSibling) { + element = element.parentNode; + } else { + break; + } + } + } + + return null; + }, + wrapBlock: function(tag, start, end) { + if(!this.tree._blockTags.include(tag)) throw "Unsuppored block container: [" + tag + "]"; + if(!start) start = this.getCurrentBlockElement(); + if(!end) end = start; + + // Check if the selection captures valid fragement + var validFragment = false; + + if(start == end) { + // are they same block? + validFragment = true; + } else if(start.parentNode == end.parentNode && !start.previousSibling && !end.nextSibling) { + // are they covering whole parent? + validFragment = true; + start = end = start.parentNode; + } else { + // are they siblings of non-LI blocks? + validFragment = + (start.parentNode == end.parentNode) && + (start.nodeName != "LI"); + } + + if(!validFragment) return null; + + var wrapper = this.createElement(tag); + + if(start == end) { + // They are same. + if(this.tree.isBlockContainer(start) && !this.tree.isListContainer(start)) { + // It's a block container. Wrap its contents. + if(this.tree.isBlockOnlyContainer(wrapper)) { + this.correctEmptyElement(start); + this.wrapAllInlineOrTextNodesAs("P", start, true); + } + this.moveChildNodes(start, wrapper); + start.appendChild(wrapper); + } else { + // It's not a block container. Wrap itself. + wrapper = this.insertNodeAt(wrapper, start, "after"); + wrapper.appendChild(start); + } + + this.correctEmptyElement(wrapper); + } else { + // They are siblings. Wrap'em all. + wrapper = this.insertNodeAt(wrapper, start, "before"); + var node = start; + + while(node != end) { + next = node.nextSibling; + wrapper.appendChild(node); + node = next; + } + wrapper.appendChild(node); + } + + return wrapper; + }, + + + + ///////////////////////////////////////////// + // Focus/Caret/Selection + + /** + * Gives focus to root element's window + */ + focus: function() {throw "Not implemented";}, + + /** + * Returns selection object + */ + sel: function() {throw "Not implemented";}, + + /** + * Returns range object + */ + rng: function() {throw "Not implemented";}, + + /** + * Returns true if DOM has selection + */ + hasSelection: function() {throw "Not implemented";}, + + /** + * Returns true if root element's window has selection + */ + hasFocus: function() { + var cur = this.getCurrentElement(); + return (cur && cur.ownerDocument == this.getDoc()); + }, + + /** + * Adjust scrollbar to make the element visible in current viewport. + * + * @param {Element} element Target element + * @param {boolean} toTop Align element to top of the viewport + * @param {boolean} moveCaret Move caret to the element + */ + scrollIntoView: function(element, toTop, moveCaret) { + element.scrollIntoView(toTop); + if(moveCaret) this.placeCaretAtStartOf(element); + }, + + /** + * Select all document + */ + selectAll: function() { + return this.execCommand('selectall'); + }, + + /** + * Select specified element. + * + * @param {Element} element element to select + * @param {boolean} entireElement true to select entire element, false to select inner content of element + */ + selectElement: function(node, entireElement) {throw "Not implemented"}, + + /** + * Select all elements between two blocks(inclusive). + * + * @param {Element} start start of selection + * @param {Element} end end of selection + */ + selectBlocksBetween: function(start, end) {throw "Not implemented"}, + + /** + * Delete selected area + */ + deleteSelection: function() {throw "Not implemented"}, + + /** + * Collapses current selection. + * + * @param {boolean} toStart true to move caret to start of selected area. + */ + collapseSelection: function(toStart) {throw "Not implemented"}, + + /** + * Returns selected area as HTML string + */ + getSelectionAsHtml: function() {throw "Not implemented"}, + + /** + * Returns selected area as text string + */ + getSelectionAsText: function() {throw "Not implemented"}, + + /** + * Places caret at start of the element + * + * @param {Element} element Target element + */ + placeCaretAtStartOf: function(element) {throw "Not implemented"}, + + /** + * Checks if the node is empty-text-node or not + */ + isEmptyTextNode: function(node) { + return node.nodeType == 3 && node.nodeValue.length == 0; + }, + + /** + * Checks if the caret is place in empty block element + */ + isCaretAtEmptyBlock: function() { + return this.isEmptyBlock(this.getCurrentBlockElement()); + }, + + /** + * Checks if the caret is place at start of the block + */ + isCaretAtBlockStart: function() {throw "Not implemented"}, + + /** + * Checks if the caret is place at end of the block + */ + isCaretAtBlockEnd: function() {throw "Not implemented"}, + + /** + * Saves current selection info + * + * @returns {Object} Bookmark for selection + */ + saveSelection: function() {throw "Not implemented"}, + + /** + * Restores current selection info + * + * @param {Object} bookmark Bookmark + */ + restoreSelection: function(bookmark) {throw "Not implemented"}, + + /** + * Create marker + */ + createMarker: function() { + var marker = this.createElement("SPAN"); + marker.id = "xquared_marker_" + (this._lastMarkerId++); + marker.className = "xquared_marker"; + return marker; + }, + + /** + * Create and insert marker into current caret position. + * Marker is an inline element which has no child nodes. It can be used with many purposes. + * For example, You can push marker to mark current caret position. + * + * @returns {Element} marker + */ + pushMarker: function() { + var marker = this.createMarker(); + return this.insertNode(marker); + }, + + /** + * Removes last marker + * + * @params {boolean} moveCaret move caret into marker before delete. + */ + popMarker: function(moveCaret) { + var id = "xquared_marker_" + (--this._lastMarkerId); + var marker = this.$(id); + if(!marker) return; + + if(moveCaret) { + this.selectElement(marker, true); + this.collapseSelection(false); + } + + this.deleteNode(marker); + }, + + + + ///////////////////////////////////////////// + // Query methods + + isMarker: function(node) { + return (node.nodeType == 1 && node.nodeName == "SPAN" && node.className == "xquared_marker"); + }, + + isFirstBlockOfBody: function(block) { + var root = this.getRoot(); + var found = this.tree.findBackward( + block, + function(node) {return (node == root) || node.previousSibling;}.bind(this) + ); + + return found == root; + }, + + /** + * Returns outer HTML of given element + */ + getOuterHTML: function(element) {throw "Not implemented"}, + + /** + * Returns inner text of given element + * + * @param {Element} element Target element + * @returns {String} Text string + */ + getInnerText: function(element) { + return element.innerHTML.stripTags(); + }, + + /** + * Checks if given node is place holder or not. + * + * @param {Node} node DOM node + */ + isPlaceHolder: function(node) {throw "Not implemented"}, + + /** + * Checks if given block is the first LI whose next sibling is a nested list. + * + * @param {Element} block Target block + */ + isFirstLiWithNestedList: function(block) { + return !block.previousSibling && + block.parentNode.nodeName == "LI" && + this.tree.isListContainer(block.nextSibling); + }, + + /** + * Search all links within given element + * + * @param {Element} [element] Container element. If not given, the root element will be used. + * @param {Array} [found] if passed, links will be appended into this array. + * @returns {Array} Array of anchors. It returns empty array if there's no links. + */ + searchAnchors: function(element, found) { + if(!element) element = this.getRoot(); + if(!found) found = []; + + var anchors = element.getElementsByTagName("A"); + for(var i = 0; i < anchors.length; i++) { + found.push(anchors[i]); + } + + return found; + }, + + /** + * Search all headings within given element + * + * @param {Element} [element] Container element. If not given, the root element will be used. + * @param {Array} [found] if passed, headings will be appended into this array. + * @returns {Array} Array of headings. It returns empty array if there's no headings. + */ + searchHeadings: function(element, found) { + if(!element) element = this.getRoot(); + if(!found) found = []; + + var regexp = /^h[1-6]/ig; + + if (!element.childNodes) return []; + $A(element.childNodes).each(function(child) { + var isContainer = child && this.tree._blockContainerTags.include(child.nodeName); + var isHeading = child && child.nodeName.match(regexp); + + if (isContainer) { + this.searchHeadings(child, found); + } else if (isHeading) { + found.push(child); + } + }.bind(this)); + + return found; + }, + + /** + * Collect structure and style informations of given element. + * + * @param {Element} element target element + * @returns {Object} object that contains information: {em: true, strong: false, block: "p", list: "ol", ...} + */ + collectStructureAndStyle: function(element) { + if(!element || element.nodeName == "#document") return {}; + + var block = this.getParentBlockElementOf(element); + var parents = this.tree.collectParentsOf(element, true, function(node) {return block.parentNode == node}); + var blockName = block.nodeName; + + var info = {}; + + var doc = this.getDoc(); + var em = doc.queryCommandState("Italic"); + var strong = doc.queryCommandState("Bold"); + var strike = doc.queryCommandState("Strikethrough"); + var underline = doc.queryCommandState("Underline") && !this.getParentElementOf(element, ["A"]); + var superscription = doc.queryCommandState("superscript"); + var subscription = doc.queryCommandState("subscript"); + + // if block is only child, select its parent + while(block.parentNode && block.parentNode != this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) { + block = block.parentNode; + } + + var list = false; + if(block.nodeName == "LI") { + var parent = block.parentNode; + var isCode = parent.nodeName == "OL" && parent.className == "code"; + list = isCode ? "CODE" : parent.nodeName; + } + + var justification = block.style.textAlign || "left"; + + return { + block:blockName, + em: em, + strong: strong, + strike: strike, + underline: underline, + superscription: superscription, + subscription: subscription, + list: list, + justification: justification + }; + }, + + /** + * Find elements by CSS selector. + * + * WARNING: Use this method carefully since prototype.js doesn't work well with designMode DOM. + */ + findBySelector: function(selector) { + return Element.getElementsBySelector(this.root, selector); + }, + + /** + * Find elements by attribute. + * + * This method will be deprecated when findBySelector get stabilized. + */ + findByAttribute: function(name, value) { + var nodes = []; + this._findByAttribute(nodes, this.root, name, value); + return nodes; + }, + + /** @private */ + _findByAttribute: function(nodes, element, name, value) { + if(element.getAttribute(name) == value) nodes.push(element); + if(!element.hasChildNodes()) return; + + var children = element.childNodes; + for(var i = 0; i < children.length; i++) { + if(children[i].nodeType == 1) this._findByAttribute(nodes, children[i], name, value); + } + }, + + /** + * Checks if the element has one or more important attributes: id, class, style + * + * @param {Element} element Target element + */ + hasImportantAttributes: function(element) {throw "Not implemented"}, + + /** + * Checks if the element is empty or not. Place-holder is not counted as a child. + * + * @param {Element} element Target element + */ + isEmptyBlock: function(element) {throw "Not implemented"}, + + /** + * Returns element that contains caret. + */ + getCurrentElement: function() {throw "Not implemented"}, + + /** + * Returns block element that contains caret. + */ + getCurrentBlockElement: function() { + var cur = this.getCurrentElement(); + if(!cur) return null; + + var block = this.getParentBlockElementOf(cur); + if(!block) return null; + + return (block.nodeName == "BODY") ? null : block; + }, + + /** + * Returns parent block element of parameter. + * If the parameter itself is a block, it will be returned. + * + * @param {Element} element Target element + * + * @returns {Element} Element or null + */ + getParentBlockElementOf: function(element) { + while(element) { + if(this.tree._blockTags.include(element.nodeName)) return element; + element = element.parentNode; + } + return null; + }, + + /** + * Returns parent element of parameter which has one of given tag name. + * If the parameter itself has the same tag name, it will be returned. + * + * @param {Element} element Target element + * @param {Array} tagNames Array of string which contains tag names + * + * @returns {Element} Element or null + */ + getParentElementOf: function(element, tagNames) { + while(element) { + if(tagNames.include(element.nodeName)) return element; + element = element.parentNode; + } + return null; + }, + + /** + * Collects all block elements between two elements + * + * @param {Element} from Start element(inclusive) + * @param {Element} to End element(inclusive) + */ + getBlockElementsBetween: function(from, to) { + return this.tree.collectNodesBetween(from, to, function(node) { + return node.nodeType == 1 && this.tree.isBlock(node); + }.bind(this)); + }, + + /** + * Returns block element that contains selection start. + * + * This method will return exactly same result with getCurrentBlockElement method + * when there's no selection. + */ + getBlockElementAtSelectionStart: function() {throw "Not implemented"}, + + /** + * Returns block element that contains selection end. + * + * This method will return exactly same result with getCurrentBlockElement method + * when there's no selection. + */ + getBlockElementAtSelectionEnd: function() {throw "Not implemented"}, + + /** + * Returns blocks at each edge of selection(start and end). + * + * TODO: implement ignoreEmptyEdges for FF + * + * @param {boolean} naturalOrder Mak the start element always comes before the end element + * @param {boolean} ignoreEmptyEdges Prevent some browser(Gecko) from selecting one more block than expected + */ + getBlockElementsAtSelectionEdge: function(naturalOrder, ignoreEmptyEdges) {throw "Not implemented"}, + + /** + * Returns array of selected block elements + */ + getSelectedBlockElements: function() { + var selectionEdges = this.getBlockElementsAtSelectionEdge(true, true); + var start = selectionEdges[0]; + var end = selectionEdges[1]; + + return this.tree.collectNodesBetween(start, end, function(node) { + return node.nodeType == 1 && this.tree.isBlock(node); + }.bind(this)); + }, + + /** + * Get element by ID + * + * @param {String} id Element's ID + * @returns {Element} element or null + */ + getElementById: function(id) {return this.doc.getElementById(id)}, + + /** + * Shortcut for #getElementById + */ + $: function(id) {return this.getElementById(id)}, + + /** + * Returns first "valid" child of given element. It ignores empty textnodes. + * + * @param {Element} element Target element + * @returns {Node} first child node or null + */ + getFirstChild: function(element) { + if(!element) return null; + + var nodes = $A(element.childNodes); + for(var i = 0; i < nodes.length; i++) { + if(!this.isEmptyTextNode(nodes[i])) return nodes[i]; + } + return null; + }, + + /** + * Returns last "valid" child of given element. It ignores empty textnodes and place-holders. + * + * @param {Element} element Target element + * @returns {Node} last child node or null + */ + getLastChild: function(element) {throw "Not implemented"}, + + getNextSibling: function(node) { + while(node = node.nextSibling) { + if(node.nodeType != 3 || node.nodeValue.strip() != "") break; + } + return node; + }, + + getBottommostFirstChild: function(node) { + while(node.firstChild && node.nodeType == 1) node = node.firstChild; + return node; + }, + + getBottommostLastChild: function(node) { + while(node.lastChild && node.nodeType == 1) node = node.lastChild; + return node; + }, + + /** @private */ + _getCssValue: function(str, defaultUnit) { + if(!str || str.length == 0) return {value:0, unit:defaultUnit}; + + var tokens = str.match(/(\d+)(.*)/); + return { + value:parseInt(tokens[1]), + unit:tokens[2] || defaultUnit + }; + } +}); + +/** + * Creates and returns instance of browser specific implementation. + */ +xq.RichDom.createInstance = function() { + if(xq.Browser.isTrident) { + return new xq.RichDomTrident(); + } else if(xq.Browser.isWebkit) { + return new xq.RichDomWebkit(); + } else { + return new xq.RichDomGecko(); + } +} +/** + * RichDom for W3C Standard Engine + */ +xq.RichDomW3 = Class.create(xq.RichDom, { + insertNode: function(node) { + var rng = this.rng(); + rng.insertNode(node); + rng.selectNode(node); + rng.collapse(false); + return node; + }, + + removeTrailingWhitespace: function(block) { + // TODO: do nothing + }, + + getOuterHTML: function(element) { + var div = element.ownerDocument.createElement("div"); + div.appendChild(element.cloneNode(true)); + return div.innerHTML; + }, + + correctEmptyElement: function(element) { + if(!element || element.nodeType != 1 || this.tree.isAtomic(element)) return; + + if(element.firstChild) + this.correctEmptyElement(element.firstChild); + else + element.appendChild(this.makePlaceHolder()); + }, + + correctParagraph: function() { + if(this.hasSelection()) return false; + + var block = this.getCurrentElement(); + var modified = false; + + if(this.tree.isBlockOnlyContainer(block)) { + this.execCommand("InsertParagraph"); + + // check for atomic block element such as HR + var newBlock = this.getCurrentElement(); + if(this.tree.isAtomic(newBlock.previousSibling)) { + var nextBlock = this.tree.findForward( + newBlock, + function(node) {return this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)}.bind(this) + ); + if(nextBlock) { + this.deleteNode(newBlock); + this.placeCaretAtStartOf(nextBlock); + } + } + modified = true; + } else if(this.tree.hasMixedContents(block)) { + this.wrapAllInlineOrTextNodesAs("P", block, true); + modified = true; + } + + block = this.getCurrentElement(); + if(this.tree.isBlock(block) && !this._hasPlaceHolderAtEnd(block)) { + block.appendChild(this.makePlaceHolder()); + modified = true; + } + + if(this.tree.isBlock(block)) { + var parentsLastChild = block.parentNode.lastChild; + if(this.isPlaceHolder(parentsLastChild)) { + this.deleteNode(parentsLastChild); + modified = true; + } + } + + return modified; + }, + + _hasPlaceHolderAtEnd: function(block) { + if(!block.hasChildNodes()) return false; + return this.isPlaceHolder(block.lastChild) || this._hasPlaceHolderAtEnd(block.lastChild); + }, + + applyBackgroundColor: function(color) { + this.execCommand("styleWithCSS", "true"); + this.execCommand("hilitecolor", color); + this.execCommand("styleWithCSS", "false"); + + // 0. Save current selection + var bookmark = this.saveSelection(); + + // 1. Get selected blocks + var blocks = this.getSelectedBlockElements(); + if(blocks.length == 0) return; + + // 2. Apply background-color to all adjust inline elements + // 3. Remove background-color from blocks + for(var i = 0; i < blocks.length; i++) { + if((i == 0 || i == blocks.length-1) && !blocks[i].style.backgroundColor) continue; + + var spans = this.wrapAllInlineOrTextNodesAs("SPAN", blocks[i], true); + for(var j = 0; j < spans.length; j++) { + spans[j].style.backgroundColor = color; + } + blocks[i].style.backgroundColor = ""; + } + + // 4. Restore selection + this.restoreSelection(bookmark); + }, + + + + + ////// + // Commands + execCommand: function(commandId, param) { + return this.doc.execCommand(commandId, false, param || null); + }, + + saveSelection: function() { + var rng = this.rng(); + return [rng.startContainer, rng.startOffset, rng.endContainer, rng.endOffset]; + }, + + restoreSelection: function(bookmark) { + var rng = this.rng(); + rng.setStart(bookmark[0], bookmark[1]); + rng.setEnd(bookmark[2], bookmark[3]); + }, + + applyRemoveFormat: function() { + this.execCommand("RemoveFormat"); + this.execCommand("Unlink"); + }, + applyEmphasis: function() { + // Generate tag. It will be replaced with tag during cleanup phase. + this.execCommand("styleWithCSS", "false"); + this.execCommand("italic"); + }, + applyStrongEmphasis: function() { + // Generate tag. It will be replaced with tag during cleanup phase. + this.execCommand("styleWithCSS", "false"); + this.execCommand("bold"); + }, + applyStrike: function() { + // Generate tag. It will be replaced with