diff --git a/.htaccess b/.htaccess
index 04b152121..aad260ec1 100644
--- a/.htaccess
+++ b/.htaccess
@@ -25,6 +25,9 @@ RewriteRule ^([[:digit:]]+)$ ./index.php?document_srl=$1 [L]
# document + act link
RewriteRule ^([[:digit:]]+)/([a-zA-Z0-9_]+)$ ./index.php?document_srl=$1&act=$2 [L]
+# document + key + act link
+RewriteRule ^([[:digit:]]+)/([a-zA-Z0-9_]+)/([a-zA-Z0-9_]+)$ ./index.php?document_srl=$1&act=$3&key=$2 [L]
+
# mid + document link
RewriteRule ^([a-zA-Z0-9_]+)/([[:digit:]]+)$ ./index.php?mid=$1&document_srl=$2 [L]
@@ -54,6 +57,3 @@ RewriteRule ^([a-zA-Z0-9_]+)/writer/(.*)$ ./index.php?mid=$1&search_target=nick_
# module link
RewriteRule ^([a-zA-Z0-9_]+)(/){0,1}$ ./index.php?mid=$1 [L]
-
-# css/img/js/htc등의 경로 처리
-RewriteRule ^(.+)/common/js/iePngFix.htc ./common/js/iePngFix.htc [L]
diff --git a/addons/blogapi/blogapi.addon.php b/addons/blogapi/blogapi.addon.php
index 9fb560153..27dbb978f 100644
--- a/addons/blogapi/blogapi.addon.php
+++ b/addons/blogapi/blogapi.addon.php
@@ -155,25 +155,41 @@
if(!$oDocument->isExists() || !$oDocument->isGranted()) {
printContent( getXmlRpcFailure(1, 'no permission') );
} else {
+ // 카테고리를 사용하는지 확인후 사용시 카테고리 목록을 구해와서 Context에 세팅
+ $category = "";
+ if($oDocument->get('category_srl')) {
+ $oDocumentModel = &getModel('document');
+ $category_list = $oDocumentModel->getCategoryList($oDocument->get('module_srl'));
+ if($category_list[$oDocument->get('category_srl')]) {
+ $category = $category_list[$oDocument->get('category_srl')]->title;
+ }
+ }
+
$content = sprintf(
'
%s', $code_type, $body); + return $output; + } +} +?> \ No newline at end of file diff --git a/modules/editor/components/code_highlighter/css/SyntaxHighlighter.css b/modules/editor/components/code_highlighter/css/SyntaxHighlighter.css new file mode 100755 index 000000000..0a5a67a10 --- /dev/null +++ b/modules/editor/components/code_highlighter/css/SyntaxHighlighter.css @@ -0,0 +1,292 @@ +.dp-highlighter +{ + font-family: "Consolas", "Courier New", Courier, mono, serif; + font-size: 12px; + background-color: #E7E5DC; + width: 99%; + overflow: auto; + margin: 18px 0 18px 0 !important; + padding-top: 1px; /* adds a little border on top when controls are hidden */ +} + +/* clear styles */ +.dp-highlighter ol, +.dp-highlighter ol li, +.dp-highlighter ol li span +{ + margin: 0; + padding: 0; + border: none; +} + +.dp-highlighter a, +.dp-highlighter a:hover +{ + background: none; + border: none; + padding: 0; + margin: 0; +} + +.dp-highlighter .bar +{ + padding-left: 45px; +} + +.dp-highlighter.collapsed .bar, +.dp-highlighter.nogutter .bar +{ + padding-left: 0px; +} + +.dp-highlighter ol +{ + list-style: decimal; /* for ie */ + background-color: #fff; + margin: 0px 0px 1px 45px !important; /* 1px bottom margin seems to fix occasional Firefox scrolling */ + padding: 0px; + color: #5C5C5C; +} + +.dp-highlighter.nogutter ol, +.dp-highlighter.nogutter ol li +{ + list-style: none !important; + margin-left: 0px !important; +} + +.dp-highlighter ol li, +.dp-highlighter .columns div +{ + list-style: decimal; /* better look for others, override cascade from OL */ + list-style-position: outside !important; + border-left: 3px solid #22AAEE; + background-color: #F8F8F8; + color: #5C5C5C; + padding: 0 3px 0 10px !important; + margin: 0 !important; + line-height: 14px; +} + +.dp-highlighter.nogutter ol li, +.dp-highlighter.nogutter .columns div +{ + border: 0; +} + +.dp-highlighter .columns +{ + background-color: #F8F8F8; + color: gray; + overflow: hidden; + width: 100%; +} + +.dp-highlighter .columns div +{ + padding-bottom: 5px; +} + +.dp-highlighter ol li.alt +{ + background-color: #FFF; + color: inherit; +} + +.dp-highlighter ol li span +{ + color: black; + background-color: inherit; +} + +/* Adjust some properties when collapsed */ + +.dp-highlighter.collapsed ol +{ + margin: 0px; +} + +.dp-highlighter.collapsed ol li +{ + display: none; +} + +/* Additional modifications when in print-view */ + +.dp-highlighter.printing +{ + border: none; +} + +.dp-highlighter.printing .tools +{ + display: none !important; +} + +.dp-highlighter.printing li +{ + display: list-item !important; +} + +/* Styles for the tools */ + +.dp-highlighter .tools +{ + padding: 3px 8px 3px 10px; + font: 9px Verdana, Geneva, Arial, Helvetica, sans-serif; + color: silver; + background-color: #f8f8f8; + padding-bottom: 10px; + border-left: 3px solid #22AAEE; +} + +.dp-highlighter.nogutter .tools +{ + border-left: 0; +} + +.dp-highlighter.collapsed .tools +{ + border-bottom: 0; +} + +.dp-highlighter .tools a +{ + font-size: 9px; + color: #a0a0a0; + background-color: inherit; + text-decoration: none; + margin-right: 10px; +} + +.dp-highlighter .tools a:hover +{ + color: red; + background-color: inherit; + text-decoration: underline; +} + +/* About dialog styles */ + +.dp-about { background-color: #fff; color: #333; margin: 0px; padding: 0px; } +.dp-about table { width: 100%; height: 100%; font-size: 11px; font-family: Tahoma, Verdana, Arial, sans-serif !important; } +.dp-about td { padding: 10px; vertical-align: top; } +.dp-about .copy { border-bottom: 1px solid #ACA899; height: 95%; } +.dp-about .title { color: red; background-color: inherit; font-weight: bold; } +.dp-about .para { margin: 0 0 4px 0; } +.dp-about .footer { background-color: #ECEADB; color: #333; border-top: 1px solid #fff; text-align: right; } +.dp-about .close { font-size: 11px; font-family: Tahoma, Verdana, Arial, sans-serif !important; background-color: #ECEADB; color: #333; width: 60px; height: 22px; } + +/* Language specific styles */ + +.dp-highlighter .comment, +.dp-highlighter .comments { color: #008200; background-color: inherit; } +.dp-highlighter .string { color: #FF00FF; background-color: inherit; } +.dp-highlighter .keyword { color: #0000FF; background-color: inherit; } +.dp-highlighter .preprocessor { color: gray; background-color: inherit; } +.dp-highlighter .func { color: #FF0000; } +.dp-highlighter .vars { color: #008080; } + + +/* Language specific styles */ + +.dp-c {} +.dp-c .comment { color: green; } +.dp-c .string { color: blue; } +.dp-c .preprocessor { color: gray; } +.dp-c .keyword { color: blue; } +.dp-c .vars { color: #d00; } + +.dp-vb {} +.dp-vb .comment { color: green; } +.dp-vb .string { color: blue; } +.dp-vb .preprocessor { color: gray; } +.dp-vb .keyword { color: blue; } + +.dp-sql {} +.dp-sql .comment { color: green; } +.dp-sql .string { color: red; } +.dp-sql .keyword { color: blue; } +.dp-sql .func { color: #ff1493; } +.dp-sql .op { color: #808080; } + +.dp-xml {} +.dp-xml .cdata { color: #ff1493; } +.dp-xml .comments { color: green; } +.dp-xml .tag { font-weight: bold; color: blue; } +.dp-xml .tag-name { color: black; font-weight: bold; } +.dp-xml .attribute { color: red; } +.dp-xml .attribute-value { color: blue; } + +.dp-delphi {} +.dp-delphi .comment { color: #008200; font-style: italic; } +.dp-delphi .string { color: blue; } +.dp-delphi .number { color: blue; } +.dp-delphi .directive { color: #008284; } +.dp-delphi .keyword { font-weight: bold; color: navy; } +.dp-delphi .vars { color: #000; } + +.dp-py {} +.dp-py .comment { color: green; } +.dp-py .string { color: red; } +.dp-py .docstring { color: green; } +.dp-py .keyword { color: blue; font-weight: bold;} +.dp-py .builtins { color: #ff1493; } +.dp-py .magicmethods { color: #808080; } +.dp-py .exceptions { color: brown; } +.dp-py .types { color: brown; font-style: italic; } +.dp-py .commonlibs { color: #8A2BE2; font-style: italic; } + +.dp-rb {} +.dp-rb .comment { color: #c00; } +.dp-rb .string { color: #f0c; } +.dp-rb .symbol { color: #02b902; } +.dp-rb .keyword { color: #069; } +.dp-rb .variable { color: #6cf; } + +.dp-css {} +.dp-css .comment { color: green; } +.dp-css .string { color: red; } +.dp-css .keyword { color: blue; } +.dp-css .colors { color: darkred; } +.dp-css .vars { color: #d00; } + +.dp-j {} +.dp-j .comment { color: rgb(63,127,95); } +.dp-j .string { color: rgb(42,0,255); } +.dp-j .keyword { color: rgb(127,0,85); font-weight: bold } +.dp-j .annotation { color: #646464; } +.dp-j .number { color: #C00000; } + +.dp-cpp {} +.dp-cpp .comment { color: #e00; } +.dp-cpp .string { color: red; } +.dp-cpp .preprocessor { color: #CD00CD; font-weight: bold; } +.dp-cpp .keyword { color: #5697D9; font-weight: bold; } +.dp-cpp .datatypes { color: #2E8B57; font-weight: bold; } + +.dp-php { color: #800000; } +.dp-php .comment { color: #008000; } +.dp-php .keyword { color: #4B00FB; } +.dp-php .string { color: #FB00FB; } +.dp-php .func { color: #FF0000; } +.dp-php .vars { color: #008080; } +.dp-php .zbxe_funcs { color: #FF6820; } +.dp-php .zbxe_class { color: #FF6820; font-weight: bold; } + + +.dp-abap { color: #800000; } +.dp-abap .comment { color: #008000; } +.dp-abap .keyword { color: #4B00FB; } +.dp-abap .string { color: #FB00FB; } +.dp-abap .datatypes { color: #2E8B57; font-weight: bold; } + + +pre[name='CodeHighLighterArea'] { + max-height: 200px; + font-size: 1.1em; + border: #666666 dotted 1px; + border-left: #22AAEE solid 5px; + padding: 5px; + overflow: auto; +} \ No newline at end of file diff --git a/modules/editor/components/code_highlighter/icon.gif b/modules/editor/components/code_highlighter/icon.gif new file mode 100755 index 000000000..4b542c04e Binary files /dev/null and b/modules/editor/components/code_highlighter/icon.gif differ diff --git a/modules/editor/components/code_highlighter/info.xml b/modules/editor/components/code_highlighter/info.xml new file mode 100755 index 000000000..b887fd79a --- /dev/null +++ b/modules/editor/components/code_highlighter/info.xml @@ -0,0 +1,20 @@ + +
dp.SyntaxHighlighter Version: {V}©2004-2007 Alex Gorbatchev. |
|
+
+
+
+
+ {$lang->about_address_use}
+
+
|
+ + + | +
";
+
+ opener.editorFocus(opener.editorPrevSrl);
+
+ var iframe_obj = opener.editorGetIFrame(opener.editorPrevSrl)
+
+ opener.editorReplaceHTML(iframe_obj, text);
+ opener.editorFocus(opener.editorPrevSrl);
+
+ window.close();
+}
+
+xAddEventListener(window, "load", getNaverMap);
+
+/* 네이버의 map openapi로 주소에 따른 좌표를 요청 */
+function search_address(selected_address) {
+ if(typeof(selected_address)=="undefined") selected_address = null;
+ var address = xGetElementById("address").value;
+ if(!address) return;
+ var params = new Array();
+ params['component'] = "naver_map";
+ params['address'] = address;
+ params['method'] = "search_address";
+
+ var response_tags = new Array('error','message','address_list');
+ exec_xml('editor', 'procEditorCall', params, complete_search_address, response_tags, selected_address);
+}
+
+function moveMap(x,y,scale) {
+ if(typeof(scale)=="undefined") scale = 3;
+ display_map.moveMap(x,y,scale);
+}
+
+function mapClicked(pos) {
+ xGetElementById("map_x").value = pos.x;
+ xGetElementById("map_y").value = pos.y;
+}
+
+var naver_address_list = new Array();
+function complete_search_address(ret_obj, response_tags, selected_address) {
+ var address_list = ret_obj['address_list'];
+ if(!address_list) return;
+
+ naver_address_list = new Array();
+
+ var html = "";
+ var address_list = address_list.split("\n");
+ for(var i=0;i
+ 
";
+ editor.insertHTML(text);
+ }
+
+ // binary파일의 경우 url_link 컴포넌트 연결
+ } else {
+ var mid = fo_obj.mid.value;
+ var url = request_uri+"/?module=file&act=procFileDownload&file_srl="+file_srl+"&sid="+sid;
+ var text = ""+filename+""+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" +}if(this.config.generateDefaultToolbar){this._addStyleRules([{selector:".xquared div.toolbar",rule:"background-image: url("+this.config.imagePathForDefaultToobar+"toolbarBg.gif)"},{selector:".xquared ul.buttons li",rule:"background-image: url("+this.config.imagePathForDefaultToobar+"toolbarButtonBg.gif)"},{selector:".xquared ul.buttons li.xq_separator",rule:"background-image: url("+this.config.imagePathForDefaultToobar+"toolbarSeparator.gif)"}]) +}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) +}},_addStyleRules:function(D){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=xq.$A(this.doc.styleSheets).last() +}}for(var A=0; +A") +},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].indexOf(B)==-1){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) +}else{this.placeCaretAtStartOf(D) +}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","
' + 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'; + } + + // override image path + if(this.config.generateDefaultToolbar) { + this._addStyleRules([ + {selector:".xquared div.toolbar", rule:"background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarBg.gif)"}, + {selector:".xquared ul.buttons li", rule:"background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarButtonBg.gif)"}, + {selector:".xquared ul.buttons li.xq_separator", rule:"background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarSeparator.gif)"} + ]); + } + + 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); + } + }, + + _addStyleRules: function(rules) { + 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 = xq.$A(this.doc.styleSheets).last(); + } + } + + for(var i = 0; i < rules.length; i++) { + var rule = rules[i]; + if(xq.Browser.isTrident) { + this.dynamicStyle.addRule(rules[i].selector, rules[i].rule); + } else { + this.dynamicStyle.insertRule(rules[i].selector + " {" + rules[i].rule + "}", this.dynamicStyle.cssRules.length); + } + } + }, + + _defaultToolbarClickHandler: function(e) { + var src = e.target || e.srcElement; + while(src.nodeName != "A") src = src.parentNode; + + if(xq.hasClassName(src.parentNode, 'disabled') || xq.hasClassName(this.toolbarContainer, 'disabled')) { + xq.stopEvent(e); + return false; + } + + if(xq.Browser.isTrident) this.focus(); + + var handler = src.handler; + var xed = this; + var stop = (typeof handler == "function") ? handler(this) : eval(handler); + + if(stop) { + xq.stopEvent(e); + return false; + } else { + return true; + } + }, + + _generateDefaultToolbar: function() { + // 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 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; + + this._toolbarAnchorsCache.push(a); + + xq.observe(a, 'mousedown', xq.cancelHandler); + xq.observe(a, 'click', this._defaultToolbarClickHandler.bindAsEventListener(this)); + + 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']; + + 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++) { + xq.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"]); + + var editable = this.editorBody.contentEditable; + editable = editable == 'inherit' ? false : editable; + + if(editable != 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"].indexOf(e.type) != -1) { + modifiedByCorrection = this.rdom.correctParagraph(); + } else if(["contextmenu"].indexOf(e.type) != -1) { + this._handleContextMenu(e); + } + + if(stop) xq.stopEvent(e); + + this._fireOnCurrentContentChanged(this); + this._fireOnAfterEvent(this, e); + + if(!undoPerformed && !modifiedByCorrection) this.editHistory.onEvent(e); + + return !stop; + }, + + /** + * TODO: remove dup with handleAutocompletion + */ + handleAutocorrection: function() { + var block = this.rdom.getCurrentBlockElement(); + + // TODO: use complete unescape algorithm + var text = this.rdom.getInnerText(block).replace(/ /gi, " "); + + var acs = this.config.autocorrections; + var performed = false; + + var stop = false; + for(var key in acs) { + var ac = acs[key]; + if(ac.criteria(text)) { + try { + this.editHistory.onCommand(); + this.editHistory.disable(); + if(typeof ac.handler == "String") { + var xed = this; + var rdom = this.rdom; + eval(ac.handler); + } else { + stop = ac.handler(this, this.rdom, block, text); + } + this.editHistory.enable(); + } catch(ignored) {} + + block = this.rdom.getCurrentBlockElement(); + text = this.rdom.getInnerText(block); + + performed = true; + if(stop) break; + } + } + + return stop; + }, + + /** + * TODO: remove dup with handleAutocorrection + */ + handleAutocompletion: function() { + var acs = this.config.autocompletions; + if(xq.isEmptyHash(acs)) return; + + if(this.rdom.hasSelection()) { + var text = this.rdom.getSelectionAsText(); + this.rdom.deleteSelection(); + var wrapper = this.rdom.insertNode(this.rdom.createElement("SPAN")); + wrapper.innerHTML = text; + + var marker = this.rdom.pushMarker(); + + var filtered = []; + for(var key in acs) { + filtered.push([key, acs[key].criteria(text)]); + } + filtered = filtered.findAll(function(elem) { + return elem[1] != -1; + }); + + if(filtered.length == 0) { + this.rdom.popMarker(true); + return; + } + + var minIndex = 0; + var min = filtered[0][1]; + for(var i = 0; i < filtered.length; i++) { + if(filtered[i][1] < min) { + minIndex = i; + min = filtered[i][1]; + } + } + + var ac = acs[filtered[minIndex][0]]; + + this.editHistory.disable(); + } else { + var marker = this.rdom.pushMarker(); + + var filtered = []; + for(var key in acs) { + filtered.push([key, this.rdom.testSmartWrap(marker, acs[key].criteria).textIndex]); + } + filtered = filtered.findAll(function(elem) { + return elem[1] != -1; + }); + + if(filtered.length == 0) { + this.rdom.popMarker(true); + return; + } + + var minIndex = 0; + var min = filtered[0][1]; + for(var i = 0; i < filtered.length; i++) { + if(filtered[i][1] < min) { + minIndex = i; + min = filtered[i][1]; + } + } + + var ac = acs[filtered[minIndex][0]]; + + this.editHistory.disable(); + + var wrapper = this.rdom.smartWrap(marker, "SPAN", ac.criteria); + } + + var block = this.rdom.getCurrentBlockElement(); + + // TODO: use complete unescape algorithm + var text = this.rdom.getInnerText(wrapper).replace(/ /gi, " "); + + try { + // call handler + if(typeof ac.handler == "String") { + var xed = this; + var rdom = this.rdom; + eval(ac.handler); + } else { + ac.handler(this, this.rdom, block, wrapper, text); + } + } catch(ignored) {} + + try { + this.rdom.unwrapElement(wrapper); + } catch(ignored) {} + + if(this.rdom.isEmptyBlock(block)) this.rdom.correctEmptyElement(block); + + this.editHistory.enable(); + this.editHistory.onCommand(); + + this.rdom.popMarker(true); + }, + + /** + * Handles click event + * + * @param {Event} e click event + * @param {Element} target target element(usually has A tag) + */ + handleClick: function(e, target) { + var href = decodeURI(target.href); + if(!xq.Browser.isTrident) { + if(!e.ctrlKey && !e.shiftKey && e.button != 1) { + window.location.href = href; + return true; + } + } else { + if(e.shiftKey) { + window.open(href, "_blank"); + } else { + window.location.href = href; + } + return true; + } + + return false; + }, + + /** + * Show link dialog + * + * TODO: should support modify/unlink + */ + 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"].indexOf(dir) != -1) { + var toNext = dir == "next"; + target = toNext ? rtable.getNextCellOf(cell) : rtable.getPreviousCellOf(cell); + } else { + var toBelow = dir == "below"; + target = toBelow ? rtable.getBelowCellOf(cell) : rtable.getAboveCellOf(cell); + } + + if(!target) { + var finder = function(node) {return ['TD', 'TH'].indexOf(node.nodeName) == -1 && this.tree.isBlock(node) && !this.tree.hasBlocks(node);}.bind(this.rdom); + var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this.rdom); + + target = (toNext || toBelow) ? + this.rdom.tree.findForward(cell, finder, exitCondition) : + this.rdom.tree.findBackward(table, finder, exitCondition); + } + + if(target) this.rdom.placeCaretAtStartOf(target); + }, + + /** + * Applies STRONG tag + */ + 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 || xq.isLeftClick(e)) return false; + } else if (e.shiftKey || e.ctrlKey || e.altKey) { + return false; + } + + var point = xq.getEventPoint(e); + var x = point.x; + var y = point.y; + + var pos = xq.getCumulativeOffset(this.getFrame()); + x += pos.left; + y += pos.top; + 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 = xq.getCumulativeOffset(this._contextMenuTargetElement); + var posFrame = xq.getCumulativeOffset(this.getFrame()); + x = pos.left + posFrame.left - this.getDoc().documentElement.scrollLeft; + y = pos.top + posFrame.top - 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) { + xq.stopEvent(e); + return true; + } + } + + return false; + }, + + showContextMenu: function(menuItems, x, y) { + if (!menuItems || menuItems.length <= 0) return; + + if (!this._contextMenuContainer) { + this._contextMenuContainer = this.doc.createElement('UL'); + this._contextMenuContainer.className = 'xqContextMenu'; + this._contextMenuContainer.style.display='none'; + + xq.observe(this.doc, 'click', this._contextMenuClicked.bindAsEventListener(this)); + xq.observe(this.rdom.getDoc(), 'click', this.hideContextMenu.bindAsEventListener(this)); + + this.body.appendChild(this._contextMenuContainer); + } else { + while (this._contextMenuContainer.childNodes.length > 0) + this._contextMenuContainer.removeChild(this._contextMenuContainer.childNodes[0]); + } + + for (var i=0; i < menuItems.length; i++) { + menuItems[i]._node = this._addContextMenuItem(menuItems[i]); + } + + this._contextMenuContainer.style.display='block'; + this._contextMenuContainer.style.left=Math.min(Math.max(this.doc.body.scrollWidth, this.doc.documentElement.clientWidth)-this._contextMenuContainer.offsetWidth, x)+'px'; + this._contextMenuContainer.style.top=Math.min(Math.max(this.doc.body.scrollHeight, this.doc.documentElement.clientHeight)-this._contextMenuContainer.offsetHeight, y)+'px'; + + this._contextMenuItems = menuItems; + }, + + hideContextMenu: function() { + if (this._contextMenuContainer) + this._contextMenuContainer.style.display='none'; + }, + + _addContextMenuItem: function(item) { + if (!this._contextMenuContainer) throw "No conext menu container exists"; + + var node = this.doc.createElement('LI'); + if (item.disabled) node.className += ' disabled'; + + if (item.title == '----') { + node.innerHTML = ' '; + node.className = 'separator'; + } else { + if(item.handler) { + node.innerHTML = ''+(item.title.toString().escapeHTML())+''; + } else { + node.innerHTML = (item.title.toString().escapeHTML()); + } + } + + if(item.className) node.className = item.className; + + this._contextMenuContainer.appendChild(node); + + return node; + }, + + _contextMenuClicked: function(e) { + this.hideContextMenu(); + + if (!this._contextMenuContainer) return; + + var node = e.srcElement || e.target; + while(node && node.nodeName != "LI") { + node = node.parentNode; + } + if (!node || !this.rdom.tree.isDescendantOf(this._contextMenuContainer, node)) return; + + for (var i=0; i < this._contextMenuItems.length; i++) { + if (this._contextMenuItems[i]._node == node) { + var handler = this._contextMenuItems[i].handler; + if (!this._contextMenuItems[i].disabled && handler) { + var xed = this; + var element = this._contextMenuTargetElement; + if(typeof handler == "function") { + handler(xed, element); + } else { + eval(handler); + } + } + break; + } + } + }, + + /** + * Inserts HTML template + * + * @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 = this.getTemplateProcessors(); + for(var key in tps) { + var value = tps[key]; + html = value.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 = xq.Class({ + initialize: function(keymapOrExpression) { + xq.addToFinalizeQueue(this); + + 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 = xq.Class({ + initialize: function() { + xq.addToFinalizeQueue(this); + this._blockTags = ["DIV", "DD", "LI", "ADDRESS", "CAPTION", "DT", "H1", "H2", "H3", "H4", "H5", "H6", "HR", "P", "BODY", "BLOCKQUOTE", "PRE", "PARAM", "DL", "OL", "UL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; + this._blockContainerTags = ["DIV", "DD", "LI", "BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; + this._listContainerTags = ["OL", "UL", "DL"]; + this._tableCellTags = ["TH", "TD"]; + this._blockOnlyContainerTags = ["BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR"]; + this._atomicTags = ["IMG", "OBJECT", "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.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; + }, + + isTableCell: function(element) { + if(!element) return false; + return this._tableCellTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; + }, + + isBlockContainer: function(element) { + if(!element) return false; + return this._blockContainerTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; + }, + + isHeading: function(element) { + if(!element) return false; + return (typeof element == 'string' ? element : element.nodeName).match(/H\d/); + }, + + isBlock: function(element) { + if(!element) return false; + return this._blockTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; + }, + + isAtomic: function(element) { + if(!element) return false; + return this._atomicTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; + }, + + isListContainer: function(element) { + if(!element) return false; + return this._listContainerTags.indexOf(typeof element == 'string' ? element : element.nodeName) != -1; + }, + + isTextOrInlineNode: function(node) { + return node && (node.nodeType == 3 || !this.isBlock(node)); + } +}); +/** + * 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 = xq.Class({ + /** + * Initialize RichDom. Target window and root element should be set after initialization. See setWin and setRoot. + * + * @constructor + */ + initialize: function() { + xq.addToFinalizeQueue(this); + + /** + * {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(var 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"].indexOf(target.nodeName) != -1 || + "BODY" == target.nodeName && ["before", "after"].indexOf(where) != -1 + ) 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 = []; + for(var i = 0; i < textNodes.length; i++) { + nodeValues.push(textNodes[i].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])) { + var wrappers = this.wrapAllInlineOrTextNodesAs(tagName, elements[i], true); + for(var j = 0; j < wrappers.length; j++) { + applied.push(wrappers[j]); + } + } else { + applied.push(this.replaceTag(tagName, elements[i])); + } + } + } + return applied; + }, + + /** + * 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'].indexOf(target.nodeName) != -1; + + 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'].indexOf(target.nodeName) != -1; + + 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"].indexOf(target.nodeName) != -1) 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); + } + } + + this.correctEmptyElement(moved); + + 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].indexOf(this.getRoot()) != -1) 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"].indexOf(parent.nodeName) != -1) 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'].indexOf(prevContainer.nodeName) != -1) && + nextContainer && (this.tree.isTableCell(nextContainer) || ['TR', 'THEAD', 'TBODY'].indexOf(nextContainer.nodeName) != -1); + + 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"].indexOf(to.nodeName) != -1) + 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" != 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=xq.$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 = xq.$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 = xq.$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=xq.$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 = xq.$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) { + for(var i = 0; i < blocks.length; i++) { + this.justifyBlock(blocks[i], dir); + } + 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.indexOf(tag) == -1) 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; + var nodes = element.childNodes; + if (!nodes) return []; + + for(var i = 0; i < nodes.length; i++) { + var isContainer = nodes[i] && this.tree._blockContainerTags.indexOf(nodes[i].nodeName) != -1; + var isHeading = nodes[i] && nodes[i].nodeName.match(regexp); + + if (isContainer) { + this.searchHeadings(nodes[i], found); + } else if (isHeading) { + found.push(nodes[i]); + } + } + + 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); + + // IE���� ��Ȥ DOM�� �� ��: element�� ���ڷ� �Ѿ�4?��찡�? + if(block == null) return {}; + + 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 + }; + }, + + /** + * 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.indexOf(element.nodeName) != -1) 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.indexOf(element.nodeName) != -1) 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 = xq.$A(element.childNodes); + return nodes.find(function(node) {return !this.isEmptyTextNode(node)}.bind(this)); + }, + + /** + * 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 = xq.Class(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