diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index f7d4fc16e..831617894 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -139,7 +139,7 @@ $this->addJsFile("./common/js/xml_handler.js"); $this->addJsFile("./common/js/xml_js_filter.js"); $this->addCSSFile("./common/css/default.css"); - $this->addCSSFile("./common/css/button.css",false); + $this->addCSSFile("./common/css/button.css"); // 관리자 페이지일 경우 관리자 공용 CSS 추가 if(Context::get('module')=='admin' || strpos(Context::get('act'),'Admin')>0) $this->addCssFile("./modules/admin/tpl/css/admin.css", false); @@ -701,13 +701,8 @@ if($get_vars['act'] == 'dispMemberFriend') $get_vars['act'] = 'dispCommunicationFriend'; elseif($get_vars['act'] == 'dispMemberMessages') $get_vars['act'] = 'dispCommunicationMessages'; - if(!$domain) { - if($get_vars['act'] && $this->isExistsSSLAction($get_vars['act'])) $path = $this->getRequestUri(ENFORCE_SSL); - else $path = $this->getRequestUri(RELEASE_SSL); - } else { - if($get_vars['act'] && $this->isExistsSSLAction($get_vars['act'])) $path = 'https://'.$domain; - else $path = 'http://'.$domain; - } + if($get_vars['act'] && $this->isExistsSSLAction($get_vars['act'])) $path = $this->getRequestUri(ENFORCE_SSL, $domain); + else $path = $this->getRequestUri(RELEASE_SSL, $domain); $var_count = count($get_vars); if(!$var_count) return $path; @@ -775,10 +770,13 @@ /** * @brief 요청이 들어온 URL에서 argument를 제거하여 return **/ - function getRequestUri($ssl_mode = FOLLOW_REQUEST_SSL) { + function getRequestUri($ssl_mode = FOLLOW_REQUEST_SSL, $domain = null) { static $url = array(); - if(isset($url[$ssl_mode])) return $url[$ssl_mode]; + if($domain) $domain_key = md5($domain); + else $domain_key = 'default'; + + if(isset($url[$ssl_mode][$domain_key])) return $url[$ssl_mode][$domain_key]; switch($ssl_mode) { case FOLLOW_REQUEST_SSL : @@ -793,21 +791,17 @@ break; } - $site_module_info = Context::get('site_module_info'); - $domain = trim($site_module_info->domain); if($domain) { - $domain = preg_replace('/^(http|https):\/\//i','', trim($domain)); - if(substr($domain,-1) != '/') $domain .= '/'; + $target_url = trim($domain); + if(substr($target_url,-1) != '/') $target_url.= '/'; } else { - $domain = preg_replace('/:'.$_SERVER['SERVER_PORT'].'$/','',$_SERVER['HTTP_HOST']).getScriptPath(); + $target_url= $_SERVER['HTTP_HOST'].getScriptPath(); } - $domain = sprintf("%s://%s",$use_ssl?'https':'http',$domain); + $url_info = parse_url('http://'.$target_url); + $url[$ssl_mode][$domain_key] = sprintf("%s://%s%s%s",$use_ssl?'https':$url_info['scheme'], $url_info['host'], $url_info['port']&&$url_info['port']!=80?':'.$url_info['port']:'',$url_info['path']); - $url_info = parse_url($domain); - $url[$ssl_mode] = sprintf("%s://%s%s%s",$url_info['scheme'], $url_info['host'], $_SERVER['SERVER_PORT']!=80?':'.$_SERVER['SERVER_PORT']:'',$url_info['path']); - - return $url[$ssl_mode]; + return $url[$ssl_mode][$domain_key]; } /** diff --git a/classes/db/DBCubrid.class.php b/classes/db/DBCubrid.class.php index 757cf7e2b..ebe899788 100644 --- a/classes/db/DBCubrid.class.php +++ b/classes/db/DBCubrid.class.php @@ -244,7 +244,7 @@ function addIndex($table_name, $index_name, $target_columns, $is_unique = false) { if(!is_array($target_columns)) $target_columns = array($target_columns); - $query = sprintf("create %s index %s on %s%s (%s);", $is_unique?'unique':'', $index_name, $this->prefix, $table_name, implode(',',$target_columns)); + $query = sprintf("create %s index %s on %s%s (%s);", $is_unique?'unique':'', $index_name, $this->prefix, $table_name, '"'.implode('","',$target_columns).'"'); $this->_query($query); } @@ -403,6 +403,7 @@ $value = $this->getConditionValue($name, $value, $operation, $type, $output->column_type); if(!$value) $value = $v['value']; if(strpos($name,'.')===false) $name = '"'.$name.'"'; + else $name = str_replace('.','."',$name).'"'; $str = $this->getConditionPart($name, $value, $operation); diff --git a/common/css/button.css b/common/css/button.css index f42e881ad..e24bffa17 100644 --- a/common/css/button.css +++ b/common/css/button.css @@ -34,18 +34,18 @@ span.button, x:-moz-any-link, x:default{ padding:0;} /* Firefox 2 Fix */ span.button button, span.button input { height:23px; left:2px; *top:-1px; _top:0; padding:0 10px 0 8px; *padding:0 5px 0 3px; line-height:24px; background-position:right top; cursor:pointer;} /* Large Size */ -span.button.large { background-position:left -30px; *top:-1px;} +span.button.large { background-position:left -30px;} span.button.large, x:-moz-any-link{ font:0/29px Sans-serif; padding:14px 0;} /* Firefox 2 Fix */ span.button.large, x:-moz-any-link, x:default{ padding:0;} /* Firefox 2 Fix */ span.button.large button, -span.button.large input { height:30px; *top:0; _top:-1px; padding:0 10px 0 8px; *padding:0 5px 0 3px; font:16px/30px Sans-serif; background-position:right -30px;} +span.button.large input { height:30px; *top:0 !important; padding:0 10px 0 8px; *padding:0 5px 0 3px; font:16px/30px Sans-serif; background-position:right -30px;} /* xLarge Size */ span.button.xLarge { background-position:left -65px;} span.button.xLarge, x:-moz-any-link{ font:0/35px Sans-serif; padding:17px 0;} /* Firefox 2 Fix */ span.button.xLarge, x:-moz-any-link, x:default{ padding:0;} /* Firefox 2 Fix */ span.button.xLarge button, span.button.xLarge input { height:35px; padding:0 10px 0 8px; *padding:0 5px 0 3px; font:20px/36px Sans-serif; background-position:right -65px;} -/* Large Size */ +/* Small Size */ span.button.small { background-position:left -107px;} span.button.small, x:-moz-any-link{ font:0/18px Sans-serif; padding:9px 0;} /* Firefox 2 Fix */ span.button.small, x:-moz-any-link, x:default{ padding:0;} /* Firefox 2 Fix */ diff --git a/common/css/default.css b/common/css/default.css index d6dd87403..ce5749b1e 100644 --- a/common/css/default.css +++ b/common/css/default.css @@ -26,7 +26,7 @@ form { margin:0; padding:0; } *:first-child+html .inputTypeText { font-size:9pt; } .inputTypeText:hover, .inputTypeText:focus { background:#f4f4f4; } -.inputTypeTextArea { border:1px solid !important; border-color:#a6a6a6 #d8d8d8 #d8d8d8 #a6a6a6 !important; background:#ffffff; font-size:1em; _font-size:9pt;} +.inputTypeTextArea { border:1px solid !important; border-color:#a6a6a6 #d8d8d8 #d8d8d8 #a6a6a6 !important; background:#ffffff; font-size:1em; _font-size:9pt; height:100px;} *:first-child+html .inputTypeTextArea { font-size:9pt; } .w40 { width:40px; } diff --git a/config/config.inc.php b/config/config.inc.php index 1201d9841..a83006794 100644 --- a/config/config.inc.php +++ b/config/config.inc.php @@ -14,7 +14,7 @@ * 이 내용은 XE의 버전을 관리자 페이지에 표시하기 위한 용도이며 * config.inc.php의 수정이 없더라도 공식 릴리즈시에 수정되어 함께 배포되어야 함 **/ - define('__ZBXE_VERSION__', '1.1.0'); + define('__ZBXE_VERSION__', '1.1.1'); /** * @brief 디버깅 메세지 출력 diff --git a/modules/addon/addon.class.php b/modules/addon/addon.class.php index bca38bf75..47449d3e0 100644 --- a/modules/addon/addon.class.php +++ b/modules/addon/addon.class.php @@ -28,11 +28,12 @@ $oAddonController->doInsert('referer'); // 몇가지 애드온을 기본 활성화 상태로 변경 - $oAddonController->doActivate('member_extra_info'); - $oAddonController->doActivate('counter'); - //$oAddonController->doActivate('blogapi'); - $oAddonController->doActivate('referer'); $oAddonController->doActivate('autolink'); + $oAddonController->doActivate('counter'); + $oAddonController->doActivate('member_communication'); + $oAddonController->doActivate('member_extra_info'); + $oAddonController->doActivate('mobile'); + $oAddonController->doActivate('referer'); $oAddonController->doActivate('resize_image'); $oAddonController->procAddonAdminToggleActivate(); return new Object(); diff --git a/modules/admin/lang/zh-TW.lang.php b/modules/admin/lang/zh-TW.lang.php index 21b98787a..bb7aaef28 100644 --- a/modules/admin/lang/zh-TW.lang.php +++ b/modules/admin/lang/zh-TW.lang.php @@ -46,7 +46,7 @@ $lang->welcome_to_xe = 'XE 管理頁面'; $lang->about_admin_page = "後台管理頁面未完成"; - $lang->about_lang_env = "可以設置顯示語言給首次訪問的使用者。
修改語言環境後,請按 [儲存] 按鈕進行儲存。"; + $lang->about_lang_env = "可以設置顯示語言給首次訪問的使用者。修改語言環境後,請按 [儲存] 按鈕進行儲存。"; $lang->xe_license = 'XE遵循 GPL協議'; $lang->about_shortcut = '可以刪除新增到常用模組中的快捷選單。'; diff --git a/modules/board/board.class.php b/modules/board/board.class.php index e273c2a32..4b9c7134a 100644 --- a/modules/board/board.class.php +++ b/modules/board/board.class.php @@ -37,6 +37,32 @@ // 2007. 10. 17 아이디 클릭시 나타나는 팝업메뉴에 작성글 보기 기능 추가 $oModuleController->insertTrigger('member.getMemberMenu', 'board', 'controller', 'triggerMemberMenu', 'after'); + + // 기본 게시판 생성 + $output = executeQuery('module.getDefaultMidInfo'); + if($output->data) return new Object(); + + // 기본 모듈을 찾음 + $oModuleModel = &getModel('module'); + $module_info = $oModuleModel->getDefaultMid(); + + // 기본 모듈이 없으면 새로 등록 + if(!$module_info->module_srl) { + $args->board_name = 'board'; + $args->browser_title = 'test module'; + $args->is_default = 'Y'; + $args->skin = 'xe_default'; + + // board 라는 이름의 모듈이 있는지 확인 + $module_info = $oModuleModel->getModuleInfoByMid($args->board_name); + if($module_info->module_srl) $args->module_srl = $module_info->module_srl; + else $args->module_srl = 0; + + // 게시판 controller 생성 + $oBoardController = &getAdminController('board'); + $oBoardController->procBoardAdminInsertBoard($args); + } + return new Object(); } diff --git a/modules/board/board.view.php b/modules/board/board.view.php index 2f32cf7d0..c33af23c3 100644 --- a/modules/board/board.view.php +++ b/modules/board/board.view.php @@ -280,6 +280,7 @@ $oDocument = $oDocumentModel->getDocument(0, $this->grant->manager); $oDocument->setDocument($document_srl); + $oDocument->add('module_srl', $this->module_srl); // 글을 수정하려고 할 경우 권한이 없는 경우 비밀번호 입력화면으로 if($oDocument->isExists()&&!$oDocument->isGranted()) return $this->setTemplateFile('input_password_form'); diff --git a/modules/board/skins/xe_board/comment.html b/modules/board/skins/xe_board/comment.html index 78a4851e1..5898b7d02 100644 --- a/modules/board/skins/xe_board/comment.html +++ b/modules/board/skins/xe_board/comment.html @@ -56,7 +56,7 @@
{$lang->msg_is_secret}
-
+
diff --git a/modules/board/skins/xe_board/comment_form.html b/modules/board/skins/xe_board/comment_form.html index 93c45bc83..4714b2da9 100644 --- a/modules/board/skins/xe_board/comment_form.html +++ b/modules/board/skins/xe_board/comment_form.html @@ -57,8 +57,8 @@
- - + +
diff --git a/modules/board/skins/xe_board/css/black.css b/modules/board/skins/xe_board/css/black.css index e4ee52587..90d5907ea 100644 --- a/modules/board/skins/xe_board/css/black.css +++ b/modules/board/skins/xe_board/css/black.css @@ -1,5 +1,7 @@ @charset "utf-8"; +.viewDocument { color:#888; } + /* board Title */ .boardHeader h3 { margin:0; border-bottom:3px solid #fe3614; } @@ -83,6 +85,7 @@ .boardRead .fileAttached ul li a { color:#AAAAAA; } .replyBox .fileAttached { border:1px solid #888888; background-color:transparent; padding:5px; margin-top:10px; overflow:hidden;} .replyBox .fileAttached ul li a { color:#AAAAAA; } +.boardWrite * { color:#888888; } .boardWrite dl.option dd * { color:#888888; } .replyBox .replyItem { background-color:transparent; border-bottom:1px dotted #888888; } .replyBox .date { color:#888888; } @@ -102,12 +105,13 @@ .smallBox .messageBox { background-color:#444444; border-bottom:1px solid #888888; color:#AAAAAA;} .smallBox .header { background:transparent !important; } + /* button */ -a.button, span.button, del.button, -a.button span, span.button button, span.button input, del.button span{ - background-image:url(../images/black/form_buttons.png); - _background-image:url(../images/black/form_buttons.gif); -} +a.button, +a.button span, +span.button, +span.button button, +span.button input { background-image:url(../../../../../common/tpl/images/buttonBlack.gif); color:#fff;} del.button span, diff --git a/modules/board/skins/xe_board/css/common.css b/modules/board/skins/xe_board/css/common.css index bfab8eb5d..f6d79e9ba 100644 --- a/modules/board/skins/xe_board/css/common.css +++ b/modules/board/skins/xe_board/css/common.css @@ -361,7 +361,7 @@ html:not([lang*=""]) .boardList th { height:33px; } .pagination.c2 a.next span{ position:absolute; width:0; height:0; overflow:hidden; visibility:hidden;} /* 게시물의 팝업 메뉴 */ -.comment_popup_menu, .document_popup_menu { text-align:right; background:none; background:url(../images/common/document_menu.gif) no-repeat right top; padding:0 15px 0 0; height:18px; } +.comment_popup_menu, .document_popup_menu { text-align:right; background:none; background:url(../images/common/document_menu.gif) no-repeat right top; padding:0 15px 0 0; height:18px; clear:both;} .comment_popup_menu a, .document_popup_menu a { cursor:pointer; display:inline; color:#555555; text-decoration:none; } /* tag list */ @@ -379,7 +379,7 @@ html:not([lang*=""]) .boardList th { height:33px; } /* 회원 서명 및 프로필 이미지 출력 */ -.memberSignature { border:1px solid #e0e1db; padding:10px; margin-top:20px;} +.memberSignature { border:1px solid #e0e1db; padding:10px; margin-top:20px; clear:both;} .memberSignature .profile { float:left; margin-right:20px; } .commentProfileImage { margin:0 10px 10px 0; float:left; } .replyBox .xe_content { display:inline; } diff --git a/modules/board/skins/xe_board/delete_comment_form.html b/modules/board/skins/xe_board/delete_comment_form.html index d2d0ba4e2..7946ba4dc 100644 --- a/modules/board/skins/xe_board/delete_comment_form.html +++ b/modules/board/skins/xe_board/delete_comment_form.html @@ -14,8 +14,8 @@
- - {$lang->cmd_cancel} + + {$lang->cmd_cancel}
diff --git a/modules/board/skins/xe_board/delete_form.html b/modules/board/skins/xe_board/delete_form.html index 1ca638c93..0cd4ebe26 100644 --- a/modules/board/skins/xe_board/delete_form.html +++ b/modules/board/skins/xe_board/delete_form.html @@ -13,8 +13,8 @@
- - {$lang->cmd_cancel} + + {$lang->cmd_cancel}
diff --git a/modules/board/skins/xe_board/delete_trackback_form.html b/modules/board/skins/xe_board/delete_trackback_form.html index f907b6793..0cf4eabbd 100644 --- a/modules/board/skins/xe_board/delete_trackback_form.html +++ b/modules/board/skins/xe_board/delete_trackback_form.html @@ -14,8 +14,8 @@
- - {$lang->cmd_cancel} + + {$lang->cmd_cancel}
diff --git a/modules/board/skins/xe_board/header.html b/modules/board/skins/xe_board/header.html index 11ee74ff7..bac12966b 100644 --- a/modules/board/skins/xe_board/header.html +++ b/modules/board/skins/xe_board/header.html @@ -12,7 +12,9 @@ - + + + {@ $btn_class = "black" } diff --git a/modules/board/skins/xe_board/input_password_form.html b/modules/board/skins/xe_board/input_password_form.html index 0f935eb92..5781b8ead 100644 --- a/modules/board/skins/xe_board/input_password_form.html +++ b/modules/board/skins/xe_board/input_password_form.html @@ -15,8 +15,8 @@
- - {$lang->cmd_cancel} + + {$lang->cmd_cancel}
diff --git a/modules/board/skins/xe_board/list.html b/modules/board/skins/xe_board/list.html index 56384a5f9..fa5df0cf3 100644 --- a/modules/board/skins/xe_board/list.html +++ b/modules/board/skins/xe_board/list.html @@ -29,15 +29,15 @@
- {$lang->cmd_manage_document} + {$lang->cmd_manage_document} - {$lang->cmd_list} + {$lang->cmd_list}
- {$lang->cmd_write} + {$lang->cmd_write}
@@ -69,10 +69,10 @@ - {$lang->cmd_search_next} + {$lang->cmd_search_next} - {$lang->cmd_search} - {$lang->cmd_cancel} + {$lang->cmd_search} + {$lang->cmd_cancel} diff --git a/modules/board/skins/xe_board/message.html b/modules/board/skins/xe_board/message.html index 3fcd79b35..79f207573 100644 --- a/modules/board/skins/xe_board/message.html +++ b/modules/board/skins/xe_board/message.html @@ -6,9 +6,9 @@
- {$lang->cmd_login} + {$lang->cmd_login} - {$lang->cmd_back} + {$lang->cmd_back}
diff --git a/modules/board/skins/xe_board/style.webzine.html b/modules/board/skins/xe_board/style.webzine.html index 5e1266afc..5eacee08d 100644 --- a/modules/board/skins/xe_board/style.webzine.html +++ b/modules/board/skins/xe_board/style.webzine.html @@ -130,7 +130,7 @@ - {$document->getSummary(120)} + {$document->getSummary(240)}   diff --git a/modules/board/skins/xe_board/tag_list.html b/modules/board/skins/xe_board/tag_list.html index 5773170c5..cd8fbd5e6 100644 --- a/modules/board/skins/xe_board/tag_list.html +++ b/modules/board/skins/xe_board/tag_list.html @@ -34,7 +34,7 @@
- {$lang->cmd_back} + {$lang->cmd_back}
diff --git a/modules/board/skins/xe_board/view_document.html b/modules/board/skins/xe_board/view_document.html index 09b9b19ac..8bb18a59a 100644 --- a/modules/board/skins/xe_board/view_document.html +++ b/modules/board/skins/xe_board/view_document.html @@ -94,7 +94,7 @@
{$lang->msg_is_secret}
-
+
@@ -145,11 +145,11 @@
- {$lang->cmd_list} + {$lang->cmd_list} - {$lang->cmd_modify} - {$lang->cmd_delete} + {$lang->cmd_modify} + {$lang->cmd_delete}
@@ -200,7 +200,7 @@
- +
diff --git a/modules/board/skins/xe_board/write_form.html b/modules/board/skins/xe_board/write_form.html index 1aa93f738..54836f7cb 100644 --- a/modules/board/skins/xe_board/write_form.html +++ b/modules/board/skins/xe_board/write_form.html @@ -106,15 +106,15 @@
- - + +
- - - + + +
diff --git a/modules/board/skins/xe_guestbook/css/black.css b/modules/board/skins/xe_guestbook/css/black.css index 983841e41..d3c78e90a 100644 --- a/modules/board/skins/xe_guestbook/css/black.css +++ b/modules/board/skins/xe_guestbook/css/black.css @@ -89,18 +89,11 @@ .boardRead .readBody { color:#BBBBBB; } /* button */ -a.button, span.button, del.button, -a.button span, span.button button, span.button input, del.button span{ - background-image:url(../images/black/form_buttons.png); - _background-image:url(../images/black/form_buttons.gif); -} - - -del.button span, -a.button, span.button, del.button, -a.button span, span.button button, span.button input, del.button span{ - color:#FFFFFF; -} +a.button, +a.button span, +span.button, +span.button button, +span.button input { background-image:url(../../../../../common/tpl/images/buttonBlack.gif); color:#fff;} .trackbackBox {border:1px solid #444444; } @@ -110,3 +103,13 @@ a.button span, span.button button, span.button input, del.button span{ .boardWrite { border:1px solid #555555; } .smallBox { border:1px solid #555555; } .smallBox .header { background-color:transparent; } + +/* Pagination*/ +.pagination a, .pagination strong{ color:#888; } +.pagination a:hover, .pagination a:active, .pagination a:focus{ background-color:#555 !important; } +.pagination strong{ color:#ff6600 !important;} +.pagination.a1 a, .pagination.a1 strong{ background:#000; border-left:1px solid #555; border-right:1px solid #666; color:#888;} +.pagination.a1 a.prev, .pagination.a1 a.prevEnd, .pagination.a1 a.next, .pagination.a1 a.nextEnd { background:#000; } + +.pagination.a2 a, .pagination.a2 strong{ background:#000; border-left:1px solid #555; border-right:1px solid #666; color:#888; } +.pagination.a2 a.prev, .pagination.a2 a.prevEnd, .pagination.a2 a.next, .pagination.a2 a.nextEnd { background:#000; } \ No newline at end of file diff --git a/modules/communication/communication.admin.controller.php b/modules/communication/communication.admin.controller.php index 4ef9c0c43..9f89afd59 100644 --- a/modules/communication/communication.admin.controller.php +++ b/modules/communication/communication.admin.controller.php @@ -18,7 +18,7 @@ **/ function procCommunicationAdminInsertConfig() { // 기본 정보를 받음 - $args = Context::gets('skin','colorset','editor_skin'); + $args = Context::gets('skin','colorset','editor_skin','editor_colorset'); if(!$args->skin) $args->skin = "default"; if(!$args->colorset) $args->colorset = "white"; diff --git a/modules/communication/communication.view.php b/modules/communication/communication.view.php index 185d0d431..23eaf8d6b 100644 --- a/modules/communication/communication.view.php +++ b/modules/communication/communication.view.php @@ -125,6 +125,7 @@ $option->disable_html = true; $option->height = 300; $option->skin = $this->communication_config->editor_skin; + $option->colorset = $this->communication_config->editor_colorset; $editor = $oEditorModel->getEditor($logged_info->member_srl, $option); Context::set('editor', $editor); diff --git a/modules/communication/skins/default/css/black.css b/modules/communication/skins/default/css/black.css index 62be2868f..1be682e2b 100644 --- a/modules/communication/skins/default/css/black.css +++ b/modules/communication/skins/default/css/black.css @@ -43,8 +43,15 @@ table.list tr td.title a { color:#BBBBBB !important; } table.list tr td { border-top:1px solid #888888 !important; } table .list tr td, table.list tr td * { color:#BBBBBB; } -.pagenation a { border:none !important; } -.pagenation strong { border:none !important; color:#EEEEEE; } +/* Pagination*/ +.pagination a, .pagination strong{ color:#888; } +.pagination a:hover, .pagination a:active, .pagination a:focus{ background-color:#555 !important; } +.pagination strong{ color:#ff6600 !important;} +.pagination.a1 a, .pagination.a1 strong{ background:#000; border-left:1px solid #555; border-right:1px solid #666; color:#888;} +.pagination.a1 a.prev, .pagination.a1 a.prevEnd, .pagination.a1 a.next, .pagination.a1 a.nextEnd { background:#000; } + +.pagination.a2 a, .pagination.a2 strong{ background:#000; border-left:1px solid #555; border-right:1px solid #666; color:#888; } +.pagination.a2 a.prev, .pagination.a2 a.prevEnd, .pagination.a2 a.next, .pagination.a2 a.nextEnd { background:#000; } .memberSmallBox .leftHeaderType th { background:transparent; color:#AAAAAA; } .memberSmallBox .leftHeaderType td { color:#AAAAAA; } diff --git a/modules/communication/tpl/index.html b/modules/communication/tpl/index.html index 3d96f34a9..c1bd08727 100644 --- a/modules/communication/tpl/index.html +++ b/modules/communication/tpl/index.html @@ -1,4 +1,5 @@ +

{$lang->communication} {$lang->cmd_management}

@@ -11,11 +12,16 @@
{$lang->editor_skin}
- + + diff --git a/modules/document/document.item.php b/modules/document/document.item.php index fe98622a9..28120daca 100644 --- a/modules/document/document.item.php +++ b/modules/document/document.item.php @@ -283,8 +283,11 @@ } function getSummary($str_size = 50) { - // 먼저 태그들을 제거함 - $content = preg_replace('!<([^>]*?)>!is','', $this->getContent(false,false)); + // 영문이나 숫자가 연결되어서 20개 이상으로 연결시에 강제 띄움 시도 + $content = preg_replace('/([a-z0-9\-\+:\/\.\~,\|\!\@\#\$\%\^\&\*\(\)\_\+]){20,}/is',"$0 ",$this->getContent(false,false)); + + // 태그 제거 + $content = preg_replace('!<([^>]*?)>!is','', $content); // < , > , " 를 치환 $content = str_replace(array('<','>','"',' '), array('<','>','"',' '), $content); diff --git a/modules/editor/components/multimedia_link/tpl/popup.js b/modules/editor/components/multimedia_link/tpl/popup.js index 4b71dc19e..9184b3a8c 100644 --- a/modules/editor/components/multimedia_link/tpl/popup.js +++ b/modules/editor/components/multimedia_link/tpl/popup.js @@ -28,7 +28,7 @@ function insertMultimedia(obj) { var url = xGetElementById("multimedia_url").value; url = url.replace(request_uri,''); - url = encodeURI(url); +// url = encodeURI(url); var caption = xGetElementById("multimedia_caption").value; diff --git a/modules/editor/skins/default/css/black.css b/modules/editor/skins/default/css/black.css index 2cc38f6b6..d4584ed53 100644 --- a/modules/editor/skins/default/css/black.css +++ b/modules/editor/skins/default/css/black.css @@ -43,4 +43,5 @@ /* TextEditor */ .xeTextEditor {padding:10px; overflow:hidden; margin:0 0 10px 0;} -.xeTextEditor textarea { display:block; margin:0; padding:2px; color:#fff;} +.xeTextEditor textarea { display:block; margin:0; padding:2px; } +.xeTextEditor.black textarea { color:#fff; background-color:#000;} diff --git a/modules/editor/skins/default/css/editor.css b/modules/editor/skins/default/css/editor.css index 3d3ca8944..614e8afdb 100644 --- a/modules/editor/skins/default/css/editor.css +++ b/modules/editor/skins/default/css/editor.css @@ -228,11 +228,14 @@ a.skipToolBox:active span{ width:auto; height:auto; padding:3px 15px; font-weigh .xeEditor .fileUploader{ clear:both; position:relative; padding-top:5px; *zoom:1; *margin-bottom:5px;} .commentEditor .xeEditor .fileUploader{ border-top:1px solid;} .xeEditor .fileUploader:after{ content:""; display:block; clear:both;} -.xeEditor .fileUploader .preview{ float:left; width:64px; height:64px; border:1px solid; padding:2px; margin:0 10px 5px 0;} +.xeEditor .fileUploader .preview{ float:left; width:64px; height:64px; border:1px solid; padding:2px; margin:0 10px 5px 10px;} +.xeEditor .fileUploader .preview.black { background-color:#000; border:1px solid #666;} .xeEditor .fileUploader .preview img{ display:block; width:64px; height:64px;} .xeEditor .fileUploader .fileListArea{ float:left; width:260px; margin:0 10px 5px 0;} .xeEditor .fileUploader .fileListArea select{ width:100%; height:70px; overflow:auto;} .xeEditor .fileUploader .fileListArea select option{ font-size:11px;} +.xeEditor .fileUploader .fileListArea.black select { background-color:#000; border:1px solid #666;} +.xeEditor .fileUploader .fileListArea.black select option { color:#aaa; } .xeEditor .fileUploader .fileUploadControl{ clear:right;} .xeEditor .fileUploader .fileUploadControl .button{ margin-bottom:5px;} .xeEditor .fileUploader .file_attach_info{ clear:right; margin:5px 0;} diff --git a/modules/editor/skins/default/editor.html b/modules/editor/skins/default/editor.html index ecd246aff..a94713a68 100644 --- a/modules/editor/skins/default/editor.html +++ b/modules/editor/skins/default/editor.html @@ -4,12 +4,13 @@ + {@ $editor_class = "black" } -
+
-
-
+
+
- - - + + +
{$upload_status}
diff --git a/modules/editor/skins/xquared/LICENSE b/modules/editor/skins/xquared/LICENSE index b47e925fd..7a9ef2b6d 100644 --- a/modules/editor/skins/xquared/LICENSE +++ b/modules/editor/skins/xquared/LICENSE @@ -2,13 +2,16 @@ Xquared is copyrighted free software by Alan Kang . You can redistribute and/or modify it under the terms of the LGPL. (http://www.gnu.org/licenses/lgpl.html) -Following is a list of dependencies: - * prototype javascript framework - * Homepage: http://prototypejs.org - * License: http://dev.rubyonrails.org/browser/spinoffs/prototype/trunk/LICENSE?format=raw +While Xquared itself has no dependencies with external libraries, you need following libraries in order to build Xquared from source code: * jsspec * Homepage: http://jania.pe.kr/aw/moin.cgi/JSSpec * License: http://www.gnu.org/licenses/lgpl.html * yui-compressor * Homepage: http://developer.yahoo.com/yui/compressor/ * License: http://developer.yahoo.com/yui/license.html + * selenium-core + * Homepage: http://selenium-core.openqa.org/ + * License: http://selenium-core.openqa.org/license.jsp + * jsdoc_toolkit + * Homepage: http://code.google.com/p/jsdoc-toolkit/ + * License: http://www.opensource.org/licenses/mit-license.php \ No newline at end of file diff --git a/modules/editor/skins/xquared/README b/modules/editor/skins/xquared/README index dcdd58778..ff702cde2 100644 --- a/modules/editor/skins/xquared/README +++ b/modules/editor/skins/xquared/README @@ -4,6 +4,6 @@ editor module aim to support major modern web browsers. This software is licensed under the terms you may find in the file named "LICENSE" in this directory. -For more information, see http://labs.openmaru.com/projects/xquared/ +For more information, see http://xquared.springbook.playmaru.net/ Thanks for using Xquared. \ No newline at end of file diff --git a/modules/editor/skins/xquared/editor.html b/modules/editor/skins/xquared/editor.html index 674d9936d..5aa6e3ef2 100644 --- a/modules/editor/skins/xquared/editor.html +++ b/modules/editor/skins/xquared/editor.html @@ -1,14 +1,14 @@ - - + + - - + + @@ -40,7 +40,7 @@
-
+ diff --git a/modules/editor/skins/xquared/images/content/placeholder.gif b/modules/editor/skins/xquared/images/content/placeholder.gif new file mode 100644 index 000000000..af4601238 Binary files /dev/null and b/modules/editor/skins/xquared/images/content/placeholder.gif differ diff --git a/modules/editor/skins/xquared/images/toolbar/iframe.gif b/modules/editor/skins/xquared/images/toolbar/iframe.gif new file mode 100644 index 000000000..6f351c37a Binary files /dev/null and b/modules/editor/skins/xquared/images/toolbar/iframe.gif differ diff --git a/modules/editor/skins/xquared/images/toolbar/movie.gif b/modules/editor/skins/xquared/images/toolbar/movie.gif new file mode 100644 index 000000000..43cbafb50 Binary files /dev/null and b/modules/editor/skins/xquared/images/toolbar/movie.gif differ diff --git a/modules/editor/skins/xquared/images/toolbar/removeLink.gif b/modules/editor/skins/xquared/images/toolbar/removeLink.gif new file mode 100644 index 000000000..0049162b7 Binary files /dev/null and b/modules/editor/skins/xquared/images/toolbar/removeLink.gif differ diff --git a/modules/editor/skins/xquared/javascripts/Browser.js b/modules/editor/skins/xquared/javascripts/Browser.js new file mode 100644 index 000000000..c9035b82e --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/Browser.js @@ -0,0 +1,110 @@ +/** + * @namespace Contains browser detection codes + * + * @requires Xquared.js + */ +xq.Browser = new function() { + // By Rendering Engines + + /** + * True if rendering engine is Trident + * @type boolean + */ + this.isTrident = navigator.appName === "Microsoft Internet Explorer", + + /** + * True if rendering engine is Webkit + * @type boolean + */ + this.isWebkit = navigator.userAgent.indexOf('AppleWebKit/') > -1, + + /** + * True if rendering engine is Gecko + * @type boolean + */ + this.isGecko = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1, + + /** + * True if rendering engine is KHTML + * @type boolean + */ + this.isKHTML = navigator.userAgent.indexOf('KHTML') !== -1, + + /** + * True if rendering engine is Presto + * @type boolean + */ + this.isPresto = navigator.appName === "Opera", + + + + // By Platforms + /** + * True if platform is Mac + * @type boolean + */ + this.isMac = navigator.userAgent.indexOf("Macintosh") !== -1, + + /** + * True if platform is Ubuntu Linux + * @type boolean + */ + this.isUbuntu = navigator.userAgent.indexOf('Ubuntu') !== -1, + + /** + * True if platform is Windows + * @type boolean + */ + this.isWin = navigator.userAgent.indexOf('Windows') !== -1, + + + + // By Browsers + /** + * True if browser is Internet Explorer + * @type boolean + */ + this.isIE = navigator.appName === "Microsoft Internet Explorer", + + /** + * True if browser is Internet Explorer 6 + * @type boolean + */ + this.isIE6 = navigator.userAgent.indexOf('MSIE 6') !== -1, + + /** + * True if browser is Internet Explorer 7 + * @type boolean + */ + this.isIE7 = navigator.userAgent.indexOf('MSIE 7') !== -1, + + /** + * True if browser is Internet Explorer 8 + * @type boolean + */ + this.isIE8 = navigator.userAgent.indexOf('MSIE 8') !== -1, + + /** + * True if browser is Firefox + * @type boolean + */ + this.isFF = navigator.userAgent.indexOf('Firefox') !== -1, + + /** + * True if browser is Firefox 2 + * @type boolean + */ + this.isFF2 = navigator.userAgent.indexOf('Firefox/2') !== -1, + + /** + * True if browser is Firefox 3 + * @type boolean + */ + this.isFF3 = navigator.userAgent.indexOf('Firefox/3') !== -1, + + /** + * True if browser is Safari + * @type boolean + */ + this.isSafari = navigator.userAgent.indexOf('Safari') !== -1 +}; \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/DomTree.js b/modules/editor/skins/xquared/javascripts/DomTree.js new file mode 100644 index 000000000..488902f74 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/DomTree.js @@ -0,0 +1,329 @@ +/** + * @requires Xquared.js + */ +xq.DomTree = xq.Class(/** @lends xq.DomTree.prototype */{ + /** + * Provides various tree operations. + * + * TODO: Add specs + * + * @constructs + */ + initialize: function() { + xq.addToFinalizeQueue(this); + + this._blockTags = ["DIV", "DD", "LI", "ADDRESS", "CAPTION", "DT", "H1", "H2", "H3", "H4", "H5", "H6", "HR", "P", "BODY", "BLOCKQUOTE", "PRE", "PARAM", "DL", "OL", "UL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; + this._blockContainerTags = ["DIV", "DD", "LI", "BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; + this._listContainerTags = ["OL", "UL", "DL"]; + this._tableCellTags = ["TH", "TD"]; + this._blockOnlyContainerTags = ["BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR"]; + this._atomicTags = ["IMG", "OBJECT", "PARAM", "BR", "HR"]; + }, + + getBlockTags: function() { + return this._blockTags; + }, + + /** + * Find common ancestor(parent) and his immediate children(left and right).
+ *
+ * A --- B -+- C -+- D -+- E
+ * |
+ * +- F -+- G
+ *
+ * For example:
+ * > findCommonAncestorAndImmediateChildrenOf("E", "G")
+ *
+ * will return
+ *
+ * > {parent:"B", left:"C", right:"F"} + */ + findCommonAncestorAndImmediateChildrenOf: function(left, right) { + if(left.parentNode === right.parentNode) { + return { + left:left, + right:right, + parent:left.parentNode + }; + } else { + var parentsOfLeft = this.collectParentsOf(left, true); + var parentsOfRight = this.collectParentsOf(right, true); + var ca = this.getCommonAncestor(parentsOfLeft, parentsOfRight); + + var leftAncestor = parentsOfLeft.find(function(node) {return node.parentNode === ca}); + var rightAncestor = parentsOfRight.find(function(node) {return node.parentNode === ca}); + + return { + left:leftAncestor, + right:rightAncestor, + parent:ca + }; + } + }, + + /** + * Find leaves at edge.
+ *
+ * A --- B -+- C -+- D -+- E
+ * |
+ * +- F -+- G
+ *
+ * For example:
+ * > getLeavesAtEdge("A")
+ *
+ * will return
+ *
+ * > ["E", "G"] + */ + getLeavesAtEdge: function(element) { + if(!element.hasChildNodes()) return [null, null]; + + var findLeft = function(el) { + for (var i = 0; i < el.childNodes.length; i++) { + if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findLeft(el.childNodes[i]); + } + return el; + }.bind(this); + + var findRight=function(el) { + for (var i = el.childNodes.length; i--;) { + if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findRight(el.childNodes[i]); + } + return el; + }.bind(this); + + var left = findLeft(element); + var right = findRight(element); + + return [left === element ? null : left, right === element ? null : right]; + }, + + getCommonAncestor: function(parents1, parents2) { + for(var i = 0; i < parents1.length; i++) { + for(var j = 0; j < parents2.length; j++) { + if(parents1[i] === parents2[j]) return parents1[i]; + } + } + }, + + collectParentsOf: function(node, includeSelf, exitCondition) { + var parents = []; + if(includeSelf) parents.push(node); + + while((node = node.parentNode) && (node.nodeName !== "HTML") && !(typeof exitCondition === "function" && exitCondition(node))) parents.push(node); + return parents; + }, + + isDescendantOf: function(parent, child) { + if(parent.length > 0) { + for(var i = 0; i < parent.length; i++) { + if(this.isDescendantOf(parent[i], child)) return true; + } + return false; + } + + if(parent === child) return false; + + while (child = child.parentNode) + if (child === parent) return true; + return false; + }, + + /** + * Perform tree walking (foreward) + */ + walkForward: function(node) { + var target = node.firstChild; + if(target) return target; + + // intentional assignment for micro performance turing + if(target = node.nextSibling) return target; + + while(node = node.parentNode) { + // intentional assignment for micro performance turing + if(target = node.nextSibling) return target; + } + + return null; + }, + + /** + * Perform tree walking (backward) + */ + walkBackward: function(node) { + if(node.previousSibling) { + node = node.previousSibling; + while(node.hasChildNodes()) {node = node.lastChild;} + return node; + } + + return node.parentNode; + }, + + /** + * Perform tree walking (to next siblings) + */ + walkNext: function(node) {return node.nextSibling}, + + /** + * Perform tree walking (to next siblings) + */ + walkPrev: function(node) {return node.previousSibling}, + + /** + * Returns true if target is followed by start + */ + checkTargetForward: function(start, target) { + return this._check(start, this.walkForward, target); + }, + + /** + * Returns true if start is followed by target + */ + checkTargetBackward: function(start, target) { + return this._check(start, this.walkBackward, target); + }, + + findForward: function(start, condition, exitCondition) { + return this._find(start, this.walkForward, condition, exitCondition); + }, + + findBackward: function(start, condition, exitCondition) { + return this._find(start, this.walkBackward, condition, exitCondition); + }, + + _check: function(start, direction, target) { + if(start === target) return false; + + while(start = direction(start)) { + if(start === target) return true; + } + return false; + }, + + _find: function(start, direction, condition, exitCondition) { + while(start = direction(start)) { + if(exitCondition && exitCondition(start)) return null; + if(condition(start)) return start; + } + return null; + }, + + /** + * Walks Forward through DOM tree from start to end, and collects all nodes that matches with a filter. + * If no filter provided, it just collects all nodes. + * + * @param {Element} start Starting element. + * @param {Element} end Ending element. + * @param {Function} filter A filter function. + */ + collectNodesBetween: function(start, end, filter) { + if(start === end) return [start, end].findAll(filter || function() {return true}); + + var nodes = this.collectForward(start, function(node) {return node === end}, filter); + if( + start !== end && + typeof filter === "function" && + filter(end) + ) nodes.push(end); + + return nodes; + }, + + collectForward: function(start, exitCondition, filter) { + return this.collect(start, this.walkForward, exitCondition, filter); + }, + + collectBackward: function(start, exitCondition, filter) { + return this.collect(start, this.walkBackward, exitCondition, filter); + }, + + collectNext: function(start, exitCondition, filter) { + return this.collect(start, this.walkNext, exitCondition, filter); + }, + + collectPrev: function(start, exitCondition, filter) { + return this.collect(start, this.walkPrev, exitCondition, filter); + }, + + collect: function(start, next, exitCondition, filter) { + var nodes = [start]; + + while(true) { + start = next(start); + if( + (start === null) || + (typeof exitCondition === "function" && exitCondition(start)) + ) break; + + nodes.push(start); + } + + return (typeof filter === "function") ? nodes.findAll(filter) : nodes; + }, + + hasBlocks: function(element) { + var nodes = element.childNodes; + for(var i = 0; i < nodes.length; i++) { + if(this.isBlock(nodes[i])) return true; + } + return false; + }, + + hasMixedContents: function(element) { + if(!this.isBlock(element)) return false; + if(!this.isBlockContainer(element)) return false; + + var hasTextOrInline = false; + var hasBlock = false; + for(var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if(!hasTextOrInline && this.isTextOrInlineNode(node)) hasTextOrInline = true; + if(!hasBlock && this.isBlock(node)) hasBlock = true; + + if(hasTextOrInline && hasBlock) break; + } + if(!hasTextOrInline || !hasBlock) return false; + + return true; + }, + + isBlockOnlyContainer: function(element) { + if(!element) return false; + return this._blockOnlyContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isTableCell: function(element) { + if(!element) return false; + return this._tableCellTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isBlockContainer: function(element) { + if(!element) return false; + return this._blockContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isHeading: function(element) { + if(!element) return false; + return (typeof element === 'string' ? element : element.nodeName).match(/H\d/); + }, + + isBlock: function(element) { + if(!element) return false; + return this._blockTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isAtomic: function(element) { + if(!element) return false; + return this._atomicTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isListContainer: function(element) { + if(!element) return false; + return this._listContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isTextOrInlineNode: function(node) { + return node && (node.nodeType === 3 || !this.isBlock(node)); + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/EditHistory.js b/modules/editor/skins/xquared/javascripts/EditHistory.js new file mode 100644 index 000000000..c0e79fea0 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/EditHistory.js @@ -0,0 +1,151 @@ +/** + * @requires Xquared.js + * @requires rdom/Factory.js + */ +xq.EditHistory = xq.Class(/** @lends xq.EditHistory.prototype */{ + /** + * Manages editing history and performs UNDO/REDO. + * + * @constructs + * @param {xq.rdom.Base} rdom Base instance + * @param {Number} [max=100] maximum UNDO buffer size. + */ + initialize: function(rdom, max) { + xq.addToFinalizeQueue(this); + if (!rdom) throw "IllegalArgumentException"; + + this.disabled = false; + this.max = max || 100; + this.rdom = rdom; + this.index = -1; + this.queue = []; + + this.lastModified = Date.get(); + }, + getLastModifiedDate: function() { + return this.lastModified; + }, + isUndoable: function() { + return this.queue.length > 0 && this.index > 0; + }, + isRedoable: function() { + return this.queue.length > 0 && this.index < this.queue.length - 1; + }, + disable: function() { + this.disabled = true; + }, + enable: function() { + this.disabled = false; + }, + undo: function() { + this.pushContent(); + + if (this.isUndoable()) { + this.index--; + this.popContent(); + return true; + } else { + return false; + } + }, + redo: function() { + if (this.isRedoable()) { + this.index++; + this.popContent(); + return true; + } else { + return false; + } + }, + onCommand: function() { + this.lastModified = Date.get(); + if(this.disabled) return false; + + return this.pushContent(); + }, + onEvent: function(event) { + this.lastModified = Date.get(); + if(this.disabled) return false; + + var arrowKeys = [33,34,35,36,37,39]; + // @WORKAROUND: Mac에서 화살표 up/down 누를 때 pushContent 하면 캐럿이 튄다 + if(!xq.Browser.isMac) arrowKeys.push(38,40); + + // ignore some event types + if(['blur', 'mouseup'].indexOf(event.type) !== -1) return false; + + // ignore normal keys + if('keydown' === event.type && !(event.ctrlKey || event.metaKey)) return false; + if(['keydown', 'keyup', 'keypress'].indexOf(event.type) !== -1 && !event.ctrlKey && !event.altKey && !event.metaKey && arrowKeys.indexOf(event.keyCode) === -1) return false; + if(['keydown', 'keyup', 'keypress'].indexOf(event.type) !== -1 && (event.ctrlKey || event.metaKey) && [89,90].indexOf(event.keyCode) !== -1) return false; + + // ignore ctrl/shift/alt/meta keys + if([16,17,18,224].indexOf(event.keyCode) !== -1) return false; + + return this.pushContent(); + }, + popContent: function() { + this.lastModified = Date.get(); + var entry = this.queue[this.index]; + if (entry.caret > 0) { + var html=entry.html.substring(0, entry.caret) + '' + entry.html.substring(entry.caret); + this.rdom.getRoot().innerHTML = html; + } else { + this.rdom.getRoot().innerHTML = entry.html; + } + this.restoreCaret(); + }, + pushContent: function(ignoreCaret) { + if(xq.Browser.isTrident && !ignoreCaret && !this.rdom.hasFocus()) return false; + if(!this.rdom.getCurrentElement()) return false; + + var html = this.rdom.getRoot().innerHTML; + if(html === (this.queue[this.index] ? this.queue[this.index].html : null)) return false; + + var caret = ignoreCaret ? -1 : this.saveCaret(); + + if(this.queue.length >= this.max) { + this.queue.shift(); + } else { + this.index++; + } + + this.queue.splice(this.index, this.queue.length - this.index, {html:html, caret:caret}); + return true; + }, + clear: function() { + this.index = -1; + this.queue = []; + this.pushContent(true); + }, + saveCaret: function() { + if(this.rdom.hasSelection()) return null; + + var bookmark = this.rdom.saveSelection(); + var marker = this.rdom.pushMarker(); + + var str = xq.Browser.isTrident ? ' + * Note that the validation will be performed regardless of this value when you switching edit mode. + * @type boolean + */ + this.config.noValidationInSourceEditMode = false; + + /** + * Automatically hooks onsubmit event. + * @type boolean + */ + this.config.automaticallyHookSubmitEvent = true; + + /** + * Set of whitelist(tag name and attributes) for use in validator + * @type Object + */ + this.config.whitelist = xq.predefinedWhitelist; + + /** + * Specifies a value of ID attribute for WYSIWYG document's body + * @type String + */ + this.config.bodyId = ""; + + /** + * Specifies a value of CLASS attribute for WYSIWYG document's body + * @type String + */ + this.config.bodyClass = "xed"; + + /** + * Plugins + * @type Object + */ + this.config.plugins = {}; + + /** + * Shortcuts + * @type Object + */ + this.config.shortcuts = {}; + + /** + * Autocorrections + * @type Object + */ + this.config.autocorrections = {}; + + /** + * Autocompletions + * @type Object + */ + this.config.autocompletions = {}; + + /** + * Template processors + * @type Object + */ + this.config.templateProcessors = {}; + + /** + * Context menu handlers + * @type Object + */ + this.config.contextMenuHandlers = {}; + + /** + * Original content element + * @type Element + */ + this.contentElement = contentElement; + + /** + * Owner document of content element + * @type Document + */ + this.doc = this.contentElement.ownerDocument; + + /** + * Body of content element + * @type Element + */ + this.body = this.doc.body; + + /** + * False or 'source' means source editing mode, true or 'wysiwyg' means WYSIWYG editing mode. + * @type Object + */ + this.currentEditMode = ''; + + /** + * Timer + * @type xq.Timer + */ + this.timer = new xq.Timer(100); + + /** + * Base instance + * @type xq.rdom.Base + */ + this.rdom = xq.rdom.Base.createInstance(); + + /** + * Base instance + * @type xq.validator.Base + */ + this.validator = null; + + /** + * Outmost wrapper div + * @type Element + */ + this.outmostWrapper = null; + + /** + * Source editor container + * @type Element + */ + this.sourceEditorDiv = null; + + /** + * Source editor textarea + * @type Element + */ + this.sourceEditorTextarea = null; + + /** + * WYSIWYG editor container + * @type Element + */ + this.wysiwygEditorDiv = null; + + /** + * Outer frame + * @type IFrame + */ + this.outerFrame = null; + + /** + * Design mode iframe + * @type IFrame + */ + this.editorFrame = null; + + this.toolbarContainer = toolbarContainer; + + /** + * Toolbar container + * @type Element + */ + this.toolbar = null; + + /** + * Undo/redo manager + * @type xq.EditHistory + */ + this.editHistory = null; + + /** + * Context menu container + * @type Element + */ + this.contextMenuContainer = null; + + /** + * Context menu items + * @type Array + */ + this.contextMenuItems = null; + + /** + * Platform dependent key event type + * @type String + */ + this.platformDepedentKeyEventType = (xq.Browser.isMac && xq.Browser.isGecko ? "keypress" : "keydown"); + + this.addShortcuts(this.getDefaultShortcuts()); + + this.addListener({ + onEditorCurrentContentChanged: function(xed) { + var curFocusElement = xed.rdom.getCurrentElement(); + if(!curFocusElement || curFocusElement.ownerDocument !== xed.rdom.getDoc()) { + return; + } + + if(xed.lastFocusElement !== curFocusElement) { + if(!xed.rdom.tree.isBlockOnlyContainer(xed.lastFocusElement) && xed.rdom.tree.isBlock(xed.lastFocusElement)) { + xed.rdom.removeTrailingWhitespace(xed.lastFocusElement); + } + xed._fireOnElementChanged(xed, xed.lastFocusElement, curFocusElement); + xed.lastFocusElement = curFocusElement; + } + + xed.toolbar.triggerUpdate(); + } + }); + }, + + finalize: function() { + for(var key in this.config.plugins) this.config.plugins[key].unload(); + }, + + + + ///////////////////////////////////////////// + // Configuration Management + + getDefaultShortcuts: function() { + if(xq.Browser.isMac) { + // Mac FF & Safari + return [ + {event:"Ctrl+Shift+SPACE", handler:"this.handleAutocompletion(); stop = true;"}, + {event:"SPACE", handler:"this.handleSpace()"}, + {event:"ENTER", handler:"this.handleEnter(false, false)"}, + {event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"}, + {event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"}, + {event:"TAB", handler:"this.handleTab()"}, + {event:"Shift+TAB", handler:"this.handleShiftTab()"}, + {event:"DELETE", handler:"this.handleDelete()"}, + {event:"BACKSPACE", handler:"this.handleBackspace()"}, + + {event:"Ctrl+B", handler:"this.handleStrongEmphasis()"}, + {event:"Meta+B", handler:"this.handleStrongEmphasis()"}, + {event:"Ctrl+I", handler:"this.handleEmphasis()"}, + {event:"Meta+I", handler:"this.handleEmphasis()"}, + {event:"Ctrl+U", handler:"this.handleUnderline()"}, + {event:"Meta+U", handler:"this.handleUnderline()"}, + {event:"Ctrl+K", handler:"this.handleStrike()"}, + {event:"Meta+K", handler:"this.handleStrike()"}, + {event:"Meta+Z", handler:"this.handleUndo()"}, + {event:"Meta+Shift+Z", handler:"this.handleRedo()"}, + {event:"Meta+Y", handler:"this.handleRedo()"} + ]; + } else if(xq.Browser.isUbuntu) { + // Ubunto FF + return [ + {event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"}, + {event:"SPACE", handler:"this.handleSpace()"}, + {event:"ENTER", handler:"this.handleEnter(false, false)"}, + {event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"}, + {event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"}, + {event:"TAB", handler:"this.handleTab()"}, + {event:"Shift+TAB", handler:"this.handleShiftTab()"}, + {event:"DELETE", handler:"this.handleDelete()"}, + {event:"BACKSPACE", handler:"this.handleBackspace()"}, + + {event:"Ctrl+B", handler:"this.handleStrongEmphasis()"}, + {event:"Ctrl+I", handler:"this.handleEmphasis()"}, + {event:"Ctrl+U", handler:"this.handleUnderline()"}, + {event:"Ctrl+K", handler:"this.handleStrike()"}, + {event:"Ctrl+Z", handler:"this.handleUndo()"}, + {event:"Ctrl+Shift+Z", handler:"this.handleRedo()"}, + {event:"Ctrl+Y", handler:"this.handleRedo()"} + ]; + } else { + // Win IE & FF + return [ + {event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"}, + {event:"SPACE", handler:"this.handleSpace()"}, + {event:"ENTER", handler:"this.handleEnter(false, false)"}, + {event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"}, + {event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"}, + {event:"TAB", handler:"this.handleTab()"}, + {event:"Shift+TAB", handler:"this.handleShiftTab()"}, + {event:"DELETE", handler:"this.handleDelete()"}, + {event:"BACKSPACE", handler:"this.handleBackspace()"}, + + {event:"Ctrl+B", handler:"this.handleStrongEmphasis()"}, + {event:"Ctrl+I", handler:"this.handleEmphasis()"}, + {event:"Ctrl+U", handler:"this.handleUnderline()"}, + {event:"Ctrl+K", handler:"this.handleStrike()"}, + {event:"Ctrl+Z", handler:"this.handleUndo()"}, + {event:"Ctrl+Shift+Z", handler:"this.handleRedo()"}, + {event:"Ctrl+Y", handler:"this.handleRedo()"} + ]; + } + }, + + /** + * Adds or replaces plugin. + * + * @param {String} id unique identifier + */ + addPlugin: function(id) { + // already added? + if(this.config.plugins[id]) return; + + // else + var clazz = xq.plugin[id + "Plugin"]; + if(!clazz) throw "Unknown plugin id: [" + id + "]"; + + var plugin = new clazz(); + this.config.plugins[id] = plugin; + plugin.load(this); + }, + + /** + * Adds several plugins at once. + * + * @param {Array} list of plugin ids. + */ + addPlugins: function(list) { + for(var i = 0; i < list.length; i++) { + this.addPlugin(list[i]); + } + }, + + /** + * Returns plugin matches with given identifier. + * + * @param {String} id unique identifier + */ + getPlugin: function(id) {return this.config.plugins[id];}, + + /** + * Returns entire plugins + */ + getPlugins: function() {return this.config.plugins;}, + + /** + * Remove plugin matches with given identifier. + * + * @param {String} id unique identifier + */ + removePlugin: function(id) { + var plugin = this.config.shortcuts[id]; + if(plugin) { + plugin.unload(); + } + + delete this.config.shortcuts[id]; + }, + + + + /** + * Adds or replaces keyboard shortcut. + * + * @param {String} shortcut keymap expression like "CTRL+Space" + * @param {Object} handler string or function to be evaluated or called + */ + addShortcut: function(shortcut, handler) { + this.config.shortcuts[shortcut] = {"event":new xq.Shortcut(shortcut), "handler":handler}; + }, + + /** + * Adds several keyboard shortcuts at once. + * + * @param {Array} list of shortcuts. each element should have following structure: {event:"keymap expression", handler:handler} + */ + addShortcuts: function(list) { + for(var i = 0; i < list.length; i++) { + this.addShortcut(list[i].event, list[i].handler); + } + }, + + /** + * Returns keyboard shortcut matches with given keymap expression. + * + * @param {String} shortcut keymap expression like "CTRL+Space" + */ + getShortcut: function(shortcut) {return this.config.shortcuts[shortcut];}, + + /** + * Returns entire keyboard shortcuts' map + */ + getShortcuts: function() {return this.config.shortcuts;}, + + /** + * Remove keyboard shortcut matches with given keymap expression. + * + * @param {String} shortcut keymap expression like "CTRL+Space" + */ + removeShortcut: function(shortcut) {delete this.config.shortcuts[shortcut];}, + + /** + * Adds or replaces autocorrection handler. + * + * @param {String} id unique identifier + * @param {Object} criteria regex pattern or function to be used as a criterion for match + * @param {Object} handler string or function to be evaluated or called when criteria met + */ + addAutocorrection: function(id, criteria, handler) { + if(criteria.exec) { + var pattern = criteria; + criteria = function(text) {return text.match(pattern)}; + } + this.config.autocorrections[id] = {"criteria":criteria, "handler":handler}; + }, + + /** + * Adds several autocorrection handlers at once. + * + * @param {Array} list of autocorrection. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler} + */ + addAutocorrections: function(list) { + for(var i = 0; i < list.length; i++) { + this.addAutocorrection(list[i].id, list[i].criteria, list[i].handler); + } + }, + + /** + * Returns autocorrection handler matches with given id + * + * @param {String} id unique identifier + */ + getAutocorrection: function(id) {return this.config.autocorrection[id];}, + + /** + * Returns entire autocorrections' map + */ + getAutocorrections: function() {return this.config.autocorrections;}, + + /** + * Removes autocorrection handler matches with given id + * + * @param {String} id unique identifier + */ + removeAutocorrection: function(id) {delete this.config.autocorrections[id];}, + + /** + * Adds or replaces autocompletion handler. + * + * @param {String} id unique identifier + * @param {Object} criteria regex pattern or function to be used as a criterion for match + * @param {Object} handler string or function to be evaluated or called when criteria met + */ + addAutocompletion: function(id, criteria, handler) { + if(criteria.exec) { + var pattern = criteria; + criteria = function(text) { + var m = pattern.exec(text); + return m ? m.index : -1; + }; + } + this.config.autocompletions[id] = {"criteria":criteria, "handler":handler}; + }, + + /** + * Adds several autocompletion handlers at once. + * + * @param {Array} list of autocompletion. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler} + */ + addAutocompletions: function(list) { + for(var i = 0; i < list.length; i++) { + this.addAutocompletion(list[i].id, list[i].criteria, list[i].handler); + } + }, + + /** + * Returns autocompletion handler matches with given id + * + * @param {String} id unique identifier + */ + getAutocompletion: function(id) {return this.config.autocompletions[id];}, + + /** + * Returns entire autocompletions' map + */ + getAutocompletions: function() {return this.config.autocompletions;}, + + /** + * Removes autocompletion handler matches with given id + * + * @param {String} id unique identifier + */ + removeAutocompletion: function(id) {delete this.config.autocompletions[id];}, + + /** + * Adds or replaces template processor. + * + * @param {String} id unique identifier + * @param {Object} handler string or function to be evaluated or called when template inserted + */ + addTemplateProcessor: function(id, handler) { + this.config.templateProcessors[id] = {"handler":handler}; + }, + + /** + * Adds several template processors at once. + * + * @param {Array} list of template processors. Each element should have following structure: {id:"identifier", handler:handler} + */ + addTemplateProcessors: function(list) { + for(var i = 0; i < list.length; i++) { + this.addTemplateProcessor(list[i].id, list[i].handler); + } + }, + + /** + * Returns template processor matches with given id + * + * @param {String} id unique identifier + */ + getTemplateProcessor: function(id) {return this.config.templateProcessors[id];}, + + /** + * Returns entire template processors' map + */ + getTemplateProcessors: function() {return this.config.templateProcessors;}, + + /** + * Removes template processor matches with given id + * + * @param {String} id unique identifier + */ + removeTemplateProcessor: function(id) {delete this.config.templateProcessors[id];}, + + + + /** + * Adds or replaces context menu handler. + * + * @param {String} id unique identifier + * @param {Object} handler string or function to be evaluated or called when onContextMenu occured + */ + addContextMenuHandler: function(id, handler) { + this.config.contextMenuHandlers[id] = {"handler":handler}; + }, + + /** + * Adds several context menu handlers at once. + * + * @param {Array} list of handlers. Each element should have following structure: {id:"identifier", handler:handler} + */ + addContextMenuHandlers: function(list) { + for(var i = 0; i < list.length; i++) { + this.addContextMenuHandler(list[i].id, list[i].handler); + } + }, + + /** + * Returns context menu handler matches with given id + * + * @param {String} id unique identifier + */ + getContextMenuHandler: function(id) {return this.config.contextMenuHandlers[id];}, + + /** + * Returns entire context menu handlers' map + */ + getContextMenuHandlers: function() {return this.config.contextMenuHandlers;}, + + /** + * Removes context menu handler matches with given id + * + * @param {String} id unique identifier + */ + removeContextMenuHandler: function(id) {delete this.config.contextMenuHandlers[id];}, + + + + /** + * Sets width of editor. + * + * @param {String} w Valid CSS value for style.width. For example, "100%", "200px". + */ + setWidth: function(w) { + this.outmostWrapper.style.width = w; + }, + + + + /** + * Sets height of editor. + * + * @param {String} h Valid CSS value for style.height. For example, "100%", "200px". + */ + setHeight: function(h) { + this.wysiwygEditorDiv.style.height = h; + this.sourceEditorDiv.style.height = h; + }, + + + + ///////////////////////////////////////////// + // Edit mode management + + /** + * Returns current edit mode - wysiwyg, source + */ + getCurrentEditMode: function() { + return this.currentEditMode; + }, + + /** + * Toggle edit mode between source and wysiwyg + */ + toggleSourceAndWysiwygMode: function() { + var mode = this.getCurrentEditMode(); + this.setEditMode(mode === 'wysiwyg' ? 'source' : 'wysiwyg'); + }, + + /** + * Switches between WYSIWYG/Source mode. + * + * @param {String} mode 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode. + */ + setEditMode: function(mode) { + if(typeof mode !== 'string') throw "[mode] is not a string." + if(['wysiwyg', 'source'].indexOf(mode) === -1) throw "Illegal [mode] value: '" + mode + "'. Use 'wysiwyg' or 'source'"; + if(this.currentEditMode === mode) return; + + // create editor frame if there's no editor frame. + var editorCreated = !!this.outmostWrapper; + if(!editorCreated) { + // create validator + this.validator = xq.validator.Base.createInstance( + this.doc.location.href, + this.config.urlValidationMode, + this.config.whitelist + ); + + this._fireOnStartInitialization(this); + + this._createEditorFrame(mode); + var temp = window.setInterval(function() { + // wait for loading + if(this.getBody()) { + window.clearInterval(temp); + + // @WORKAROUND: it is needed to fix IE6 horizontal scrollbar problem + if(xq.Browser.isIE6) { + this.rdom.getDoc().documentElement.style.overflowY='auto'; + this.rdom.getDoc().documentElement.style.overflowX='hidden'; + } + + this.setEditMode(mode); + if(this.config.autoFocusOnInit) this.focus(); + + this.timer.start(); + this._fireOnInitialized(this); + } + }.bind(this), 10); + + return; + } + + // switch mode + if(mode === 'wysiwyg') { + this._setEditModeToWysiwyg(); + } else { // mode === 'source' + this._setEditModeToSource(); + } + + // fire event + var oldEditMode = this.currentEditMode; + this.currentEditMode = mode; + + this._fireOnCurrentEditModeChanged(this, oldEditMode, this.currentEditMode); + }, + + _setEditModeToWysiwyg: function() { + // Turn off static content and source editor + this.contentElement.style.display = "none"; + this.sourceEditorDiv.style.display = "none"; + + // Update contents + if(this.currentEditMode === 'source') { + // get html from source editor + var html = this.getSourceContent(true); + + // invalidate it and load it into wysiwyg editor + var invalidHtml = this.validator.invalidate(html); + invalidHtml = this.removeUnnecessarySpaces(invalidHtml); + if(invalidHtml.isBlank()) { + this.rdom.clearRoot(); + } else { + this.rdom.getRoot().innerHTML = invalidHtml; + this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true); + } + } else { + // invalidate static html and load it into wysiwyg editor + var invalidHtml = this.validator.invalidate(this.getStaticContent()); + invalidHtml = this.removeUnnecessarySpaces(invalidHtml); + if(invalidHtml.isBlank()) { + this.rdom.clearRoot(); + } else { + this.rdom.getRoot().innerHTML = invalidHtml; + this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true); + } + } + + // Turn on wysiwyg editor + this.wysiwygEditorDiv.style.display = "block"; + this.outmostWrapper.style.display = "block"; + + // Without this, xq.rdom.Base.focus() doesn't work correctly. + if(xq.Browser.isGecko) this.rdom.placeCaretAtStartOf(this.rdom.getRoot()); + + if(this.toolbar) this.toolbar.enableButtons(); + }, + + _setEditModeToSource: function() { + // Update contents + var validHtml = null; + if(this.currentEditMode === 'wysiwyg') { + validHtml = this.getWysiwygContent(); + } else { + validHtml = this.getStaticContent(); + } + this.sourceEditorTextarea.value = validHtml + + // Turn off static content and wysiwyg editor + this.contentElement.style.display = "none"; + this.wysiwygEditorDiv.style.display = "none"; + + // Turn on source editor + this.sourceEditorDiv.style.display = "block"; + this.outmostWrapper.style.display = "block"; + if(this.toolbar) this.toolbar.disableButtons(['html']); + }, + + /** + * Load CSS into WYSIWYG mode document + * + * @param {string} path URL + */ + loadStylesheet: function(path) { + var head = this.getDoc().getElementsByTagName("HEAD")[0]; + var link = this.getDoc().createElement("LINK"); + link.rel = "Stylesheet"; + link.type = "text/css"; + link.href = path; + head.appendChild(link); + }, + + /** + * Sets editor's dynamic content from static content + */ + loadCurrentContentFromStaticContent: function() { + if(this.getCurrentEditMode() == 'wysiwyg') { + // update WYSIWYG editor + var html = this.validator.invalidate(this.getStaticContent()); + html = this.removeUnnecessarySpaces(html); + + if(html.isBlank()) { + this.rdom.clearRoot(); + } else { + this.rdom.getRoot().innerHTML = html; + this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true); + } + } else { // 'source' + this.sourceEditorTextarea.value = this.getStaticContent(); + } + + this._fireOnCurrentContentChanged(this); + }, + + /** + * Removes unnecessary spaces, tabs and new lines. + * + * @param {String} html HTML string. + * @returns {String} Modified HTML string. + */ + removeUnnecessarySpaces: function(html) { + var blocks = this.rdom.tree.getBlockTags().join("|"); + var regex = new RegExp("\\s*<(/?)(" + blocks + ")>\\s*", "img"); + return html.replace(regex, '<$1$2>'); + }, + + /** + * Gets editor's dynamic content from current editor(source or WYSIWYG) + * + * @return {Object} HTML String + */ + getCurrentContent: function() { + if(this.getCurrentEditMode() === 'source') { + return this.getSourceContent(this.config.noValidationInSourceEditMode); + } else { + return this.getWysiwygContent(); + } + }, + + /** + * Gets editor's dynamic content from WYSIWYG editor + * + * @return {Object} HTML String + */ + getWysiwygContent: function() { + return this.validator.validate(this.rdom.getRoot()); + }, + + /** + * Gets editor's dynamic content from source editor + * + * @return {Object} HTML String + */ + getSourceContent: function(noValidation) { + var raw = this.sourceEditorTextarea.value; + if(noValidation) return raw; + + var tempDiv = document.createElement('div'); + tempDiv.innerHTML = this.removeUnnecessarySpaces(raw); + + var rdom = xq.rdom.Base.createInstance(); + rdom.wrapAllInlineOrTextNodesAs("P", tempDiv, true); + + return this.validator.validate(tempDiv, true); + }, + + /** + * Sets editor's original content + * + * @param {Object} content HTML String + */ + setStaticContent: function(content) { + this.contentElement.value = content; + this._fireOnStaticContentChanged(this, content); + }, + + /** + * Gets editor's original content + * + * @return {Object} HTML String + */ + getStaticContent: function() { + return this.contentElement.value; + }, + + /** + * Gets editor's original content as (newely created) DOM node + * + * @return {Element} DIV element + */ + getStaticContentAsDOM: function() { + var div = this.doc.createElement('DIV'); + div.innerHTML = this.contentElement.value; + return div; + }, + + /** + * Gives focus to editor + */ + focus: function() { + if(this.getCurrentEditMode() === 'wysiwyg') { + this.rdom.focus(); + if(this.toolbar) this.toolbar.triggerUpdate(); + } else if(this.getCurrentEditMode() === 'source') { + this.sourceEditorTextarea.focus(); + } + }, + + getWysiwygEditorDiv: function() { + return this.wysiwygEditorDiv; + }, + + getSourceEditorDiv: function() { + return this.sourceEditorDiv; + }, + + /** + * Returns outer iframe object + */ + getOuterFrame: function() { + return this.outerFrame; + }, + + /** + * Returns outer iframe document + */ + getOuterDoc: function() { + return this.outerFrame.contentWindow.document; + }, + + /** + * Returns designmode iframe object + */ + getFrame: function() { + return this.editorFrame; + }, + + /** + * Returns designmode window object + */ + getWin: function() { + return this.rdom.getWin(); + }, + + /** + * Returns designmode document object + */ + getDoc: function() { + return this.rdom.getDoc(); + }, + + /** + * Returns designmode body object + */ + getBody: function() { + return this.rdom.getRoot(); + }, + + /** + * Returns outmost wrapper element + */ + getOutmostWrapper: function() { + return this.outmostWrapper; + }, + + _createIFrame: function(doc, width, height) { + var frame = doc.createElement("iframe"); + + // IE displays warning when a protocol is HTTPS, because IE6 treats IFRAME + // without SRC attribute as insecure. + if(xq.Browser.isIE) frame.src = 'javascript:""'; + + frame.style.width = width || "100%"; + frame.style.height = height || "100%"; + frame.setAttribute("frameBorder", "0"); + frame.setAttribute("marginWidth", "0"); + frame.setAttribute("marginHeight", "0"); + frame.setAttribute("allowTransparency", "auto"); + return frame; + }, + + _createDoc: function(frame, head, cssList, bodyId, bodyClass, body) { + var sb = []; + if(!xq.Browser.isTrident) { + // @WORKAROUND: IE6/7 has caret movement and scrolling problem if I include following DTD. + sb.push(''); + } + sb.push(''); + sb.push(''); + sb.push(''); + if(head) sb.push(head); + + if(cssList) for(var i = 0; i < cssList.length; i++) { + sb.push(''); + } + sb.push(''); + sb.push(''); + if(body) sb.push(body); + sb.push(''); + sb.push(''); + + var doc = frame.contentWindow.document; + doc.open(); + doc.write(sb.join("")); + doc.close(); + return doc; + }, + + _createEditorFrame: function(mode) { + // turn off static content + this.contentElement.style.display = "none"; + + // create outer DIV + this.outmostWrapper = this.doc.createElement('div'); + this.outmostWrapper.className = "xquared"; + this.contentElement.parentNode.insertBefore(this.outmostWrapper, this.contentElement); + + // create toolbar + if(this.toolbarContainer || this.config.generateDefaultToolbar) { + this.toolbar = new xq.ui.Toolbar( + this, + this.toolbarContainer, + this.outmostWrapper, + this.config.defaultToolbarButtonMap, + this.config.imagePathForDefaultToolbar, + function() { + var element = this.getCurrentEditMode() === 'wysiwyg' ? this.lastFocusElement : null; + return element && element.nodeName != "BODY" ? this.rdom.collectStructureAndStyle(element) : null; + }.bind(this) + ); + } + + // create source editor div + this.sourceEditorDiv = this.doc.createElement('div'); + this.sourceEditorDiv.className = "editor source_editor"; //TODO: remove editor + this.sourceEditorDiv.style.display = "none"; + this.outmostWrapper.appendChild(this.sourceEditorDiv); + + // create TEXTAREA for source editor + this.sourceEditorTextarea = this.doc.createElement('textarea'); + this.sourceEditorDiv.appendChild(this.sourceEditorTextarea); + + // create WYSIWYG editor div + this.wysiwygEditorDiv = this.doc.createElement('div'); + this.wysiwygEditorDiv.className = "editor wysiwyg_editor"; //TODO: remove editor + this.outmostWrapper.appendChild(this.wysiwygEditorDiv); + + // create outer iframe for WYSIWYG editor + this.outerFrame = this._createIFrame(document); + this.wysiwygEditorDiv.appendChild(this.outerFrame); + var outerDoc = this._createDoc( + this.outerFrame, + '' + ); + + // create designmode iframe for WYSIWYG editor + this.editorFrame = this._createIFrame(outerDoc); + + outerDoc.body.appendChild(this.editorFrame); + var editorDoc = this._createDoc( + this.editorFrame, + '' + + (!xq.Browser.isTrident ? '' : '') + // @WORKAROUND: it is needed to force href of pasted content to be an absolute url + (this.config.changeCursorOnLink ? '' : ''), + this.config.contentCssList, + this.config.bodyId, + this.config.bodyClass, + '' + ); + this.rdom.setWin(this.editorFrame.contentWindow); + this.editHistory = new xq.EditHistory(this.rdom); + + // turn on designmode + this.rdom.getDoc().designMode = "On"; + + // turn off Firefox's table editing feature + if(xq.Browser.isGecko) { + try {this.rdom.getDoc().execCommand("enableInlineTableEditing", false, "false")} catch(ignored) {} + } + + // register event handlers + this._registerEventHandlers(); + + // hook onsubmit of form + if(this.config.automaticallyHookSubmitEvent && this.contentElement.form) { + var original = this.contentElement.form.onsubmit; + this.contentElement.form.onsubmit = function() { + this.contentElement.value = this.getCurrentContent(); + return original ? original.bind(this.contentElement.form)() : true; + }.bind(this); + } + }, + + + + ///////////////////////////////////////////// + // Event Management + + _registerEventHandlers: function() { + var events = [this.platformDepedentKeyEventType, 'click', 'keyup', 'mouseup', 'contextmenu']; + + if(xq.Browser.isTrident && this.config.changeCursorOnLink) events.push('mousemove'); + + var handler = this._handleEvent.bindAsEventListener(this); + for(var i = 0; i < events.length; i++) { + xq.observe(this.getDoc(), events[i], handler); + } + + if(xq.Browser.isGecko) { + xq.observe(this.getDoc(), "focus", handler); + xq.observe(this.getDoc(), "blur", handler); + xq.observe(this.getDoc(), "scroll", handler); + xq.observe(this.getDoc(), "dragdrop", handler); + } else { + xq.observe(this.getWin(), "focus", handler); + xq.observe(this.getWin(), "blur", handler); + xq.observe(this.getWin(), "scroll", handler); + } + }, + + _handleEvent: function(e) { + this._fireOnBeforeEvent(this, e); + if(e.stopProcess) { + xq.stopEvent(e); + return false; + } + + // Trident only + if(e.type === 'mousemove') { + if(!this.config.changeCursorOnLink) return true; + + var link = !!this.rdom.getParentElementOf(e.srcElement, ["A"]); + + var editable = this.getBody().contentEditable; + editable = editable === 'inherit' ? false : editable; + + if(editable !== link && !this.rdom.hasSelection()) this.getBody().contentEditable = !link; + return true; + } + + var stop = false; + var modifiedByCorrection = false; + if(e.type === this.platformDepedentKeyEventType) { + var undoPerformed = false; + modifiedByCorrection = this.rdom.correctParagraph(); + for(var key in this.config.shortcuts) { + if(!this.config.shortcuts[key].event.matches(e)) continue; + + var handler = this.config.shortcuts[key].handler; + var xed = this; + stop = (typeof handler === "function") ? handler(this) : eval(handler); + + if(key === "undo") undoPerformed = true; + } + } else if(e.type === 'click' && e.button === 0 && this.config.enableLinkClick) { + var a = this.rdom.getParentElementOf(e.target || e.srcElement, ["A"]); + if(a) stop = this.handleClick(e, a); + } else if(["keyup", "mouseup"].indexOf(e.type) !== -1) { + modifiedByCorrection = this.rdom.correctParagraph(); + } else if(["contextmenu"].indexOf(e.type) !== -1) { + this._handleContextMenu(e); + } else if("focus" == e.type) { + this.rdom.focused = true; + } else if("blur" == e.type) { + this.rdom.focused = false; + } + + if(stop) xq.stopEvent(e); + + this._fireOnCurrentContentChanged(this); + this._fireOnAfterEvent(this, e); + + if(!undoPerformed && !modifiedByCorrection) this.editHistory.onEvent(e); + + return !stop; + }, + + /** + * TODO: remove dup with handleAutocompletion + */ + handleAutocorrection: function() { + var block = this.rdom.getCurrentBlockElement(); + // TODO: use complete unescape algorithm + var text = this.rdom.getInnerText(block).replace(/ /gi, " "); + + var acs = this.config.autocorrections; + var performed = false; + + var stop = false; + for(var key in acs) { + var ac = acs[key]; + if(ac.criteria(text)) { + try { + this.editHistory.onCommand(); + this.editHistory.disable(); + if(typeof ac.handler === "String") { + var xed = this; + var rdom = this.rdom; + eval(ac.handler); + } else { + stop = ac.handler(this, this.rdom, block, text); + } + this.editHistory.enable(); + } catch(ignored) {} + + block = this.rdom.getCurrentBlockElement(); + text = this.rdom.getInnerText(block); + + performed = true; + if(stop) break; + } + } + + return stop; + }, + + /** + * TODO: remove dup with handleAutocorrection + */ + handleAutocompletion: function() { + var acs = this.config.autocompletions; + if(xq.isEmptyHash(acs)) return; + + if(this.rdom.hasSelection()) { + var text = this.rdom.getSelectionAsText(); + this.rdom.deleteSelection(); + var wrapper = this.rdom.insertNode(this.rdom.createElement("SPAN")); + wrapper.innerHTML = text; + + var marker = this.rdom.pushMarker(); + + var filtered = []; + for(var key in acs) { + filtered.push([key, acs[key].criteria(text)]); + } + filtered = filtered.findAll(function(elem) { + return elem[1] !== -1; + }); + + if(filtered.length === 0) { + this.rdom.popMarker(true); + return; + } + + var minIndex = 0; + var min = filtered[0][1]; + for(var i = 0; i < filtered.length; i++) { + if(filtered[i][1] < min) { + minIndex = i; + min = filtered[i][1]; + } + } + + var ac = acs[filtered[minIndex][0]]; + + this.editHistory.disable(); + this.rdom.selectElement(wrapper); + } else { + var marker = this.rdom.pushMarker(); + + var filtered = []; + for(var key in acs) { + filtered.push([key, this.rdom.testSmartWrap(marker, acs[key].criteria).textIndex]); + } + filtered = filtered.findAll(function(elem) { + return elem[1] !== -1; + }); + + if(filtered.length === 0) { + this.rdom.popMarker(true); + return; + } + + var minIndex = 0; + var min = filtered[0][1]; + for(var i = 0; i < filtered.length; i++) { + if(filtered[i][1] < min) { + minIndex = i; + min = filtered[i][1]; + } + } + + var ac = acs[filtered[minIndex][0]]; + + this.editHistory.disable(); + + var wrapper = this.rdom.smartWrap(marker, "SPAN", ac.criteria); + } + + var block = this.rdom.getCurrentBlockElement(); + + // TODO: use complete unescape algorithm + var text = this.rdom.getInnerText(wrapper).replace(/ /gi, " "); + + try { + // call handler + if(typeof ac.handler === "String") { + var xed = this; + var rdom = this.rdom; + eval(ac.handler); + } else { + ac.handler(this, this.rdom, block, wrapper, text); + } + } catch(ignored) {} + + try { + this.rdom.unwrapElement(wrapper); + } catch(ignored) {} + + if(this.rdom.isEmptyBlock(block)) this.rdom.correctEmptyElement(block); + + this.editHistory.enable(); + this.editHistory.onCommand(); + + this.rdom.popMarker(true); + }, + + /** + * Handles click event + * + * @param {Event} e click event + * @param {Element} target target element(usually has A tag) + */ + handleClick: function(e, target) { + var href = decodeURI(target.href); + if(!xq.Browser.isTrident) { + if(!e.ctrlKey && !e.shiftKey && e.button !== 1) { + window.location.href = href; + return true; + } + } else { + if(e.shiftKey) { + window.open(href, "_blank"); + } else { + window.location.href = href; + } + return true; + } + + return false; + }, + + /** + * Show link dialog + * + * TODO: should support modify/unlink + * TODO: Add selenium test + */ + handleLink: function() { + var text = this.rdom.getSelectionAsText() || ''; + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicLinkDialog, + function(dialog) { + if(text) { + dialog.form.text.value = text; + dialog.form.url.focus(); + dialog.form.url.select(); + } + }, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + if(!data) return; + this.handleInsertLink(false, data.url, data.text, data.text); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + + dialog.show({position: 'centerOfEditor'}); + + return true; + }, + + /** + * Inserts link or apply link into selected area + * @TODO Add selenium test + * + * @param {boolean} autoSelection if set true and there's no selection, automatically select word to link(if possible) + * @param {String} url url + * @param {String} title title of link + * @param {String} text text of link. If there's a selection(manually or automatically), it will be replaced with this text + * + * @returns {Element} created element + */ + handleInsertLink: function(autoSelection, url, title, text) { + if(autoSelection && !this.rdom.hasSelection()) { + var marker = this.rdom.pushMarker(); + var a = this.rdom.smartWrap(marker, "A", function(text) { + var index = text.lastIndexOf(" "); + return index === -1 ? index : index + 1; + }); + a.href = url; + a.title = title; + if(text) { + a.innerHTML = "" + a.appendChild(this.rdom.createTextNode(text)); + } else if(!a.hasChildNodes()) { + this.rdom.deleteNode(a); + } + this.rdom.popMarker(true); + } else { + text = text || (this.rdom.hasSelection() ? this.rdom.getSelectionAsText() : null); + if(!text) return; + + this.rdom.deleteSelection(); + + var a = this.rdom.createElement('A'); + a.href = url; + a.title = title; + a.appendChild(this.rdom.createTextNode(text)); + this.rdom.insertNode(a); + } + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * @TODO Add selenium test + */ + handleSpace: function() { + // If it has selection, perform default action. + if(this.rdom.hasSelection()) return false; + + // Trident performs URL replacing automatically + if(!xq.Browser.isTrident) { + this.replaceUrlToLink(); + } + + return false; + }, + + /** + * Called when enter key pressed. + * @TODO Add selenium test + * + * @param {boolean} skipAutocorrection if set true, skips autocorrection + * @param {boolean} forceInsertParagraph if set true, inserts paragraph + */ + handleEnter: function(skipAutocorrection, forceInsertParagraph) { + // If it has selection, perform default action. + if(this.rdom.hasSelection()) return false; + + // @WORKAROUND: + // If caret is in HR, default action should be performed and + // this._handleEvent() will correct broken HTML + if(xq.Browser.isTrident && this.rdom.tree.isBlockOnlyContainer(this.rdom.getCurrentElement()) && this.rdom.recentHR) { + this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.recentHR, "before"); + this.rdom.recentHR = null; + return true; + } + + // Perform autocorrection + if(!skipAutocorrection && this.handleAutocorrection()) return true; + + var block = this.rdom.getCurrentBlockElement(); + var info = this.rdom.collectStructureAndStyle(block); + + // Perform URL replacing. Trident performs URL replacing automatically + if(!xq.Browser.isTrident) { + this.replaceUrlToLink(); + } + + var atEmptyBlock = this.rdom.isCaretAtEmptyBlock(); + var atStart = atEmptyBlock || this.rdom.isCaretAtBlockStart(); + var atEnd = atEmptyBlock || (!atStart && this.rdom.isCaretAtBlockEnd()); + var atEdge = atEmptyBlock || atStart || atEnd; + + if(!atEdge) { + var marker = this.rdom.pushMarker(); + + if(this.rdom.isFirstLiWithNestedList(block) && !forceInsertParagraph) { + var parent = block.parentNode; + this.rdom.unwrapElement(block); + block = parent; + } else if(block.nodeName !== "LI" && this.rdom.tree.isBlockContainer(block)) { + block = this.rdom.wrapAllInlineOrTextNodesAs("P", block, true).first(); + } + this.rdom.splitElementUpto(marker, block); + + this.rdom.popMarker(true); + } else if(atEmptyBlock) { + this._handleEnterAtEmptyBlock(); + + if(!xq.Browser.isWebkit) { + if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize); + if(info.fontName) this.handleFontFace(info.fontName); + } + } else { + this._handleEnterAtEdge(atStart, forceInsertParagraph); + + if(!xq.Browser.isWebkit) { + if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize); + if(info.fontName) this.handleFontFace(info.fontName); + } + } + + return true; + }, + + /** + * Moves current block upward or downward + * + * @param {boolean} up moves current block upward + */ + handleMoveBlock: function(up) { + var block = this.rdom.moveBlock(this.rdom.getCurrentBlockElement(), up); + if(block) { + this.rdom.selectElement(block, false); + if(this.rdom.isEmptyBlock(block)) this.rdom.collapseSelection(true); + + block.scrollIntoView(false); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } + return true; + }, + + /** + * Called when tab key pressed + * @TODO: Add selenium test + */ + handleTab: function() { + var hasSelection = this.rdom.hasSelection(); + var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]); + + if(hasSelection) { + this.handleIndent(); + } else if (table && table.className === "datatable") { + this.handleMoveToNextCell(); + } else if (this.rdom.isCaretAtBlockStart()) { + this.handleIndent(); + } else { + this.handleInsertTab(); + } + + return true; + }, + + /** + * Called when shift+tab key pressed + * @TODO: Add selenium test + */ + handleShiftTab: function() { + var hasSelection = this.rdom.hasSelection(); + var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]); + + if(hasSelection) { + this.handleOutdent(); + } else if (table && table.className === "datatable") { + this.handleMoveToPreviousCell(); + } else { + this.handleOutdent(); + } + + return true; + }, + + /** + * Inserts three non-breaking spaces + * @TODO: Add selenium test + */ + handleInsertTab: function() { + this.rdom.insertHtml(' '); + this.rdom.insertHtml(' '); + this.rdom.insertHtml(' '); + + return true; + }, + + /** + * Called when delete key pressed + * @TODO: Add selenium test + */ + handleDelete: function() { + if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockEnd()) return false; + return this._handleMerge(true); + }, + + /** + * Called when backspace key pressed + * @TODO: Add selenium test + */ + handleBackspace: function() { + if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockStart()) return false; + return this._handleMerge(false); + }, + + _handleMerge: function(withNext) { + var block = this.rdom.getCurrentBlockElement(); + + if(this.rdom.isEmptyBlock(block) && !this.rdom.tree.isBlockContainer(block.nextSibling) && withNext) { + var blockToMove = this.rdom.removeBlock(block); + this.rdom.placeCaretAtStartOf(blockToMove); + blockToMove.scrollIntoView(false); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } else { + // save caret position; + var marker = this.rdom.pushMarker(); + + // perform merge + var merged = this.rdom.mergeElement(block, withNext, withNext); + if(!merged && !withNext) this.rdom.extractOutElementFromParent(block); + + // restore caret position + this.rdom.popMarker(true); + if(merged) this.rdom.correctEmptyElement(merged); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + return !!merged; + } + }, + + /** + * (in table) Moves caret to the next cell + * @TODO: Add selenium test + */ + handleMoveToNextCell: function() { + this._handleMoveToCell("next"); + }, + + /** + * (in table) Moves caret to the previous cell + * @TODO: Add selenium test + */ + handleMoveToPreviousCell: function() { + this._handleMoveToCell("prev"); + }, + + /** + * (in table) Moves caret to the above cell + * @TODO: Add selenium test + */ + handleMoveToAboveCell: function() { + this._handleMoveToCell("above"); + }, + + /** + * (in table) Moves caret to the below cell + * @TODO: Add selenium test + */ + handleMoveToBelowCell: function() { + this._handleMoveToCell("below"); + }, + + _handleMoveToCell: function(dir) { + var block = this.rdom.getCurrentBlockElement(); + var cell = this.rdom.getParentElementOf(block, ["TD", "TH"]); + var table = this.rdom.getParentElementOf(cell, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + var target = null; + + if(["next", "prev"].indexOf(dir) !== -1) { + var toNext = dir === "next"; + target = toNext ? rtable.getNextCellOf(cell) : rtable.getPreviousCellOf(cell); + } else { + var toBelow = dir === "below"; + target = toBelow ? rtable.getBelowCellOf(cell) : rtable.getAboveCellOf(cell); + } + + if(!target) { + var finder = function(node) {return ['TD', 'TH'].indexOf(node.nodeName) === -1 && this.tree.isBlock(node) && !this.tree.hasBlocks(node);}.bind(this.rdom); + var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this.rdom); + + target = (toNext || toBelow) ? + this.rdom.tree.findForward(cell, finder, exitCondition) : + this.rdom.tree.findBackward(table, finder, exitCondition); + } + + if(target) this.rdom.placeCaretAtStartOf(target); + }, + + /** + * Applies STRONG tag + * @TODO: Add selenium test + */ + handleStrongEmphasis: function() { + this.rdom.applyStrongEmphasis(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies EM tag + * @TODO: Add selenium test + */ + handleEmphasis: function() { + this.rdom.applyEmphasis(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies EM.underline tag + * @TODO: Add selenium test + */ + handleUnderline: function() { + this.rdom.applyUnderline(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies SPAN.strike tag + * @TODO: Add selenium test + */ + handleStrike: function() { + this.rdom.applyStrike(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Removes all style + * @TODO: Add selenium test + */ + handleRemoveFormat: function() { + this.rdom.applyRemoveFormat(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Remove link + * @TODO: Add selenium test + */ + handleRemoveLink: function() { + this.rdom.applyRemoveLink(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Inserts table + * @TODO: Add selenium test + * + * @param {Number} cols number of columns + * @param {Number} rows number of rows + * @param {String} headerPosition position of THs. "T" or "L" or "TL". "T" means top, "L" means left. + */ + handleTable: function(cols, rows, headerPositions) { + var cur = this.rdom.getCurrentBlockElement(); + if(this.rdom.getParentElementOf(cur, ["TABLE"])) return true; + + var rtable = xq.RichTable.create(this.rdom, cols, rows, headerPositions); + if(this.rdom.tree.isBlockContainer(cur)) { + var wrappers = this.rdom.wrapAllInlineOrTextNodesAs("P", cur, true); + cur = wrappers.last(); + } + var tableDom = this.rdom.insertNodeAt(rtable.getDom(), cur, "after"); + this.rdom.placeCaretAtStartOf(rtable.getCellAt(0, 0)); + + if(this.rdom.isEmptyBlock(cur)) this.rdom.deleteNode(cur, true); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + handleInsertNewRowAt: function(where) { + var cur = this.rdom.getCurrentBlockElement(); + var tr = this.rdom.getParentElementOf(cur, ["TR"]); + if(!tr) return true; + + var table = this.rdom.getParentElementOf(tr, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + var row = rtable.insertNewRowAt(tr, where); + + this.rdom.placeCaretAtStartOf(row.cells[0]); + return true; + }, + + /** + * @TODO: Add selenium test + */ + handleInsertNewColumnAt: function(where) { + var cur = this.rdom.getCurrentBlockElement(); + var td = this.rdom.getParentElementOf(cur, ["TD"], true); + if(!td) return true; + + var table = this.rdom.getParentElementOf(td, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + rtable.insertNewCellAt(td, where); + + this.rdom.placeCaretAtStartOf(cur); + return true; + }, + + /** + * @TODO: Add selenium test + */ + handleDeleteRow: function() { + var cur = this.rdom.getCurrentBlockElement(); + var tr = this.rdom.getParentElementOf(cur, ["TR"]); + if(!tr) return true; + + var table = this.rdom.getParentElementOf(tr, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + var blockToMove = rtable.deleteRow(tr); + + this.rdom.placeCaretAtStartOf(blockToMove); + return true; + }, + + /** + * @TODO: Add selenium test + */ + handleDeleteColumn: function() { + var cur = this.rdom.getCurrentBlockElement(); + var td = this.rdom.getParentElementOf(cur, ["TD"], true); + if(!td) return true; + + var table = this.rdom.getParentElementOf(td, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + rtable.deleteCell(td); + + //this.rdom.placeCaretAtStartOf(table); + return true; + }, + + /** + * Performs block indentation + * @TODO: Add selenium test + */ + handleIndent: function() { + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); + if(blocks.first() !== blocks.last()) { + var affected = this.rdom.indentElements(blocks.first(), blocks.last()); + this.rdom.selectBlocksBetween(affected.first(), affected.last()); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } + } + + var block = this.rdom.getCurrentBlockElement(); + var affected = this.rdom.indentElement(block); + + if(affected) { + this.rdom.placeCaretAtStartOf(affected); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } + + return true; + }, + + /** + * Performs block outdentation + * @TODO: Add selenium test + */ + handleOutdent: function() { + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); + if(blocks.first() !== blocks.last()) { + var affected = this.rdom.outdentElements(blocks.first(), blocks.last()); + this.rdom.selectBlocksBetween(affected.first(), affected.last()); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } + } + + var block = this.rdom.getCurrentBlockElement(); + var affected = this.rdom.outdentElement(block); + + if(affected) { + this.rdom.placeCaretAtStartOf(affected); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } + + return true; + }, + + /** + * Applies list. + * @TODO: Add selenium test + * + * @param {String} type "UL" or "OL" + * @param {String} CSS class name + */ + handleList: function(type, className) { + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); + if(blocks.first() !== blocks.last()) { + blocks = this.rdom.applyLists(blocks.first(), blocks.last(), type, className); + } else { + blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type, className); + } + this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); + } else { + var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type, className); + this.rdom.placeCaretAtStartOf(block); + } + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies justification + * @TODO: Add selenium test + * + * @param {String} dir "left", "center", "right" or "both" + */ + handleJustify: function(dir) { + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getSelectedBlockElements(); + var dir = (dir === "left" || dir === "both") && (blocks[0].style.textAlign === "left" || blocks[0].style.textAlign === "") ? "both" : dir; + this.rdom.justifyBlocks(blocks, dir); + this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); + } else { + var block = this.rdom.getCurrentBlockElement(); + var dir = (dir === "left" || dir === "both") && (block.style.textAlign === "left" || block.style.textAlign === "") ? "both" : dir; + this.rdom.justifyBlock(block, dir); + } + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Removes current block element + * @TODO: Add selenium test + */ + handleRemoveBlock: function() { + var block = this.rdom.getCurrentBlockElement(); + var blockToMove = this.rdom.removeBlock(block); + this.rdom.placeCaretAtStartOf(blockToMove); + blockToMove.scrollIntoView(false); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies background color + * @TODO: Add selenium test + * + * @param {String} color CSS color string + */ + handleBackgroundColor: function(color) { + if(color) { + this.rdom.applyBackgroundColor(color); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicColorPickerDialog, + function(dialog) {}, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + if(!data) return; + + this.handleBackgroundColor(data.color); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + + dialog.show({position: 'centerOfEditor'}); + } + return true; + }, + + /** + * Applies foreground color + * @TODO: Add selenium test + * + * @param {String} color CSS color string + */ + handleForegroundColor: function(color) { + if(color) { + this.rdom.applyForegroundColor(color); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicColorPickerDialog, + function(dialog) {}, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + if(!data) return; + + this.handleForegroundColor(data.color); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + + dialog.show({position: 'centerOfEditor'}); + } + return true; + }, + + /** + * Applies font face + * @TODO: Add selenium test + * + * @param {String} face font face + */ + handleFontFace: function(face) { + if(face) { + this.rdom.applyFontFace(face); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + //TODO: popup font dialog + } + return true; + }, + + /** + * Applies font size + * + * @param {Number} font size (1 to 6) + */ + handleFontSize: function(size) { + if(size) { + this.rdom.applyFontSize(size); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + //TODO: popup font dialog + } + return true; + }, + + /** + * Applies superscription + * @TODO: Add selenium test + */ + handleSuperscription: function() { + this.rdom.applySuperscription(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies subscription + * @TODO: Add selenium test + */ + handleSubscription: function() { + this.rdom.applySubscription(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Change or wrap current block(or selected blocks)'s tag + * @TODO: Add selenium test + * + * @param {String} [tagName] Name of tag. If not provided, it does not modify current tag name + * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. + */ + handleApplyBlock: function(tagName, className) { + if(!tagName && !className) return true; + + // if current selection contains multi-blocks + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); + if(blocks.first() !== blocks.last()) { + var applied = this.rdom.applyTagIntoElements(tagName, blocks.first(), blocks.last(), className); + this.rdom.selectBlocksBetween(applied.first(), applied.last()); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } + } + + // else + var block = this.rdom.getCurrentBlockElement(); + this.rdom.pushMarker(); + var applied = + this.rdom.applyTagIntoElement(tagName, block, className) || + block; + this.rdom.popMarker(true); + + if(this.rdom.isEmptyBlock(applied)) { + this.rdom.correctEmptyElement(applied); + this.rdom.placeCaretAtStartOf(applied); + } + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Inserts seperator (HR) + * @TODO: Add selenium test + */ + handleSeparator: function() { + this.rdom.collapseSelection(); + + var curBlock = this.rdom.getCurrentBlockElement(); + var atStart = this.rdom.isCaretAtBlockStart(); + if(this.rdom.tree.isBlockContainer(curBlock)) curBlock = this.rdom.wrapAllInlineOrTextNodesAs("P", curBlock, true)[0]; + + this.rdom.insertNodeAt(this.rdom.createElement("HR"), curBlock, atStart ? "before" : "after"); + this.rdom.placeCaretAtStartOf(curBlock); + + // add undo history + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Performs UNDO + * @TODO: Add selenium test + */ + handleUndo: function() { + var performed = this.editHistory.undo(); + this._fireOnCurrentContentChanged(this); + + var curBlock = this.rdom.getCurrentBlockElement(); + if(!xq.Browser.isTrident && curBlock) { + curBlock.scrollIntoView(false); + } + return true; + }, + + /** + * Performs REDO + * @TODO: Add selenium test + */ + handleRedo: function() { + var performed = this.editHistory.redo(); + this._fireOnCurrentContentChanged(this); + + var curBlock = this.rdom.getCurrentBlockElement(); + if(!xq.Browser.isTrident && curBlock) { + curBlock.scrollIntoView(false); + } + return true; + }, + + + + _handleContextMenu: function(e) { + if (xq.Browser.isWebkit) { + if (e.metaKey || xq.isLeftClick(e)) return false; + } else if (e.shiftKey || e.ctrlKey || e.altKey) { + return false; + } + + var point = xq.getEventPoint(e); + var x = point.x; + var y = point.y; + + var pos = xq.getCumulativeOffset(this.wysiwygEditorDiv); + x += pos.left; + y += pos.top; + this._contextMenuTargetElement = e.target || e.srcElement; + + if (!xq.Browser.isTrident) { + var doc = this.getDoc(); + var body = this.getBody(); + + x -= doc.documentElement.scrollLeft; + y -= doc.documentElement.scrollTop; + + x -= body.scrollLeft; + y -= body.scrollTop; + } + + for(var cmh in this.config.contextMenuHandlers) { + var stop = this.config.contextMenuHandlers[cmh].handler(this, this._contextMenuTargetElement, x, y); + if(stop) { + xq.stopEvent(e); + return true; + } + } + + return false; + }, + + showContextMenu: function(menuItems, x, y) { + if (!menuItems || menuItems.length <= 0) return; + + if (!this.contextMenuContainer) { + this.contextMenuContainer = this.doc.createElement('UL'); + this.contextMenuContainer.className = 'xqContextMenu'; + this.contextMenuContainer.style.display='none'; + + xq.observe(this.doc, 'click', this._contextMenuClicked.bindAsEventListener(this)); + xq.observe(this.rdom.getDoc(), 'click', this.hideContextMenu.bindAsEventListener(this)); + + this.body.appendChild(this.contextMenuContainer); + } else { + while (this.contextMenuContainer.childNodes.length > 0) + this.contextMenuContainer.removeChild(this.contextMenuContainer.childNodes[0]); + } + + for (var i=0; i < menuItems.length; i++) { + menuItems[i]._node = this._addContextMenuItem(menuItems[i]); + } + + this.contextMenuContainer.style.display='block'; + this.contextMenuContainer.style.left = Math.min(Math.max(this.doc.body.scrollWidth, this.doc.documentElement.clientWidth) - this.contextMenuContainer.offsetWidth, x) + 'px'; + this.contextMenuContainer.style.top = Math.min(Math.max(this.doc.body.scrollHeight, this.doc.documentElement.clientHeight) - this.contextMenuContainer.offsetHeight, y) + 'px'; + + this.contextMenuItems = menuItems; + }, + + hideContextMenu: function() { + if (this.contextMenuContainer) + this.contextMenuContainer.style.display='none'; + }, + + _addContextMenuItem: function(item) { + if (!this.contextMenuContainer) throw "No conext menu container exists"; + + var node = this.doc.createElement('LI'); + if (item.disabled) node.className += ' disabled'; + + if (item.title === '----') { + node.innerHTML = ' '; + node.className = 'separator'; + } else { + if(item.handler) { + node.innerHTML = ''+(item.title.toString().escapeHTML())+''; + } else { + node.innerHTML = (item.title.toString().escapeHTML()); + } + } + + if(item.className) node.className = item.className; + + this.contextMenuContainer.appendChild(node); + + return node; + }, + + _contextMenuClicked: function(e) { + this.hideContextMenu(); + + if (!this.contextMenuContainer) return; + + var node = e.srcElement || e.target; + while(node && node.nodeName !== "LI") { + node = node.parentNode; + } + if (!node || !this.rdom.tree.isDescendantOf(this.contextMenuContainer, node)) return; + + for (var i=0; i < this.contextMenuItems.length; i++) { + if (this.contextMenuItems[i]._node === node) { + var handler = this.contextMenuItems[i].handler; + if (!this.contextMenuItems[i].disabled && handler) { + var xed = this; + var element = this._contextMenuTargetElement; + if(typeof handler === "function") { + handler(xed, element); + } else { + eval(handler); + } + } + break; + } + } + }, + + /** + * Inserts HTML template + * @TODO: Add selenium test + * + * @param {String} html Template string. It should have single root element + * @returns {Element} inserted element + */ + insertTemplate: function(html) { + return this.rdom.insertHtml(this._processTemplate(html)); + }, + + /** + * Places given HTML template nearby target. + * @TODO: Add selenium test + * + * @param {String} html Template string. It should have single root element + * @param {Node} target Target node. + * @param {String} where Possible values: "before", "start", "end", "after" + * + * @returns {Element} Inserted element. + */ + insertTemplateAt: function(html, target, where) { + return this.rdom.insertHtmlAt(this._processTemplate(html), target, where); + }, + + _processTemplate: function(html) { + // apply template processors + var tps = this.getTemplateProcessors(); + for(var key in tps) { + var value = tps[key]; + html = value.handler(html); + } + + // remove all whitespace characters between block tags + return this.removeUnnecessarySpaces(html); + }, + + + + /** @private */ + _handleEnterAtEmptyBlock: function() { + var block = this.rdom.getCurrentBlockElement(); + if(this.rdom.tree.isTableCell(block) && this.rdom.isFirstBlockOfBody(block)) { + block = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start"); + } else { + block = + this.rdom.outdentElement(block) || + this.rdom.extractOutElementFromParent(block) || + this.rdom.replaceTag("P", block) || + this.rdom.insertNewBlockAround(block); + } + + this.rdom.placeCaretAtStartOf(block); + if(!xq.Browser.isTrident) block.scrollIntoView(false); + }, + + /** @private */ + _handleEnterAtEdge: function(atStart, forceInsertParagraph) { + var block = this.rdom.getCurrentBlockElement(); + var blockToPlaceCaret; + + if(atStart && this.rdom.isFirstBlockOfBody(block)) { + blockToPlaceCaret = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start"); + } else { + if(this.rdom.tree.isTableCell(block)) forceInsertParagraph = true; + var newBlock = this.rdom.insertNewBlockAround(block, atStart, forceInsertParagraph ? "P" : null); + blockToPlaceCaret = !atStart ? newBlock : newBlock.nextSibling; + } + + this.rdom.placeCaretAtStartOf(blockToPlaceCaret); + if(!xq.Browser.isTrident) blockToPlaceCaret.scrollIntoView(false); + }, + + /** + * Replace URL text nearby caret into a link + * @TODO: Add selenium test + */ + replaceUrlToLink: function() { + // If there's link nearby caret, nothing happens + if(this.rdom.getParentElementOf(this.rdom.getCurrentElement(), ["A"])) return; + + var marker = this.rdom.pushMarker(); + var criteria = function(text) { + var m = /(http|https|ftp|mailto)\:\/\/[^\s]+$/.exec(text); + return m ? m.index : -1; + }; + + var test = this.rdom.testSmartWrap(marker, criteria); + if(test.textIndex !== -1) { + var a = this.rdom.smartWrap(marker, "A", criteria); + a.href = encodeURI(test.text); + } + this.rdom.popMarker(true); + } +}); diff --git a/modules/editor/skins/xquared/javascripts/Json2.js b/modules/editor/skins/xquared/javascripts/Json2.js new file mode 100644 index 000000000..6fc8ed622 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/Json2.js @@ -0,0 +1,275 @@ +/* + json2.js + 2008-02-14 + + Public Domain + + No warranty expressed or implied. Use at your own risk. + + See http://www.JSON.org/js.html + + This file creates a global JSON object containing two methods: + + JSON.stringify(value, whitelist) + value any JavaScript value, usually an object or array. + + whitelist an optional array parameter that determines how object + values are stringified. + + This method produces a JSON text from a JavaScript value. + There are three possible ways to stringify an object, depending + on the optional whitelist parameter. + + If an object has a toJSON method, then the toJSON() method will be + called. The value returned from the toJSON method will be + stringified. + + Otherwise, if the optional whitelist parameter is an array, then + the elements of the array will be used to select members of the + object for stringification. + + Otherwise, if there is no whitelist parameter, then all of the + members of the object will be stringified. + + Values that do not have JSON representaions, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays will be replaced with null. + JSON.stringify(undefined) returns undefined. Dates will be + stringified as quoted ISO dates. + + Example: + + var text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + JSON.parse(text, filter) + This method parses a JSON text to produce an object or + array. It can throw a SyntaxError exception. + + The optional filter parameter is a function that can filter and + transform the results. It receives each of the keys and values, and + its return value is used instead of the original value. If it + returns what it received, then structure is not modified. If it + returns undefined then the member is deleted. + + Example: + + // Parse the text. If a key contains the string 'date' then + // convert the value to a date. + + myData = JSON.parse(text, function (key, value) { + return key.indexOf('date') >= 0 ? new Date(value) : value; + }); + + This is a reference implementation. You are free to copy, modify, or + redistribute. + + Use your own copy. It is extremely unwise to load third party + code into your pages. +*/ + +/*jslint evil: true */ + +/*global JSON */ + +/*members "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + charCodeAt, floor, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, length, + parse, propertyIsEnumerable, prototype, push, replace, stringify, test, + toJSON, toString +*/ + +if (!this.JSON) { + + JSON = function () { + + function f(n) { // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + Date.prototype.toJSON = function () { + +// Eventually, this method will be based on the date.toISOString method. + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + + var m = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }; + + function stringify(value, whitelist) { + var a, // The array holding the partial texts. + i, // The loop counter. + k, // The member key. + l, // Length. + r = /["\\\x00-\x1f\x7f-\x9f]/g, + v; // The member value. + + switch (typeof value) { + case 'string': + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe sequences. + + return r.test(value) ? + '"' + value.replace(r, function (a) { + var c = m[a]; + if (c) { + return c; + } + c = a.charCodeAt(); + return '\\u00' + Math.floor(c / 16).toString(16) + + (c % 16).toString(16); + }) + '"' : + '"' + value + '"'; + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + return String(value); + + case 'object': + +// Due to a specification blunder in ECMAScript, +// typeof null is 'object', so watch out for that case. + + if (!value) { + return 'null'; + } + +// If the object has a toJSON method, call it, and stringify the result. + + if (typeof value.toJSON === 'function') { + return stringify(value.toJSON()); + } + a = []; + if (typeof value.length === 'number' && + !(value.propertyIsEnumerable('length'))) { + +// The object is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + l = value.length; + for (i = 0; i < l; i += 1) { + a.push(stringify(value[i], whitelist) || 'null'); + } + +// Join all of the elements together and wrap them in brackets. + + return '[' + a.join(',') + ']'; + } + if (whitelist) { + +// If a whitelist (array of keys) is provided, use it to select the components +// of the object. + + l = whitelist.length; + for (i = 0; i < l; i += 1) { + k = whitelist[i]; + if (typeof k === 'string') { + v = stringify(value[k], whitelist); + if (v) { + a.push(stringify(k) + ':' + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (typeof k === 'string') { + v = stringify(value[k], whitelist); + if (v) { + a.push(stringify(k) + ':' + v); + } + } + } + } + +// Join all of the member texts together and wrap them in braces. + + return '{' + a.join(',') + '}'; + } + } + + return { + stringify: stringify, + parse: function (text, filter) { + var j; + + function walk(k, v) { + var i, n; + if (v && typeof v === 'object') { + for (i in v) { + if (Object.prototype.hasOwnProperty.apply(v, [i])) { + n = walk(i, v[i]); + if (n !== undefined) { + v[i] = n; + } else { + delete v[i]; + } + } + } + } + return filter(k, v); + } + + +// Parsing happens in three stages. In the first stage, we run the text against +// regular expressions that look for non-JSON patterns. We are especially +// concerned with '()' and 'new' because they can cause invocation, and '=' +// because it can cause mutation. But just to be safe, we want to reject all +// unexpected forms. + +// We split the first stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace all backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/.test(text.replace(/\\./g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the second stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional third stage, we recursively walk the new structure, passing +// each name/value pair to a filter function for possible transformation. + + return typeof filter === 'function' ? walk('', j) : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('parseJSON'); + } + }; + }(); +} diff --git a/modules/editor/skins/xquared/javascripts/Layer.js b/modules/editor/skins/xquared/javascripts/Layer.js new file mode 100644 index 000000000..9ecf0f657 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/Layer.js @@ -0,0 +1,79 @@ +/** + * @requires Xquared.js + * @requires Editor.js + */ +xq.Layer = xq.Class(/** @lends xq.Layer.prototype */{ + /** + * @constructs + * + * @param {xq.Editor} editor editor instance + * @param {Element} element designMode document's element. Layer instance will be attached to this element + * @param {String} html HTML for body. + */ + initialize: function(editor, element, html) { + xq.addToFinalizeQueue(this); + + this.margin = 4; + this.editor = editor; + this.element = element; + this.frame = this.editor._createIFrame(this.editor.getOuterDoc(), this.element.offsetWidth - (this.margin * 2) + "px", this.element.offsetHeight + (this.margin * 2) + "px"); + this.editor.getOuterDoc().body.appendChild(this.frame); + this.doc = editor._createDoc( + this.frame, + '', + [], null, null, html + ); + this.frame.style.position = "absolute"; + this.updatePosition(); + }, + + getFrame: function() { + return this.frame; + }, + + getDoc: function() { + return this.doc; + }, + + getBody: function() { + return this.doc.body; + }, + + isValid: function() { + return this.element && this.element.parentNode && this.element.offsetParent; + }, + + detach: function() { + this.frame.parentNode.removeChild(this.frame); + + this.frame = null; + this.element = null; + }, + + updatePosition: function() { + // calculate element position + var offset = xq.getCumulativeOffset(this.element, this.editor.rdom.getRoot()); + + // and scroll position + var doc = this.editor.getDoc(); + var body = this.editor.getBody(); + offset.left -= doc.documentElement.scrollLeft + body.scrollLeft - this.margin; + offset.top -= doc.documentElement.scrollTop + body.scrollTop - this.margin; + + // apply new position + this.frame.style.left = offset.left + "px"; + this.frame.style.top = offset.top + "px"; + + // perform autofit + var newWidth = this.doc.body.scrollWidth + (this.margin - 1) * 2; + var newHeight = this.doc.body.scrollHeight + (this.margin - 1) * 2; + + // without -1, the element increasing slowly. + this.element.width = newWidth; + this.element.height = newHeight; + + // resize frame + this.frame.style.width = this.element.offsetWidth - (this.margin * 2) + "px"; + this.frame.style.height = this.element.offsetHeight - (this.margin * 2) + "px"; + } +}); diff --git a/modules/editor/skins/xquared/javascripts/RichTable.js b/modules/editor/skins/xquared/javascripts/RichTable.js new file mode 100644 index 000000000..e5af9e1dd --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/RichTable.js @@ -0,0 +1,215 @@ +/** + * @requires Xquared.js + * @requires rdom/Base.js + */ +xq.RichTable = xq.Class(/** @lends xq.RichTable.prototype */{ + /** + * TODO: Add description + * + * @constructs + */ + initialize: function(rdom, table) { + xq.addToFinalizeQueue(this); + + this.rdom = rdom; + this.table = table; + }, + insertNewRowAt: function(tr, where) { + var row = this.rdom.createElement("TR"); + var cells = tr.cells; + for(var i = 0; i < cells.length; i++) { + var cell = this.rdom.createElement(cells[i].nodeName); + this.rdom.correctEmptyElement(cell); + row.appendChild(cell); + } + return this.rdom.insertNodeAt(row, tr, where); + }, + insertNewCellAt: function(cell, where) { + // collect cells; + var cells = []; + var x = this.getXIndexOf(cell); + var y = 0; + while(true) { + var cur = this.getCellAt(x, y); + if(!cur) break; + cells.push(cur); + y++; + } + + // insert new cells + for(var i = 0; i < cells.length; i++) { + var cell = this.rdom.createElement(cells[i].nodeName); + this.rdom.correctEmptyElement(cell); + this.rdom.insertNodeAt(cell, cells[i], where); + } + }, + deleteRow: function(tr) { + return this.rdom.removeBlock(tr); + }, + deleteCell: function(cell) { + if(!cell.previousSibling && !cell.nextSibling) { + this.rdom.deleteNode(this.table); + return; + } + + // collect cells; + var cells = []; + var x = this.getXIndexOf(cell); + var y = 0; + while(true) { + var cur = this.getCellAt(x, y); + if(!cur) break; + cells.push(cur); + y++; + } + + for(var i = 0; i < cells.length; i++) { + this.rdom.deleteNode(cells[i]); + } + }, + getPreviousCellOf: function(cell) { + if(cell.previousSibling) return cell.previousSibling; + var adjRow = this.getPreviousRowOf(cell.parentNode); + if(adjRow) return adjRow.lastChild; + return null; + }, + getNextCellOf: function(cell) { + if(cell.nextSibling) return cell.nextSibling; + var adjRow = this.getNextRowOf(cell.parentNode); + if(adjRow) return adjRow.firstChild; + return null; + }, + getPreviousRowOf: function(row) { + if(row.previousSibling) return row.previousSibling; + var rowContainer = row.parentNode; + if(rowContainer.previousSibling && rowContainer.previousSibling.lastChild) return rowContainer.previousSibling.lastChild; + return null; + }, + getNextRowOf: function(row) { + if(row.nextSibling) return row.nextSibling; + var rowContainer = row.parentNode; + if(rowContainer.nextSibling && rowContainer.nextSibling.firstChild) return rowContainer.nextSibling.firstChild; + return null; + }, + getAboveCellOf: function(cell) { + var row = this.getPreviousRowOf(cell.parentNode); + if(!row) return null; + + var x = this.getXIndexOf(cell); + return row.cells[x]; + }, + getBelowCellOf: function(cell) { + var row = this.getNextRowOf(cell.parentNode); + if(!row) return null; + + var x = this.getXIndexOf(cell); + return row.cells[x]; + }, + getXIndexOf: function(cell) { + var row = cell.parentNode; + for(var i = 0; i < row.cells.length; i++) { + if(row.cells[i] === cell) return i; + } + + return -1; + }, + getYIndexOf: function(cell) { + var y = -1; + + // find y + var group = row.parentNode; + for(var i = 0; i col) ? row.cells[col] : null; + }, + getRowAt: function(index) { + if(this.hasHeadingAtTop()) { + return index === 0 ? this.table.tHead.rows[0] : this.table.tBodies[0].rows[index - 1]; + } else { + var rows = this.table.tBodies[0].rows; + return (rows.length > index) ? rows[index] : null; + } + }, + getDom: function() { + return this.table; + }, + hasHeadingAtTop: function() { + return !!(this.table.tHead && this.table.tHead.rows[0]); + }, + hasHeadingAtLeft: function() { + return this.table.tBodies[0].rows[0].cells[0].nodeName === "TH"; + }, + correctEmptyCells: function() { + var cells = xq.$A(this.table.getElementsByTagName("TH")); + var tds = xq.$A(this.table.getElementsByTagName("TD")); + for(var i = 0; i < tds.length; i++) { + cells.push(tds[i]); + } + + for(var i = 0; i < cells.length; i++) { + if(this.rdom.isEmptyBlock(cells[i])) this.rdom.correctEmptyElement(cells[i]) + } + } +}); + +xq.RichTable.create = function(rdom, cols, rows, headerPositions) { + if(["t", "tl", "lt"].indexOf(headerPositions) !== -1) var headingAtTop = true + if(["l", "tl", "lt"].indexOf(headerPositions) !== -1) var headingAtLeft = true + + var sb = [] + sb.push('') + + // thead + if(headingAtTop) { + sb.push('') + for(var i = 0; i < cols; i++) sb.push('') + sb.push('') + rows -= 1 + } + + // tbody + sb.push('') + for(var i = 0; i < rows; i++) { + sb.push('') + + for(var j = 0; j < cols; j++) { + if(headingAtLeft && j === 0) { + sb.push('') + } else { + sb.push('') + } + } + + sb.push('') + } + sb.push('') + + sb.push('
') + + // create DOM element + var container = rdom.createElement("div"); + container.innerHTML = sb.join(""); + + // correct empty cells and return + var rtable = new xq.RichTable(rdom, container.firstChild); + rtable.correctEmptyCells(); + return rtable; +} \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/Shortcut.js b/modules/editor/skins/xquared/javascripts/Shortcut.js new file mode 100644 index 000000000..4eee13fbb --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/Shortcut.js @@ -0,0 +1,139 @@ +/** + * @requires Xquared.js + */ +xq.Shortcut = xq.Class(/** @lends xq.Shortcut.prototype */{ + /** + * Interpretes keyboard event. + * + * @constructs + */ + initialize: function(keymapOrExpression) { + xq.addToFinalizeQueue(this); + this.keymap = keymapOrExpression; + }, + matches: function(e) { + if(typeof this.keymap === "string") this.keymap = xq.Shortcut.interprete(this.keymap).keymap; + + // check for key code + 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(!keyMatches) return false; + + // check for modifier + if(typeof e.metaKey === "undefined") e.metaKey = false; + + var modifierMatches = + (this.keymap.shiftKey === e.shiftKey || typeof this.keymap.shiftKey === "undefined") && + (this.keymap.altKey === e.altKey || typeof this.keymap.altKey === "undefined") && + (this.keymap.ctrlKey === e.ctrlKey || typeof this.keymap.ctrlKey === "undefined") && + // Webkit turns on meta key flag when alt key is pressed + (xq.Browser.isWin && xq.Browser.isWebkit || this.keymap.metaKey === e.metaKey || typeof this.keymap.metaKey === "undefined") + + return modifierMatches; + } +}); + +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 + } diff --git a/modules/editor/skins/xquared/javascripts/Timer.js b/modules/editor/skins/xquared/javascripts/Timer.js new file mode 100644 index 000000000..d77d0909a --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/Timer.js @@ -0,0 +1,90 @@ +/** + * @requires Xquared.js + */ +xq.Timer = xq.Class(/** @lends xq.Timer.prototype */{ + /** + * @constructs + * + * @param {Number} precision precision in milliseconds + */ + initialize: function(precision) { + xq.addToFinalizeQueue(this); + + this.precision = precision; + this.jobs = {}; + this.nextJobId = 0; + + this.checker = null; + }, + + finalize: function() { + this.stop(); + }, + + /** + * starts timer + */ + start: function() { + this.stop(); + + this.checker = window.setInterval(function() { + this.executeJobs(); + }.bind(this), this.precision); + }, + + /** + * stops timer + */ + stop: function() { + if(this.checker) window.clearInterval(this.checker); + }, + + /** + * registers new job + * + * @param {Function} job function to execute + * @param {Number} interval interval in milliseconds + * + * @return {Number} job id + */ + register: function(job, interval) { + var jobId = this.nextJobId++; + + this.jobs[jobId] = { + func:job, + interval: interval, + lastExecution: Date.get() + }; + + return jobId; + }, + + /** + * unregister job by job id + * + * @param {Number} job id + */ + unregister: function(jobId) { + delete this.jobs[jobId]; + }, + + /** + * Execute all expired jobs immedialty. This method will be called automatically by interval timer. + */ + executeJobs: function() { + var curDate = new Date(); + + for(var id in this.jobs) { + var job = this.jobs[id]; + if(job.lastExecution.elapsed(job.interval, curDate)) { + try { + job.lastReturn = job.func(); + } catch(e) { + job.lastException = e; + } finally { + job.lastExecution = curDate; + } + } + } + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/XQuared.js b/modules/editor/skins/xquared/javascripts/XQuared.js new file mode 100644 index 000000000..dbb56d3c1 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/XQuared.js @@ -0,0 +1,708 @@ +/*! Xquared is copyrighted free software by Alan Kang . + * For more information, see http://xquared.springbook.playmaru.net/ + */ +if(!window.xq) { + /** + * @namespace Contains all variables. + */ + var xq = {}; +} + +xq.majorVersion = '0.7'; +xq.minorVersion = '20080402'; + + + +/** + * Compiles regular expression pattern if possible. + * + * @param {String} p Regular expression. + * @param {String} f Flags. + */ +xq.compilePattern = function(p, f) { + if(!RegExp.prototype.compile) return new RegExp(p, f); + + var r = new RegExp(); + r.compile(p, f); + return r; +} + + + +/** + * @class Simple class based OOP framework + */ +xq.Class = function() { + var parent = null, properties = xq.$A(arguments), key; + if (typeof properties[0] === "function") { + parent = properties.shift(); + } + + function klass() { + this.initialize.apply(this, arguments); + } + + if(parent) { + for (key in parent.prototype) { + klass.prototype[key] = parent.prototype[key]; + } + } + + for (key in properties[0]) if(properties[0].hasOwnProperty(key)){ + klass.prototype[key] = properties[0][key]; + } + + if (!klass.prototype.initialize) { + klass.prototype.initialize = function() {}; + } + + klass.prototype.constructor = klass; + + return klass; +}; + +/** + * Registers event handler + * + * @param {Element} element Target element. + * @param {String} eventName Name of event. For example "keydown". + * @param {Function} handler Event handler. + */ +xq.observe = function(element, eventName, handler) { + if (element.addEventListener) { + element.addEventListener(eventName, handler, false); + } else { + element.attachEvent('on' + eventName, handler); + } + element = null; +}; + +/** + * Unregisters event handler + */ +xq.stopObserving = function(element, eventName, handler) { + if (element.removeEventListener) { + element.removeEventListener(eventName, handler, false); + } else { + element.detachEvent("on" + eventName, handler); + } + element = null; +}; + +/** + * Predefined event handler which simply cancels given event + * + * @param {Event} e Event to cancel. + */ +xq.cancelHandler = function(e) {xq.stopEvent(e); return false;}; + +/** + * Stops event propagation. + * + * @param {Event} e Event to stop. + */ +xq.stopEvent = function(e) { + if(e.preventDefault) { + e.preventDefault(); + } + if(e.stopPropagation) { + e.stopPropagation(); + } + e.returnValue = false; + e.cancelBubble = true; + e.stopped = true; +}; + +xq.isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); +}; +xq.isLeftClick = function(event) {return xq.isButton(event, 0);}; +xq.isMiddleClick = function(event) {return xq.isButton(event, 1);}; +xq.isRightClick = function(event) {return xq.isButton(event, 2);}; + +xq.getEventPoint = function(event) { + return { + x: event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft)), + y: event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop)) + }; +}; + +xq.getCumulativeOffset = function(element, until) { + var top = 0, left = 0; + + do { + top += element.offsetTop || 0; + left += element.offsetLeft || 0; + element = element.offsetParent; + } while (element && element != until); + + return {top:top, left:left}; +}; + +xq.$ = function(id) { + return document.getElementById(id); +}; + +xq.isEmptyHash = function(h) { + for(var key in h) if(h.hasOwnProperty(key)){ + return false; + } + return true; +}; + +xq.emptyFunction = function() {}; + +xq.$A = function(arraylike) { + var len = arraylike.length, a = []; + while (len--) { + a[len] = arraylike[len]; + } + return a; +}; + +xq.addClassName = function(element, className) { + if (!xq.hasClassName(element, className)) { + element.className += (element.className ? ' ' : '') + className; + } + return element; +}; +xq.removeClassName = function(element, className) { + if (xq.hasClassName(element, className)) { + element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + } + return element; +}; +xq.hasClassName = function(element, className) { + var classNames = element.className; + return (classNames.length > 0 && (classNames === className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(classNames))); +}; + +xq.serializeForm = function(f) { + var options = {hash: true}; + var data = {}; + var elements = f.getElementsByTagName("*"); + for(var i = 0; i < elements.length; i++) { + var element = elements[i]; + var tagName = element.tagName.toLowerCase(); + if(element.disabled || !element.name || ['input', 'textarea', 'option', 'select'].indexOf(tagName) === -1) { + continue; + } + + var key = element.name; + var value = xq.getValueOfElement(element); + + if(value === undefined) { + continue; + } + + if(key in data) { + if(data[key].constructor === Array) { + data[key] = [data[key]]; + } + data[key].push(value); + } else { + data[key] = value; + } + } + return data; +}; + +xq.getValueOfElement = function(e) { + var type = e.type.toLowerCase(); + if(type === 'checkbox' || type === 'radio') { + return e.checked ? e.value : undefined; + } else { + return e.value; + } +}; + +/** + * Find elements by class name (and tag name) + * + * @param {Element} element Root element + * @param {String} className Target class name + * @param {String} tagName Optional tag name + */ +xq.getElementsByClassName = function(element, className, tagName) { + if(!tagName && element.getElementsByClassName) { + return element.getElementsByClassName(className); + } + + var elements = element.getElementsByTagName(tagName || "*"); + var len = elements.length; + var result = []; + var p = xq.compilePattern("(^|\\s)" + className + "($|\\s)", "i"); + for(var i = 0; i < len; i++) { + var cur = elements[i]; + if(p.test(cur.className)) { + result.push(cur); + } + } + return result; +}; + +if(!window.Prototype) { + if(!Function.prototype.bind) { + Function.prototype.bind = function() { + var m = this, arg = xq.$A(arguments), o = arg.shift(); + return function() { + return m.apply(o, arg.concat(xq.$A(arguments))); + }; + }; + } + + if(!Function.prototype.bindAsEventListener) { + Function.prototype.bindAsEventListener = function() { + var m = this, arg = xq.$A(arguments), o = arg.shift(); + return function(event) { + return m.apply(o, [event || window.event].concat(arg)); + }; + }; + } + + Array.prototype.find = function(f) { + for(var i = 0; i < this.length; i++) { + if(f(this[i])) { + return this[i]; + } + } + }; + + Array.prototype.findAll = function(f) { + var result = []; + for(var i = 0; i < this.length; i++) { + if(f(this[i])) { + result.push(this[i]); + } + } + return result; + }; + + Array.prototype.first = function() {return this[0];}; + + Array.prototype.last = function() {return this[this.length - 1];}; + + Array.prototype.flatten = function() { + var result = []; + var recursive = function(array) { + for(var i = 0; i < array.length; i++) { + if(array[i].constructor === Array) { + recursive(array[i]); + } else { + result.push(array[i]); + } + } + }; + recursive(this); + + return result; + }; + + xq.pStripTags = xq.compilePattern("]+>", "gi"); + String.prototype.stripTags = function() { + return this.replace(xq.pStripTags, ''); + }; + String.prototype.escapeHTML = function() { + xq.textNode.data = this; + return xq.divNode.innerHTML; + }; + xq.textNode = document.createTextNode(''); + xq.divNode = document.createElement('div'); + xq.divNode.appendChild(xq.textNode); + + xq.pStrip1 = xq.compilePattern("^\\s+"); + xq.pStrip2 = xq.compilePattern("\\s+$"); + String.prototype.strip = function() { + return this.replace(xq.pStrip1, '').replace(xq.pStrip2, ''); + }; + + Array.prototype.indexOf = function(n) { + for(var i = 0; i < this.length; i++) { + if(this[i] === n) { + return i; + } + } + + return -1; + }; +} + +Array.prototype.includeElement = function(o) { + if (this.indexOf(o) !== -1) { + return true; + } + + var found = false; + for(var i = 0; i < this.length; i++) { + if(this[i] === o) { + return true; + } + } + + return false; +}; + + +/** + * Make given object as event source + * + * @param {Object} object target object + * @param {String} prefix prefix for generated functions + * @param {Array} events array of string which contains name of events + */ +xq.asEventSource = function(object, prefix, events) { + object.autoRegisteredEventListeners = []; + object.registerEventFirer = function(prefix, name) { + this["_fireOn" + name] = function() { + for(var i = 0; i < this.autoRegisteredEventListeners.length; i++) { + var listener = this.autoRegisteredEventListeners[i]; + var func = listener["on" + prefix + name]; + if(func) { + func.apply(listener, xq.$A(arguments)); + } + } + }; + }; + object.addListener = function(l) { + this.autoRegisteredEventListeners.push(l); + }; + + for(var i = 0; i < events.length; i++) { + object.registerEventFirer(prefix, events[i]); + } +}; + + + +/** + * JSON to Element mapper + */ +xq.json2element = function(json, doc) { + var div = doc.createElement("DIV"); + div.innerHTML = xq.json2html(json); + return div.firstChild || {}; +}; + +/** + * Element to JSON mapper + */ +xq.element2json = function(element) { + var o, i, childElements; + + if(element.nodeName === 'DL') { + o = {}; + childElements = xq.findChildElements(element); + for(i = 0; i < childElements.length; i++) { + var dt = childElements[i]; + var dd = childElements[++i]; + o[dt.innerHTML] = xq.element2json(xq.findChildElements(dd)[0]); + } + return o; + } else if (element.nodeName === 'OL') { + o = []; + childElements = xq.findChildElements(element); + for(i = 0; i < childElements.length; i++) { + var li = childElements[i]; + o[i] = xq.element2json(xq.findChildElements(li)[0]); + } + } else if(element.nodeName === 'SPAN' && element.className === 'number') { + return parseFloat(element.innerHTML); + } else if(element.nodeName === 'SPAN' && element.className === 'string') { + return element.innerHTML; + } else { // ignore textnode or unknown tag + return null; + } +}; + +/** + * JSON to HTML string mapper + */ +xq.json2html = function(json) { + var sb = []; + xq._json2html(json, sb); + return sb.join(''); +}; + +xq._json2html = function(o, sb) { + if(typeof o === 'number') { + sb.push('' + o + ''); + } else if(typeof o === 'string') { + sb.push('' + o.escapeHTML() + ''); + } else if(o.constructor === Array) { + sb.push('
    '); + for(var i = 0; i < o.length; i++) { + sb.push('
  1. '); + xq._json2html(o[i], sb); + sb.push('
  2. '); + } + sb.push('
'); + } else { // Object + sb.push('
'); + for (var key in o) if (o.hasOwnProperty(key)) { + sb.push('
' + key + '
'); + sb.push('
'); + xq._json2html(o[key], sb); + sb.push('
'); + } + sb.push('
'); + } +}; + +xq.findChildElements = function(parent) { + var childNodes = parent.childNodes; + var elements = []; + for(var i = 0; i < childNodes.length; i++) { + if(childNodes[i].nodeType === 1) { + elements.push(childNodes[i]); + } + } + return elements; +}; + + + +Date.preset = null; +Date.pass = function(msec) { + if(Date.preset !== null) { + Date.preset = new Date(Date.preset.getTime() + msec); + } +}; +Date.get = function() { + return Date.preset === null ? new Date() : Date.preset; +}; +Date.prototype.elapsed = function(msec, curDate) { + return (curDate || Date.get()).getTime() - this.getTime() >= msec; +}; + +String.prototype.merge = function(data) { + var newString = this; + for(var k in data) if(data.hasOwnProperty(k)) { + newString = newString.replace("{" + k + "}", data[k]); + } + return newString; +}; +xq.pBlank = xq.compilePattern("^\\s*$"); +String.prototype.isBlank = function() { + return xq.pBlank.test(this); +}; +xq.pURL = xq.compilePattern("((((\\w+)://(((([^@:]+)(:([^@]+))?)@)?([^:/\\?#]+)?(:(\\d+))?))?([^\\?#]+)?)(\\?([^#]+))?)(#(.+))?"); +String.prototype.parseURL = function() { + var m = this.match(xq.pURL); + + var includeAnchor = m[0]; + var includeQuery = m[1] || undefined; + var includePath = m[2] || undefined; + var includeHost = m[3] || undefined; + var includeBase = null; + var protocol = m[4] || undefined; + var user = m[8] || undefined; + var password = m[10] || undefined; + var domain = m[11] || undefined; + var port = m[13] || undefined; + var path = m[14] || undefined; + var query = m[16] || undefined; + var anchor = m[18] || undefined; + + if(!path || path === '/') { + includeBase = includeHost + '/'; + } else { + var index = path.lastIndexOf('/'); + includeBase = includeHost + path.substring(0, index + 1); + } + + return { + includeAnchor: includeAnchor, + includeQuery: includeQuery, + includePath: includePath, + includeBase: includeBase, + includeHost: includeHost, + protocol: protocol, + user: user, + password: password, + domain: domain, + port: port, + path: path, + query: query, + anchor: anchor + }; +}; + + + +xq.commonAttrs = ['title', 'class', 'id', 'style'];; + +/** + * Pre-defined whitelist + */ +xq.predefinedWhitelist = { + 'a': xq.commonAttrs.concat('href', 'charset', 'rev', 'rel', 'type', 'hreflang', 'tabindex'), + 'abbr': xq.commonAttrs.concat(), + 'acronym': xq.commonAttrs.concat(), + 'address': xq.commonAttrs.concat(), + 'blockquote': xq.commonAttrs.concat('cite'), + 'br': xq.commonAttrs.concat(), + 'button': xq.commonAttrs.concat('disabled', 'type', 'name', 'value'), + 'caption': xq.commonAttrs.concat(), + 'cite': xq.commonAttrs.concat(), + 'code': xq.commonAttrs.concat(), + 'dd': xq.commonAttrs.concat(), + 'dfn': xq.commonAttrs.concat(), + 'div': xq.commonAttrs.concat(), + 'dl': xq.commonAttrs.concat(), + 'dt': xq.commonAttrs.concat(), + 'em': xq.commonAttrs.concat(), + 'embed': xq.commonAttrs.concat('src', 'width', 'height', 'allowscriptaccess', 'type', 'allowfullscreen', 'bgcolor'), + 'h1': xq.commonAttrs.concat(), + 'h2': xq.commonAttrs.concat(), + 'h3': xq.commonAttrs.concat(), + 'h4': xq.commonAttrs.concat(), + 'h5': xq.commonAttrs.concat(), + 'h6': xq.commonAttrs.concat(), + 'hr': xq.commonAttrs.concat(), + 'iframe': xq.commonAttrs.concat('name', 'src', 'frameborder', 'scrolling', 'width', 'height', 'longdesc'), + 'input': xq.commonAttrs.concat('type', 'name', 'value', 'size', 'checked', 'readonly', 'src', 'maxlength'), + 'img': xq.commonAttrs.concat('alt', 'width', 'height', 'src', 'longdesc'), + 'label': xq.commonAttrs.concat('for'), + 'kbd': xq.commonAttrs.concat(), + 'li': xq.commonAttrs.concat(), + 'object': xq.commonAttrs.concat('align', 'classid', 'codetype', 'archive', 'width', 'type', 'codebase', 'height', 'data', 'name', 'standby', 'declare'), + 'ol': xq.commonAttrs.concat(), + 'option': xq.commonAttrs.concat('disabled', 'selected', 'laabel', 'value'), + 'p': xq.commonAttrs.concat(), + 'param': xq.commonAttrs.concat('name', 'value', 'valuetype', 'type'), + 'pre': xq.commonAttrs.concat(), + 'q': xq.commonAttrs.concat('cite'), + 'samp': xq.commonAttrs.concat(), + 'script': xq.commonAttrs.concat('src', 'type'), + 'select': xq.commonAttrs.concat('disabled', 'size', 'multiple', 'name'), + 'span': xq.commonAttrs.concat(), + 'sup': xq.commonAttrs.concat(), + 'sub': xq.commonAttrs.concat(), + 'strong': xq.commonAttrs.concat(), + 'table': xq.commonAttrs.concat('summary', 'width'), + 'thead': xq.commonAttrs.concat(), + 'textarea': xq.commonAttrs.concat('cols', 'disabled', 'rows', 'readonly', 'name'), + 'tbody': xq.commonAttrs.concat(), + 'th': xq.commonAttrs.concat('colspan', 'rowspan'), + 'td': xq.commonAttrs.concat('colspan', 'rowspan'), + 'tr': xq.commonAttrs.concat(), + 'tt': xq.commonAttrs.concat(), + 'ul': xq.commonAttrs.concat(), + 'var': xq.commonAttrs.concat() +}; + + + +/** + * Automatic finalization queue + */ +xq.autoFinalizeQueue = []; + +/** + * Automatic finalizer + */ +xq.addToFinalizeQueue = function(obj) { + xq.autoFinalizeQueue.push(obj); +}; + +/** + * Finalizes given object + */ +xq.finalize = function(obj) { + if(typeof obj.finalize === "function") { + try {obj.finalize();} catch(ignored) {} + } + + for(var key in obj) if(obj.hasOwnProperty(key)) { + obj[key] = null; + } +}; + +xq.observe(window, "unload", function() { + // "xq" and "xq.autoFinalizeQueue" could be removed by another libraries' clean-up mechanism. + if(xq && xq.autoFinalizeQueue) { + for(var i = 0; i < xq.autoFinalizeQueue.length; i++) { + xq.finalize(xq.autoFinalizeQueue[i]); + } + xq = null; + } +}); + + + +/** + * Finds Xquared's '); +}; + +/** + * Returns all Xquared script file names + */ +xq.getXquaredScriptFileNames = function() { + return [ + 'Xquared.js', + 'Browser.js', + 'DomTree.js', + 'rdom/Base.js', + 'rdom/W3.js', + 'rdom/Gecko.js', + 'rdom/Webkit.js', + 'rdom/Trident.js', + 'rdom/Factory.js', + 'validator/Base.js', + 'validator/W3.js', + 'validator/Gecko.js', + 'validator/Webkit.js', + 'validator/Trident.js', + 'validator/Factory.js', + 'macro/Base.js', + 'macro/Factory.js', + 'macro/FlashMovieMacro.js', + 'macro/IFrameMacro.js', + 'macro/JavascriptMacro.js', + 'EditHistory.js', + 'plugin/Base.js', + 'RichTable.js', + 'Timer.js', + 'Layer.js', + 'ui/Base.js', + 'ui/Control.js', + 'ui/Toolbar.js', + 'ui/_templates.js', + 'Json2.js', + 'Shortcut.js', + 'Editor.js' + ]; +}; +xq.getXquaredScriptBasePath = function() { + var script = xq.findXquaredScript(); + return script.src.match(/(.*\/)xquared\.js.*/i)[1]; +}; + +xq.loadOthers = function() { + var basePath = xq.getXquaredScriptBasePath(); + var others = xq.getXquaredScriptFileNames(); + + // Xquared.js(this file) should not be loaded again. So the value of "i" starts with 1 instead of 0 + for(var i = 1; i < others.length; i++) { + xq.loadScript(basePath + others[i]); + } +}; + +if(xq.shouldLoadOthers()) { + xq.loadOthers(); +} \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/macro/Base.js b/modules/editor/skins/xquared/javascripts/macro/Base.js new file mode 100644 index 000000000..394a83c7b --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/macro/Base.js @@ -0,0 +1,55 @@ +/** + * @namespace + */ +xq.macro = {}; + +/** + * @requires Xquared.js + */ +xq.macro.Base = xq.Class(/** @lends xq.macro.Base.prototype */{ + /** + * @constructs + * + * @param {Object} Parameters or HTML fragment. + * @param {String} URL to place holder image. + */ + initialize: function(id, paramsOrHtml, placeHolderImgSrc) { + this.id = id; + this.placeHolderImgSrc = placeHolderImgSrc; + + if(typeof paramsOrHtml === "string") { + this.html = paramsOrHtml; + this.params = {}; + + this.initFromHtml(); + } else { + this.html = null; + this.params = paramsOrHtml; + + this.initFromParams(); + } + }, + + initFromHtml: function() {}, + initFromParams: function() {}, + createHtml: function() {throw "Not implemented";}, + onLayerInitialzied: function(layer) {}, + + createPlaceHolderHtml: function() { + var size = {width: 5, height: 5}; + var def = {}; + def.id = this.id; + def.params = this.params; + + sb = []; + sb.push(''); + + return sb.join(''); + } +}) \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/macro/Factory.js b/modules/editor/skins/xquared/javascripts/macro/Factory.js new file mode 100644 index 000000000..af4dd3814 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/macro/Factory.js @@ -0,0 +1,52 @@ +/** + * @requires Xquared.js + * @requires macro/Base.js + */ +xq.macro.Factory = xq.Class(/** @lends xq.macro.Factory.prototype */{ + /** + * @constructs + * + * @param {String} URL to place holder image. + */ + initialize: function(placeHolderImgSrc) { + this.placeHolderImgSrc = placeHolderImgSrc; + this.macroClazzes = {}; + }, + /** + * Registers new macro by ID. + * + * @param {String} id Macro id. + */ + register: function(id) { + var clazz = xq.macro[id + "Macro"]; + if(!clazz) throw "Unknown macro id: [" + id + "]"; + + this.macroClazzes[id] = clazz; + }, + /** + * Creates macro instance by given HTML fragment. + * + * @param {String} html HTML fragment. + * @returns {xq.macro.Base} Macro instance or null if recognization of the HTML fragment fails. + */ + createMacroFromHtml: function(html) { + for(var id in this.macroClazzes) { + var clazz = this.macroClazzes[id]; + if(clazz.recognize(html)) return new clazz(id, html, this.placeHolderImgSrc); + } + return null; + }, + /** + * Creates macro instance by given macro definition. + * + * @param {Object} def Macro definition. + * @returns {xq.macro.Base} Macro instance + * @throws If macro not found by def[id]. + */ + createMacroFromDefinition: function(def) { + var clazz = this.macroClazzes[def.id]; + if(!clazz) return null; + + return new clazz(def.id, def.params, this.placeHolderImgSrc); + } +}) \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/macro/FlashMovieMacro.js b/modules/editor/skins/xquared/javascripts/macro/FlashMovieMacro.js new file mode 100644 index 000000000..8e0a9ed03 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/macro/FlashMovieMacro.js @@ -0,0 +1,39 @@ +/** + * @requires macro/Base.js + */ +xq.macro.FlashMovieMacro = xq.Class(xq.macro.Base, + /** + * Flash movie macro + * + * @name xq.macro.FlashMovieMacro + * @lends xq.macro.FlashMovieMacro.prototype + * @extends xq.macro.Base + * @constructor + */ + { + initFromHtml: function() { + this.params.html = this.html; + }, + initFromParams: function() { + if(!xq.macro.FlashMovieMacro.recognize(this.params.html)) throw "Unknown src"; + }, + createHtml: function() { + return this.params.html; + } +}); +xq.macro.FlashMovieMacro.recognize = function(html) { + var providers = { + tvpot: /http:\/\/flvs\.daum\.net\/flvPlayer\.swf\?/, + youtube: /http:\/\/(?:www\.)?youtube\.com\/v\//, + pandoratv: /http:\/\/flvr\.pandora\.tv\/flv2pan\/flvmovie\.dll\?/, + pandoratv2: /http:\/\/imgcdn\.pandora\.tv\/gplayer\/pandora\_EGplayer\.swf\?/, + mncast: /http:\/\/dory\.mncast\.com\/mncHMovie\.swf\?/, + yahoo: /http:\/\/d\.yimg\.com\// + }; + + for(var id in providers) { + if(html.match(providers[id])) return true; + } + + return false; +} \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/macro/IFrameMacro.js b/modules/editor/skins/xquared/javascripts/macro/IFrameMacro.js new file mode 100644 index 000000000..8f481ee82 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/macro/IFrameMacro.js @@ -0,0 +1,36 @@ +/** + * @requires macro/Base.js + */ +xq.macro.IFrameMacro = xq.Class(xq.macro.Base, + /** + * IFrame macro + * + * @name xq.macro.IFrameMacro + * @lends xq.macro.IFrameMacro.prototype + * @extends xq.macro.Base + * @constructor + */ + { + initFromHtml: function() { + this.params.html = this.html; + }, + initFromParams: function() { + if(this.params.html) return; + + var sb = []; + sb.push(''); + this.params = {html:sb.join("")}; + }, + createHtml: function() { + return this.params.html; + } +}); +xq.macro.IFrameMacro.recognize = function(html) { + var p = xq.compilePattern("]+(?:/>|>.*?)", "img"); + return !!html.match(p); +} \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/macro/JavascriptMacro.js b/modules/editor/skins/xquared/javascripts/macro/JavascriptMacro.js new file mode 100644 index 000000000..b7d9f6b86 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/macro/JavascriptMacro.js @@ -0,0 +1,42 @@ +/** + * @requires macro/Base.js + */ +xq.macro.JavascriptMacro = xq.Class(xq.macro.Base, + /** + * Javascript macro + * + * @name xq.macro.JavascriptMacro + * @lends xq.macro.JavascriptMacro.prototype + * @extends xq.macro.Base + * @constructor + */ + { + initFromHtml: function() { + var p = xq.compilePattern("src=[\"'](.+?)[\"']", "img"); + this.params.url = p.exec(this.html)[1]; + }, + initFromParams: function() { + if(!xq.macro.JavascriptMacro.isSafeScript(this.params.url)) throw "Unknown src"; + }, + createHtml: function() {return ''}, + + onLayerInitialzied: function(layer) { + layer.getDoc().write(this.createHtml()); + } +}); + +xq.macro.JavascriptMacro.recognize = function(html) { + var p = xq.compilePattern("]*src=[\"']([^\"']+)[\"'][^>]*(?:/>|>.*?)", "img"); + var m = p.exec(html); + if(!m || !m[1]) return false; + return this.isSafeScript(m[1]); +} +xq.macro.JavascriptMacro.isSafeScript = function(url) { + var safeSrcs = { + googleGadget: /http:\/\/gmodules\.com\/ig\/ifr\?/img + }; + for(var id in safeSrcs) { + if(url.match(safeSrcs[id])) return true; + } + return false; +} diff --git a/modules/editor/skins/xquared/javascripts/module/Full.js b/modules/editor/skins/xquared/javascripts/module/Full.js new file mode 100644 index 000000000..849cde049 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/module/Full.js @@ -0,0 +1,9 @@ +/** + * @requires Xquared.js + * @requires Editor.js + * @requires plugin/MacroPlugin.js + * @requires plugin/FlashMovieMacroPlugin.js + * @requires plugin/IFrameMacroPlugin.js + * @requires plugin/JavascriptMacroPlugin.js + */ +xq.moduleName = "Full"; \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/module/Full_list b/modules/editor/skins/xquared/javascripts/module/Full_list new file mode 100644 index 000000000..c5b6a5c2d --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/module/Full_list @@ -0,0 +1,37 @@ +Xquared.js +Browser.js +Timer.js +DomTree.js +rdom/Base.js +rdom/Trident.js +rdom/W3.js +rdom/Gecko.js +rdom/Webkit.js +rdom/Factory.js +validator/Base.js +validator/Trident.js +validator/W3.js +validator/Gecko.js +validator/Webkit.js +validator/Factory.js +EditHistory.js +plugin/Base.js +RichTable.js +ui/Base.js +ui/Control.js +ui/Toolbar.js +ui/_templates.js +Shortcut.js +Editor.js +macro/Base.js +macro/Factory.js +Layer.js +Json2.js +plugin/MacroPlugin.js +macro/FlashMovieMacro.js +plugin/FlashMovieMacroPlugin.js +macro/IFrameMacro.js +plugin/IFrameMacroPlugin.js +macro/JavascriptMacro.js +plugin/JavascriptMacroPlugin.js +module/Full.js \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/module/Full_merged.js b/modules/editor/skins/xquared/javascripts/module/Full_merged.js new file mode 100644 index 000000000..4d782d650 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/module/Full_merged.js @@ -0,0 +1,9999 @@ +/*! Xquared is copyrighted free software by Alan Kang . + * For more information, see http://xquared.springbook.playmaru.net/ + */ +if(!window.xq) { + /** + * @namespace Contains all variables. + */ + var xq = {}; +} + +xq.majorVersion = '0.7'; +xq.minorVersion = '20080402'; + + + +/** + * Compiles regular expression pattern if possible. + * + * @param {String} p Regular expression. + * @param {String} f Flags. + */ +xq.compilePattern = function(p, f) { + if(!RegExp.prototype.compile) return new RegExp(p, f); + + var r = new RegExp(); + r.compile(p, f); + return r; +} + + + +/** + * @class Simple class based OOP framework + */ +xq.Class = function() { + var parent = null, properties = xq.$A(arguments), key; + if (typeof properties[0] === "function") { + parent = properties.shift(); + } + + function klass() { + this.initialize.apply(this, arguments); + } + + if(parent) { + for (key in parent.prototype) { + klass.prototype[key] = parent.prototype[key]; + } + } + + for (key in properties[0]) if(properties[0].hasOwnProperty(key)){ + klass.prototype[key] = properties[0][key]; + } + + if (!klass.prototype.initialize) { + klass.prototype.initialize = function() {}; + } + + klass.prototype.constructor = klass; + + return klass; +}; + +/** + * Registers event handler + * + * @param {Element} element Target element. + * @param {String} eventName Name of event. For example "keydown". + * @param {Function} handler Event handler. + */ +xq.observe = function(element, eventName, handler) { + if (element.addEventListener) { + element.addEventListener(eventName, handler, false); + } else { + element.attachEvent('on' + eventName, handler); + } + element = null; +}; + +/** + * Unregisters event handler + */ +xq.stopObserving = function(element, eventName, handler) { + if (element.removeEventListener) { + element.removeEventListener(eventName, handler, false); + } else { + element.detachEvent("on" + eventName, handler); + } + element = null; +}; + +/** + * Predefined event handler which simply cancels given event + * + * @param {Event} e Event to cancel. + */ +xq.cancelHandler = function(e) {xq.stopEvent(e); return false;}; + +/** + * Stops event propagation. + * + * @param {Event} e Event to stop. + */ +xq.stopEvent = function(e) { + if(e.preventDefault) { + e.preventDefault(); + } + if(e.stopPropagation) { + e.stopPropagation(); + } + e.returnValue = false; + e.cancelBubble = true; + e.stopped = true; +}; + +xq.isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); +}; +xq.isLeftClick = function(event) {return xq.isButton(event, 0);}; +xq.isMiddleClick = function(event) {return xq.isButton(event, 1);}; +xq.isRightClick = function(event) {return xq.isButton(event, 2);}; + +xq.getEventPoint = function(event) { + return { + x: event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft)), + y: event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop)) + }; +}; + +xq.getCumulativeOffset = function(element, until) { + var top = 0, left = 0; + + do { + top += element.offsetTop || 0; + left += element.offsetLeft || 0; + element = element.offsetParent; + } while (element && element != until); + + return {top:top, left:left}; +}; + +xq.$ = function(id) { + return document.getElementById(id); +}; + +xq.isEmptyHash = function(h) { + for(var key in h) if(h.hasOwnProperty(key)){ + return false; + } + return true; +}; + +xq.emptyFunction = function() {}; + +xq.$A = function(arraylike) { + var len = arraylike.length, a = []; + while (len--) { + a[len] = arraylike[len]; + } + return a; +}; + +xq.addClassName = function(element, className) { + if (!xq.hasClassName(element, className)) { + element.className += (element.className ? ' ' : '') + className; + } + return element; +}; +xq.removeClassName = function(element, className) { + if (xq.hasClassName(element, className)) { + element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + } + return element; +}; +xq.hasClassName = function(element, className) { + var classNames = element.className; + return (classNames.length > 0 && (classNames === className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(classNames))); +}; + +xq.serializeForm = function(f) { + var options = {hash: true}; + var data = {}; + var elements = f.getElementsByTagName("*"); + for(var i = 0; i < elements.length; i++) { + var element = elements[i]; + var tagName = element.tagName.toLowerCase(); + if(element.disabled || !element.name || ['input', 'textarea', 'option', 'select'].indexOf(tagName) === -1) { + continue; + } + + var key = element.name; + var value = xq.getValueOfElement(element); + + if(value === undefined) { + continue; + } + + if(key in data) { + if(data[key].constructor === Array) { + data[key] = [data[key]]; + } + data[key].push(value); + } else { + data[key] = value; + } + } + return data; +}; + +xq.getValueOfElement = function(e) { + var type = e.type.toLowerCase(); + if(type === 'checkbox' || type === 'radio') { + return e.checked ? e.value : undefined; + } else { + return e.value; + } +}; + +/** + * Find elements by class name (and tag name) + * + * @param {Element} element Root element + * @param {String} className Target class name + * @param {String} tagName Optional tag name + */ +xq.getElementsByClassName = function(element, className, tagName) { + if(!tagName && element.getElementsByClassName) { + return element.getElementsByClassName(className); + } + + var elements = element.getElementsByTagName(tagName || "*"); + var len = elements.length; + var result = []; + var p = xq.compilePattern("(^|\\s)" + className + "($|\\s)", "i"); + for(var i = 0; i < len; i++) { + var cur = elements[i]; + if(p.test(cur.className)) { + result.push(cur); + } + } + return result; +}; + +if(!window.Prototype) { + if(!Function.prototype.bind) { + Function.prototype.bind = function() { + var m = this, arg = xq.$A(arguments), o = arg.shift(); + return function() { + return m.apply(o, arg.concat(xq.$A(arguments))); + }; + }; + } + + if(!Function.prototype.bindAsEventListener) { + Function.prototype.bindAsEventListener = function() { + var m = this, arg = xq.$A(arguments), o = arg.shift(); + return function(event) { + return m.apply(o, [event || window.event].concat(arg)); + }; + }; + } + + Array.prototype.find = function(f) { + for(var i = 0; i < this.length; i++) { + if(f(this[i])) { + return this[i]; + } + } + }; + + Array.prototype.findAll = function(f) { + var result = []; + for(var i = 0; i < this.length; i++) { + if(f(this[i])) { + result.push(this[i]); + } + } + return result; + }; + + Array.prototype.first = function() {return this[0];}; + + Array.prototype.last = function() {return this[this.length - 1];}; + + Array.prototype.flatten = function() { + var result = []; + var recursive = function(array) { + for(var i = 0; i < array.length; i++) { + if(array[i].constructor === Array) { + recursive(array[i]); + } else { + result.push(array[i]); + } + } + }; + recursive(this); + + return result; + }; + + xq.pStripTags = xq.compilePattern("]+>", "gi"); + String.prototype.stripTags = function() { + return this.replace(xq.pStripTags, ''); + }; + String.prototype.escapeHTML = function() { + xq.textNode.data = this; + return xq.divNode.innerHTML; + }; + xq.textNode = document.createTextNode(''); + xq.divNode = document.createElement('div'); + xq.divNode.appendChild(xq.textNode); + + xq.pStrip1 = xq.compilePattern("^\\s+"); + xq.pStrip2 = xq.compilePattern("\\s+$"); + String.prototype.strip = function() { + return this.replace(xq.pStrip1, '').replace(xq.pStrip2, ''); + }; + + Array.prototype.indexOf = function(n) { + for(var i = 0; i < this.length; i++) { + if(this[i] === n) { + return i; + } + } + + return -1; + }; +} + +Array.prototype.includeElement = function(o) { + if (this.indexOf(o) !== -1) { + return true; + } + + var found = false; + for(var i = 0; i < this.length; i++) { + if(this[i] === o) { + return true; + } + } + + return false; +}; + + +/** + * Make given object as event source + * + * @param {Object} object target object + * @param {String} prefix prefix for generated functions + * @param {Array} events array of string which contains name of events + */ +xq.asEventSource = function(object, prefix, events) { + object.autoRegisteredEventListeners = []; + object.registerEventFirer = function(prefix, name) { + this["_fireOn" + name] = function() { + for(var i = 0; i < this.autoRegisteredEventListeners.length; i++) { + var listener = this.autoRegisteredEventListeners[i]; + var func = listener["on" + prefix + name]; + if(func) { + func.apply(listener, xq.$A(arguments)); + } + } + }; + }; + object.addListener = function(l) { + this.autoRegisteredEventListeners.push(l); + }; + + for(var i = 0; i < events.length; i++) { + object.registerEventFirer(prefix, events[i]); + } +}; + + + +/** + * JSON to Element mapper + */ +xq.json2element = function(json, doc) { + var div = doc.createElement("DIV"); + div.innerHTML = xq.json2html(json); + return div.firstChild || {}; +}; + +/** + * Element to JSON mapper + */ +xq.element2json = function(element) { + var o, i, childElements; + + if(element.nodeName === 'DL') { + o = {}; + childElements = xq.findChildElements(element); + for(i = 0; i < childElements.length; i++) { + var dt = childElements[i]; + var dd = childElements[++i]; + o[dt.innerHTML] = xq.element2json(xq.findChildElements(dd)[0]); + } + return o; + } else if (element.nodeName === 'OL') { + o = []; + childElements = xq.findChildElements(element); + for(i = 0; i < childElements.length; i++) { + var li = childElements[i]; + o[i] = xq.element2json(xq.findChildElements(li)[0]); + } + } else if(element.nodeName === 'SPAN' && element.className === 'number') { + return parseFloat(element.innerHTML); + } else if(element.nodeName === 'SPAN' && element.className === 'string') { + return element.innerHTML; + } else { // ignore textnode or unknown tag + return null; + } +}; + +/** + * JSON to HTML string mapper + */ +xq.json2html = function(json) { + var sb = []; + xq._json2html(json, sb); + return sb.join(''); +}; + +xq._json2html = function(o, sb) { + if(typeof o === 'number') { + sb.push('' + o + ''); + } else if(typeof o === 'string') { + sb.push('' + o.escapeHTML() + ''); + } else if(o.constructor === Array) { + sb.push('
    '); + for(var i = 0; i < o.length; i++) { + sb.push('
  1. '); + xq._json2html(o[i], sb); + sb.push('
  2. '); + } + sb.push('
'); + } else { // Object + sb.push('
'); + for (var key in o) if (o.hasOwnProperty(key)) { + sb.push('
' + key + '
'); + sb.push('
'); + xq._json2html(o[key], sb); + sb.push('
'); + } + sb.push('
'); + } +}; + +xq.findChildElements = function(parent) { + var childNodes = parent.childNodes; + var elements = []; + for(var i = 0; i < childNodes.length; i++) { + if(childNodes[i].nodeType === 1) { + elements.push(childNodes[i]); + } + } + return elements; +}; + + + +Date.preset = null; +Date.pass = function(msec) { + if(Date.preset !== null) { + Date.preset = new Date(Date.preset.getTime() + msec); + } +}; +Date.get = function() { + return Date.preset === null ? new Date() : Date.preset; +}; +Date.prototype.elapsed = function(msec, curDate) { + return (curDate || Date.get()).getTime() - this.getTime() >= msec; +}; + +String.prototype.merge = function(data) { + var newString = this; + for(var k in data) if(data.hasOwnProperty(k)) { + newString = newString.replace("{" + k + "}", data[k]); + } + return newString; +}; +xq.pBlank = xq.compilePattern("^\\s*$"); +String.prototype.isBlank = function() { + return xq.pBlank.test(this); +}; +xq.pURL = xq.compilePattern("((((\\w+)://(((([^@:]+)(:([^@]+))?)@)?([^:/\\?#]+)?(:(\\d+))?))?([^\\?#]+)?)(\\?([^#]+))?)(#(.+))?"); +String.prototype.parseURL = function() { + var m = this.match(xq.pURL); + + var includeAnchor = m[0]; + var includeQuery = m[1] || undefined; + var includePath = m[2] || undefined; + var includeHost = m[3] || undefined; + var includeBase = null; + var protocol = m[4] || undefined; + var user = m[8] || undefined; + var password = m[10] || undefined; + var domain = m[11] || undefined; + var port = m[13] || undefined; + var path = m[14] || undefined; + var query = m[16] || undefined; + var anchor = m[18] || undefined; + + if(!path || path === '/') { + includeBase = includeHost + '/'; + } else { + var index = path.lastIndexOf('/'); + includeBase = includeHost + path.substring(0, index + 1); + } + + return { + includeAnchor: includeAnchor, + includeQuery: includeQuery, + includePath: includePath, + includeBase: includeBase, + includeHost: includeHost, + protocol: protocol, + user: user, + password: password, + domain: domain, + port: port, + path: path, + query: query, + anchor: anchor + }; +}; + + + +xq.commonAttrs = ['title', 'class', 'id', 'style'];; + +/** + * Pre-defined whitelist + */ +xq.predefinedWhitelist = { + 'a': xq.commonAttrs.concat('href', 'charset', 'rev', 'rel', 'type', 'hreflang', 'tabindex'), + 'abbr': xq.commonAttrs.concat(), + 'acronym': xq.commonAttrs.concat(), + 'address': xq.commonAttrs.concat(), + 'blockquote': xq.commonAttrs.concat('cite'), + 'br': xq.commonAttrs.concat(), + 'button': xq.commonAttrs.concat('disabled', 'type', 'name', 'value'), + 'caption': xq.commonAttrs.concat(), + 'cite': xq.commonAttrs.concat(), + 'code': xq.commonAttrs.concat(), + 'dd': xq.commonAttrs.concat(), + 'dfn': xq.commonAttrs.concat(), + 'div': xq.commonAttrs.concat(), + 'dl': xq.commonAttrs.concat(), + 'dt': xq.commonAttrs.concat(), + 'em': xq.commonAttrs.concat(), + 'embed': xq.commonAttrs.concat('src', 'width', 'height', 'allowscriptaccess', 'type', 'allowfullscreen', 'bgcolor'), + 'h1': xq.commonAttrs.concat(), + 'h2': xq.commonAttrs.concat(), + 'h3': xq.commonAttrs.concat(), + 'h4': xq.commonAttrs.concat(), + 'h5': xq.commonAttrs.concat(), + 'h6': xq.commonAttrs.concat(), + 'hr': xq.commonAttrs.concat(), + 'iframe': xq.commonAttrs.concat('name', 'src', 'frameborder', 'scrolling', 'width', 'height', 'longdesc'), + 'input': xq.commonAttrs.concat('type', 'name', 'value', 'size', 'checked', 'readonly', 'src', 'maxlength'), + 'img': xq.commonAttrs.concat('alt', 'width', 'height', 'src', 'longdesc'), + 'label': xq.commonAttrs.concat('for'), + 'kbd': xq.commonAttrs.concat(), + 'li': xq.commonAttrs.concat(), + 'object': xq.commonAttrs.concat('align', 'classid', 'codetype', 'archive', 'width', 'type', 'codebase', 'height', 'data', 'name', 'standby', 'declare'), + 'ol': xq.commonAttrs.concat(), + 'option': xq.commonAttrs.concat('disabled', 'selected', 'laabel', 'value'), + 'p': xq.commonAttrs.concat(), + 'param': xq.commonAttrs.concat('name', 'value', 'valuetype', 'type'), + 'pre': xq.commonAttrs.concat(), + 'q': xq.commonAttrs.concat('cite'), + 'samp': xq.commonAttrs.concat(), + 'script': xq.commonAttrs.concat('src', 'type'), + 'select': xq.commonAttrs.concat('disabled', 'size', 'multiple', 'name'), + 'span': xq.commonAttrs.concat(), + 'sup': xq.commonAttrs.concat(), + 'sub': xq.commonAttrs.concat(), + 'strong': xq.commonAttrs.concat(), + 'table': xq.commonAttrs.concat('summary', 'width'), + 'thead': xq.commonAttrs.concat(), + 'textarea': xq.commonAttrs.concat('cols', 'disabled', 'rows', 'readonly', 'name'), + 'tbody': xq.commonAttrs.concat(), + 'th': xq.commonAttrs.concat('colspan', 'rowspan'), + 'td': xq.commonAttrs.concat('colspan', 'rowspan'), + 'tr': xq.commonAttrs.concat(), + 'tt': xq.commonAttrs.concat(), + 'ul': xq.commonAttrs.concat(), + 'var': xq.commonAttrs.concat() +}; + + + +/** + * Automatic finalization queue + */ +xq.autoFinalizeQueue = []; + +/** + * Automatic finalizer + */ +xq.addToFinalizeQueue = function(obj) { + xq.autoFinalizeQueue.push(obj); +}; + +/** + * Finalizes given object + */ +xq.finalize = function(obj) { + if(typeof obj.finalize === "function") { + try {obj.finalize();} catch(ignored) {} + } + + for(var key in obj) if(obj.hasOwnProperty(key)) { + obj[key] = null; + } +}; + +xq.observe(window, "unload", function() { + // "xq" and "xq.autoFinalizeQueue" could be removed by another libraries' clean-up mechanism. + if(xq && xq.autoFinalizeQueue) { + for(var i = 0; i < xq.autoFinalizeQueue.length; i++) { + xq.finalize(xq.autoFinalizeQueue[i]); + } + xq = null; + } +}); + + + +/** + * Finds Xquared's '); +}; + +/** + * Returns all Xquared script file names + */ +xq.getXquaredScriptFileNames = function() { + return [ + 'Xquared.js', + 'Browser.js', + 'DomTree.js', + 'rdom/Base.js', + 'rdom/W3.js', + 'rdom/Gecko.js', + 'rdom/Webkit.js', + 'rdom/Trident.js', + 'rdom/Factory.js', + 'validator/Base.js', + 'validator/W3.js', + 'validator/Gecko.js', + 'validator/Webkit.js', + 'validator/Trident.js', + 'validator/Factory.js', + 'macro/Base.js', + 'macro/Factory.js', + 'macro/FlashMovieMacro.js', + 'macro/IFrameMacro.js', + 'macro/JavascriptMacro.js', + 'EditHistory.js', + 'plugin/Base.js', + 'RichTable.js', + 'Timer.js', + 'Layer.js', + 'ui/Base.js', + 'ui/Control.js', + 'ui/Toolbar.js', + 'ui/_templates.js', + 'Json2.js', + 'Shortcut.js', + 'Editor.js' + ]; +}; +xq.getXquaredScriptBasePath = function() { + var script = xq.findXquaredScript(); + return script.src.match(/(.*\/)xquared\.js.*/i)[1]; +}; + +xq.loadOthers = function() { + var basePath = xq.getXquaredScriptBasePath(); + var others = xq.getXquaredScriptFileNames(); + + // Xquared.js(this file) should not be loaded again. So the value of "i" starts with 1 instead of 0 + for(var i = 1; i < others.length; i++) { + xq.loadScript(basePath + others[i]); + } +}; + +if(xq.shouldLoadOthers()) { + xq.loadOthers(); +} +/** + * @namespace Contains browser detection codes + * + * @requires Xquared.js + */ +xq.Browser = new function() { + // By Rendering Engines + + /** + * True if rendering engine is Trident + * @type boolean + */ + this.isTrident = navigator.appName === "Microsoft Internet Explorer", + + /** + * True if rendering engine is Webkit + * @type boolean + */ + this.isWebkit = navigator.userAgent.indexOf('AppleWebKit/') > -1, + + /** + * True if rendering engine is Gecko + * @type boolean + */ + this.isGecko = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1, + + /** + * True if rendering engine is KHTML + * @type boolean + */ + this.isKHTML = navigator.userAgent.indexOf('KHTML') !== -1, + + /** + * True if rendering engine is Presto + * @type boolean + */ + this.isPresto = navigator.appName === "Opera", + + + + // By Platforms + /** + * True if platform is Mac + * @type boolean + */ + this.isMac = navigator.userAgent.indexOf("Macintosh") !== -1, + + /** + * True if platform is Ubuntu Linux + * @type boolean + */ + this.isUbuntu = navigator.userAgent.indexOf('Ubuntu') !== -1, + + /** + * True if platform is Windows + * @type boolean + */ + this.isWin = navigator.userAgent.indexOf('Windows') !== -1, + + + + // By Browsers + /** + * True if browser is Internet Explorer + * @type boolean + */ + this.isIE = navigator.appName === "Microsoft Internet Explorer", + + /** + * True if browser is Internet Explorer 6 + * @type boolean + */ + this.isIE6 = navigator.userAgent.indexOf('MSIE 6') !== -1, + + /** + * True if browser is Internet Explorer 7 + * @type boolean + */ + this.isIE7 = navigator.userAgent.indexOf('MSIE 7') !== -1, + + /** + * True if browser is Internet Explorer 8 + * @type boolean + */ + this.isIE8 = navigator.userAgent.indexOf('MSIE 8') !== -1, + + /** + * True if browser is Firefox + * @type boolean + */ + this.isFF = navigator.userAgent.indexOf('Firefox') !== -1, + + /** + * True if browser is Firefox 2 + * @type boolean + */ + this.isFF2 = navigator.userAgent.indexOf('Firefox/2') !== -1, + + /** + * True if browser is Firefox 3 + * @type boolean + */ + this.isFF3 = navigator.userAgent.indexOf('Firefox/3') !== -1, + + /** + * True if browser is Safari + * @type boolean + */ + this.isSafari = navigator.userAgent.indexOf('Safari') !== -1 +}; +/** + * @requires Xquared.js + */ +xq.Timer = xq.Class(/** @lends xq.Timer.prototype */{ + /** + * @constructs + * + * @param {Number} precision precision in milliseconds + */ + initialize: function(precision) { + xq.addToFinalizeQueue(this); + + this.precision = precision; + this.jobs = {}; + this.nextJobId = 0; + + this.checker = null; + }, + + finalize: function() { + this.stop(); + }, + + /** + * starts timer + */ + start: function() { + this.stop(); + + this.checker = window.setInterval(function() { + this.executeJobs(); + }.bind(this), this.precision); + }, + + /** + * stops timer + */ + stop: function() { + if(this.checker) window.clearInterval(this.checker); + }, + + /** + * registers new job + * + * @param {Function} job function to execute + * @param {Number} interval interval in milliseconds + * + * @return {Number} job id + */ + register: function(job, interval) { + var jobId = this.nextJobId++; + + this.jobs[jobId] = { + func:job, + interval: interval, + lastExecution: Date.get() + }; + + return jobId; + }, + + /** + * unregister job by job id + * + * @param {Number} job id + */ + unregister: function(jobId) { + delete this.jobs[jobId]; + }, + + /** + * Execute all expired jobs immedialty. This method will be called automatically by interval timer. + */ + executeJobs: function() { + var curDate = new Date(); + + for(var id in this.jobs) { + var job = this.jobs[id]; + if(job.lastExecution.elapsed(job.interval, curDate)) { + try { + job.lastReturn = job.func(); + } catch(e) { + job.lastException = e; + } finally { + job.lastExecution = curDate; + } + } + } + } +}); +/** + * @requires Xquared.js + */ +xq.DomTree = xq.Class(/** @lends xq.DomTree.prototype */{ + /** + * Provides various tree operations. + * + * TODO: Add specs + * + * @constructs + */ + initialize: function() { + xq.addToFinalizeQueue(this); + + this._blockTags = ["DIV", "DD", "LI", "ADDRESS", "CAPTION", "DT", "H1", "H2", "H3", "H4", "H5", "H6", "HR", "P", "BODY", "BLOCKQUOTE", "PRE", "PARAM", "DL", "OL", "UL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; + this._blockContainerTags = ["DIV", "DD", "LI", "BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; + this._listContainerTags = ["OL", "UL", "DL"]; + this._tableCellTags = ["TH", "TD"]; + this._blockOnlyContainerTags = ["BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR"]; + this._atomicTags = ["IMG", "OBJECT", "PARAM", "BR", "HR"]; + }, + + getBlockTags: function() { + return this._blockTags; + }, + + /** + * Find common ancestor(parent) and his immediate children(left and right).
+ *
+ * A --- B -+- C -+- D -+- E
+ * |
+ * +- F -+- G
+ *
+ * For example:
+ * > findCommonAncestorAndImmediateChildrenOf("E", "G")
+ *
+ * will return
+ *
+ * > {parent:"B", left:"C", right:"F"} + */ + findCommonAncestorAndImmediateChildrenOf: function(left, right) { + if(left.parentNode === right.parentNode) { + return { + left:left, + right:right, + parent:left.parentNode + }; + } else { + var parentsOfLeft = this.collectParentsOf(left, true); + var parentsOfRight = this.collectParentsOf(right, true); + var ca = this.getCommonAncestor(parentsOfLeft, parentsOfRight); + + var leftAncestor = parentsOfLeft.find(function(node) {return node.parentNode === ca}); + var rightAncestor = parentsOfRight.find(function(node) {return node.parentNode === ca}); + + return { + left:leftAncestor, + right:rightAncestor, + parent:ca + }; + } + }, + + /** + * Find leaves at edge.
+ *
+ * A --- B -+- C -+- D -+- E
+ * |
+ * +- F -+- G
+ *
+ * For example:
+ * > getLeavesAtEdge("A")
+ *
+ * will return
+ *
+ * > ["E", "G"] + */ + getLeavesAtEdge: function(element) { + if(!element.hasChildNodes()) return [null, null]; + + var findLeft = function(el) { + for (var i = 0; i < el.childNodes.length; i++) { + if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findLeft(el.childNodes[i]); + } + return el; + }.bind(this); + + var findRight=function(el) { + for (var i = el.childNodes.length; i--;) { + if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findRight(el.childNodes[i]); + } + return el; + }.bind(this); + + var left = findLeft(element); + var right = findRight(element); + + return [left === element ? null : left, right === element ? null : right]; + }, + + getCommonAncestor: function(parents1, parents2) { + for(var i = 0; i < parents1.length; i++) { + for(var j = 0; j < parents2.length; j++) { + if(parents1[i] === parents2[j]) return parents1[i]; + } + } + }, + + collectParentsOf: function(node, includeSelf, exitCondition) { + var parents = []; + if(includeSelf) parents.push(node); + + while((node = node.parentNode) && (node.nodeName !== "HTML") && !(typeof exitCondition === "function" && exitCondition(node))) parents.push(node); + return parents; + }, + + isDescendantOf: function(parent, child) { + if(parent.length > 0) { + for(var i = 0; i < parent.length; i++) { + if(this.isDescendantOf(parent[i], child)) return true; + } + return false; + } + + if(parent === child) return false; + + while (child = child.parentNode) + if (child === parent) return true; + return false; + }, + + /** + * Perform tree walking (foreward) + */ + walkForward: function(node) { + var target = node.firstChild; + if(target) return target; + + // intentional assignment for micro performance turing + if(target = node.nextSibling) return target; + + while(node = node.parentNode) { + // intentional assignment for micro performance turing + if(target = node.nextSibling) return target; + } + + return null; + }, + + /** + * Perform tree walking (backward) + */ + walkBackward: function(node) { + if(node.previousSibling) { + node = node.previousSibling; + while(node.hasChildNodes()) {node = node.lastChild;} + return node; + } + + return node.parentNode; + }, + + /** + * Perform tree walking (to next siblings) + */ + walkNext: function(node) {return node.nextSibling}, + + /** + * Perform tree walking (to next siblings) + */ + walkPrev: function(node) {return node.previousSibling}, + + /** + * Returns true if target is followed by start + */ + checkTargetForward: function(start, target) { + return this._check(start, this.walkForward, target); + }, + + /** + * Returns true if start is followed by target + */ + checkTargetBackward: function(start, target) { + return this._check(start, this.walkBackward, target); + }, + + findForward: function(start, condition, exitCondition) { + return this._find(start, this.walkForward, condition, exitCondition); + }, + + findBackward: function(start, condition, exitCondition) { + return this._find(start, this.walkBackward, condition, exitCondition); + }, + + _check: function(start, direction, target) { + if(start === target) return false; + + while(start = direction(start)) { + if(start === target) return true; + } + return false; + }, + + _find: function(start, direction, condition, exitCondition) { + while(start = direction(start)) { + if(exitCondition && exitCondition(start)) return null; + if(condition(start)) return start; + } + return null; + }, + + /** + * Walks Forward through DOM tree from start to end, and collects all nodes that matches with a filter. + * If no filter provided, it just collects all nodes. + * + * @param {Element} start Starting element. + * @param {Element} end Ending element. + * @param {Function} filter A filter function. + */ + collectNodesBetween: function(start, end, filter) { + if(start === end) return [start, end].findAll(filter || function() {return true}); + + var nodes = this.collectForward(start, function(node) {return node === end}, filter); + if( + start !== end && + typeof filter === "function" && + filter(end) + ) nodes.push(end); + + return nodes; + }, + + collectForward: function(start, exitCondition, filter) { + return this.collect(start, this.walkForward, exitCondition, filter); + }, + + collectBackward: function(start, exitCondition, filter) { + return this.collect(start, this.walkBackward, exitCondition, filter); + }, + + collectNext: function(start, exitCondition, filter) { + return this.collect(start, this.walkNext, exitCondition, filter); + }, + + collectPrev: function(start, exitCondition, filter) { + return this.collect(start, this.walkPrev, exitCondition, filter); + }, + + collect: function(start, next, exitCondition, filter) { + var nodes = [start]; + + while(true) { + start = next(start); + if( + (start === null) || + (typeof exitCondition === "function" && exitCondition(start)) + ) break; + + nodes.push(start); + } + + return (typeof filter === "function") ? nodes.findAll(filter) : nodes; + }, + + hasBlocks: function(element) { + var nodes = element.childNodes; + for(var i = 0; i < nodes.length; i++) { + if(this.isBlock(nodes[i])) return true; + } + return false; + }, + + hasMixedContents: function(element) { + if(!this.isBlock(element)) return false; + if(!this.isBlockContainer(element)) return false; + + var hasTextOrInline = false; + var hasBlock = false; + for(var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if(!hasTextOrInline && this.isTextOrInlineNode(node)) hasTextOrInline = true; + if(!hasBlock && this.isBlock(node)) hasBlock = true; + + if(hasTextOrInline && hasBlock) break; + } + if(!hasTextOrInline || !hasBlock) return false; + + return true; + }, + + isBlockOnlyContainer: function(element) { + if(!element) return false; + return this._blockOnlyContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isTableCell: function(element) { + if(!element) return false; + return this._tableCellTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isBlockContainer: function(element) { + if(!element) return false; + return this._blockContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isHeading: function(element) { + if(!element) return false; + return (typeof element === 'string' ? element : element.nodeName).match(/H\d/); + }, + + isBlock: function(element) { + if(!element) return false; + return this._blockTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isAtomic: function(element) { + if(!element) return false; + return this._atomicTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isListContainer: function(element) { + if(!element) return false; + return this._listContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isTextOrInlineNode: function(node) { + return node && (node.nodeType === 3 || !this.isBlock(node)); + } +}); +/** + * @namespace + */ +xq.rdom = {} + +/** + * @requires Xquared.js + * @requires DomTree.js + */ +xq.rdom.Base = xq.Class(/** @lends xq.rdom.Base.prototype */{ + /** + * Encapsulates browser incompatibility problem and provides rich set of DOM manipulation API.
+ *
+ * Base provides basic CRUD + Advanced DOM manipulation API, various query methods and caret/selection management API. + * + * @constructs + */ + initialize: function() { + xq.addToFinalizeQueue(this); + + /** + * Instance of DomTree + * @type xq.DomTree + */ + this.tree = new xq.DomTree(); + this.focused = false; + this._lastMarkerId = 0; + }, + + + + /** + * Initialize Base instance using window object. + * Reads document and body from window object and sets them as a property + * + * @param {Window} win Browser's window object + */ + setWin: function(win) { + if(!win) throw "[win] is null"; + this.win = win; + }, + + /** + * Initialize Base instance using root element. + * Reads window and document from root element and sets them as a property. + * + * @param {Element} root Root element + */ + setRoot: function(root) { + if(!root) throw "[root] is null"; + this.root = root; + }, + + /** + * @returns Browser's window object. + */ + getWin: function() { + return this.win || + (this.root ? (this.root.ownerDocument.defaultView || this.root.ownerDocument.parentWindow) : window); + }, + + /** + * @returns Root element. + */ + getRoot: function() { + return this.root || this.win.document.body; + }, + + /** + * @returns Document object of root element. + */ + getDoc: function() { + return this.getWin().document || this.getRoot().ownerDocument; + }, + + + + ///////////////////////////////////////////// + // CRUDs + + clearRoot: function() { + this.getRoot().innerHTML = ""; + this.getRoot().appendChild(this.makeEmptyParagraph()); + }, + + /** + * Removes place holders and empty text nodes of given element. + * + * @param {Element} element target element + */ + removePlaceHoldersAndEmptyNodes: function(element) { + if(!element.hasChildNodes()) return; + + var stopAt = this.getBottommostLastChild(element); + if(!stopAt) return; + stopAt = this.tree.walkForward(stopAt); + + while(element && element !== stopAt) { + if( + this.isPlaceHolder(element) || + (element.nodeType === 3 && (element.nodeValue === "" || (!element.nextSibling && element.nodeValue.isBlank()))) + ) { + 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.getDoc().createTextNode(value);}, + + /** + * Creates empty element by given tag name. + * + * @param {String} tagName name of tag + * @returns {Element} Created element + */ + createElement: function(tagName) {return this.getDoc().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; + if(node.nodeName === "BODY") throw "Cannot delete BODY"; + + 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". + * @param {String} className CSS class name. + * + * @return {Element} LI element + */ + turnElementIntoListItem: function(element, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var container = this.createElement(type); + if(className) container.className = className; + + 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.getRoot() || element.parentNode === this.getRoot() || !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. If not provided, it does not modify tag name. + * @param {Element} element Target element + * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. + * + * @return {Element} wrapper element or replaced element. + */ + applyTagIntoElement: function(tag, element, className) { + if(!tag && !className) return null; + + var result = element; + + if(tag) { + if(this.tree.isBlockOnlyContainer(tag)) { + result = this.wrapBlock(tag, element); + } else if(this.tree.isBlockContainer(element)) { + var wrapper = this.createElement(tag); + this.moveChildNodes(element, wrapper); + result = this.insertNodeAt(wrapper, element, "start"); + } else if(this.tree.isBlockContainer(tag) && this.hasImportantAttributes(element)) { + result = this.wrapBlock(tag, element); + } else { + result = this.replaceTag(tag, element); + } + } + + if(className) { + result.className = className; + } + + return result; + }, + + /** + * Wrap or replace elements with given tag name. + * + * @param {String} [tag] Tag name. If not provided, it does not modify tag name. + * @param {Element} from Start boundary (inclusive) + * @param {Element} to End boundary (inclusive) + * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. + * + * @returns {Array} Array of wrappers or replaced elements + */ + applyTagIntoElements: function(tagName, from, to, className) { + if(!tagName && !className) return [from, to]; + + var applied = []; + + if(tagName) { + 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]) || elements[i]); + } + } + } + } + + if(className) { + var elements = this.tree.collectNodesBetween(from, to, function(n) {return n.nodeType == 1;}); + for(var i = 0; i < elements.length; i++) { + elements[i].className = className; + } + } + + 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" + * @param {String} className CSS class name + * + * @returns {Element} changed element + */ + changeListTypeTo: function(element, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var li = this.getParentElementOf(element, ["LI"]); + if(!li) throw "IllegalArgumentException"; + + var container = li.parentNode; + + this.splitContainerOf(li); + + var newContainer = this.insertNodeAt(this.createElement(type), container, "before"); + if(className) newContainer.className = className; + + 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 && nextContainer.nodeName !== "LI" && this.outdentElement(next)) return element; + + // nextContainer is first li and next of it is list container ([I] represents caret position): + // + // * A[I] + // * B + // * C + if(nextContainer && nextContainer.nodeName === 'LI' && this.tree.isListContainer(next.nextSibling)) { + // move child nodes and... + this.moveChildNodes(nextContainer, prevContainer); + + // merge two paragraphs + this.removePlaceHoldersAndEmptyNodes(prev); + this.moveChildNodes(next, prev); + this.deleteNode(next); + + 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.includeElement(leaves[0])) { + var affected = this.indentElement(node, true); + if (affected) { + affect.push(affected); + return; + } + } + + if (blocks.includeElement(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.includeElement(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.includeElement(leaves[0]) && !this.outdentElementsCode(leaves[0])) { + var affected = this.outdentElement(node, true); + if (affected) { + affect.push(affected); + return; + } + } + + if (blocks.includeElement(node)) { + var children = xq.$A(node.parentNode.childNodes); + var isCode = this.outdentElementsCode(node); + var affected = this.outdentElement(node, true, isCode); + if (affected) { + if (children.includeElement(affected) && this.tree.isListContainer(node.parentNode) && !isCode) { + for (var i=0; i < children.length; i++) { + if (blocks.includeElement(children[i]) && !affect.includeElement(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.includeElement(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" + * @param {String} className CSS className + * @returns {Element} affected element + */ + applyList: function(element, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var containerTag = type; + + if(element.nodeName === "LI" || (element.parentNode.nodeName === "LI" && !element.previousSibling)) { + var element = this.getParentElementOf(element, ["LI"]); + var container = element.parentNode; + if(container.nodeName === containerTag && container.className === className) { + return this.extractOutElementFromParent(element); + } else { + return this.changeListTypeTo(element, type, className); + } + } else { + return this.turnElementIntoListItem(element, type, className); + } + }, + + applyLists: function(from, to, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var containerTag = type; + 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, className); + } + + 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); + }, + + /** + * Applies font face to selected area + * + * @param {String} face font face + */ + applyFontFace: function(face) { + this.execCommand("fontname", face); + }, + + /** + * Applies font size to selected area + * + * @param {Number} size font size (px) + */ + applyFontSize: function(size) { + this.execCommand("fontsize", size); + }, + + 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() { + return this.focused; + }, + + /** + * 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 caret is place at start of the block + */ + isCaretAtBlockStart: function() { + if(this.isCaretAtEmptyBlock()) return true; + if(this.hasSelection()) return false; + var node = this.getCurrentBlockElement(); + var marker = this.pushMarker(); + + var isTrue = false; + while (node = this.getFirstChild(node)) { + if (node === marker) { + isTrue = true; + break; + } + } + + this.popMarker(); + + return isTrue; + }, + + /** + * Checks if the caret is place at end of the block + */ + isCaretAtBlockEnd: function() {throw "Not implemented"}, + + /** + * Checks if the node is empty-text-node or not + */ + isEmptyTextNode: function(node) { + return node.nodeType === 3 && (node.nodeValue.length === 0 || (node.nodeValue.length === 1 && (node.nodeValue.charAt(0) === 32 || node.nodeValue.charAt(0) === 160))); + }, + + /** + * Checks if the caret is place in empty block element + */ + isCaretAtEmptyBlock: function() { + return this.isEmptyBlock(this.getCurrentBlockElement()); + }, + + /** + * 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(); + if(this.isFirstLiWithNestedList(block)) block = block.parentNode; + + var found = this.tree.findBackward( + block, + function(node) { + return node === root || (this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)); + }.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); + + if(block === null || (xq.Browser.isTrident && ["ready", "complete"].indexOf(block.readyState) === -1)) 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"); + var foregroundColor = doc.queryCommandValue("forecolor"); + var fontName = doc.queryCommandValue("fontname"); + var fontSize = doc.queryCommandValue("fontsize"); + // @WORKAROUND: Trident's fontSize value is affected by CSS + if(xq.Browser.isTrident && fontSize === "5" && this.getParentElementOf(element, ["H1", "H2", "H3", "H4", "H5", "H6"])) fontSize = ""; + + // @TODO: remove conditional + var backgroundColor; + if(xq.Browser.isGecko) { + this.execCommand("styleWithCSS", "true"); + try { + backgroundColor = doc.queryCommandValue("hilitecolor"); + } catch(e) { + // if there's selection and the first element of the selection is + // an empty block... + backgroundColor = ""; + } + this.execCommand("styleWithCSS", "false"); + } else { + backgroundColor = doc.queryCommandValue("backcolor"); + } + + // 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"; + var hasClass = parent.className.length > 0; + if(isCode) { + list = "CODE"; + } else if(hasClass) { + list = false; + } else { + list = 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, + foregroundColor: foregroundColor, + backgroundColor: backgroundColor, + fontSize: fontSize, + fontName: fontName + }; + }, + + /** + * 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. Trident overrides this method. + */ + 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.getDoc().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.isBlank()) 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 + }; + } +}); +/** + * @requires Xquared.js + * @requires rdom/Base.js + */ +xq.rdom.Trident = xq.Class(xq.rdom.Base, + /** + * @name xq.rdom.Trident + * @lends xq.rdom.Trident.prototype + * @extends xq.rdom.Base + * @constructor + */ + { + makePlaceHolder: function() { + return this.createTextNode(" "); + }, + + makePlaceHolderString: function() { + return ' '; + }, + + makeEmptyParagraph: function() { + return this.createElementFromHtml("

 

"); + }, + + isPlaceHolder: function(node) { + return false; + }, + + getOuterHTML: function(element) { + return element.outerHTML; + }, + + getCurrentBlockElement: function() { + var cur = this.getCurrentElement(); + if(!cur) return null; + + var block = this.getParentBlockElementOf(cur); + if(!block) return null; + + if(block.nodeName === "BODY") { + // Atomic block such as HR + var newParagraph = this.insertNode(this.makeEmptyParagraph()); + var next = newParagraph.nextSibling; + if(this.tree.isAtomic(next)) { + this.deleteNode(newParagraph); + return next; + } + } else { + return block; + } + }, + + insertNode: function(node) { + if(this.hasSelection()) this.collapseSelection(true); + + this.rng().pasteHTML(''); + var marker = this.$('xquared_temp'); + if(node.id === 'xquared_temp') return marker; + + if(marker) marker.replaceNode(node); + return node; + }, + + removeTrailingWhitespace: function(block) { + if(!block) return; + + // @TODO: reimplement to handle atomic tags and so on. (use DomTree) + if(this.tree.isBlockOnlyContainer(block)) return; + if(this.isEmptyBlock(block)) return; + + var text = block.innerText; + var html = block.innerHTML; + var lastCharCode = text.charCodeAt(text.length - 1); + if(text.length <= 1 || [32,160].indexOf(lastCharCode) === -1) return; + + // shortcut for most common case + if(text == html.replace(/ /g, " ")) { + block.innerHTML = html.replace(/ $/, ""); + return; + } + + var node = block; + while(node && node.nodeType !== 3) node = node.lastChild; + if(!node) return; + + // DO NOT REMOVE OR MODIFY FOLLOWING CODE. Modifying following code will crash IE7 + var nodeValue = node.nodeValue; + if(nodeValue.length <= 1) { + this.deleteNode(node, true); + } else { + node.nodeValue = nodeValue.substring(0, nodeValue.length - 1); + } + }, + + correctEmptyElement: function(element) { + if(!element || element.nodeType !== 1 || this.tree.isAtomic(element)) return; + + if(element.firstChild) { + this.correctEmptyElement(element.firstChild); + } else { + element.innerHTML = " "; + } + }, + + copyAttributes: function(from, to, copyId) { + to.mergeAttributes(from, !copyId); + }, + + correctParagraph: function() { + if(!this.hasFocus()) return false; + if(this.hasSelection()) return false; + + var block = this.getCurrentElement(); + + // if caret is at + // * atomic block level elements(HR) or + // * ... + // then following is true + if(this.tree.isBlockOnlyContainer(block)) { + // check for atomic block element such as HR + block = this.insertNode(this.makeEmptyParagraph()); + if(this.tree.isAtomic(block.nextSibling)) { + // @WORKAROUND: + // At this point, HR has a caret but getCurrentElement() doesn't return the HR and + // I couldn't find a way to get this HR. So I have to keep this reference. + // I will be used in Editor._handleEnter. + this.recentHR = block.nextSibling; + this.deleteNode(block); + return false; + } else { + // I can't remember exactly when following is executed and what it does :-( + // * Case 1: Performing Ctrl+A and Ctrl+X repeatedly + // * ... + var nextBlock = this.tree.findForward( + block, + function(node) {return this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)}.bind(this) + ); + + if(nextBlock) { + this.deleteNode(block); + this.placeCaretAtStartOf(nextBlock); + } else { + this.placeCaretAtStartOf(block); + } + + return true; + } + } else { + block = this.getCurrentBlockElement(); + if(block.nodeType === 3) block = block.parentNode; + + if(this.tree.hasMixedContents(block)) { + var marker = this.pushMarker(); + this.wrapAllInlineOrTextNodesAs("P", block, true); + this.popMarker(true); + return true; + } else if((this.tree.isTextOrInlineNode(block.previousSibling) || this.tree.isTextOrInlineNode(block.nextSibling)) && this.tree.hasMixedContents(block.parentNode)) { + // @WORKAROUND: + // IE?서??Block?Inline/Text??접??경우 getCurrentElement ?이 ?작?한?? + // ?라???재 Block 주?까? ?번???아주어???다. + this.wrapAllInlineOrTextNodesAs("P", block.parentNode, true); + return true; + } else { + return false; + } + } + }, + + + + ////// + // Commands + execCommand: function(commandId, param) { + return this.getDoc().execCommand(commandId, false, param); + }, + + applyBackgroundColor: function(color) { + this.execCommand("BackColor", color); + }, + + applyEmphasis: function() { + // Generate tag. It will be replaced with tag during cleanup phase. + this.execCommand("Italic"); + }, + applyStrongEmphasis: function() { + // Generate tag. It will be replaced with tag during cleanup phase. + this.execCommand("Bold"); + }, + applyStrike: function() { + // Generate tag. It will be replaced with ' + ); + + // create designmode iframe for WYSIWYG editor + this.editorFrame = this._createIFrame(outerDoc); + + outerDoc.body.appendChild(this.editorFrame); + var editorDoc = this._createDoc( + this.editorFrame, + '' + + (!xq.Browser.isTrident ? '' : '') + // @WORKAROUND: it is needed to force href of pasted content to be an absolute url + (this.config.changeCursorOnLink ? '' : ''), + this.config.contentCssList, + this.config.bodyId, + this.config.bodyClass, + '' + ); + this.rdom.setWin(this.editorFrame.contentWindow); + this.editHistory = new xq.EditHistory(this.rdom); + + // turn on designmode + this.rdom.getDoc().designMode = "On"; + + // turn off Firefox's table editing feature + if(xq.Browser.isGecko) { + try {this.rdom.getDoc().execCommand("enableInlineTableEditing", false, "false")} catch(ignored) {} + } + + // register event handlers + this._registerEventHandlers(); + + // hook onsubmit of form + if(this.config.automaticallyHookSubmitEvent && this.contentElement.form) { + var original = this.contentElement.form.onsubmit; + this.contentElement.form.onsubmit = function() { + this.contentElement.value = this.getCurrentContent(); + return original ? original.bind(this.contentElement.form)() : true; + }.bind(this); + } + }, + + + + ///////////////////////////////////////////// + // Event Management + + _registerEventHandlers: function() { + var events = [this.platformDepedentKeyEventType, 'click', 'keyup', 'mouseup', 'contextmenu']; + + if(xq.Browser.isTrident && this.config.changeCursorOnLink) events.push('mousemove'); + + var handler = this._handleEvent.bindAsEventListener(this); + for(var i = 0; i < events.length; i++) { + xq.observe(this.getDoc(), events[i], handler); + } + + if(xq.Browser.isGecko) { + xq.observe(this.getDoc(), "focus", handler); + xq.observe(this.getDoc(), "blur", handler); + xq.observe(this.getDoc(), "scroll", handler); + xq.observe(this.getDoc(), "dragdrop", handler); + } else { + xq.observe(this.getWin(), "focus", handler); + xq.observe(this.getWin(), "blur", handler); + xq.observe(this.getWin(), "scroll", handler); + } + }, + + _handleEvent: function(e) { + this._fireOnBeforeEvent(this, e); + if(e.stopProcess) { + xq.stopEvent(e); + return false; + } + + // Trident only + if(e.type === 'mousemove') { + if(!this.config.changeCursorOnLink) return true; + + var link = !!this.rdom.getParentElementOf(e.srcElement, ["A"]); + + var editable = this.getBody().contentEditable; + editable = editable === 'inherit' ? false : editable; + + if(editable !== link && !this.rdom.hasSelection()) this.getBody().contentEditable = !link; + return true; + } + + var stop = false; + var modifiedByCorrection = false; + if(e.type === this.platformDepedentKeyEventType) { + var undoPerformed = false; + modifiedByCorrection = this.rdom.correctParagraph(); + for(var key in this.config.shortcuts) { + if(!this.config.shortcuts[key].event.matches(e)) continue; + + var handler = this.config.shortcuts[key].handler; + var xed = this; + stop = (typeof handler === "function") ? handler(this) : eval(handler); + + if(key === "undo") undoPerformed = true; + } + } else if(e.type === 'click' && e.button === 0 && this.config.enableLinkClick) { + var a = this.rdom.getParentElementOf(e.target || e.srcElement, ["A"]); + if(a) stop = this.handleClick(e, a); + } else if(["keyup", "mouseup"].indexOf(e.type) !== -1) { + modifiedByCorrection = this.rdom.correctParagraph(); + } else if(["contextmenu"].indexOf(e.type) !== -1) { + this._handleContextMenu(e); + } else if("focus" == e.type) { + this.rdom.focused = true; + } else if("blur" == e.type) { + this.rdom.focused = false; + } + + if(stop) xq.stopEvent(e); + + this._fireOnCurrentContentChanged(this); + this._fireOnAfterEvent(this, e); + + if(!undoPerformed && !modifiedByCorrection) this.editHistory.onEvent(e); + + return !stop; + }, + + /** + * TODO: remove dup with handleAutocompletion + */ + handleAutocorrection: function() { + var block = this.rdom.getCurrentBlockElement(); + // TODO: use complete unescape algorithm + var text = this.rdom.getInnerText(block).replace(/ /gi, " "); + + var acs = this.config.autocorrections; + var performed = false; + + var stop = false; + for(var key in acs) { + var ac = acs[key]; + if(ac.criteria(text)) { + try { + this.editHistory.onCommand(); + this.editHistory.disable(); + if(typeof ac.handler === "String") { + var xed = this; + var rdom = this.rdom; + eval(ac.handler); + } else { + stop = ac.handler(this, this.rdom, block, text); + } + this.editHistory.enable(); + } catch(ignored) {} + + block = this.rdom.getCurrentBlockElement(); + text = this.rdom.getInnerText(block); + + performed = true; + if(stop) break; + } + } + + return stop; + }, + + /** + * TODO: remove dup with handleAutocorrection + */ + handleAutocompletion: function() { + var acs = this.config.autocompletions; + if(xq.isEmptyHash(acs)) return; + + if(this.rdom.hasSelection()) { + var text = this.rdom.getSelectionAsText(); + this.rdom.deleteSelection(); + var wrapper = this.rdom.insertNode(this.rdom.createElement("SPAN")); + wrapper.innerHTML = text; + + var marker = this.rdom.pushMarker(); + + var filtered = []; + for(var key in acs) { + filtered.push([key, acs[key].criteria(text)]); + } + filtered = filtered.findAll(function(elem) { + return elem[1] !== -1; + }); + + if(filtered.length === 0) { + this.rdom.popMarker(true); + return; + } + + var minIndex = 0; + var min = filtered[0][1]; + for(var i = 0; i < filtered.length; i++) { + if(filtered[i][1] < min) { + minIndex = i; + min = filtered[i][1]; + } + } + + var ac = acs[filtered[minIndex][0]]; + + this.editHistory.disable(); + this.rdom.selectElement(wrapper); + } else { + var marker = this.rdom.pushMarker(); + + var filtered = []; + for(var key in acs) { + filtered.push([key, this.rdom.testSmartWrap(marker, acs[key].criteria).textIndex]); + } + filtered = filtered.findAll(function(elem) { + return elem[1] !== -1; + }); + + if(filtered.length === 0) { + this.rdom.popMarker(true); + return; + } + + var minIndex = 0; + var min = filtered[0][1]; + for(var i = 0; i < filtered.length; i++) { + if(filtered[i][1] < min) { + minIndex = i; + min = filtered[i][1]; + } + } + + var ac = acs[filtered[minIndex][0]]; + + this.editHistory.disable(); + + var wrapper = this.rdom.smartWrap(marker, "SPAN", ac.criteria); + } + + var block = this.rdom.getCurrentBlockElement(); + + // TODO: use complete unescape algorithm + var text = this.rdom.getInnerText(wrapper).replace(/ /gi, " "); + + try { + // call handler + if(typeof ac.handler === "String") { + var xed = this; + var rdom = this.rdom; + eval(ac.handler); + } else { + ac.handler(this, this.rdom, block, wrapper, text); + } + } catch(ignored) {} + + try { + this.rdom.unwrapElement(wrapper); + } catch(ignored) {} + + if(this.rdom.isEmptyBlock(block)) this.rdom.correctEmptyElement(block); + + this.editHistory.enable(); + this.editHistory.onCommand(); + + this.rdom.popMarker(true); + }, + + /** + * Handles click event + * + * @param {Event} e click event + * @param {Element} target target element(usually has A tag) + */ + handleClick: function(e, target) { + var href = decodeURI(target.href); + if(!xq.Browser.isTrident) { + if(!e.ctrlKey && !e.shiftKey && e.button !== 1) { + window.location.href = href; + return true; + } + } else { + if(e.shiftKey) { + window.open(href, "_blank"); + } else { + window.location.href = href; + } + return true; + } + + return false; + }, + + /** + * Show link dialog + * + * TODO: should support modify/unlink + * TODO: Add selenium test + */ + handleLink: function() { + var text = this.rdom.getSelectionAsText() || ''; + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicLinkDialog, + function(dialog) { + if(text) { + dialog.form.text.value = text; + dialog.form.url.focus(); + dialog.form.url.select(); + } + }, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + if(!data) return; + this.handleInsertLink(false, data.url, data.text, data.text); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + + dialog.show({position: 'centerOfEditor'}); + + return true; + }, + + /** + * Inserts link or apply link into selected area + * @TODO Add selenium test + * + * @param {boolean} autoSelection if set true and there's no selection, automatically select word to link(if possible) + * @param {String} url url + * @param {String} title title of link + * @param {String} text text of link. If there's a selection(manually or automatically), it will be replaced with this text + * + * @returns {Element} created element + */ + handleInsertLink: function(autoSelection, url, title, text) { + if(autoSelection && !this.rdom.hasSelection()) { + var marker = this.rdom.pushMarker(); + var a = this.rdom.smartWrap(marker, "A", function(text) { + var index = text.lastIndexOf(" "); + return index === -1 ? index : index + 1; + }); + a.href = url; + a.title = title; + if(text) { + a.innerHTML = "" + a.appendChild(this.rdom.createTextNode(text)); + } else if(!a.hasChildNodes()) { + this.rdom.deleteNode(a); + } + this.rdom.popMarker(true); + } else { + text = text || (this.rdom.hasSelection() ? this.rdom.getSelectionAsText() : null); + if(!text) return; + + this.rdom.deleteSelection(); + + var a = this.rdom.createElement('A'); + a.href = url; + a.title = title; + a.appendChild(this.rdom.createTextNode(text)); + this.rdom.insertNode(a); + } + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * @TODO Add selenium test + */ + handleSpace: function() { + // If it has selection, perform default action. + if(this.rdom.hasSelection()) return false; + + // Trident performs URL replacing automatically + if(!xq.Browser.isTrident) { + this.replaceUrlToLink(); + } + + return false; + }, + + /** + * Called when enter key pressed. + * @TODO Add selenium test + * + * @param {boolean} skipAutocorrection if set true, skips autocorrection + * @param {boolean} forceInsertParagraph if set true, inserts paragraph + */ + handleEnter: function(skipAutocorrection, forceInsertParagraph) { + // If it has selection, perform default action. + if(this.rdom.hasSelection()) return false; + + // @WORKAROUND: + // If caret is in HR, default action should be performed and + // this._handleEvent() will correct broken HTML + if(xq.Browser.isTrident && this.rdom.tree.isBlockOnlyContainer(this.rdom.getCurrentElement()) && this.rdom.recentHR) { + this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.recentHR, "before"); + this.rdom.recentHR = null; + return true; + } + + // Perform autocorrection + if(!skipAutocorrection && this.handleAutocorrection()) return true; + + var block = this.rdom.getCurrentBlockElement(); + var info = this.rdom.collectStructureAndStyle(block); + + // Perform URL replacing. Trident performs URL replacing automatically + if(!xq.Browser.isTrident) { + this.replaceUrlToLink(); + } + + var atEmptyBlock = this.rdom.isCaretAtEmptyBlock(); + var atStart = atEmptyBlock || this.rdom.isCaretAtBlockStart(); + var atEnd = atEmptyBlock || (!atStart && this.rdom.isCaretAtBlockEnd()); + var atEdge = atEmptyBlock || atStart || atEnd; + + if(!atEdge) { + var marker = this.rdom.pushMarker(); + + if(this.rdom.isFirstLiWithNestedList(block) && !forceInsertParagraph) { + var parent = block.parentNode; + this.rdom.unwrapElement(block); + block = parent; + } else if(block.nodeName !== "LI" && this.rdom.tree.isBlockContainer(block)) { + block = this.rdom.wrapAllInlineOrTextNodesAs("P", block, true).first(); + } + this.rdom.splitElementUpto(marker, block); + + this.rdom.popMarker(true); + } else if(atEmptyBlock) { + this._handleEnterAtEmptyBlock(); + + if(!xq.Browser.isWebkit) { + if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize); + if(info.fontName) this.handleFontFace(info.fontName); + } + } else { + this._handleEnterAtEdge(atStart, forceInsertParagraph); + + if(!xq.Browser.isWebkit) { + if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize); + if(info.fontName) this.handleFontFace(info.fontName); + } + } + + return true; + }, + + /** + * Moves current block upward or downward + * + * @param {boolean} up moves current block upward + */ + handleMoveBlock: function(up) { + var block = this.rdom.moveBlock(this.rdom.getCurrentBlockElement(), up); + if(block) { + this.rdom.selectElement(block, false); + if(this.rdom.isEmptyBlock(block)) this.rdom.collapseSelection(true); + + block.scrollIntoView(false); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } + return true; + }, + + /** + * Called when tab key pressed + * @TODO: Add selenium test + */ + handleTab: function() { + var hasSelection = this.rdom.hasSelection(); + var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]); + + if(hasSelection) { + this.handleIndent(); + } else if (table && table.className === "datatable") { + this.handleMoveToNextCell(); + } else if (this.rdom.isCaretAtBlockStart()) { + this.handleIndent(); + } else { + this.handleInsertTab(); + } + + return true; + }, + + /** + * Called when shift+tab key pressed + * @TODO: Add selenium test + */ + handleShiftTab: function() { + var hasSelection = this.rdom.hasSelection(); + var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]); + + if(hasSelection) { + this.handleOutdent(); + } else if (table && table.className === "datatable") { + this.handleMoveToPreviousCell(); + } else { + this.handleOutdent(); + } + + return true; + }, + + /** + * Inserts three non-breaking spaces + * @TODO: Add selenium test + */ + handleInsertTab: function() { + this.rdom.insertHtml(' '); + this.rdom.insertHtml(' '); + this.rdom.insertHtml(' '); + + return true; + }, + + /** + * Called when delete key pressed + * @TODO: Add selenium test + */ + handleDelete: function() { + if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockEnd()) return false; + return this._handleMerge(true); + }, + + /** + * Called when backspace key pressed + * @TODO: Add selenium test + */ + handleBackspace: function() { + if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockStart()) return false; + return this._handleMerge(false); + }, + + _handleMerge: function(withNext) { + var block = this.rdom.getCurrentBlockElement(); + + if(this.rdom.isEmptyBlock(block) && !this.rdom.tree.isBlockContainer(block.nextSibling) && withNext) { + var blockToMove = this.rdom.removeBlock(block); + this.rdom.placeCaretAtStartOf(blockToMove); + blockToMove.scrollIntoView(false); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } else { + // save caret position; + var marker = this.rdom.pushMarker(); + + // perform merge + var merged = this.rdom.mergeElement(block, withNext, withNext); + if(!merged && !withNext) this.rdom.extractOutElementFromParent(block); + + // restore caret position + this.rdom.popMarker(true); + if(merged) this.rdom.correctEmptyElement(merged); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + return !!merged; + } + }, + + /** + * (in table) Moves caret to the next cell + * @TODO: Add selenium test + */ + handleMoveToNextCell: function() { + this._handleMoveToCell("next"); + }, + + /** + * (in table) Moves caret to the previous cell + * @TODO: Add selenium test + */ + handleMoveToPreviousCell: function() { + this._handleMoveToCell("prev"); + }, + + /** + * (in table) Moves caret to the above cell + * @TODO: Add selenium test + */ + handleMoveToAboveCell: function() { + this._handleMoveToCell("above"); + }, + + /** + * (in table) Moves caret to the below cell + * @TODO: Add selenium test + */ + handleMoveToBelowCell: function() { + this._handleMoveToCell("below"); + }, + + _handleMoveToCell: function(dir) { + var block = this.rdom.getCurrentBlockElement(); + var cell = this.rdom.getParentElementOf(block, ["TD", "TH"]); + var table = this.rdom.getParentElementOf(cell, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + var target = null; + + if(["next", "prev"].indexOf(dir) !== -1) { + var toNext = dir === "next"; + target = toNext ? rtable.getNextCellOf(cell) : rtable.getPreviousCellOf(cell); + } else { + var toBelow = dir === "below"; + target = toBelow ? rtable.getBelowCellOf(cell) : rtable.getAboveCellOf(cell); + } + + if(!target) { + var finder = function(node) {return ['TD', 'TH'].indexOf(node.nodeName) === -1 && this.tree.isBlock(node) && !this.tree.hasBlocks(node);}.bind(this.rdom); + var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this.rdom); + + target = (toNext || toBelow) ? + this.rdom.tree.findForward(cell, finder, exitCondition) : + this.rdom.tree.findBackward(table, finder, exitCondition); + } + + if(target) this.rdom.placeCaretAtStartOf(target); + }, + + /** + * Applies STRONG tag + * @TODO: Add selenium test + */ + handleStrongEmphasis: function() { + this.rdom.applyStrongEmphasis(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies EM tag + * @TODO: Add selenium test + */ + handleEmphasis: function() { + this.rdom.applyEmphasis(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies EM.underline tag + * @TODO: Add selenium test + */ + handleUnderline: function() { + this.rdom.applyUnderline(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies SPAN.strike tag + * @TODO: Add selenium test + */ + handleStrike: function() { + this.rdom.applyStrike(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Removes all style + * @TODO: Add selenium test + */ + handleRemoveFormat: function() { + this.rdom.applyRemoveFormat(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Remove link + * @TODO: Add selenium test + */ + handleRemoveLink: function() { + this.rdom.applyRemoveLink(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Inserts table + * @TODO: Add selenium test + * + * @param {Number} cols number of columns + * @param {Number} rows number of rows + * @param {String} headerPosition position of THs. "T" or "L" or "TL". "T" means top, "L" means left. + */ + handleTable: function(cols, rows, headerPositions) { + var cur = this.rdom.getCurrentBlockElement(); + if(this.rdom.getParentElementOf(cur, ["TABLE"])) return true; + + var rtable = xq.RichTable.create(this.rdom, cols, rows, headerPositions); + if(this.rdom.tree.isBlockContainer(cur)) { + var wrappers = this.rdom.wrapAllInlineOrTextNodesAs("P", cur, true); + cur = wrappers.last(); + } + var tableDom = this.rdom.insertNodeAt(rtable.getDom(), cur, "after"); + this.rdom.placeCaretAtStartOf(rtable.getCellAt(0, 0)); + + if(this.rdom.isEmptyBlock(cur)) this.rdom.deleteNode(cur, true); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + handleInsertNewRowAt: function(where) { + var cur = this.rdom.getCurrentBlockElement(); + var tr = this.rdom.getParentElementOf(cur, ["TR"]); + if(!tr) return true; + + var table = this.rdom.getParentElementOf(tr, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + var row = rtable.insertNewRowAt(tr, where); + + this.rdom.placeCaretAtStartOf(row.cells[0]); + return true; + }, + + /** + * @TODO: Add selenium test + */ + handleInsertNewColumnAt: function(where) { + var cur = this.rdom.getCurrentBlockElement(); + var td = this.rdom.getParentElementOf(cur, ["TD"], true); + if(!td) return true; + + var table = this.rdom.getParentElementOf(td, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + rtable.insertNewCellAt(td, where); + + this.rdom.placeCaretAtStartOf(cur); + return true; + }, + + /** + * @TODO: Add selenium test + */ + handleDeleteRow: function() { + var cur = this.rdom.getCurrentBlockElement(); + var tr = this.rdom.getParentElementOf(cur, ["TR"]); + if(!tr) return true; + + var table = this.rdom.getParentElementOf(tr, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + var blockToMove = rtable.deleteRow(tr); + + this.rdom.placeCaretAtStartOf(blockToMove); + return true; + }, + + /** + * @TODO: Add selenium test + */ + handleDeleteColumn: function() { + var cur = this.rdom.getCurrentBlockElement(); + var td = this.rdom.getParentElementOf(cur, ["TD"], true); + if(!td) return true; + + var table = this.rdom.getParentElementOf(td, ["TABLE"]); + var rtable = new xq.RichTable(this.rdom, table); + rtable.deleteCell(td); + + //this.rdom.placeCaretAtStartOf(table); + return true; + }, + + /** + * Performs block indentation + * @TODO: Add selenium test + */ + handleIndent: function() { + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); + if(blocks.first() !== blocks.last()) { + var affected = this.rdom.indentElements(blocks.first(), blocks.last()); + this.rdom.selectBlocksBetween(affected.first(), affected.last()); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } + } + + var block = this.rdom.getCurrentBlockElement(); + var affected = this.rdom.indentElement(block); + + if(affected) { + this.rdom.placeCaretAtStartOf(affected); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } + + return true; + }, + + /** + * Performs block outdentation + * @TODO: Add selenium test + */ + handleOutdent: function() { + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); + if(blocks.first() !== blocks.last()) { + var affected = this.rdom.outdentElements(blocks.first(), blocks.last()); + this.rdom.selectBlocksBetween(affected.first(), affected.last()); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } + } + + var block = this.rdom.getCurrentBlockElement(); + var affected = this.rdom.outdentElement(block); + + if(affected) { + this.rdom.placeCaretAtStartOf(affected); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } + + return true; + }, + + /** + * Applies list. + * @TODO: Add selenium test + * + * @param {String} type "UL" or "OL" + * @param {String} CSS class name + */ + handleList: function(type, className) { + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); + if(blocks.first() !== blocks.last()) { + blocks = this.rdom.applyLists(blocks.first(), blocks.last(), type, className); + } else { + blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type, className); + } + this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); + } else { + var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type, className); + this.rdom.placeCaretAtStartOf(block); + } + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies justification + * @TODO: Add selenium test + * + * @param {String} dir "left", "center", "right" or "both" + */ + handleJustify: function(dir) { + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getSelectedBlockElements(); + var dir = (dir === "left" || dir === "both") && (blocks[0].style.textAlign === "left" || blocks[0].style.textAlign === "") ? "both" : dir; + this.rdom.justifyBlocks(blocks, dir); + this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); + } else { + var block = this.rdom.getCurrentBlockElement(); + var dir = (dir === "left" || dir === "both") && (block.style.textAlign === "left" || block.style.textAlign === "") ? "both" : dir; + this.rdom.justifyBlock(block, dir); + } + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Removes current block element + * @TODO: Add selenium test + */ + handleRemoveBlock: function() { + var block = this.rdom.getCurrentBlockElement(); + var blockToMove = this.rdom.removeBlock(block); + this.rdom.placeCaretAtStartOf(blockToMove); + blockToMove.scrollIntoView(false); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies background color + * @TODO: Add selenium test + * + * @param {String} color CSS color string + */ + handleBackgroundColor: function(color) { + if(color) { + this.rdom.applyBackgroundColor(color); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicColorPickerDialog, + function(dialog) {}, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + if(!data) return; + + this.handleBackgroundColor(data.color); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + + dialog.show({position: 'centerOfEditor'}); + } + return true; + }, + + /** + * Applies foreground color + * @TODO: Add selenium test + * + * @param {String} color CSS color string + */ + handleForegroundColor: function(color) { + if(color) { + this.rdom.applyForegroundColor(color); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicColorPickerDialog, + function(dialog) {}, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + if(!data) return; + + this.handleForegroundColor(data.color); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + + dialog.show({position: 'centerOfEditor'}); + } + return true; + }, + + /** + * Applies font face + * @TODO: Add selenium test + * + * @param {String} face font face + */ + handleFontFace: function(face) { + if(face) { + this.rdom.applyFontFace(face); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + //TODO: popup font dialog + } + return true; + }, + + /** + * Applies font size + * + * @param {Number} font size (1 to 6) + */ + handleFontSize: function(size) { + if(size) { + this.rdom.applyFontSize(size); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + //TODO: popup font dialog + } + return true; + }, + + /** + * Applies superscription + * @TODO: Add selenium test + */ + handleSuperscription: function() { + this.rdom.applySuperscription(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Applies subscription + * @TODO: Add selenium test + */ + handleSubscription: function() { + this.rdom.applySubscription(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Change or wrap current block(or selected blocks)'s tag + * @TODO: Add selenium test + * + * @param {String} [tagName] Name of tag. If not provided, it does not modify current tag name + * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. + */ + handleApplyBlock: function(tagName, className) { + if(!tagName && !className) return true; + + // if current selection contains multi-blocks + if(this.rdom.hasSelection()) { + var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); + if(blocks.first() !== blocks.last()) { + var applied = this.rdom.applyTagIntoElements(tagName, blocks.first(), blocks.last(), className); + this.rdom.selectBlocksBetween(applied.first(), applied.last()); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } + } + + // else + var block = this.rdom.getCurrentBlockElement(); + this.rdom.pushMarker(); + var applied = + this.rdom.applyTagIntoElement(tagName, block, className) || + block; + this.rdom.popMarker(true); + + if(this.rdom.isEmptyBlock(applied)) { + this.rdom.correctEmptyElement(applied); + this.rdom.placeCaretAtStartOf(applied); + } + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Inserts seperator (HR) + * @TODO: Add selenium test + */ + handleSeparator: function() { + this.rdom.collapseSelection(); + + var curBlock = this.rdom.getCurrentBlockElement(); + var atStart = this.rdom.isCaretAtBlockStart(); + if(this.rdom.tree.isBlockContainer(curBlock)) curBlock = this.rdom.wrapAllInlineOrTextNodesAs("P", curBlock, true)[0]; + + this.rdom.insertNodeAt(this.rdom.createElement("HR"), curBlock, atStart ? "before" : "after"); + this.rdom.placeCaretAtStartOf(curBlock); + + // add undo history + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + + /** + * Performs UNDO + * @TODO: Add selenium test + */ + handleUndo: function() { + var performed = this.editHistory.undo(); + this._fireOnCurrentContentChanged(this); + + var curBlock = this.rdom.getCurrentBlockElement(); + if(!xq.Browser.isTrident && curBlock) { + curBlock.scrollIntoView(false); + } + return true; + }, + + /** + * Performs REDO + * @TODO: Add selenium test + */ + handleRedo: function() { + var performed = this.editHistory.redo(); + this._fireOnCurrentContentChanged(this); + + var curBlock = this.rdom.getCurrentBlockElement(); + if(!xq.Browser.isTrident && curBlock) { + curBlock.scrollIntoView(false); + } + return true; + }, + + + + _handleContextMenu: function(e) { + if (xq.Browser.isWebkit) { + if (e.metaKey || xq.isLeftClick(e)) return false; + } else if (e.shiftKey || e.ctrlKey || e.altKey) { + return false; + } + + var point = xq.getEventPoint(e); + var x = point.x; + var y = point.y; + + var pos = xq.getCumulativeOffset(this.wysiwygEditorDiv); + x += pos.left; + y += pos.top; + this._contextMenuTargetElement = e.target || e.srcElement; + + if (!xq.Browser.isTrident) { + var doc = this.getDoc(); + var body = this.getBody(); + + x -= doc.documentElement.scrollLeft; + y -= doc.documentElement.scrollTop; + + x -= body.scrollLeft; + y -= body.scrollTop; + } + + for(var cmh in this.config.contextMenuHandlers) { + var stop = this.config.contextMenuHandlers[cmh].handler(this, this._contextMenuTargetElement, x, y); + if(stop) { + xq.stopEvent(e); + return true; + } + } + + return false; + }, + + showContextMenu: function(menuItems, x, y) { + if (!menuItems || menuItems.length <= 0) return; + + if (!this.contextMenuContainer) { + this.contextMenuContainer = this.doc.createElement('UL'); + this.contextMenuContainer.className = 'xqContextMenu'; + this.contextMenuContainer.style.display='none'; + + xq.observe(this.doc, 'click', this._contextMenuClicked.bindAsEventListener(this)); + xq.observe(this.rdom.getDoc(), 'click', this.hideContextMenu.bindAsEventListener(this)); + + this.body.appendChild(this.contextMenuContainer); + } else { + while (this.contextMenuContainer.childNodes.length > 0) + this.contextMenuContainer.removeChild(this.contextMenuContainer.childNodes[0]); + } + + for (var i=0; i < menuItems.length; i++) { + menuItems[i]._node = this._addContextMenuItem(menuItems[i]); + } + + this.contextMenuContainer.style.display='block'; + this.contextMenuContainer.style.left = Math.min(Math.max(this.doc.body.scrollWidth, this.doc.documentElement.clientWidth) - this.contextMenuContainer.offsetWidth, x) + 'px'; + this.contextMenuContainer.style.top = Math.min(Math.max(this.doc.body.scrollHeight, this.doc.documentElement.clientHeight) - this.contextMenuContainer.offsetHeight, y) + 'px'; + + this.contextMenuItems = menuItems; + }, + + hideContextMenu: function() { + if (this.contextMenuContainer) + this.contextMenuContainer.style.display='none'; + }, + + _addContextMenuItem: function(item) { + if (!this.contextMenuContainer) throw "No conext menu container exists"; + + var node = this.doc.createElement('LI'); + if (item.disabled) node.className += ' disabled'; + + if (item.title === '----') { + node.innerHTML = ' '; + node.className = 'separator'; + } else { + if(item.handler) { + node.innerHTML = ''+(item.title.toString().escapeHTML())+''; + } else { + node.innerHTML = (item.title.toString().escapeHTML()); + } + } + + if(item.className) node.className = item.className; + + this.contextMenuContainer.appendChild(node); + + return node; + }, + + _contextMenuClicked: function(e) { + this.hideContextMenu(); + + if (!this.contextMenuContainer) return; + + var node = e.srcElement || e.target; + while(node && node.nodeName !== "LI") { + node = node.parentNode; + } + if (!node || !this.rdom.tree.isDescendantOf(this.contextMenuContainer, node)) return; + + for (var i=0; i < this.contextMenuItems.length; i++) { + if (this.contextMenuItems[i]._node === node) { + var handler = this.contextMenuItems[i].handler; + if (!this.contextMenuItems[i].disabled && handler) { + var xed = this; + var element = this._contextMenuTargetElement; + if(typeof handler === "function") { + handler(xed, element); + } else { + eval(handler); + } + } + break; + } + } + }, + + /** + * Inserts HTML template + * @TODO: Add selenium test + * + * @param {String} html Template string. It should have single root element + * @returns {Element} inserted element + */ + insertTemplate: function(html) { + return this.rdom.insertHtml(this._processTemplate(html)); + }, + + /** + * Places given HTML template nearby target. + * @TODO: Add selenium test + * + * @param {String} html Template string. It should have single root element + * @param {Node} target Target node. + * @param {String} where Possible values: "before", "start", "end", "after" + * + * @returns {Element} Inserted element. + */ + insertTemplateAt: function(html, target, where) { + return this.rdom.insertHtmlAt(this._processTemplate(html), target, where); + }, + + _processTemplate: function(html) { + // apply template processors + var tps = this.getTemplateProcessors(); + for(var key in tps) { + var value = tps[key]; + html = value.handler(html); + } + + // remove all whitespace characters between block tags + return this.removeUnnecessarySpaces(html); + }, + + + + /** @private */ + _handleEnterAtEmptyBlock: function() { + var block = this.rdom.getCurrentBlockElement(); + if(this.rdom.tree.isTableCell(block) && this.rdom.isFirstBlockOfBody(block)) { + block = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start"); + } else { + block = + this.rdom.outdentElement(block) || + this.rdom.extractOutElementFromParent(block) || + this.rdom.replaceTag("P", block) || + this.rdom.insertNewBlockAround(block); + } + + this.rdom.placeCaretAtStartOf(block); + if(!xq.Browser.isTrident) block.scrollIntoView(false); + }, + + /** @private */ + _handleEnterAtEdge: function(atStart, forceInsertParagraph) { + var block = this.rdom.getCurrentBlockElement(); + var blockToPlaceCaret; + + if(atStart && this.rdom.isFirstBlockOfBody(block)) { + blockToPlaceCaret = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start"); + } else { + if(this.rdom.tree.isTableCell(block)) forceInsertParagraph = true; + var newBlock = this.rdom.insertNewBlockAround(block, atStart, forceInsertParagraph ? "P" : null); + blockToPlaceCaret = !atStart ? newBlock : newBlock.nextSibling; + } + + this.rdom.placeCaretAtStartOf(blockToPlaceCaret); + if(!xq.Browser.isTrident) blockToPlaceCaret.scrollIntoView(false); + }, + + /** + * Replace URL text nearby caret into a link + * @TODO: Add selenium test + */ + replaceUrlToLink: function() { + // If there's link nearby caret, nothing happens + if(this.rdom.getParentElementOf(this.rdom.getCurrentElement(), ["A"])) return; + + var marker = this.rdom.pushMarker(); + var criteria = function(text) { + var m = /(http|https|ftp|mailto)\:\/\/[^\s]+$/.exec(text); + return m ? m.index : -1; + }; + + var test = this.rdom.testSmartWrap(marker, criteria); + if(test.textIndex !== -1) { + var a = this.rdom.smartWrap(marker, "A", criteria); + a.href = encodeURI(test.text); + } + this.rdom.popMarker(true); + } +}); +/** + * @namespace + */ +xq.macro = {}; + +/** + * @requires Xquared.js + */ +xq.macro.Base = xq.Class(/** @lends xq.macro.Base.prototype */{ + /** + * @constructs + * + * @param {Object} Parameters or HTML fragment. + * @param {String} URL to place holder image. + */ + initialize: function(id, paramsOrHtml, placeHolderImgSrc) { + this.id = id; + this.placeHolderImgSrc = placeHolderImgSrc; + + if(typeof paramsOrHtml === "string") { + this.html = paramsOrHtml; + this.params = {}; + + this.initFromHtml(); + } else { + this.html = null; + this.params = paramsOrHtml; + + this.initFromParams(); + } + }, + + initFromHtml: function() {}, + initFromParams: function() {}, + createHtml: function() {throw "Not implemented";}, + onLayerInitialzied: function(layer) {}, + + createPlaceHolderHtml: function() { + var size = {width: 5, height: 5}; + var def = {}; + def.id = this.id; + def.params = this.params; + + sb = []; + sb.push(''); + + return sb.join(''); + } +}) +/** + * @requires Xquared.js + * @requires macro/Base.js + */ +xq.macro.Factory = xq.Class(/** @lends xq.macro.Factory.prototype */{ + /** + * @constructs + * + * @param {String} URL to place holder image. + */ + initialize: function(placeHolderImgSrc) { + this.placeHolderImgSrc = placeHolderImgSrc; + this.macroClazzes = {}; + }, + /** + * Registers new macro by ID. + * + * @param {String} id Macro id. + */ + register: function(id) { + var clazz = xq.macro[id + "Macro"]; + if(!clazz) throw "Unknown macro id: [" + id + "]"; + + this.macroClazzes[id] = clazz; + }, + /** + * Creates macro instance by given HTML fragment. + * + * @param {String} html HTML fragment. + * @returns {xq.macro.Base} Macro instance or null if recognization of the HTML fragment fails. + */ + createMacroFromHtml: function(html) { + for(var id in this.macroClazzes) { + var clazz = this.macroClazzes[id]; + if(clazz.recognize(html)) return new clazz(id, html, this.placeHolderImgSrc); + } + return null; + }, + /** + * Creates macro instance by given macro definition. + * + * @param {Object} def Macro definition. + * @returns {xq.macro.Base} Macro instance + * @throws If macro not found by def[id]. + */ + createMacroFromDefinition: function(def) { + var clazz = this.macroClazzes[def.id]; + if(!clazz) return null; + + return new clazz(def.id, def.params, this.placeHolderImgSrc); + } +}) +/** + * @requires Xquared.js + * @requires Editor.js + */ +xq.Layer = xq.Class(/** @lends xq.Layer.prototype */{ + /** + * @constructs + * + * @param {xq.Editor} editor editor instance + * @param {Element} element designMode document's element. Layer instance will be attached to this element + * @param {String} html HTML for body. + */ + initialize: function(editor, element, html) { + xq.addToFinalizeQueue(this); + + this.margin = 4; + this.editor = editor; + this.element = element; + this.frame = this.editor._createIFrame(this.editor.getOuterDoc(), this.element.offsetWidth - (this.margin * 2) + "px", this.element.offsetHeight + (this.margin * 2) + "px"); + this.editor.getOuterDoc().body.appendChild(this.frame); + this.doc = editor._createDoc( + this.frame, + '', + [], null, null, html + ); + this.frame.style.position = "absolute"; + this.updatePosition(); + }, + + getFrame: function() { + return this.frame; + }, + + getDoc: function() { + return this.doc; + }, + + getBody: function() { + return this.doc.body; + }, + + isValid: function() { + return this.element && this.element.parentNode && this.element.offsetParent; + }, + + detach: function() { + this.frame.parentNode.removeChild(this.frame); + + this.frame = null; + this.element = null; + }, + + updatePosition: function() { + // calculate element position + var offset = xq.getCumulativeOffset(this.element, this.editor.rdom.getRoot()); + + // and scroll position + var doc = this.editor.getDoc(); + var body = this.editor.getBody(); + offset.left -= doc.documentElement.scrollLeft + body.scrollLeft - this.margin; + offset.top -= doc.documentElement.scrollTop + body.scrollTop - this.margin; + + // apply new position + this.frame.style.left = offset.left + "px"; + this.frame.style.top = offset.top + "px"; + + // perform autofit + var newWidth = this.doc.body.scrollWidth + (this.margin - 1) * 2; + var newHeight = this.doc.body.scrollHeight + (this.margin - 1) * 2; + + // without -1, the element increasing slowly. + this.element.width = newWidth; + this.element.height = newHeight; + + // resize frame + this.frame.style.width = this.element.offsetWidth - (this.margin * 2) + "px"; + this.frame.style.height = this.element.offsetHeight - (this.margin * 2) + "px"; + } +}); + +/* + json2.js + 2008-02-14 + + Public Domain + + No warranty expressed or implied. Use at your own risk. + + See http://www.JSON.org/js.html + + This file creates a global JSON object containing two methods: + + JSON.stringify(value, whitelist) + value any JavaScript value, usually an object or array. + + whitelist an optional array parameter that determines how object + values are stringified. + + This method produces a JSON text from a JavaScript value. + There are three possible ways to stringify an object, depending + on the optional whitelist parameter. + + If an object has a toJSON method, then the toJSON() method will be + called. The value returned from the toJSON method will be + stringified. + + Otherwise, if the optional whitelist parameter is an array, then + the elements of the array will be used to select members of the + object for stringification. + + Otherwise, if there is no whitelist parameter, then all of the + members of the object will be stringified. + + Values that do not have JSON representaions, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays will be replaced with null. + JSON.stringify(undefined) returns undefined. Dates will be + stringified as quoted ISO dates. + + Example: + + var text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + JSON.parse(text, filter) + This method parses a JSON text to produce an object or + array. It can throw a SyntaxError exception. + + The optional filter parameter is a function that can filter and + transform the results. It receives each of the keys and values, and + its return value is used instead of the original value. If it + returns what it received, then structure is not modified. If it + returns undefined then the member is deleted. + + Example: + + // Parse the text. If a key contains the string 'date' then + // convert the value to a date. + + myData = JSON.parse(text, function (key, value) { + return key.indexOf('date') >= 0 ? new Date(value) : value; + }); + + This is a reference implementation. You are free to copy, modify, or + redistribute. + + Use your own copy. It is extremely unwise to load third party + code into your pages. +*/ + +/*jslint evil: true */ + +/*global JSON */ + +/*members "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + charCodeAt, floor, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, length, + parse, propertyIsEnumerable, prototype, push, replace, stringify, test, + toJSON, toString +*/ + +if (!this.JSON) { + + JSON = function () { + + function f(n) { // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + Date.prototype.toJSON = function () { + +// Eventually, this method will be based on the date.toISOString method. + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + + var m = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }; + + function stringify(value, whitelist) { + var a, // The array holding the partial texts. + i, // The loop counter. + k, // The member key. + l, // Length. + r = /["\\\x00-\x1f\x7f-\x9f]/g, + v; // The member value. + + switch (typeof value) { + case 'string': + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe sequences. + + return r.test(value) ? + '"' + value.replace(r, function (a) { + var c = m[a]; + if (c) { + return c; + } + c = a.charCodeAt(); + return '\\u00' + Math.floor(c / 16).toString(16) + + (c % 16).toString(16); + }) + '"' : + '"' + value + '"'; + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + return String(value); + + case 'object': + +// Due to a specification blunder in ECMAScript, +// typeof null is 'object', so watch out for that case. + + if (!value) { + return 'null'; + } + +// If the object has a toJSON method, call it, and stringify the result. + + if (typeof value.toJSON === 'function') { + return stringify(value.toJSON()); + } + a = []; + if (typeof value.length === 'number' && + !(value.propertyIsEnumerable('length'))) { + +// The object is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + l = value.length; + for (i = 0; i < l; i += 1) { + a.push(stringify(value[i], whitelist) || 'null'); + } + +// Join all of the elements together and wrap them in brackets. + + return '[' + a.join(',') + ']'; + } + if (whitelist) { + +// If a whitelist (array of keys) is provided, use it to select the components +// of the object. + + l = whitelist.length; + for (i = 0; i < l; i += 1) { + k = whitelist[i]; + if (typeof k === 'string') { + v = stringify(value[k], whitelist); + if (v) { + a.push(stringify(k) + ':' + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (typeof k === 'string') { + v = stringify(value[k], whitelist); + if (v) { + a.push(stringify(k) + ':' + v); + } + } + } + } + +// Join all of the member texts together and wrap them in braces. + + return '{' + a.join(',') + '}'; + } + } + + return { + stringify: stringify, + parse: function (text, filter) { + var j; + + function walk(k, v) { + var i, n; + if (v && typeof v === 'object') { + for (i in v) { + if (Object.prototype.hasOwnProperty.apply(v, [i])) { + n = walk(i, v[i]); + if (n !== undefined) { + v[i] = n; + } else { + delete v[i]; + } + } + } + } + return filter(k, v); + } + + +// Parsing happens in three stages. In the first stage, we run the text against +// regular expressions that look for non-JSON patterns. We are especially +// concerned with '()' and 'new' because they can cause invocation, and '=' +// because it can cause mutation. But just to be safe, we want to reject all +// unexpected forms. + +// We split the first stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace all backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/.test(text.replace(/\\./g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the second stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional third stage, we recursively walk the new structure, passing +// each name/value pair to a filter function for possible transformation. + + return typeof filter === 'function' ? walk('', j) : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('parseJSON'); + } + }; + }(); +} + +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * + * @requires macro/Factory.js + * @requires Layer.js + * @requires Json2.js + * + * @requires plugin/Base.js + */ +xq.plugin.MacroPlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.MacroPlugin + * @lends xq.plugin.MacroPlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + isEventListener: function() {return true;}, + + onAfterLoad: function(xed) { + this.xed = xed; + this.xed.config.macroIds = []; + this.layers = []; + }, + + onEditorStartInitialization: function(xed) { + this.xed.validator.addListener(this); + + this.xed.macroFactory = new xq.macro.Factory(this.xed.config.imagePathForContent + 'placeholder.gif'); + for(var i = 0; i < this.xed.config.macroIds.length; i++) { + this.xed.macroFactory.register(this.xed.config.macroIds[i]); + } + + xed.timer.register(this.updateLayers.bind(this), 100); + xed.timer.register(this.updateLayerList.bind(this), 2000); + }, + + /** + * @param {Element} [placeHolder] place holder element + */ + attachMacro: function(element) { + var longdesc = element.getAttribute("longdesc") || element.longdesc; + var def = JSON.parse(unescape(longdesc)); + var macro = this.xed.macroFactory.createMacroFromDefinition(def); + var layer = new xq.Layer(this.xed, element, macro.createHtml()); + macro.onLayerInitialzied(layer); + this.layers.push(layer); + }, + + isAttachedPlaceHolder: function(element) { + for(var i = 0; i < this.layers.length; i++) { + if(this.layers[i].element === element) return true; + } + return false; + }, + + updateLayerList: function() { + if(this.xed.getCurrentEditMode() !== 'wysiwyg') { + for(var i = 0; i < this.layers.length; i++) { + this.layers[i].detach(); + } + this.layers = []; + } else { + var placeHolders = xq.getElementsByClassName(this.xed.rdom.getRoot(), "xqlayer", xq.Browser.isTrident ? "img" : null); + for(var i = 0; i < placeHolders.length; i++) { + if(!this.isAttachedPlaceHolder(placeHolders[i])) { + this.attachMacro(placeHolders[i]); + } + } + } + }, + + /** + * Updates all layers immediately. If there're invalid layers, detachs and removes them. + */ + updateLayers: function() { + if(this.xed.getCurrentEditMode() !== 'wysiwyg') return; + + for(var i = 0; i < this.layers.length; i++) { + var layer = this.layers[i]; + if(layer.isValid()) { + layer.updatePosition(); + } else { + layer.detach(); + this.layers.splice(i, 1); + } + } + }, + + onValidatorPreprocessing: function(html) { + var p = xq.compilePattern("<(IFRAME|SCRIPT|OBJECT|EMBED)\\s+[^>]+(?:/>|>.*?)", "img"); + html.value = html.value.replace(p, function(str, tag) { + var macro = this.xed.macroFactory.createMacroFromHtml(str); + return macro ? macro.createPlaceHolderHtml() : ""; + }.bind(this)); + }, + + onValidatorAfterStringValidation: function(html) { + var p1 = /]*class="xqlayer"\s+[^>]*\/>/mg; + var p2 = /]*longdesc="(.+?)"\s+[^>]*\/>/m; + + html.value = html.value.replace(p1, function(img) { + var def = JSON.parse(unescape(img.match(p2)[1])); + var macro = this.xed.macroFactory.createMacroFromDefinition(def); + return macro.createHtml(); + }.bind(this)); + } +}); +/** + * @requires macro/Base.js + */ +xq.macro.FlashMovieMacro = xq.Class(xq.macro.Base, + /** + * Flash movie macro + * + * @name xq.macro.FlashMovieMacro + * @lends xq.macro.FlashMovieMacro.prototype + * @extends xq.macro.Base + * @constructor + */ + { + initFromHtml: function() { + this.params.html = this.html; + }, + initFromParams: function() { + if(!xq.macro.FlashMovieMacro.recognize(this.params.html)) throw "Unknown src"; + }, + createHtml: function() { + return this.params.html; + } +}); +xq.macro.FlashMovieMacro.recognize = function(html) { + var providers = { + tvpot: /http:\/\/flvs\.daum\.net\/flvPlayer\.swf\?/, + youtube: /http:\/\/(?:www\.)?youtube\.com\/v\//, + pandoratv: /http:\/\/flvr\.pandora\.tv\/flv2pan\/flvmovie\.dll\?/, + pandoratv2: /http:\/\/imgcdn\.pandora\.tv\/gplayer\/pandora\_EGplayer\.swf\?/, + mncast: /http:\/\/dory\.mncast\.com\/mncHMovie\.swf\?/, + yahoo: /http:\/\/d\.yimg\.com\// + }; + + for(var id in providers) { + if(html.match(providers[id])) return true; + } + + return false; +} +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * @requires plugin/Base.js + * @requires ui/Control.js + * @requires macro/Factory.js + * @requires macro/FlashMovieMacro.js + */ +xq.plugin.FlashMovieMacroPlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.FlashMovieMacroPlugin + * @lends xq.plugin.FlashMovieMacroPlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + onAfterLoad: function(xed) { + xed.config.macroIds.push("FlashMovie"); + xed.config.defaultToolbarButtonGroups.insert.push( + {className:"movie", title:"Movie", handler:"xed.handleMovie()"} + ) + + xed.handleInsertMovie = function(html) { + var macro = this.macroFactory.createMacroFromDefinition({id:"FlashMovie", params:{html:html}}); + if(macro) { + var placeHolder = macro.createPlaceHolderHtml(); + this.rdom.insertHtml(placeHolder); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + alert("Unknown URL pattern"); + } + return true; + }; + + xed.handleMovie = function() { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicMovieDialog, + function(dialog) {}, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + // cancel? + if(!data) return; + + this.handleInsertMovie(data.html); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + dialog.show({position: 'centerOfEditor'}); + + return true; + } + } +}); +/** + * @requires macro/Base.js + */ +xq.macro.IFrameMacro = xq.Class(xq.macro.Base, + /** + * IFrame macro + * + * @name xq.macro.IFrameMacro + * @lends xq.macro.IFrameMacro.prototype + * @extends xq.macro.Base + * @constructor + */ + { + initFromHtml: function() { + this.params.html = this.html; + }, + initFromParams: function() { + if(this.params.html) return; + + var sb = []; + sb.push(''); + this.params = {html:sb.join("")}; + }, + createHtml: function() { + return this.params.html; + } +}); +xq.macro.IFrameMacro.recognize = function(html) { + var p = xq.compilePattern("]+(?:/>|>.*?)", "img"); + return !!html.match(p); +} +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * @requires plugin/Base.js + * @requires ui/Control.js + * @requires macro/Factory.js + * @requires macro/IFrameMacro.js + */ +xq.plugin.IFrameMacroPlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.IFrameMacroPlugin + * @lends xq.plugin.IFrameMacroPlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + onAfterLoad: function(xed) { + xed.config.macroIds.push("IFrame"); + xed.config.defaultToolbarButtonGroups.insert.push( + {className:"iframe", title:"IFrame", handler:"xed.handleIFrame()"} + ) + + xed.handleIFrame = function() { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicIFrameDialog, + function(dialog) {}, + function(data) { + this.focus(); + + // cancel? + if(!data) return; + + var macro = this.macroFactory.createMacroFromDefinition({id:"IFrame", params:data}); + if(macro) { + var placeHolder = macro.createPlaceHolderHtml(); + this.rdom.insertHtml(placeHolder); + } else { + alert("Unknown error"); + } + }.bind(this) + ); + + dialog.show({position: 'centerOfEditor'}); + + return true; + } + } +}); +/** + * @requires macro/Base.js + */ +xq.macro.JavascriptMacro = xq.Class(xq.macro.Base, + /** + * Javascript macro + * + * @name xq.macro.JavascriptMacro + * @lends xq.macro.JavascriptMacro.prototype + * @extends xq.macro.Base + * @constructor + */ + { + initFromHtml: function() { + var p = xq.compilePattern("src=[\"'](.+?)[\"']", "img"); + this.params.url = p.exec(this.html)[1]; + }, + initFromParams: function() { + if(!xq.macro.JavascriptMacro.isSafeScript(this.params.url)) throw "Unknown src"; + }, + createHtml: function() {return ''}, + + onLayerInitialzied: function(layer) { + layer.getDoc().write(this.createHtml()); + } +}); + +xq.macro.JavascriptMacro.recognize = function(html) { + var p = xq.compilePattern("]*src=[\"']([^\"']+)[\"'][^>]*(?:/>|>.*?)", "img"); + var m = p.exec(html); + if(!m || !m[1]) return false; + return this.isSafeScript(m[1]); +} +xq.macro.JavascriptMacro.isSafeScript = function(url) { + var safeSrcs = { + googleGadget: /http:\/\/gmodules\.com\/ig\/ifr\?/img + }; + for(var id in safeSrcs) { + if(url.match(safeSrcs[id])) return true; + } + return false; +} + +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * @requires plugin/Base.js + * @requires ui/Control.js + * @requires macro/Factory.js + * @requires macro/JavascriptMacro.js + */ +xq.plugin.JavascriptMacroPlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.JavascriptMacroPlugin + * @lends xq.plugin.JavascriptMacroPlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + onAfterLoad: function(xed) { + xed.config.macroIds.push("Javascript"); + xed.config.defaultToolbarButtonGroups.insert.push( + {className:"script", title:"Script", handler:"xed.handleScript()"} + ) + + xed.handleInsertScript = function(url) { + var params = {url: url}; + var macro = this.macroFactory.createMacroFromDefinition({id:"Javascript", params:params}); + if(macro) { + var placeHolder = macro.createPlaceHolderHtml(); + this.rdom.insertHtml(placeHolder); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + alert("Unknown URL pattern"); + } + return true; + }; + + xed.handleScript = function() { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicScriptDialog, + function(dialog) {}, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + // cancel? + if(!data) return; + + this.handleInsertScript(data.url); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + dialog.show({position: 'centerOfEditor'}); + + return true; + } + } +}); +/** + * @requires Xquared.js + * @requires Editor.js + * @requires plugin/MacroPlugin.js + * @requires plugin/FlashMovieMacroPlugin.js + * @requires plugin/IFrameMacroPlugin.js + * @requires plugin/JavascriptMacroPlugin.js + */ +xq.moduleName = "Full"; diff --git a/modules/editor/skins/xquared/javascripts/module/Full_merged_min.js b/modules/editor/skins/xquared/javascripts/module/Full_merged_min.js new file mode 100644 index 000000000..91854468e --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/module/Full_merged_min.js @@ -0,0 +1,3531 @@ +/* Xquared is copyrighted free software by Alan Kang . + * For more information, see http://xquared.springbook.playmaru.net/ + */ +if(!window.xq){var xq={} +}xq.majorVersion="0.7"; +xq.minorVersion="20080402"; +xq.compilePattern=function(C,B){if(!RegExp.prototype.compile){return new RegExp(C,B) +}var A=new RegExp(); +A.compile(C,B); +return A +}; +xq.Class=function(){var D=null,C=xq.$A(arguments),B; +if(typeof C[0]==="function"){D=C.shift() +}function A(){this.initialize.apply(this,arguments) +}if(D){for(B in D.prototype){A.prototype[B]=D.prototype[B] +}}for(B in C[0]){if(C[0].hasOwnProperty(B)){A.prototype[B]=C[0][B] +}}if(!A.prototype.initialize){A.prototype.initialize=function(){} +}A.prototype.constructor=A; +return A +}; +xq.observe=function(B,A,C){if(B.addEventListener){B.addEventListener(A,C,false) +}else{B.attachEvent("on"+A,C) +}B=null +}; +xq.stopObserving=function(B,A,C){if(B.removeEventListener){B.removeEventListener(A,C,false) +}else{B.detachEvent("on"+A,C) +}B=null +}; +xq.cancelHandler=function(A){xq.stopEvent(A); +return false +}; +xq.stopEvent=function(A){if(A.preventDefault){A.preventDefault() +}if(A.stopPropagation){A.stopPropagation() +}A.returnValue=false; +A.cancelBubble=true; +A.stopped=true +}; +xq.isButton=function(B,A){return B.which?(B.which===A+1):(B.button===A) +}; +xq.isLeftClick=function(A){return xq.isButton(A,0) +}; +xq.isMiddleClick=function(A){return xq.isButton(A,1) +}; +xq.isRightClick=function(A){return xq.isButton(A,2) +}; +xq.getEventPoint=function(A){return{x:A.pageX||(A.clientX+(document.documentElement.scrollLeft||document.body.scrollLeft)),y:A.pageY||(A.clientY+(document.documentElement.scrollTop||document.body.scrollTop))} +}; +xq.getCumulativeOffset=function(A,D){var C=0,B=0; +do{C+=A.offsetTop||0; +B+=A.offsetLeft||0; +A=A.offsetParent +}while(A&&A!=D); +return{top:C,left:B} +}; +xq.$=function(A){return document.getElementById(A) +}; +xq.isEmptyHash=function(B){for(var A in B){if(B.hasOwnProperty(A)){return false +}}return true +}; +xq.emptyFunction=function(){}; +xq.$A=function(C){var A=C.length,B=[]; +while(A--){B[A]=C[A] +}return B +}; +xq.addClassName=function(A,B){if(!xq.hasClassName(A,B)){A.className+=(A.className?" ":"")+B +}return A +}; +xq.removeClassName=function(A,B){if(xq.hasClassName(A,B)){A.className=A.className.replace(new RegExp("(^|\\s+)"+B+"(\\s+|$)")," ").strip() +}return A +}; +xq.hasClassName=function(A,B){var C=A.className; +return(C.length>0&&(C===B||new RegExp("(^|\\s)"+B+"(\\s|$)").test(C))) +}; +xq.serializeForm=function(F){var I={hash:true}; +var C={}; +var A=F.getElementsByTagName("*"); +for(var D=0; +D]+>","gi"); +String.prototype.stripTags=function(){return this.replace(xq.pStripTags,"") +}; +String.prototype.escapeHTML=function(){xq.textNode.data=this; +return xq.divNode.innerHTML +}; +xq.textNode=document.createTextNode(""); +xq.divNode=document.createElement("div"); +xq.divNode.appendChild(xq.textNode); +xq.pStrip1=xq.compilePattern("^\\s+"); +xq.pStrip2=xq.compilePattern("\\s+$"); +String.prototype.strip=function(){return this.replace(xq.pStrip1,"").replace(xq.pStrip2,"") +}; +Array.prototype.indexOf=function(B){for(var A=0; +A'+C+"
") +}else{if(typeof C==="string"){D.push(''+C.escapeHTML()+"") +}else{if(C.constructor===Array){D.push("
    "); +for(var B=0; +B"); +xq._json2html(C[B],D); +D.push("") +}D.push("
") +}else{D.push("
"); +for(var A in C){if(C.hasOwnProperty(A)){D.push("
"+A+"
"); +D.push("
"); +xq._json2html(C[A],D); +D.push("
") +}}D.push("
") +}}}}; +xq.findChildElements=function(B){var D=B.childNodes; +var C=[]; +for(var A=0; +A=B +}; +String.prototype.merge=function(C){var B=this; +for(var A in C){if(C.hasOwnProperty(A)){B=B.replace("{"+A+"}",C[A]) +}}return B +}; +xq.pBlank=xq.compilePattern("^\\s*$"); +String.prototype.isBlank=function(){return xq.pBlank.test(this) +}; +xq.pURL=xq.compilePattern("((((\\w+)://(((([^@:]+)(:([^@]+))?)@)?([^:/\\?#]+)?(:(\\d+))?))?([^\\?#]+)?)(\\?([^#]+))?)(#(.+))?"); +String.prototype.parseURL=function(){var E=this.match(xq.pURL); +var D=E[0]; +var B=E[1]||undefined; +var M=E[2]||undefined; +var L=E[3]||undefined; +var A=null; +var N=E[4]||undefined; +var G=E[8]||undefined; +var K=E[10]||undefined; +var F=E[11]||undefined; +var C=E[13]||undefined; +var O=E[14]||undefined; +var J=E[16]||undefined; +var H=E[18]||undefined; +if(!O||O==="/"){A=L+"/" +}else{var I=O.lastIndexOf("/"); +A=L+O.substring(0,I+1) +}return{includeAnchor:D,includeQuery:B,includePath:M,includeBase:A,includeHost:L,protocol:N,user:G,password:K,domain:F,port:C,path:O,query:J,anchor:H} +}; +xq.commonAttrs=["title","class","id","style"]; +xq.predefinedWhitelist={a:xq.commonAttrs.concat("href","charset","rev","rel","type","hreflang","tabindex"),abbr:xq.commonAttrs.concat(),acronym:xq.commonAttrs.concat(),address:xq.commonAttrs.concat(),blockquote:xq.commonAttrs.concat("cite"),br:xq.commonAttrs.concat(),button:xq.commonAttrs.concat("disabled","type","name","value"),caption:xq.commonAttrs.concat(),cite:xq.commonAttrs.concat(),code:xq.commonAttrs.concat(),dd:xq.commonAttrs.concat(),dfn:xq.commonAttrs.concat(),div:xq.commonAttrs.concat(),dl:xq.commonAttrs.concat(),dt:xq.commonAttrs.concat(),em:xq.commonAttrs.concat(),embed:xq.commonAttrs.concat("src","width","height","allowscriptaccess","type","allowfullscreen","bgcolor"),h1:xq.commonAttrs.concat(),h2:xq.commonAttrs.concat(),h3:xq.commonAttrs.concat(),h4:xq.commonAttrs.concat(),h5:xq.commonAttrs.concat(),h6:xq.commonAttrs.concat(),hr:xq.commonAttrs.concat(),iframe:xq.commonAttrs.concat("name","src","frameborder","scrolling","width","height","longdesc"),input:xq.commonAttrs.concat("type","name","value","size","checked","readonly","src","maxlength"),img:xq.commonAttrs.concat("alt","width","height","src","longdesc"),label:xq.commonAttrs.concat("for"),kbd:xq.commonAttrs.concat(),li:xq.commonAttrs.concat(),object:xq.commonAttrs.concat("align","classid","codetype","archive","width","type","codebase","height","data","name","standby","declare"),ol:xq.commonAttrs.concat(),option:xq.commonAttrs.concat("disabled","selected","laabel","value"),p:xq.commonAttrs.concat(),param:xq.commonAttrs.concat("name","value","valuetype","type"),pre:xq.commonAttrs.concat(),q:xq.commonAttrs.concat("cite"),samp:xq.commonAttrs.concat(),script:xq.commonAttrs.concat("src","type"),select:xq.commonAttrs.concat("disabled","size","multiple","name"),span:xq.commonAttrs.concat(),sup:xq.commonAttrs.concat(),sub:xq.commonAttrs.concat(),strong:xq.commonAttrs.concat(),table:xq.commonAttrs.concat("summary","width"),thead:xq.commonAttrs.concat(),textarea:xq.commonAttrs.concat("cols","disabled","rows","readonly","name"),tbody:xq.commonAttrs.concat(),th:xq.commonAttrs.concat("colspan","rowspan"),td:xq.commonAttrs.concat("colspan","rowspan"),tr:xq.commonAttrs.concat(),tt:xq.commonAttrs.concat(),ul:xq.commonAttrs.concat(),"var":xq.commonAttrs.concat()}; +xq.autoFinalizeQueue=[]; +xq.addToFinalizeQueue=function(A){xq.autoFinalizeQueue.push(A) +}; +xq.finalize=function(C){if(typeof C.finalize==="function"){try{C.finalize() +}catch(A){}}for(var B in C){if(C.hasOwnProperty(B)){C[B]=null +}}}; +xq.observe(window,"unload",function(){if(xq&&xq.autoFinalizeQueue){for(var A=0; +A<\/script>') +}; +xq.getXquaredScriptFileNames=function(){return["Xquared.js","Browser.js","DomTree.js","rdom/Base.js","rdom/W3.js","rdom/Gecko.js","rdom/Webkit.js","rdom/Trident.js","rdom/Factory.js","validator/Base.js","validator/W3.js","validator/Gecko.js","validator/Webkit.js","validator/Trident.js","validator/Factory.js","macro/Base.js","macro/Factory.js","macro/FlashMovieMacro.js","macro/IFrameMacro.js","macro/JavascriptMacro.js","EditHistory.js","plugin/Base.js","RichTable.js","Timer.js","Layer.js","ui/Base.js","ui/Control.js","ui/Toolbar.js","ui/_templates.js","Json2.js","Shortcut.js","Editor.js"] +}; +xq.getXquaredScriptBasePath=function(){var A=xq.findXquaredScript(); +return A.src.match(/(.*\/)xquared\.js.*/i)[1] +}; +xq.loadOthers=function(){var C=xq.getXquaredScriptBasePath(); +var B=xq.getXquaredScriptFileNames(); +for(var A=1; +A-1,this.isGecko=navigator.userAgent.indexOf("Gecko")>-1&&navigator.userAgent.indexOf("KHTML")===-1,this.isKHTML=navigator.userAgent.indexOf("KHTML")!==-1,this.isPresto=navigator.appName==="Opera",this.isMac=navigator.userAgent.indexOf("Macintosh")!==-1,this.isUbuntu=navigator.userAgent.indexOf("Ubuntu")!==-1,this.isWin=navigator.userAgent.indexOf("Windows")!==-1,this.isIE=navigator.appName==="Microsoft Internet Explorer",this.isIE6=navigator.userAgent.indexOf("MSIE 6")!==-1,this.isIE7=navigator.userAgent.indexOf("MSIE 7")!==-1,this.isIE8=navigator.userAgent.indexOf("MSIE 8")!==-1,this.isFF=navigator.userAgent.indexOf("Firefox")!==-1,this.isFF2=navigator.userAgent.indexOf("Firefox/2")!==-1,this.isFF3=navigator.userAgent.indexOf("Firefox/3")!==-1,this.isSafari=navigator.userAgent.indexOf("Safari")!==-1 +}; +xq.Timer=xq.Class({initialize:function(A){xq.addToFinalizeQueue(this); +this.precision=A; +this.jobs={}; +this.nextJobId=0; +this.checker=null +},finalize:function(){this.stop() +},start:function(){this.stop(); +this.checker=window.setInterval(function(){this.executeJobs() +}.bind(this),this.precision) +},stop:function(){if(this.checker){window.clearInterval(this.checker) +}},register:function(C,B){var A=this.nextJobId++; +this.jobs[A]={func:C,interval:B,lastExecution:Date.get()}; +return A +},unregister:function(A){delete this.jobs[A] +},executeJobs:function(){var A=new Date(); +for(var D in this.jobs){var B=this.jobs[D]; +if(B.lastExecution.elapsed(B.interval,A)){try{B.lastReturn=B.func() +}catch(C){B.lastException=C +}finally{B.lastExecution=A +}}}}}); +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","PARAM","BR","HR"] +},getBlockTags:function(){return this._blockTags +},findCommonAncestorAndImmediateChildrenOf:function(E,C){if(E.parentNode===C.parentNode){return{left:E,right:C,parent:E.parentNode} +}else{var D=this.collectParentsOf(E,true); +var G=this.collectParentsOf(C,true); +var B=this.getCommonAncestor(D,G); +var F=D.find(function(H){return H.parentNode===B +}); +var A=G.find(function(H){return H.parentNode===B +}); +return{left:F,right:A,parent:B} +}},getLeavesAtEdge:function(C){if(!C.hasChildNodes()){return[null,null] +}var D=function(G){for(var F=0; +F0){for(var A=0; +AQ[L].length){C-=Q[L].length +}else{M=L; +break +}}if(R){return{text:P,textIndex:N,nodeIndex:M,breakPoint:C} +}if(C!==0){var I=B[M].splitText(C); +M++; +B.splice(M,0,I) +}var A=B[M]||H.firstChild; +var O=this.tree.findCommonAncestorAndImmediateChildrenOf(A,G); +var K=O.parent; +if(K){if(A.parentNode!==K){A=this.splitElementUpto(A,K,true) +}if(G.parentNode!==K){G=this.splitElementUpto(G,K,true) +}var D=A.previousSibling; +var J=G.nextSibling; +if(D&&D.nodeType===1&&this.isEmptyBlock(D)){this.deleteNode(D) +}if(J&&J.nodeType===1&&this.isEmptyBlock(J)){this.deleteNode(J) +}var E=this.insertNodeAt(this.createElement(S),A,"before"); +while(E.nextSibling!==G){E.appendChild(E.nextSibling) +}return E +}else{var E=this.insertNodeAt(this.createElement(S),G,"before"); +return E +}},wrapAllInlineOrTextNodesAs:function(A,B,E){var D=[]; +if(!E&&!this.tree.hasMixedContents(B)){return D +}var C=B.firstChild; +while(C){if(this.tree.isTextOrInlineNode(C)){var F=this.wrapInlineOrTextNodesAs(A,C); +D.push(F); +C=F.nextSibling +}else{C=C.nextSibling +}}return D +},wrapInlineOrTextNodesAs:function(A,B){var D=this.createElement(A); +var C=B; +C.parentNode.replaceChild(D,C); +D.appendChild(C); +while(D.nextSibling&&this.tree.isTextOrInlineNode(D.nextSibling)){D.appendChild(D.nextSibling) +}return D +},turnElementIntoListItem:function(C,E,D){E=E.toUpperCase(); +D=D||""; +var B=this.createElement(E); +if(D){B.className=D +}if(this.tree.isTableCell(C)){var F=this.wrapAllInlineOrTextNodesAs("P",C,true)[0]; +B=this.insertNodeAt(B,C,"start"); +var A=this.insertNodeAt(this.createElement("LI"),B,"start"); +A.appendChild(F) +}else{B=this.insertNodeAt(B,C,"after"); +var A=this.insertNodeAt(this.createElement("LI"),B,"start"); +A.appendChild(C) +}this.unwrapUnnecessaryParagraph(A); +this.mergeAdjustLists(B); +return A +},extractOutElementFromParent:function(B){if(B===this.getRoot()||B.parentNode===this.getRoot()||!B.offsetParent){return null +}if(B.nodeName==="LI"){this.wrapAllInlineOrTextNodesAs("P",B,true); +B=B.firstChild +}var A=B.parentNode; +var D=null; +if(A.nodeName==="LI"&&A.parentNode.parentNode.nodeName==="LI"){if(B.previousSibling){this.splitContainerOf(B,true); +this.correctEmptyElement(B) +}this.outdentListItem(B); +D=B +}else{if(A.nodeName==="LI"){if(this.tree.isListContainer(B.nextSibling)){var E=A.parentNode; +this.splitContainerOf(A,true); +this.correctEmptyElement(B); +D=A.firstChild; +while(A.firstChild){this.insertNodeAt(A.firstChild,E,"before") +}var C=E.previousSibling; +this.deleteNode(E); +if(C&&this.tree.isListContainer(C)){this.mergeAdjustLists(C) +}}else{this.splitContainerOf(B,true); +this.correctEmptyElement(B); +var E=this.splitContainerOf(A); +this.insertNodeAt(B,E.parentNode,"before"); +this.deleteNode(E.parentNode); +D=B +}}else{if(this.tree.isTableCell(A)||this.tree.isTableCell(B)){}else{this.splitContainerOf(B,true); +this.correctEmptyElement(B); +D=this.insertNodeAt(B,A,"before"); +this.deleteNode(A) +}}}return D +},insertNewBlockAround:function(E,D,B){var C=E.nodeName==="LI"||E.parentNode.nodeName==="LI"; +this.removeTrailingWhitespace(E); +if(this.isFirstLiWithNestedList(E)&&!B&&D){var A=this.getParentElementOf(E,["LI"]); +var F=this._insertNewBlockAround(A,D); +return F +}else{if(C&&!B){var A=this.getParentElementOf(E,["LI"]); +var F=this._insertNewBlockAround(E,D); +if(A!==E){F=this.splitContainerOf(F,false,"prev") +}return F +}else{if(this.tree.isBlockContainer(E)){this.wrapAllInlineOrTextNodesAs("P",E,true); +return this._insertNewBlockAround(E.firstChild,D,B) +}else{return this._insertNewBlockAround(E,D,this.tree.isHeading(E)?"P":B) +}}}},_insertNewBlockAround:function(B,C,A){var D=this.createElement(A||B.nodeName); +this.copyAttributes(B,D,false); +this.correctEmptyElement(D); +D=this.insertNodeAt(D,B,C?"before":"after"); +return D +},applyTagIntoElement:function(B,C,D){if(!B&&!D){return null +}var A=C; +if(B){if(this.tree.isBlockOnlyContainer(B)){A=this.wrapBlock(B,C) +}else{if(this.tree.isBlockContainer(C)){var E=this.createElement(B); +this.moveChildNodes(C,E); +A=this.insertNodeAt(E,C,"start") +}else{if(this.tree.isBlockContainer(B)&&this.hasImportantAttributes(C)){A=this.wrapBlock(B,C) +}else{A=this.replaceTag(B,C) +}}}}if(D){A.className=D +}return A +},applyTagIntoElements:function(C,L,M,J){if(!C&&!J){return[L,M] +}var F=[]; +if(C){if(this.tree.isBlockContainer(C)){var H=this.tree.findCommonAncestorAndImmediateChildrenOf(L,M); +var D=H.left; +var B=this.insertNodeAt(this.createElement(C),D,"before"); +var N=H.parent.nodeName==="LI"&&H.parent.parentNode.childNodes.length===1&&!H.left.previousSilbing&&!H.right.nextSibling; +if(N){var I=D.parentNode.parentNode; +this.insertNodeAt(B,I,"before"); +B.appendChild(I) +}else{while(D!==H.right){next=D.nextSibling; +B.appendChild(D); +D=next +}B.appendChild(H.right) +}F.push(B) +}else{var A=this.getBlockElementsBetween(L,M); +for(var G=0; +G=2&&this.isMarker(D.lastChild.previousSibling)&&D.lastChild.nodeType===3&&D.lastChild.nodeValue.length===1&&D.lastChild.nodeValue.charCodeAt(0)===160){this.deleteNode(D.lastChild) +}this.removePlaceHoldersAndEmptyNodes(D); +if(this.isEmptyBlock(D)){if(this.tree.isAtomic(D)){D=this.replaceTag("P",D) +}D=this.replaceTag(F.nodeName,D)||D; +D.innerHTML="" +}else{if(D.firstChild===D.lastChild&&this.isMarker(D.firstChild)){D=this.replaceTag(F.nodeName,D)||D +}}if(this.isEmptyBlock(F)){if(this.tree.isAtomic(F)){F=this.replaceTag("P",F) +}F.innerHTML="" +}this.moveChildNodes(F,D); +this.deleteNode(F); +return D +}finally{if(H&&this.isEmptyBlock(H)){this.deleteNode(H,true) +}if(G&&this.isEmptyBlock(G)){this.deleteNode(G,true) +}if(H){this.unwrapUnnecessaryParagraph(H) +}if(G){this.unwrapUnnecessaryParagraph(G) +}}},mergeAdjustLists:function(A,G,D){var F=A.previousSibling; +var C=F&&(F.nodeName===A.nodeName&&F.className===A.className); +if((!D||D.toLowerCase()==="prev")&&(C||(G&&this.tree.isListContainer(F)))){while(F.lastChild){this.insertNodeAt(F.lastChild,A,"start") +}this.deleteNode(F) +}var E=A.nextSibling; +var B=E&&(E.nodeName===A.nodeName&&E.className===A.className); +if((!D||D.toLowerCase()==="next")&&(B||(G&&this.tree.isListContainer(E)))){while(E.firstChild){this.insertNodeAt(E.firstChild,A,"end") +}this.deleteNode(E) +}},moveChildNodes:function(B,A){if(this.tree.isDescendantOf(B,A)||["HTML","HEAD"].indexOf(A.nodeName)!==-1){throw"Illegal argument. Cannot move children of element["+B.nodeName+"] to element["+A.nodeName+"]" +}if(B===A){return +}while(B.firstChild){A.appendChild(B.firstChild) +}},copyAttributes:function(E,D,B){var A=E.attributes; +if(!A){return +}for(var C=0; +C0?D:E +},outdentElementsCode:function(A){if(A.tagName==="LI"){A=A.parentNode +}if(A.tagName==="OL"&&A.className==="code"){return true +}return false +},_outdentElements:function(C,F,E){for(var B=0; +B0; +var D=P.length>0; +var M=null; +if(E){M=H +}else{if(D){M=P +}else{M=B +}}for(var F=0; +F0; +if(D){R="CODE" +}else{if(B){R=false +}else{R=K.nodeName +}}}var P=I.style.textAlign||"left"; +return{block:O,em:C,strong:H,strike:A,underline:J,superscription:L,subscription:Q,list:R,justification:P,foregroundColor:N,backgroundColor:G,fontSize:F,fontName:M} +},hasImportantAttributes:function(A){throw"Not implemented" +},isEmptyBlock:function(A){throw"Not implemented" +},getCurrentElement:function(){throw"Not implemented" +},getCurrentBlockElement:function(){var B=this.getCurrentElement(); +if(!B){return null +}var A=this.getParentBlockElementOf(B); +if(!A){return null +}return(A.nodeName==="BODY")?null:A +},getParentBlockElementOf:function(A){while(A){if(this.tree._blockTags.indexOf(A.nodeName)!==-1){return A +}A=A.parentNode +}return null +},getParentElementOf:function(B,A){while(B){if(A.indexOf(B.nodeName)!==-1){return B +}B=B.parentNode +}return null +},getBlockElementsBetween:function(B,A){return this.tree.collectNodesBetween(B,A,function(C){return C.nodeType===1&&this.tree.isBlock(C) +}.bind(this)) +},getBlockElementAtSelectionStart:function(){throw"Not implemented" +},getBlockElementAtSelectionEnd:function(){throw"Not implemented" +},getBlockElementsAtSelectionEdge:function(B,A){throw"Not implemented" +},getSelectedBlockElements:function(){var B=this.getBlockElementsAtSelectionEdge(true,true); +var C=B[0]; +var A=B[1]; +return this.tree.collectNodesBetween(C,A,function(D){return D.nodeType===1&&this.tree.isBlock(D) +}.bind(this)) +},getElementById:function(A){return this.getDoc().getElementById(A) +},$:function(A){return this.getElementById(A) +},getFirstChild:function(B){if(!B){return null +}var A=xq.$A(B.childNodes); +return A.find(function(C){return !this.isEmptyTextNode(C) +}.bind(this)) +},getLastChild:function(A){throw"Not implemented" +},getNextSibling:function(A){while(A=A.nextSibling){if(A.nodeType!==3||!A.nodeValue.isBlank()){break +}}return A +},getBottommostFirstChild:function(A){while(A.firstChild&&A.nodeType===1){A=A.firstChild +}return A +},getBottommostLastChild:function(A){while(A.lastChild&&A.nodeType===1){A=A.lastChild +}return A +},_getCssValue:function(C,A){if(!C||C.length===0){return{value:0,unit:A} +}var B=C.match(/(\d+)(.*)/); +return{value:parseInt(B[1]),unit:B[2]||A} +}}); +xq.rdom.Trident=xq.Class(xq.rdom.Base,{makePlaceHolder:function(){return this.createTextNode(" ") +},makePlaceHolderString:function(){return" " +},makeEmptyParagraph:function(){return this.createElementFromHtml("

 

") +},isPlaceHolder:function(A){return false +},getOuterHTML:function(A){return A.outerHTML +},getCurrentBlockElement:function(){var D=this.getCurrentElement(); +if(!D){return null +}var C=this.getParentBlockElementOf(D); +if(!C){return null +}if(C.nodeName==="BODY"){var B=this.insertNode(this.makeEmptyParagraph()); +var A=B.nextSibling; +if(this.tree.isAtomic(A)){this.deleteNode(B); +return A +}}else{return C +}},insertNode:function(B){if(this.hasSelection()){this.collapseSelection(true) +}this.rng().pasteHTML(''); +var A=this.$("xquared_temp"); +if(B.id==="xquared_temp"){return A +}if(A){A.replaceNode(B) +}return B +},removeTrailingWhitespace:function(F){if(!F){return +}if(this.tree.isBlockOnlyContainer(F)){return +}if(this.isEmptyBlock(F)){return +}var E=F.innerText; +var C=F.innerHTML; +var B=E.charCodeAt(E.length-1); +if(E.length<=1||[32,160].indexOf(B)===-1){return +}if(E==C.replace(/ /g," ")){F.innerHTML=C.replace(/ $/,""); +return +}var D=F; +while(D&&D.nodeType!==3){D=D.lastChild +}if(!D){return +}var A=D.nodeValue; +if(A.length<=1){this.deleteNode(D,true) +}else{D.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 C=this.getCurrentElement(); +if(this.tree.isBlockOnlyContainer(C)){C=this.insertNode(this.makeEmptyParagraph()); +if(this.tree.isAtomic(C.nextSibling)){this.recentHR=C.nextSibling; +this.deleteNode(C); +return false +}else{var B=this.tree.findForward(C,function(D){return this.tree.isBlock(D)&&!this.tree.isBlockOnlyContainer(D) +}.bind(this)); +if(B){this.deleteNode(C); +this.placeCaretAtStartOf(B) +}else{this.placeCaretAtStartOf(C) +}return true +}}else{C=this.getCurrentBlockElement(); +if(C.nodeType===3){C=C.parentNode +}if(this.tree.hasMixedContents(C)){var A=this.pushMarker(); +this.wrapAllInlineOrTextNodesAs("P",C,true); +this.popMarker(true); +return true +}else{if((this.tree.isTextOrInlineNode(C.previousSibling)||this.tree.isTextOrInlineNode(C.nextSibling))&&this.tree.hasMixedContents(C.parentNode)){this.wrapAllInlineOrTextNodesAs("P",C.parentNode,true); +return true +}else{return false +}}}},execCommand:function(A,B){return this.getDoc().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") +},applyRemoveLink:function(){this.execCommand("Unlink") +},focus:function(){this.getWin().focus() +},sel:function(){return this.getDoc().selection +},crng:function(){return this.getDoc().body.createControlRange() +},rng:function(){try{var B=this.sel(); +return(B===null)?null:B.createRange() +}catch(A){return null +}},hasSelection:function(){var A=this.sel().type.toLowerCase(); +if("none"===A){return false +}if("text"===A&&this.getSelectionAsHtml().length===0){return false +}return true +},deleteSelection:function(){if(this.getSelectionAsText()!==""){this.sel().clear() +}},placeCaretAtStartOf:function(A){var B=this.insertNodeAt(this.createElement("SPAN"),A,"start"); +this.selectElement(B); +this.collapseSelection(false); +this.deleteNode(B) +},selectElement:function(B,C,D){if(!B){throw"[element] is null" +}if(B.nodeType!==1){throw"[element] is not an element" +}var A=null; +if(!D&&this.tree.isAtomic(B)){A=this.crng(); +A.addElement(B) +}else{var A=this.rng(); +A.moveToElementText(B) +}A.select() +},selectBlocksBetween:function(D,B){var A=this.rng(); +var C=this.rng(); +C.moveToElementText(D); +A.setEndPoint("StartToStart",C); +C.moveToElementText(B); +A.setEndPoint("EndToEnd",C); +A.select() +},collapseSelection:function(C){if(this.sel().type.toLowerCase()==="control"){var B=this.getCurrentElement(); +this.sel().empty(); +this.selectElement(B,false,true) +}var A=this.rng(); +A.collapse(C); +A.select() +},getSelectionAsHtml:function(){var A=this.rng(); +return A&&A.htmlText?A.htmlText:"" +},getSelectionAsText:function(){var A=this.rng(); +return A&&A.text?A.text:"" +},hasImportantAttributes:function(A){return !!(A.id||A.className||A.style.cssText) +},isEmptyBlock:function(A){if(!A.hasChildNodes()){return true +}if(A.nodeType===3&&!A.nodeValue){return true +}if([" "," ",""].indexOf(A.innerHTML)!==-1){return true +}return false +},getLastChild:function(C){if(!C||!C.hasChildNodes()){return null +}var A=xq.$A(C.childNodes).reverse(); +for(var B=0; +B' +},makeEmptyParagraph:function(){return this.createElementFromHtml('


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


') +},isPlaceHolder:function(A){return A.className==="webkit-block-placeholder" +},selectElement:function(B,C){if(!B){throw"[element] is null" +}if(B.nodeType!==1){throw"[element] is not an element" +}var A=this.rng()||this.getDoc().createRange(); +if(C){A.selectNode(B) +}else{A.selectNodeContents(B) +}this._setSelectionByRange(A) +},getSelectionAsHtml:function(){var B=this.createElement("div"); +var A=this.rng(); +var C=this.rng().cloneContents(); +if(C){B.appendChild(C) +}return B.innerHTML +},collapseSelection:function(B){var A=this.rng(); +A.collapse(B); +this._setSelectionByRange(A) +},_setSelectionByRange:function(A){var B=this.sel(); +B.setBaseAndExtent(A.startContainer,A.startOffset,A.endContainer,A.endOffset) +}}); +xq.rdom.Base.createInstance=function(){if(xq.Browser.isTrident){return new xq.rdom.Trident() +}else{if(xq.Browser.isWebkit){return new xq.rdom.Webkit() +}else{return new xq.rdom.Gecko() +}}}; +xq.validator={}; +xq.validator.Base=xq.Class({initialize:function(C,B,A){xq.addToFinalizeQueue(this); +xq.asEventSource(this,"Validator",["Preprocessing","BeforeDomValidation","AfterDomValidation","BeforeStringValidation","AfterStringValidation","BeforeDomInvalidation","AfterDomInvalidation","BeforeStringInvalidation","AfterStringInvalidation"]); +this.whitelist=A||xq.predefinedWhitelist; +this.pRGB=xq.compilePattern("rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)"); +this.curUrl=C; +this.curUrlParts=C?C.parseURL():null; +this.urlValidationMode=B +},validate:function(C,A){C=A?C:C.cloneNode(true); +this._fireOnBeforeDomValidation(C); +this.validateDom(C); +this._fireOnAfterDomValidation(C); +var B={value:C.innerHTML}; +this._fireOnBeforeStringValidation(B); +B.value=this.validateString(B.value); +this._fireOnAfterStringValidation(B); +return B.value +},validateDom:function(A){throw"Not implemented" +},validateString:function(A){throw"Not implemented" +},invalidate:function(B){var B={value:B}; +this._fireOnPreprocessing(B); +var A=document.createElement("DIV"); +A.innerHTML=B.value; +this._fireOnBeforeDomInvalidation(A); +this.invalidateDom(A); +this._fireOnAfterDomInvalidation(A); +B.value=A.innerHTML; +this._fireOnBeforeStringInvalidation(B); +B.value=this.invalidateString(B.value); +this._fireOnAfterStringInvalidation(B); +return B.value +},invalidateDom:function(A){throw"Not implemented" +},invalidateString:function(A){throw"Not implemented" +},invalidateStrikesAndUnderlines:function(B){var G=xq.rdom.Base.createInstance(); +G.setRoot(B); +var A=xq.Browser.isTrident?"className":"class"; +var E=xq.getElementsByClassName(G.getRoot(),"underline","em"); +var D=xq.compilePattern("(^|\\s)underline($|\\s)"); +var H=E.length; +for(var C=0; +C|\s+[^>]*>)/ig,'/ig,""); +return A +},validateUnderline:function(A){A=A.replace(/|\s+[^>]*>)/ig,'/ig,""); +return A +},replaceTag:function(A,C,B){return A.replace(new RegExp("(|\\s+[^>]*>)","ig"),"$1"+B+"$2") +},validateSelfClosingTags:function(A){return A.replace(/<(br|hr|img|value)([^>]*?)>/img,function(D,B,C){return"<"+B+C+" />" +}) +},validateFont:function(D){var H=xq.rdom.Base.createInstance(); +H.setRoot(D); +var I=D.getElementsByTagName("FONT"); +var B=["xx-small","x-small","small","medium","large","x-large","xx-large"]; +var F=I.length-1; +for(var E=F; +E>=0; +E--){var A=I[E]; +var C=A.getAttribute("color"); +var K=A.style.backgroundColor; +var G=A.getAttribute("face"); +var L=B[parseInt(A.getAttribute("size"))%8-1]; +if(C||K||G||L){var J=H.replaceTag("span",A); +J.removeAttribute("color"); +J.removeAttribute("face"); +J.removeAttribute("size"); +if(C){J.style.color=C +}if(K){J.style.backgroundColor=K +}if(G){J.style.fontFamily=G +}if(L){J.style.fontSize=L +}}}},invalidateFont:function(D){var I=xq.rdom.Base.createInstance(); +I.setRoot(D); +var G=D.getElementsByTagName("SPAN"); +var B={"xx-small":1,"x-small":2,small:3,medium:4,large:5,"x-large":6,"xx-large":7}; +var F=G.length-1; +for(var E=F; +E>=0; +E--){var K=G[E]; +if(K.className==="strike"){continue +}var C=K.style.color; +var J=K.style.backgroundColor; +var H=K.style.fontFamily; +var L=B[K.style.fontSize]; +if(C||J||H||L){var A=I.replaceTag("font",K); +A.style.cssText=""; +if(C){A.setAttribute("color",this.asRGB(C)) +}if(J){A.style.backgroundColor=J +}if(H){A.setAttribute("face",H) +}if(L){A.setAttribute("size",L) +}}}},asRGB:function(C){if(C.indexOf("#")===0){return C +}var B=this.pRGB.exec(C); +if(!B){return C +}var E=Number(B[1]).toString(16); +var D=Number(B[2]).toString(16); +var A=Number(B[3]).toString(16); +if(E.length===1){E="0"+E +}if(D.length===1){D="0"+D +}if(A.length===1){A="0"+A +}return"#"+E+D+A +},removeComments:function(A){return A.replace(//img,"") +},removeDangerousElements:function(C){var A=C.getElementsByTagName("SCRIPT"); +for(var B=A.length-1; +B>=0; +B--){A[B].parentNode.removeChild(A[B]) +}},applyWhitelist:function(B){var A=this.whitelist; +var E=null; +var D=xq.compilePattern('(^|\\s")([^"=]+)(\\s|$)',"g"); +var C=xq.compilePattern('(\\S+?)="[^"]*"',"g"); +return B.replace(new RegExp("(]+?)(>|\\s+([^>]*?)(\\s?/?)>)","g"),function(J,M,O,I,N,K){if(!(E=A[O])){return"" +}if(N){if(xq.Browser.isTrident){N=N.replace(D,'$1$2="$2"$3') +}var L=[]; +var G=N.match(C); +for(var H=0; +H" +}else{return M+O+K+">" +}}else{return J +}}) +},makeUrlsRelative:function(A){var D=this.curUrl; +var E=this.curUrlParts; +var C=xq.compilePattern('(href|src)="([^"]+)"',"g"); +var B=xq.compilePattern("^\\w+://"); +return A.replace(/(<\w+\s+)(\/|([^>]+?)(\/?))>/g,function(J,I,F,H,G){if(H){H=H.replace(C,function(O,N,M){var L=null; +if(M.charAt(0)==="#"){L=E.includeQuery+M +}else{if(M.charAt(0)==="?"){L=E.includePath+M +}else{if(M.charAt(0)==="/"){L=E.includeHost+M +}else{if(M.match(B)){L=M +}else{L=E.includeBase+M +}}}}var K=L; +if(L===E.includeHost){K="/" +}else{if(L.indexOf(E.includeQuery)===0){K=L.substring(E.includeQuery.length) +}else{if(L.indexOf(E.includePath)===0){K=L.substring(E.includePath.length) +}else{if(L.indexOf(E.includeBase)===0){K=L.substring(E.includeBase.length) +}else{if(L.indexOf(E.includeHost)===0){K=L.substring(E.includeHost.length) +}}}}}if(K===""){K="#" +}return N+'="'+K+'"' +}); +return I+H+G+">" +}else{return J +}}); +return A +},makeUrlsHostRelative:function(A){var D=this.curUrl; +var E=this.curUrlParts; +var C=xq.compilePattern('(href|src)="([^"]+)"',"g"); +var B=xq.compilePattern("^\\w+://"); +return A.replace(/(<\w+\s+)(\/|([^>]+?)(\/?))>/g,function(J,I,F,H,G){if(H){H=H.replace(C,function(O,N,M){var L=null; +if(M.charAt(0)==="#"){L=E.includeQuery+M +}else{if(M.charAt(0)==="?"){L=E.includePath+M +}else{if(M.charAt(0)==="/"){L=E.includeHost+M +}else{if(M.match(B)){L=M +}else{L=E.includeBase+M +}}}}var K=L; +if(L===E.includeHost){K="/" +}else{if(L.indexOf(E.includeQuery)===0&&L.indexOf("#")!==-1){K=L.substring(L.indexOf("#")) +}else{if(L.indexOf(E.includeHost)===0){K=L.substring(E.includeHost.length) +}}}if(K===""){K="#" +}return N+'="'+K+'"' +}); +return I+H+G+">" +}else{return J +}}); +return A +},makeUrlsAbsolute:function(A){var D=this.curUrl; +var E=this.curUrlParts; +var C=xq.compilePattern('(href|src)="([^"]+)"',"g"); +var B=xq.compilePattern("^\\w+://"); +return A.replace(/(<\w+\s+)(\/|([^>]+?)(\/?))>/g,function(J,I,F,H,G){if(H){H=H.replace(C,function(N,M,L){var K=null; +if(L.charAt(0)==="#"){K=E.includeQuery+L +}else{if(L.charAt(0)==="?"){K=E.includePath+L +}else{if(L.charAt(0)==="/"){K=E.includeHost+L +}else{if(L.match(B)){K=L +}else{K=E.includeBase+L +}}}}return M+'="'+K+'"' +}); +return I+H+G+">" +}else{return J +}}) +}}); +xq.validator.Trident=xq.Class(xq.validator.Base,{validateDom:function(A){this.removeDangerousElements(A); +this.validateFont(A) +},validateString:function(B){try{B=this.validateStrike(B); +B=this.validateUnderline(B); +B=this.performFullValidation(B) +}catch(A){}return B +},invalidateDom:function(A){this.invalidateFont(A); +this.invalidateStrikesAndUnderlines(A) +},invalidateString:function(A){A=this.removeComments(A); +return A +},performFullValidation:function(A){A=this.lowerTagNamesAndUniformizeQuotation(A); +A=this.validateSelfClosingTags(A); +A=this.applyWhitelist(A); +if(this.urlValidationMode==="relative"){A=this.makeUrlsRelative(A) +}else{if(this.urlValidationMode==="host_relative"){A=this.makeUrlsHostRelative(A) +}else{if(this.urlValidationMode==="absolute"){}}}return A +},lowerTagNamesAndUniformizeQuotation:function(A){this.pAttrQuotation1=xq.compilePattern('\\s(\\w+?)=\\s+"([^"]+)"',"mg"); +this.pAttrQuotation2=xq.compilePattern('\\s(\\w+?)=([^ "]+)',"mg"); +this.pAttrQuotation3=xq.compilePattern('\\sNAME="(\\w+?)" VALUE="(\\w+?)"',"mg"); +A=A.replace(/<(\/?)(\w+)([^>]*?)>/img,function(E,B,D,C){return"<"+B+D.toLowerCase()+this.correctHtmlAttrQuotation(C)+">" +}.bind(this)); +return A +},correctHtmlAttrQuotation:function(A){A=A.replace(this.pAttrQuotation1,function(D,B,C){return" "+B.toLowerCase()+'="'+C+'"' +}); +A=A.replace(this.pAttrQuotation2,function(D,B,C){return" "+B.toLowerCase()+'="'+C+'"' +}); +A=A.replace(this.pAttrQuotation3,function(D,B,C){return' name="'+B+'" value="'+C+'"' +}); +return A +}}); +xq.validator.W3=xq.Class(xq.validator.Base,{validateDom:function(A){var B=xq.rdom.Base.createInstance(); +B.setRoot(A); +this.removeDangerousElements(A); +B.removePlaceHoldersAndEmptyNodes(A); +this.validateFont(A) +},validateString:function(B){try{B=this.replaceTag(B,"b","strong"); +B=this.replaceTag(B,"i","em"); +B=this.validateStrike(B); +B=this.validateUnderline(B); +B=this.addNbspToEmptyBlocks(B); +B=this.performFullValidation(B); +B=this.insertNewlineBetweenBlockElements(B) +}catch(A){}return B +},invalidateDom:function(A){this.invalidateFont(A); +this.invalidateStrikesAndUnderlines(A) +},invalidateString:function(A){A=this.replaceTag(A,"strong","b"); +A=this.replaceTag(A,"em","i"); +A=this.removeComments(A); +A=this.replaceNbspToBr(A); +return A +},performFullValidation:function(A){A=this.validateSelfClosingTags(A); +A=this.applyWhitelist(A); +if(this.urlValidationMode==="relative"){A=this.makeUrlsRelative(A) +}else{if(this.urlValidationMode==="host_relative"){A=this.makeUrlsHostRelative(A) +}else{if(this.urlValidationMode==="absolute"){A=this.makeUrlsAbsolute(A) +}}}return A +},insertNewlineBetweenBlockElements:function(A){var C=new xq.DomTree().getBlockTags().join("|"); +var B=new RegExp("([^\n])","img"); +return A.replace(B,"\n$2") +},addNbspToEmptyBlocks:function(B){var C=new xq.DomTree().getBlockTags().join("|"); +var A=new RegExp("<("+C+")>\\s*?","img"); +return B.replace(A,"<$1> ") +},replaceNbspToBr:function(B){var D=new xq.DomTree().getBlockTags().join("|"); +var A=new RegExp("<("+D+")>( |\xA0)?","img"); +var C=xq.rdom.Base.createInstance(); +return B.replace(A,"<$1>"+C.makePlaceHolderString()+"") +}}); +xq.validator.Gecko=xq.Class(xq.validator.W3,{}); +xq.validator.Webkit=xq.Class(xq.validator.W3,{validateDom:function(A){var B=xq.rdom.Base.createInstance(); +B.setRoot(A); +this.removeDangerousElements(A); +B.removePlaceHoldersAndEmptyNodes(A); +this.validateAppleStyleTags(A) +},validateString:function(B){try{B=this.addNbspToEmptyBlocks(B); +B=this.performFullValidation(B); +B=this.insertNewlineBetweenBlockElements(B) +}catch(A){}return B +},invalidateDom:function(A){this.invalidateAppleStyleTags(A) +},invalidateString:function(A){A=this.replaceTag(A,"strong","b"); +A=this.replaceTag(A,"em","i"); +A=this.removeComments(A); +A=this.replaceNbspToBr(A); +return A +},validateAppleStyleTags:function(C){var E=xq.rdom.Base.createInstance(); +E.setRoot(C); +var A=xq.getElementsByClassName(E.getRoot(),"apple-style-span"); +for(var B=0; +B0&&this.index>0 +},isRedoable:function(){return this.queue.length>0&&this.index0){var A=B.html.substring(0,B.caret)+''+B.html.substring(B.caret); +this.rdom.getRoot().innerHTML=A +}else{this.rdom.getRoot().innerHTML=B.html +}this.restoreCaret() +},pushContent:function(B){if(xq.Browser.isTrident&&!B&&!this.rdom.hasFocus()){return false +}if(!this.rdom.getCurrentElement()){return false +}var A=this.rdom.getRoot().innerHTML; +if(A===(this.queue[this.index]?this.queue[this.index].html:null)){return false +}var C=B?-1:this.saveCaret(); +if(this.queue.length>=this.max){this.queue.shift() +}else{this.index++ +}this.queue.splice(this.index,this.queue.length-this.index,{html:A,caret:C}); +return true +},clear:function(){this.index=-1; +this.queue=[]; +this.pushContent(true) +},saveCaret:function(){if(this.rdom.hasSelection()){return null +}var B=this.rdom.saveSelection(); +var A=this.rdom.pushMarker(); +var D=xq.Browser.isTrident?"A)?B.cells[A]:null +},getRowAt:function(A){if(this.hasHeadingAtTop()){return A===0?this.table.tHead.rows[0]:this.table.tBodies[0].rows[A-1] +}else{var B=this.table.tBodies[0].rows; +return(B.length>A)?B[A]:null +}},getDom:function(){return this.table +},hasHeadingAtTop:function(){return !!(this.table.tHead&&this.table.tHead.rows[0]) +},hasHeadingAtLeft:function(){return this.table.tBodies[0].rows[0].cells[0].nodeName==="TH" +},correctEmptyCells:function(){var A=xq.$A(this.table.getElementsByTagName("TH")); +var C=xq.$A(this.table.getElementsByTagName("TD")); +for(var B=0; +B'); +if(I){F.push(""); +for(var D=0; +D") +}F.push(""); +J-=1 +}F.push(""); +for(var D=0; +D"); +for(var B=0; +B") +}else{F.push("") +}}F.push("") +}F.push(""); +F.push(""); +var A=E.createElement("div"); +A.innerHTML=F.join(""); +var H=new xq.RichTable(E,A.firstChild); +H.correctEmptyCells(); +return H +}; +xq.ui={}; +xq.ui.FormDialog=xq.Class({initialize:function(D,C,B,A){xq.addToFinalizeQueue(this); +this.xed=D; +this.html=C; +this.onLoadHandler=B||function(){}; +this.onCloseHandler=A||function(){}; +this.form=null +},show:function(C){C=C||{}; +C.position=C.position||"centerOfWindow"; +C.mode=C.mode||"modal"; +C.cancelOnEsc=C.cancelOnEsc||true; +var B=this; +var A=document.createElement("DIV"); +A.style.display="none"; +document.body.appendChild(A); +A.innerHTML=this.html; +this.form=A.getElementsByTagName("FORM")[0]; +this.form.onsubmit=function(){B.onCloseHandler(xq.serializeForm(this)); +B.close(); +return false +}; +var E=xq.getElementsByClassName(this.form,"cancel")[0]; +E.onclick=function(){B.onCloseHandler(); +B.close() +}; +if(C.mode==="modal"){this.dimmed=document.createElement("DIV"); +this.dimmed.style.position="absolute"; +this.dimmed.style.backgroundColor="black"; +this.dimmed.style.opacity=0.5; +this.dimmed.style.filter="alpha(opacity=50)"; +this.dimmed.style.zIndex=902; +this.dimmed.style.top="0px"; +this.dimmed.style.left="0px"; +document.body.appendChild(this.dimmed); +this.resizeDimmedDiv=function(F){this.dimmed.style.display="none"; +this.dimmed.style.width=document.documentElement.scrollWidth+"px"; +this.dimmed.style.height=document.documentElement.scrollHeight+"px"; +this.dimmed.style.display="block" +}.bind(this); +xq.observe(window,"resize",this.resizeDimmedDiv); +this.resizeDimmedDiv() +}document.body.appendChild(this.form); +A.parentNode.removeChild(A); +this.setPosition(C.position); +var D=xq.getElementsByClassName(this.form,"initialFocus"); +if(D.length>0){D[0].focus() +}if(C.cancelOnEsc){xq.observe(this.form,"keydown",function(F){if(F.keyCode===27){this.onCloseHandler(); +this.close() +}}.bind(this)) +}this.onLoadHandler(this) +},close:function(){this.form.parentNode.removeChild(this.form); +if(this.dimmed){this.dimmed.parentNode.removeChild(this.dimmed); +this.dimmed=null; +xq.stopObserving(window,"resize",this.resizeDimmedDiv); +this.resizeDimmedDiv=null +}},setPosition:function(E){var F=null; +var B=0; +var H=0; +if(E==="centerOfWindow"){F=document.documentElement; +B+=F.scrollLeft; +H+=F.scrollTop +}else{if(E==="centerOfEditor"){F=this.xed.getCurrentEditMode()=="wysiwyg"?this.xed.wysiwygEditorDiv:this.xed.sourceEditorDiv; +var A=F; +do{B+=A.offsetLeft; +H+=A.offsetTop +}while(A=A.offsetParent) +}else{if(E==="nearbyCaret"){throw"Not implemented yet" +}else{throw"Invalid argument: "+E +}}}var I=F.clientWidth; +var D=F.clientHeight; +var C=this.form.clientWidth; +var G=this.form.clientHeight; +B+=parseInt((I-C)/2); +H+=parseInt((D-G)/2); +this.form.style.left=B+"px"; +this.form.style.top=H+"px" +}}); +xq.ui.QuickSearchDialog=xq.Class({initialize:function(A,B){xq.addToFinalizeQueue(this); +this.xed=A; +this.rdom=xq.rdom.Base.createInstance(); +this.param=B; +if(!this.param.renderItem){this.param.renderItem=function(C){return this.rdom.getInnerText(C) +}.bind(this) +}this.container=null +},getQuery:function(){if(!this.container){return"" +}return this._getInputField().value +},onSubmit:function(A){if(this.matchCount()>0){this.param.onSelect(this.xed,this.list[this._getSelectedIndex()]) +}this.close(); +xq.stopEvent(A); +return false +},onCancel:function(A){if(this.param.onCancel){this.param.onCancel(this.xed) +}this.close() +},onBlur:function(A){setTimeout(function(){this.onCancel(A) +}.bind(this),400) +},onKey:function(C){var B=new xq.Shortcut("ESC"); +var D=new xq.Shortcut("ENTER"); +var A=new xq.Shortcut("UP"); +var E=new xq.Shortcut("DOWN"); +if(B.matches(C)){this.onCancel(C) +}else{if(D.matches(C)){this.onSubmit(C) +}else{if(A.matches(C)){this._moveSelectionUp() +}else{if(E.matches(C)){this._moveSelectionDown() +}else{this.updateList() +}}}}},onClick:function(C){var B=C.srcElement||C.target; +if(B.nodeName==="LI"){var A=this._getIndexOfLI(B); +this.param.onSelect(this.xed,this.list[A]) +}},onList:function(A){this.list=A; +this.renderList(A) +},updateList:function(){window.setTimeout(function(){this.param.listProvider(this.getQuery(),this.xed,this.onList.bind(this)) +}.bind(this),0) +},renderList:function(D){var B=this._getListContainer(); +B.innerHTML=""; +for(var C=0; +C=C){B=0 +}A.childNodes[B].className="selected" +}}); +xq.ui.Toolbar=xq.Class({initialize:function(E,C,F,D,B,A){xq.addToFinalizeQueue(this); +this.xed=E; +if(typeof C==="string"){C=xq.$(C) +}if(C&&C.nodeType!==1){throw"[container] is not an element" +}this.wrapper=F; +this.doc=this.wrapper.ownerDocument; +this.buttonMap=D; +this.imagePath=B; +this.structureAndStyleCollector=A; +this.buttons=null; +this.anchorsCache=[]; +this._scheduledUpdate=null; +if(!C){this.create(); +this._addStyleRules([{selector:".xquared div.toolbar",rule:"background-image: url("+B+"toolbarBg.gif)"},{selector:".xquared ul.buttons li",rule:"background-image: url("+B+"toolbarButtonBg.gif)"},{selector:".xquared ul.buttons li.xq_separator",rule:"background-image: url("+B+"toolbarSeparator.gif)"}]) +}else{this.container=C +}},finalize:function(){for(var A=0; +A0?D[0]:null; +if(A){this.buttons[F[B]]=A +}}}var C=this.buttons; +this._updateButtonStatus("emphasis",E.em); +this._updateButtonStatus("strongEmphasis",E.strong); +this._updateButtonStatus("underline",E.underline); +this._updateButtonStatus("strike",E.strike); +this._updateButtonStatus("superscription",E.superscription); +this._updateButtonStatus("subscription",E.subscription); +this._updateButtonStatus("justifyLeft",E.justification==="left"); +this._updateButtonStatus("justifyCenter",E.justification==="center"); +this._updateButtonStatus("justifyRight",E.justification==="right"); +this._updateButtonStatus("justifyBoth",E.justification==="justify"); +this._updateButtonStatus("orderedList",E.list==="OL"); +this._updateButtonStatus("unorderedList",E.list==="UL"); +this._updateButtonStatus("code",E.list==="CODE"); +this._updateButtonStatus("paragraph",E.block==="P"); +this._updateButtonStatus("heading1",E.block==="H1"); +this._updateButtonStatus("heading2",E.block==="H2"); +this._updateButtonStatus("heading3",E.block==="H3"); +this._updateButtonStatus("heading4",E.block==="H4"); +this._updateButtonStatus("heading5",E.block==="H5"); +this._updateButtonStatus("heading6",E.block==="H6") +},enableButtons:function(A){if(!this.container){return +}this._execForAllButtons(A,function(B,C){B.firstChild.className=!C?"":"disabled" +}); +if(xq.Browser.isIE6){this.container.style.display="none"; +setTimeout(function(){this.container.style.display="block" +}.bind(this),0) +}},disableButtons:function(A){this._execForAllButtons(A,function(B,C){B.firstChild.className=C?"":"disabled" +}) +},create:function(){this.container=this.doc.createElement("div"); +this.container.className="toolbar"; +var F=this.doc.createElement("ul"); +F.className="buttons"; +this.container.appendChild(F); +for(var D=0; +D\\s*","img"); +return A.replace(B,"<$1$2>") +},getCurrentContent:function(){if(this.getCurrentEditMode()==="source"){return this.getSourceContent(this.config.noValidationInSourceEditMode) +}else{return this.getWysiwygContent() +}},getWysiwygContent:function(){return this.validator.validate(this.rdom.getRoot()) +},getSourceContent:function(C){var B=this.sourceEditorTextarea.value; +if(C){return B +}var A=document.createElement("div"); +A.innerHTML=this.removeUnnecessarySpaces(B); +var D=xq.rdom.Base.createInstance(); +D.wrapAllInlineOrTextNodesAs("P",A,true); +return this.validator.validate(A,true) +},setStaticContent:function(A){this.contentElement.value=A; +this._fireOnStaticContentChanged(this,A) +},getStaticContent:function(){return this.contentElement.value +},getStaticContentAsDOM:function(){var A=this.doc.createElement("DIV"); +A.innerHTML=this.contentElement.value; +return A +},focus:function(){if(this.getCurrentEditMode()==="wysiwyg"){this.rdom.focus(); +if(this.toolbar){this.toolbar.triggerUpdate() +}}else{if(this.getCurrentEditMode()==="source"){this.sourceEditorTextarea.focus() +}}},getWysiwygEditorDiv:function(){return this.wysiwygEditorDiv +},getSourceEditorDiv:function(){return this.sourceEditorDiv +},getOuterFrame:function(){return this.outerFrame +},getOuterDoc:function(){return this.outerFrame.contentWindow.document +},getFrame:function(){return this.editorFrame +},getWin:function(){return this.rdom.getWin() +},getDoc:function(){return this.rdom.getDoc() +},getBody:function(){return this.rdom.getRoot() +},getOutmostWrapper:function(){return this.outmostWrapper +},_createIFrame:function(C,B,A){var D=C.createElement("iframe"); +if(xq.Browser.isIE){D.src='javascript:""' +}D.style.width=B||"100%"; +D.style.height=A||"100%"; +D.setAttribute("frameBorder","0"); +D.setAttribute("marginWidth","0"); +D.setAttribute("marginHeight","0"); +D.setAttribute("allowTransparency","auto"); +return D +},_createDoc:function(A,E,I,G,H,C){var D=[]; +if(!xq.Browser.isTrident){D.push('') +}D.push(''); +D.push(""); +D.push(''); +if(E){D.push(E) +}if(I){for(var B=0; +B') +}}D.push(""); +D.push(""); +if(C){D.push(C) +}D.push(""); +D.push(""); +var F=A.contentWindow.document; +F.open(); +F.write(D.join("")); +F.close(); +return F +},_createEditorFrame:function(E){this.contentElement.style.display="none"; +this.outmostWrapper=this.doc.createElement("div"); +this.outmostWrapper.className="xquared"; +this.contentElement.parentNode.insertBefore(this.outmostWrapper,this.contentElement); +if(this.toolbarContainer||this.config.generateDefaultToolbar){this.toolbar=new xq.ui.Toolbar(this,this.toolbarContainer,this.outmostWrapper,this.config.defaultToolbarButtonMap,this.config.imagePathForDefaultToolbar,function(){var F=this.getCurrentEditMode()==="wysiwyg"?this.lastFocusElement:null; +return F&&F.nodeName!="BODY"?this.rdom.collectStructureAndStyle(F):null +}.bind(this)) +}this.sourceEditorDiv=this.doc.createElement("div"); +this.sourceEditorDiv.className="editor source_editor"; +this.sourceEditorDiv.style.display="none"; +this.outmostWrapper.appendChild(this.sourceEditorDiv); +this.sourceEditorTextarea=this.doc.createElement("textarea"); +this.sourceEditorDiv.appendChild(this.sourceEditorTextarea); +this.wysiwygEditorDiv=this.doc.createElement("div"); +this.wysiwygEditorDiv.className="editor wysiwyg_editor"; +this.outmostWrapper.appendChild(this.wysiwygEditorDiv); +this.outerFrame=this._createIFrame(document); +this.wysiwygEditorDiv.appendChild(this.outerFrame); +var C=this._createDoc(this.outerFrame,''); +this.editorFrame=this._createIFrame(C); +C.body.appendChild(this.editorFrame); +var D=this._createDoc(this.editorFrame,''+(!xq.Browser.isTrident?'':"")+(this.config.changeCursorOnLink?"":""),this.config.contentCssList,this.config.bodyId,this.config.bodyClass,""); +this.rdom.setWin(this.editorFrame.contentWindow); +this.editHistory=new xq.EditHistory(this.rdom); +this.rdom.getDoc().designMode="On"; +if(xq.Browser.isGecko){try{this.rdom.getDoc().execCommand("enableInlineTableEditing",false,"false") +}catch(A){}}this._registerEventHandlers(); +if(this.config.automaticallyHookSubmitEvent&&this.contentElement.form){var B=this.contentElement.form.onsubmit; +this.contentElement.form.onsubmit=function(){this.contentElement.value=this.getCurrentContent(); +return B?B.bind(this.contentElement.form)():true +}.bind(this) +}},_registerEventHandlers:function(){var B=[this.platformDepedentKeyEventType,"click","keyup","mouseup","contextmenu"]; +if(xq.Browser.isTrident&&this.config.changeCursorOnLink){B.push("mousemove") +}var C=this._handleEvent.bindAsEventListener(this); +for(var A=0; +A0){this.contextMenuContainer.removeChild(this.contextMenuContainer.childNodes[0]) +}}for(var B=0; +B" +}else{A.innerHTML=(B.title.toString().escapeHTML()) +}}if(B.className){A.className=B.className +}this.contextMenuContainer.appendChild(A); +return A +},_contextMenuClicked:function(e){this.hideContextMenu(); +if(!this.contextMenuContainer){return +}var node=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"); +return sb.join("") +}}); +xq.macro.Factory=xq.Class({initialize:function(A){this.placeHolderImgSrc=A; +this.macroClazzes={} +},register:function(B){var A=xq.macro[B+"Macro"]; +if(!A){throw"Unknown macro id: ["+B+"]" +}this.macroClazzes[B]=A +},createMacroFromHtml:function(B){for(var C in this.macroClazzes){var A=this.macroClazzes[C]; +if(A.recognize(B)){return new A(C,B,this.placeHolderImgSrc) +}}return null +},createMacroFromDefinition:function(B){var A=this.macroClazzes[B.id]; +if(!A){return null +}return new A(B.id,B.params,this.placeHolderImgSrc) +}}); +xq.Layer=xq.Class({initialize:function(C,B,A){xq.addToFinalizeQueue(this); +this.margin=4; +this.editor=C; +this.element=B; +this.frame=this.editor._createIFrame(this.editor.getOuterDoc(),this.element.offsetWidth-(this.margin*2)+"px",this.element.offsetHeight+(this.margin*2)+"px"); +this.editor.getOuterDoc().body.appendChild(this.frame); +this.doc=C._createDoc(this.frame,'',[],null,null,A); +this.frame.style.position="absolute"; +this.updatePosition() +},getFrame:function(){return this.frame +},getDoc:function(){return this.doc +},getBody:function(){return this.doc.body +},isValid:function(){return this.element&&this.element.parentNode&&this.element.offsetParent +},detach:function(){this.frame.parentNode.removeChild(this.frame); +this.frame=null; +this.element=null +},updatePosition:function(){var E=xq.getCumulativeOffset(this.element,this.editor.rdom.getRoot()); +var D=this.editor.getDoc(); +var A=this.editor.getBody(); +E.left-=D.documentElement.scrollLeft+A.scrollLeft-this.margin; +E.top-=D.documentElement.scrollTop+A.scrollTop-this.margin; +this.frame.style.left=E.left+"px"; +this.frame.style.top=E.top+"px"; +var C=this.doc.body.scrollWidth+(this.margin-1)*2; +var B=this.doc.body.scrollHeight+(this.margin-1)*2; +this.element.width=C; +this.element.height=B; +this.frame.style.width=this.element.offsetWidth-(this.margin*2)+"px"; +this.frame.style.height=this.element.offsetHeight-(this.margin*2)+"px" +}}); +if(!this.JSON){JSON=function(){function f(n){return n<10?"0"+n:n +}Date.prototype.toJSON=function(){return this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z" +}; +var m={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"}; +function stringify(value,whitelist){var a,i,k,l,r=/["\\\x00-\x1f\x7f-\x9f]/g,v; +switch(typeof value){case"string":return r.test(value)?'"'+value.replace(r,function(a){var c=m[a]; +if(c){return c +}c=a.charCodeAt(); +return"\\u00"+Math.floor(c/16).toString(16)+(c%16).toString(16) +})+'"':'"'+value+'"'; +case"number":return isFinite(value)?String(value):"null"; +case"boolean":case"null":return String(value); +case"object":if(!value){return"null" +}if(typeof value.toJSON==="function"){return stringify(value.toJSON()) +}a=[]; +if(typeof value.length==="number"&&!(value.propertyIsEnumerable("length"))){l=value.length; +for(i=0; +i]+(?:/>|>.*?)","img"); +A.value=A.value.replace(B,function(E,C){var D=this.xed.macroFactory.createMacroFromHtml(E); +return D?D.createPlaceHolderHtml():"" +}.bind(this)) +},onValidatorAfterStringValidation:function(A){var C=/]*class="xqlayer"\s+[^>]*\/>/mg; +var B=/]*longdesc="(.+?)"\s+[^>]*\/>/m; +A.value=A.value.replace(C,function(D){var F=JSON.parse(unescape(D.match(B)[1])); +var E=this.xed.macroFactory.createMacroFromDefinition(F); +return E.createHtml() +}.bind(this)) +}}); +xq.macro.FlashMovieMacro=xq.Class(xq.macro.Base,{initFromHtml:function(){this.params.html=this.html +},initFromParams:function(){if(!xq.macro.FlashMovieMacro.recognize(this.params.html)){throw"Unknown src" +}},createHtml:function(){return this.params.html +}}); +xq.macro.FlashMovieMacro.recognize=function(A){var B={tvpot:/http:\/\/flvs\.daum\.net\/flvPlayer\.swf\?/,youtube:/http:\/\/(?:www\.)?youtube\.com\/v\//,pandoratv:/http:\/\/flvr\.pandora\.tv\/flv2pan\/flvmovie\.dll\?/,pandoratv2:/http:\/\/imgcdn\.pandora\.tv\/gplayer\/pandora\_EGplayer\.swf\?/,mncast:/http:\/\/dory\.mncast\.com\/mncHMovie\.swf\?/,yahoo:/http:\/\/d\.yimg\.com\//}; +for(var C in B){if(A.match(B[C])){return true +}}return false +}; +xq.plugin.FlashMovieMacroPlugin=xq.Class(xq.plugin.Base,{onAfterLoad:function(A){A.config.macroIds.push("FlashMovie"); +A.config.defaultToolbarButtonGroups.insert.push({className:"movie",title:"Movie",handler:"xed.handleMovie()"}); +A.handleInsertMovie=function(B){var D=this.macroFactory.createMacroFromDefinition({id:"FlashMovie",params:{html:B}}); +if(D){var C=D.createPlaceHolderHtml(); +this.rdom.insertHtml(C); +var E=this.editHistory.onCommand(); +this._fireOnCurrentContentChanged(this) +}else{alert("Unknown URL pattern") +}return true +}; +A.handleMovie=function(){var B=new xq.ui.FormDialog(this,xq.ui_templates.basicMovieDialog,function(D){},function(E){this.focus(); +if(xq.Browser.isTrident){var D=this.rdom.rng(); +D.moveToBookmark(C); +D.select() +}if(!E){return +}this.handleInsertMovie(E.html) +}.bind(this)); +if(xq.Browser.isTrident){var C=this.rdom.rng().getBookmark() +}B.show({position:"centerOfEditor"}); +return true +} +}}); +xq.macro.IFrameMacro=xq.Class(xq.macro.Base,{initFromHtml:function(){this.params.html=this.html +},initFromParams:function(){if(this.params.html){return +}var C=[]; +C.push(""); +this.params={html:C.join("")} +},createHtml:function(){return this.params.html +}}); +xq.macro.IFrameMacro.recognize=function(A){var B=xq.compilePattern("]+(?:/>|>.*?)","img"); +return !!A.match(B) +}; +xq.plugin.IFrameMacroPlugin=xq.Class(xq.plugin.Base,{onAfterLoad:function(A){A.config.macroIds.push("IFrame"); +A.config.defaultToolbarButtonGroups.insert.push({className:"iframe",title:"IFrame",handler:"xed.handleIFrame()"}); +A.handleIFrame=function(){var B=new xq.ui.FormDialog(this,xq.ui_templates.basicIFrameDialog,function(C){},function(E){this.focus(); +if(!E){return +}var D=this.macroFactory.createMacroFromDefinition({id:"IFrame",params:E}); +if(D){var C=D.createPlaceHolderHtml(); +this.rdom.insertHtml(C) +}else{alert("Unknown error") +}}.bind(this)); +B.show({position:"centerOfEditor"}); +return true +} +}}); +xq.macro.JavascriptMacro=xq.Class(xq.macro.Base,{initFromHtml:function(){var A=xq.compilePattern("src=[\"'](.+?)[\"']","img"); +this.params.url=A.exec(this.html)[1] +},initFromParams:function(){if(!xq.macro.JavascriptMacro.isSafeScript(this.params.url)){throw"Unknown src" +}},createHtml:function(){return''); -} -xq.loadOthers = function() { - var script = xq.findXquaredScript(); - var basePath = script.src.match(/(.*\/)xquared\.js.*/i)[1]; - var others = [ - 'Editor.js', +}; + +/** + * Returns all Xquared script file names + */ +xq.getXquaredScriptFileNames = function() { + return [ + 'Xquared.js', 'Browser.js', - 'Shortcut.js', 'DomTree.js', - 'RichDom.js', - 'RichDomW3.js', - 'RichDomGecko.js', - 'RichDomWebkit.js', - 'RichDomTrident.js', - 'RichTable.js', - 'Validator.js', - 'ValidatorW3.js', - 'ValidatorGecko.js', - 'ValidatorWebkit.js', - 'ValidatorTrident.js', + 'rdom/Base.js', + 'rdom/W3.js', + 'rdom/Gecko.js', + 'rdom/Webkit.js', + 'rdom/Trident.js', + 'rdom/Factory.js', + 'validator/Base.js', + 'validator/W3.js', + 'validator/Gecko.js', + 'validator/Webkit.js', + 'validator/Trident.js', + 'validator/Factory.js', + 'macro/Base.js', + 'macro/Factory.js', + 'macro/FlashMovieMacro.js', + 'macro/IFrameMacro.js', + 'macro/JavascriptMacro.js', 'EditHistory.js', - 'Controls.js', - '_ui_templates.js' + 'plugin/Base.js', + 'RichTable.js', + 'Timer.js', + 'Layer.js', + 'ui/Base.js', + 'ui/Control.js', + 'ui/Toolbar.js', + 'ui/_templates.js', + 'Json2.js', + 'Shortcut.js', + 'Editor.js' ]; - for(var i = 0; i < others.length; i++) { +}; +xq.getXquaredScriptBasePath = function() { + var script = xq.findXquaredScript(); + return script.src.match(/(.*\/)xquared\.js.*/i)[1]; +}; + +xq.loadOthers = function() { + var basePath = xq.getXquaredScriptBasePath(); + var others = xq.getXquaredScriptFileNames(); + + // Xquared.js(this file) should not be loaded again. So the value of "i" starts with 1 instead of 0 + for(var i = 1; i < others.length; i++) { xq.loadScript(basePath + others[i]); - }; + } +}; + +if(xq.shouldLoadOthers()) { + xq.loadOthers(); +} +/** + * @namespace Contains browser detection codes + * + * @requires Xquared.js + */ +xq.Browser = new function() { + // By Rendering Engines + + /** + * True if rendering engine is Trident + * @type boolean + */ + this.isTrident = navigator.appName === "Microsoft Internet Explorer", + + /** + * True if rendering engine is Webkit + * @type boolean + */ + this.isWebkit = navigator.userAgent.indexOf('AppleWebKit/') > -1, + + /** + * True if rendering engine is Gecko + * @type boolean + */ + this.isGecko = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1, + + /** + * True if rendering engine is KHTML + * @type boolean + */ + this.isKHTML = navigator.userAgent.indexOf('KHTML') !== -1, + + /** + * True if rendering engine is Presto + * @type boolean + */ + this.isPresto = navigator.appName === "Opera", + + + + // By Platforms + /** + * True if platform is Mac + * @type boolean + */ + this.isMac = navigator.userAgent.indexOf("Macintosh") !== -1, + + /** + * True if platform is Ubuntu Linux + * @type boolean + */ + this.isUbuntu = navigator.userAgent.indexOf('Ubuntu') !== -1, + + /** + * True if platform is Windows + * @type boolean + */ + this.isWin = navigator.userAgent.indexOf('Windows') !== -1, + + + + // By Browsers + /** + * True if browser is Internet Explorer + * @type boolean + */ + this.isIE = navigator.appName === "Microsoft Internet Explorer", + + /** + * True if browser is Internet Explorer 6 + * @type boolean + */ + this.isIE6 = navigator.userAgent.indexOf('MSIE 6') !== -1, + + /** + * True if browser is Internet Explorer 7 + * @type boolean + */ + this.isIE7 = navigator.userAgent.indexOf('MSIE 7') !== -1, + + /** + * True if browser is Internet Explorer 8 + * @type boolean + */ + this.isIE8 = navigator.userAgent.indexOf('MSIE 8') !== -1, + + /** + * True if browser is Firefox + * @type boolean + */ + this.isFF = navigator.userAgent.indexOf('Firefox') !== -1, + + /** + * True if browser is Firefox 2 + * @type boolean + */ + this.isFF2 = navigator.userAgent.indexOf('Firefox/2') !== -1, + + /** + * True if browser is Firefox 3 + * @type boolean + */ + this.isFF3 = navigator.userAgent.indexOf('Firefox/3') !== -1, + + /** + * True if browser is Safari + * @type boolean + */ + this.isSafari = navigator.userAgent.indexOf('Safari') !== -1 +}; +/** + * @requires Xquared.js + */ +xq.Timer = xq.Class(/** @lends xq.Timer.prototype */{ + /** + * @constructs + * + * @param {Number} precision precision in milliseconds + */ + initialize: function(precision) { + xq.addToFinalizeQueue(this); + + this.precision = precision; + this.jobs = {}; + this.nextJobId = 0; + + this.checker = null; + }, + + finalize: function() { + this.stop(); + }, + + /** + * starts timer + */ + start: function() { + this.stop(); + + this.checker = window.setInterval(function() { + this.executeJobs(); + }.bind(this), this.precision); + }, + + /** + * stops timer + */ + stop: function() { + if(this.checker) window.clearInterval(this.checker); + }, + + /** + * registers new job + * + * @param {Function} job function to execute + * @param {Number} interval interval in milliseconds + * + * @return {Number} job id + */ + register: function(job, interval) { + var jobId = this.nextJobId++; + + this.jobs[jobId] = { + func:job, + interval: interval, + lastExecution: Date.get() + }; + + return jobId; + }, + + /** + * unregister job by job id + * + * @param {Number} job id + */ + unregister: function(jobId) { + delete this.jobs[jobId]; + }, + + /** + * Execute all expired jobs immedialty. This method will be called automatically by interval timer. + */ + executeJobs: function() { + var curDate = new Date(); + + for(var id in this.jobs) { + var job = this.jobs[id]; + if(job.lastExecution.elapsed(job.interval, curDate)) { + try { + job.lastReturn = job.func(); + } catch(e) { + job.lastException = e; + } finally { + job.lastExecution = curDate; + } + } + } + } +}); +/** + * @requires Xquared.js + */ +xq.DomTree = xq.Class(/** @lends xq.DomTree.prototype */{ + /** + * Provides various tree operations. + * + * TODO: Add specs + * + * @constructs + */ + initialize: function() { + xq.addToFinalizeQueue(this); + + this._blockTags = ["DIV", "DD", "LI", "ADDRESS", "CAPTION", "DT", "H1", "H2", "H3", "H4", "H5", "H6", "HR", "P", "BODY", "BLOCKQUOTE", "PRE", "PARAM", "DL", "OL", "UL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; + this._blockContainerTags = ["DIV", "DD", "LI", "BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; + this._listContainerTags = ["OL", "UL", "DL"]; + this._tableCellTags = ["TH", "TD"]; + this._blockOnlyContainerTags = ["BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR"]; + this._atomicTags = ["IMG", "OBJECT", "PARAM", "BR", "HR"]; + }, + + getBlockTags: function() { + return this._blockTags; + }, + + /** + * Find common ancestor(parent) and his immediate children(left and right).
+ *
+ * A --- B -+- C -+- D -+- E
+ * |
+ * +- F -+- G
+ *
+ * For example:
+ * > findCommonAncestorAndImmediateChildrenOf("E", "G")
+ *
+ * will return
+ *
+ * > {parent:"B", left:"C", right:"F"} + */ + findCommonAncestorAndImmediateChildrenOf: function(left, right) { + if(left.parentNode === right.parentNode) { + return { + left:left, + right:right, + parent:left.parentNode + }; + } else { + var parentsOfLeft = this.collectParentsOf(left, true); + var parentsOfRight = this.collectParentsOf(right, true); + var ca = this.getCommonAncestor(parentsOfLeft, parentsOfRight); + + var leftAncestor = parentsOfLeft.find(function(node) {return node.parentNode === ca}); + var rightAncestor = parentsOfRight.find(function(node) {return node.parentNode === ca}); + + return { + left:leftAncestor, + right:rightAncestor, + parent:ca + }; + } + }, + + /** + * Find leaves at edge.
+ *
+ * A --- B -+- C -+- D -+- E
+ * |
+ * +- F -+- G
+ *
+ * For example:
+ * > getLeavesAtEdge("A")
+ *
+ * will return
+ *
+ * > ["E", "G"] + */ + getLeavesAtEdge: function(element) { + if(!element.hasChildNodes()) return [null, null]; + + var findLeft = function(el) { + for (var i = 0; i < el.childNodes.length; i++) { + if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findLeft(el.childNodes[i]); + } + return el; + }.bind(this); + + var findRight=function(el) { + for (var i = el.childNodes.length; i--;) { + if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findRight(el.childNodes[i]); + } + return el; + }.bind(this); + + var left = findLeft(element); + var right = findRight(element); + + return [left === element ? null : left, right === element ? null : right]; + }, + + getCommonAncestor: function(parents1, parents2) { + for(var i = 0; i < parents1.length; i++) { + for(var j = 0; j < parents2.length; j++) { + if(parents1[i] === parents2[j]) return parents1[i]; + } + } + }, + + collectParentsOf: function(node, includeSelf, exitCondition) { + var parents = []; + if(includeSelf) parents.push(node); + + while((node = node.parentNode) && (node.nodeName !== "HTML") && !(typeof exitCondition === "function" && exitCondition(node))) parents.push(node); + return parents; + }, + + isDescendantOf: function(parent, child) { + if(parent.length > 0) { + for(var i = 0; i < parent.length; i++) { + if(this.isDescendantOf(parent[i], child)) return true; + } + return false; + } + + if(parent === child) return false; + + while (child = child.parentNode) + if (child === parent) return true; + return false; + }, + + /** + * Perform tree walking (foreward) + */ + walkForward: function(node) { + var target = node.firstChild; + if(target) return target; + + // intentional assignment for micro performance turing + if(target = node.nextSibling) return target; + + while(node = node.parentNode) { + // intentional assignment for micro performance turing + if(target = node.nextSibling) return target; + } + + return null; + }, + + /** + * Perform tree walking (backward) + */ + walkBackward: function(node) { + if(node.previousSibling) { + node = node.previousSibling; + while(node.hasChildNodes()) {node = node.lastChild;} + return node; + } + + return node.parentNode; + }, + + /** + * Perform tree walking (to next siblings) + */ + walkNext: function(node) {return node.nextSibling}, + + /** + * Perform tree walking (to next siblings) + */ + walkPrev: function(node) {return node.previousSibling}, + + /** + * Returns true if target is followed by start + */ + checkTargetForward: function(start, target) { + return this._check(start, this.walkForward, target); + }, + + /** + * Returns true if start is followed by target + */ + checkTargetBackward: function(start, target) { + return this._check(start, this.walkBackward, target); + }, + + findForward: function(start, condition, exitCondition) { + return this._find(start, this.walkForward, condition, exitCondition); + }, + + findBackward: function(start, condition, exitCondition) { + return this._find(start, this.walkBackward, condition, exitCondition); + }, + + _check: function(start, direction, target) { + if(start === target) return false; + + while(start = direction(start)) { + if(start === target) return true; + } + return false; + }, + + _find: function(start, direction, condition, exitCondition) { + while(start = direction(start)) { + if(exitCondition && exitCondition(start)) return null; + if(condition(start)) return start; + } + return null; + }, + + /** + * Walks Forward through DOM tree from start to end, and collects all nodes that matches with a filter. + * If no filter provided, it just collects all nodes. + * + * @param {Element} start Starting element. + * @param {Element} end Ending element. + * @param {Function} filter A filter function. + */ + collectNodesBetween: function(start, end, filter) { + if(start === end) return [start, end].findAll(filter || function() {return true}); + + var nodes = this.collectForward(start, function(node) {return node === end}, filter); + if( + start !== end && + typeof filter === "function" && + filter(end) + ) nodes.push(end); + + return nodes; + }, + + collectForward: function(start, exitCondition, filter) { + return this.collect(start, this.walkForward, exitCondition, filter); + }, + + collectBackward: function(start, exitCondition, filter) { + return this.collect(start, this.walkBackward, exitCondition, filter); + }, + + collectNext: function(start, exitCondition, filter) { + return this.collect(start, this.walkNext, exitCondition, filter); + }, + + collectPrev: function(start, exitCondition, filter) { + return this.collect(start, this.walkPrev, exitCondition, filter); + }, + + collect: function(start, next, exitCondition, filter) { + var nodes = [start]; + + while(true) { + start = next(start); + if( + (start === null) || + (typeof exitCondition === "function" && exitCondition(start)) + ) break; + + nodes.push(start); + } + + return (typeof filter === "function") ? nodes.findAll(filter) : nodes; + }, + + hasBlocks: function(element) { + var nodes = element.childNodes; + for(var i = 0; i < nodes.length; i++) { + if(this.isBlock(nodes[i])) return true; + } + return false; + }, + + hasMixedContents: function(element) { + if(!this.isBlock(element)) return false; + if(!this.isBlockContainer(element)) return false; + + var hasTextOrInline = false; + var hasBlock = false; + for(var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if(!hasTextOrInline && this.isTextOrInlineNode(node)) hasTextOrInline = true; + if(!hasBlock && this.isBlock(node)) hasBlock = true; + + if(hasTextOrInline && hasBlock) break; + } + if(!hasTextOrInline || !hasBlock) return false; + + return true; + }, + + isBlockOnlyContainer: function(element) { + if(!element) return false; + return this._blockOnlyContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isTableCell: function(element) { + if(!element) return false; + return this._tableCellTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isBlockContainer: function(element) { + if(!element) return false; + return this._blockContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isHeading: function(element) { + if(!element) return false; + return (typeof element === 'string' ? element : element.nodeName).match(/H\d/); + }, + + isBlock: function(element) { + if(!element) return false; + return this._blockTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isAtomic: function(element) { + if(!element) return false; + return this._atomicTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isListContainer: function(element) { + if(!element) return false; + return this._listContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; + }, + + isTextOrInlineNode: function(node) { + return node && (node.nodeType === 3 || !this.isBlock(node)); + } +}); +/** + * @namespace + */ +xq.rdom = {} + +/** + * @requires Xquared.js + * @requires DomTree.js + */ +xq.rdom.Base = xq.Class(/** @lends xq.rdom.Base.prototype */{ + /** + * Encapsulates browser incompatibility problem and provides rich set of DOM manipulation API.
+ *
+ * Base provides basic CRUD + Advanced DOM manipulation API, various query methods and caret/selection management API. + * + * @constructs + */ + initialize: function() { + xq.addToFinalizeQueue(this); + + /** + * Instance of DomTree + * @type xq.DomTree + */ + this.tree = new xq.DomTree(); + this.focused = false; + this._lastMarkerId = 0; + }, + + + + /** + * Initialize Base instance using window object. + * Reads document and body from window object and sets them as a property + * + * @param {Window} win Browser's window object + */ + setWin: function(win) { + if(!win) throw "[win] is null"; + this.win = win; + }, + + /** + * Initialize Base instance using root element. + * Reads window and document from root element and sets them as a property. + * + * @param {Element} root Root element + */ + setRoot: function(root) { + if(!root) throw "[root] is null"; + this.root = root; + }, + + /** + * @returns Browser's window object. + */ + getWin: function() { + return this.win || + (this.root ? (this.root.ownerDocument.defaultView || this.root.ownerDocument.parentWindow) : window); + }, + + /** + * @returns Root element. + */ + getRoot: function() { + return this.root || this.win.document.body; + }, + + /** + * @returns Document object of root element. + */ + getDoc: function() { + return this.getWin().document || this.getRoot().ownerDocument; + }, + + + + ///////////////////////////////////////////// + // CRUDs + + clearRoot: function() { + this.getRoot().innerHTML = ""; + this.getRoot().appendChild(this.makeEmptyParagraph()); + }, + + /** + * Removes place holders and empty text nodes of given element. + * + * @param {Element} element target element + */ + removePlaceHoldersAndEmptyNodes: function(element) { + if(!element.hasChildNodes()) return; + + var stopAt = this.getBottommostLastChild(element); + if(!stopAt) return; + stopAt = this.tree.walkForward(stopAt); + + while(element && element !== stopAt) { + if( + this.isPlaceHolder(element) || + (element.nodeType === 3 && (element.nodeValue === "" || (!element.nextSibling && element.nodeValue.isBlank()))) + ) { + 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.getDoc().createTextNode(value);}, + + /** + * Creates empty element by given tag name. + * + * @param {String} tagName name of tag + * @returns {Element} Created element + */ + createElement: function(tagName) {return this.getDoc().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; + if(node.nodeName === "BODY") throw "Cannot delete BODY"; + + 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". + * @param {String} className CSS class name. + * + * @return {Element} LI element + */ + turnElementIntoListItem: function(element, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var container = this.createElement(type); + if(className) container.className = className; + + 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.getRoot() || element.parentNode === this.getRoot() || !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. If not provided, it does not modify tag name. + * @param {Element} element Target element + * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. + * + * @return {Element} wrapper element or replaced element. + */ + applyTagIntoElement: function(tag, element, className) { + if(!tag && !className) return null; + + var result = element; + + if(tag) { + if(this.tree.isBlockOnlyContainer(tag)) { + result = this.wrapBlock(tag, element); + } else if(this.tree.isBlockContainer(element)) { + var wrapper = this.createElement(tag); + this.moveChildNodes(element, wrapper); + result = this.insertNodeAt(wrapper, element, "start"); + } else if(this.tree.isBlockContainer(tag) && this.hasImportantAttributes(element)) { + result = this.wrapBlock(tag, element); + } else { + result = this.replaceTag(tag, element); + } + } + + if(className) { + result.className = className; + } + + return result; + }, + + /** + * Wrap or replace elements with given tag name. + * + * @param {String} [tag] Tag name. If not provided, it does not modify tag name. + * @param {Element} from Start boundary (inclusive) + * @param {Element} to End boundary (inclusive) + * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. + * + * @returns {Array} Array of wrappers or replaced elements + */ + applyTagIntoElements: function(tagName, from, to, className) { + if(!tagName && !className) return [from, to]; + + var applied = []; + + if(tagName) { + 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]) || elements[i]); + } + } + } + } + + if(className) { + var elements = this.tree.collectNodesBetween(from, to, function(n) {return n.nodeType == 1;}); + for(var i = 0; i < elements.length; i++) { + elements[i].className = className; + } + } + + 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" + * @param {String} className CSS class name + * + * @returns {Element} changed element + */ + changeListTypeTo: function(element, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var li = this.getParentElementOf(element, ["LI"]); + if(!li) throw "IllegalArgumentException"; + + var container = li.parentNode; + + this.splitContainerOf(li); + + var newContainer = this.insertNodeAt(this.createElement(type), container, "before"); + if(className) newContainer.className = className; + + 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 && nextContainer.nodeName !== "LI" && this.outdentElement(next)) return element; + + // nextContainer is first li and next of it is list container ([I] represents caret position): + // + // * A[I] + // * B + // * C + if(nextContainer && nextContainer.nodeName === 'LI' && this.tree.isListContainer(next.nextSibling)) { + // move child nodes and... + this.moveChildNodes(nextContainer, prevContainer); + + // merge two paragraphs + this.removePlaceHoldersAndEmptyNodes(prev); + this.moveChildNodes(next, prev); + this.deleteNode(next); + + 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.includeElement(leaves[0])) { + var affected = this.indentElement(node, true); + if (affected) { + affect.push(affected); + return; + } + } + + if (blocks.includeElement(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.includeElement(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.includeElement(leaves[0]) && !this.outdentElementsCode(leaves[0])) { + var affected = this.outdentElement(node, true); + if (affected) { + affect.push(affected); + return; + } + } + + if (blocks.includeElement(node)) { + var children = xq.$A(node.parentNode.childNodes); + var isCode = this.outdentElementsCode(node); + var affected = this.outdentElement(node, true, isCode); + if (affected) { + if (children.includeElement(affected) && this.tree.isListContainer(node.parentNode) && !isCode) { + for (var i=0; i < children.length; i++) { + if (blocks.includeElement(children[i]) && !affect.includeElement(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.includeElement(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" + * @param {String} className CSS className + * @returns {Element} affected element + */ + applyList: function(element, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var containerTag = type; + + if(element.nodeName === "LI" || (element.parentNode.nodeName === "LI" && !element.previousSibling)) { + var element = this.getParentElementOf(element, ["LI"]); + var container = element.parentNode; + if(container.nodeName === containerTag && container.className === className) { + return this.extractOutElementFromParent(element); + } else { + return this.changeListTypeTo(element, type, className); + } + } else { + return this.turnElementIntoListItem(element, type, className); + } + }, + + applyLists: function(from, to, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var containerTag = type; + 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, className); + } + + 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); + }, + + /** + * Applies font face to selected area + * + * @param {String} face font face + */ + applyFontFace: function(face) { + this.execCommand("fontname", face); + }, + + /** + * Applies font size to selected area + * + * @param {Number} size font size (px) + */ + applyFontSize: function(size) { + this.execCommand("fontsize", size); + }, + + 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() { + return this.focused; + }, + + /** + * 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 caret is place at start of the block + */ + isCaretAtBlockStart: function() { + if(this.isCaretAtEmptyBlock()) return true; + if(this.hasSelection()) return false; + var node = this.getCurrentBlockElement(); + var marker = this.pushMarker(); + + var isTrue = false; + while (node = this.getFirstChild(node)) { + if (node === marker) { + isTrue = true; + break; + } + } + + this.popMarker(); + + return isTrue; + }, + + /** + * Checks if the caret is place at end of the block + */ + isCaretAtBlockEnd: function() {throw "Not implemented"}, + + /** + * Checks if the node is empty-text-node or not + */ + isEmptyTextNode: function(node) { + return node.nodeType === 3 && (node.nodeValue.length === 0 || (node.nodeValue.length === 1 && (node.nodeValue.charAt(0) === 32 || node.nodeValue.charAt(0) === 160))); + }, + + /** + * Checks if the caret is place in empty block element + */ + isCaretAtEmptyBlock: function() { + return this.isEmptyBlock(this.getCurrentBlockElement()); + }, + + /** + * 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(); + if(this.isFirstLiWithNestedList(block)) block = block.parentNode; + + var found = this.tree.findBackward( + block, + function(node) { + return node === root || (this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)); + }.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); + + if(block === null || (xq.Browser.isTrident && ["ready", "complete"].indexOf(block.readyState) === -1)) 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"); + var foregroundColor = doc.queryCommandValue("forecolor"); + var fontName = doc.queryCommandValue("fontname"); + var fontSize = doc.queryCommandValue("fontsize"); + // @WORKAROUND: Trident's fontSize value is affected by CSS + if(xq.Browser.isTrident && fontSize === "5" && this.getParentElementOf(element, ["H1", "H2", "H3", "H4", "H5", "H6"])) fontSize = ""; + + // @TODO: remove conditional + var backgroundColor; + if(xq.Browser.isGecko) { + this.execCommand("styleWithCSS", "true"); + try { + backgroundColor = doc.queryCommandValue("hilitecolor"); + } catch(e) { + // if there's selection and the first element of the selection is + // an empty block... + backgroundColor = ""; + } + this.execCommand("styleWithCSS", "false"); + } else { + backgroundColor = doc.queryCommandValue("backcolor"); + } + + // 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"; + var hasClass = parent.className.length > 0; + if(isCode) { + list = "CODE"; + } else if(hasClass) { + list = false; + } else { + list = 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, + foregroundColor: foregroundColor, + backgroundColor: backgroundColor, + fontSize: fontSize, + fontName: fontName + }; + }, + + /** + * 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. Trident overrides this method. + */ + 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.getDoc().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.isBlank()) 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 + }; + } +}); +/** + * @requires Xquared.js + * @requires rdom/Base.js + */ +xq.rdom.Trident = xq.Class(xq.rdom.Base, + /** + * @name xq.rdom.Trident + * @lends xq.rdom.Trident.prototype + * @extends xq.rdom.Base + * @constructor + */ + { + makePlaceHolder: function() { + return this.createTextNode(" "); + }, + + makePlaceHolderString: function() { + return ' '; + }, + + makeEmptyParagraph: function() { + return this.createElementFromHtml("

 

"); + }, + + isPlaceHolder: function(node) { + return false; + }, + + getOuterHTML: function(element) { + return element.outerHTML; + }, + + getCurrentBlockElement: function() { + var cur = this.getCurrentElement(); + if(!cur) return null; + + var block = this.getParentBlockElementOf(cur); + if(!block) return null; + + if(block.nodeName === "BODY") { + // Atomic block such as HR + var newParagraph = this.insertNode(this.makeEmptyParagraph()); + var next = newParagraph.nextSibling; + if(this.tree.isAtomic(next)) { + this.deleteNode(newParagraph); + return next; + } + } else { + return block; + } + }, + + insertNode: function(node) { + if(this.hasSelection()) this.collapseSelection(true); + + this.rng().pasteHTML(''); + var marker = this.$('xquared_temp'); + if(node.id === 'xquared_temp') return marker; + + if(marker) marker.replaceNode(node); + return node; + }, + + removeTrailingWhitespace: function(block) { + if(!block) return; + + // @TODO: reimplement to handle atomic tags and so on. (use DomTree) + if(this.tree.isBlockOnlyContainer(block)) return; + if(this.isEmptyBlock(block)) return; + + var text = block.innerText; + var html = block.innerHTML; + var lastCharCode = text.charCodeAt(text.length - 1); + if(text.length <= 1 || [32,160].indexOf(lastCharCode) === -1) return; + + // shortcut for most common case + if(text == html.replace(/ /g, " ")) { + block.innerHTML = html.replace(/ $/, ""); + return; + } + + var node = block; + while(node && node.nodeType !== 3) node = node.lastChild; + if(!node) return; + + // DO NOT REMOVE OR MODIFY FOLLOWING CODE. Modifying following code will crash IE7 + var nodeValue = node.nodeValue; + if(nodeValue.length <= 1) { + this.deleteNode(node, true); + } else { + node.nodeValue = nodeValue.substring(0, nodeValue.length - 1); + } + }, + + correctEmptyElement: function(element) { + if(!element || element.nodeType !== 1 || this.tree.isAtomic(element)) return; + + if(element.firstChild) { + this.correctEmptyElement(element.firstChild); + } else { + element.innerHTML = " "; + } + }, + + copyAttributes: function(from, to, copyId) { + to.mergeAttributes(from, !copyId); + }, + + correctParagraph: function() { + if(!this.hasFocus()) return false; + if(this.hasSelection()) return false; + + var block = this.getCurrentElement(); + + // if caret is at + // * atomic block level elements(HR) or + // * ... + // then following is true + if(this.tree.isBlockOnlyContainer(block)) { + // check for atomic block element such as HR + block = this.insertNode(this.makeEmptyParagraph()); + if(this.tree.isAtomic(block.nextSibling)) { + // @WORKAROUND: + // At this point, HR has a caret but getCurrentElement() doesn't return the HR and + // I couldn't find a way to get this HR. So I have to keep this reference. + // I will be used in Editor._handleEnter. + this.recentHR = block.nextSibling; + this.deleteNode(block); + return false; + } else { + // I can't remember exactly when following is executed and what it does :-( + // * Case 1: Performing Ctrl+A and Ctrl+X repeatedly + // * ... + var nextBlock = this.tree.findForward( + block, + function(node) {return this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)}.bind(this) + ); + + if(nextBlock) { + this.deleteNode(block); + this.placeCaretAtStartOf(nextBlock); + } else { + this.placeCaretAtStartOf(block); + } + + return true; + } + } else { + block = this.getCurrentBlockElement(); + if(block.nodeType === 3) block = block.parentNode; + + if(this.tree.hasMixedContents(block)) { + var marker = this.pushMarker(); + this.wrapAllInlineOrTextNodesAs("P", block, true); + this.popMarker(true); + return true; + } else if((this.tree.isTextOrInlineNode(block.previousSibling) || this.tree.isTextOrInlineNode(block.nextSibling)) && this.tree.hasMixedContents(block.parentNode)) { + // @WORKAROUND: + // IE?서??Block?Inline/Text??접??경우 getCurrentElement ?이 ?작?한?? + // ?라???재 Block 주?까? ?번???아주어???다. + this.wrapAllInlineOrTextNodesAs("P", block.parentNode, true); + return true; + } else { + return false; + } + } + }, + + + + ////// + // Commands + execCommand: function(commandId, param) { + return this.getDoc().execCommand(commandId, false, param); + }, + + applyBackgroundColor: function(color) { + this.execCommand("BackColor", color); + }, + + applyEmphasis: function() { + // Generate tag. It will be replaced with tag during cleanup phase. + this.execCommand("Italic"); + }, + applyStrongEmphasis: function() { + // Generate tag. It will be replaced with tag during cleanup phase. + this.execCommand("Bold"); + }, + applyStrike: function() { + // Generate tag. It will be replaced with ' + ); + // create designmode iframe for WYSIWYG editor - this.editorFrame = this.doc.createElement('iframe'); - this.rdom.setAttributes(this.editorFrame, { - "frameBorder": "0", - "marginWidth": "0", - "marginHeight": "0", - "leftMargin": "0", - "topMargin": "0", - "allowTransparency": "true" - }); - this.wysiwygEditorDiv.appendChild(this.editorFrame); + this.editorFrame = this._createIFrame(outerDoc); - var doc = this.editorFrame.contentWindow.document; - if(xq.Browser.isTrident) doc.designMode = 'On'; + outerDoc.body.appendChild(this.editorFrame); + var editorDoc = this._createDoc( + this.editorFrame, + '' + + (!xq.Browser.isTrident ? '' : '') + // @WORKAROUND: it is needed to force href of pasted content to be an absolute url + (this.config.changeCursorOnLink ? '' : ''), + this.config.contentCssList, + this.config.bodyId, + this.config.bodyClass, + '' + ); + this.rdom.setWin(this.editorFrame.contentWindow); + this.editHistory = new xq.EditHistory(this.rdom); - doc.open(); - doc.write(''); - doc.write(''); - doc.write(''); + // turn on designmode + this.rdom.getDoc().designMode = "On"; - // it is needed to force href of pasted content to be an absolute url - if(!xq.Browser.isTrident) doc.write(''); - - doc.write(''); - doc.write('XQuared'); - if(this.config.changeCursorOnLink) doc.write(''); - doc.write(''); - doc.write('

' + this.rdom.makePlaceHolderString() + '

'); - doc.write(''); - doc.close(); - - this.editorWin = this.editorFrame.contentWindow; - this.editorDoc = this.editorWin.document; - this.editorBody = this.editorDoc.body; - this.editorBody.className = "xed"; - - // it is needed to fix IE6 horizontal scrollbar problem - if(xq.Browser.isIE6) { - this.editorDoc.documentElement.style.overflowY='auto'; - this.editorDoc.documentElement.style.overflowX='hidden'; + // turn off Firefox's table editing feature + if(xq.Browser.isGecko) { + try {this.rdom.getDoc().execCommand("enableInlineTableEditing", false, "false")} catch(ignored) {} } - // 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); + // register event handlers + this._registerEventHandlers(); // hook onsubmit of form - if(this.config.automaticallyHookSubmitEvent && this.contentElement.nodeName == 'TEXTAREA' && this.contentElement.form) { + if(this.config.automaticallyHookSubmitEvent && 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; - } + this.contentElement.value = this.getCurrentContent(); + return original ? original.bind(this.contentElement.form)() : 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']; + var events = [this.platformDepedentKeyEventType, 'click', 'keyup', 'mouseup', 'contextmenu']; if(xq.Browser.isTrident && this.config.changeCursorOnLink) events.push('mousemove'); - if(xq.Browser.isMac && xq.Browser.isGecko) events.push('keypress'); + var handler = this._handleEvent.bindAsEventListener(this); for(var i = 0; i < events.length; i++) { - xq.observe(this.getDoc(), events[i], this._handleEvent.bindAsEventListener(this)); + xq.observe(this.getDoc(), events[i], handler); + } + + if(xq.Browser.isGecko) { + xq.observe(this.getDoc(), "focus", handler); + xq.observe(this.getDoc(), "blur", handler); + xq.observe(this.getDoc(), "scroll", handler); + xq.observe(this.getDoc(), "dragdrop", handler); + } else { + xq.observe(this.getWin(), "focus", handler); + xq.observe(this.getWin(), "blur", handler); + xq.observe(this.getWin(), "scroll", handler); } }, _handleEvent: function(e) { this._fireOnBeforeEvent(this, e); + if(e.stopProcess) { + xq.stopEvent(e); + return false; + } - var stop = false; - - var modifiedByCorrection = false; - - if(e.type == 'mousemove' && this.config.changeCursorOnLink) { - // Trident only + // Trident only + if(e.type === 'mousemove') { + if(!this.config.changeCursorOnLink) return true; + var link = !!this.rdom.getParentElementOf(e.srcElement, ["A"]); - var editable = this.editorBody.contentEditable; - editable = editable == 'inherit' ? false : editable; + var editable = this.getBody().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")) { + if(editable !== link && !this.rdom.hasSelection()) this.getBody().contentEditable = !link; + return true; + } + + var stop = false; + var modifiedByCorrection = false; + if(e.type === this.platformDepedentKeyEventType) { var undoPerformed = false; - modifiedByCorrection = this.rdom.correctParagraph(); for(var key in this.config.shortcuts) { if(!this.config.shortcuts[key].event.matches(e)) continue; var handler = this.config.shortcuts[key].handler; var xed = this; - stop = (typeof handler == "function") ? handler(this) : eval(handler); + stop = (typeof handler === "function") ? handler(this) : eval(handler); - if(key == "undo") undoPerformed = true; + if(key === "undo") undoPerformed = true; } - } else if(["mouseup", "keyup"].indexOf(e.type) != -1) { + } else if(e.type === 'click' && e.button === 0 && this.config.enableLinkClick) { + var a = this.rdom.getParentElementOf(e.target || e.srcElement, ["A"]); + if(a) stop = this.handleClick(e, a); + } else if(["keyup", "mouseup"].indexOf(e.type) !== -1) { modifiedByCorrection = this.rdom.correctParagraph(); - } else if(["contextmenu"].indexOf(e.type) != -1) { + } else if(["contextmenu"].indexOf(e.type) !== -1) { this._handleContextMenu(e); + } else if("focus" == e.type) { + this.rdom.focused = true; + } else if("blur" == e.type) { + this.rdom.focused = false; } if(stop) xq.stopEvent(e); @@ -1596,7 +7863,6 @@ xq.Editor = xq.Class({ */ handleAutocorrection: function() { var block = this.rdom.getCurrentBlockElement(); - // TODO: use complete unescape algorithm var text = this.rdom.getInnerText(block).replace(/ /gi, " "); @@ -1610,7 +7876,7 @@ xq.Editor = xq.Class({ try { this.editHistory.onCommand(); this.editHistory.disable(); - if(typeof ac.handler == "String") { + if(typeof ac.handler === "String") { var xed = this; var rdom = this.rdom; eval(ac.handler); @@ -1637,7 +7903,7 @@ xq.Editor = xq.Class({ handleAutocompletion: function() { var acs = this.config.autocompletions; if(xq.isEmptyHash(acs)) return; - + if(this.rdom.hasSelection()) { var text = this.rdom.getSelectionAsText(); this.rdom.deleteSelection(); @@ -1651,14 +7917,14 @@ xq.Editor = xq.Class({ filtered.push([key, acs[key].criteria(text)]); } filtered = filtered.findAll(function(elem) { - return elem[1] != -1; + return elem[1] !== -1; }); - - if(filtered.length == 0) { + + 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++) { @@ -1671,6 +7937,7 @@ xq.Editor = xq.Class({ var ac = acs[filtered[minIndex][0]]; this.editHistory.disable(); + this.rdom.selectElement(wrapper); } else { var marker = this.rdom.pushMarker(); @@ -1679,10 +7946,10 @@ xq.Editor = xq.Class({ filtered.push([key, this.rdom.testSmartWrap(marker, acs[key].criteria).textIndex]); } filtered = filtered.findAll(function(elem) { - return elem[1] != -1; + return elem[1] !== -1; }); - if(filtered.length == 0) { + if(filtered.length === 0) { this.rdom.popMarker(true); return; } @@ -1710,7 +7977,7 @@ xq.Editor = xq.Class({ try { // call handler - if(typeof ac.handler == "String") { + if(typeof ac.handler === "String") { var xed = this; var rdom = this.rdom; eval(ac.handler); @@ -1740,7 +8007,7 @@ xq.Editor = xq.Class({ handleClick: function(e, target) { var href = decodeURI(target.href); if(!xq.Browser.isTrident) { - if(!e.ctrlKey && !e.shiftKey && e.button != 1) { + if(!e.ctrlKey && !e.shiftKey && e.button !== 1) { window.location.href = href; return true; } @@ -1760,10 +8027,11 @@ xq.Editor = xq.Class({ * Show link dialog * * TODO: should support modify/unlink + * TODO: Add selenium test */ handleLink: function() { var text = this.rdom.getSelectionAsText() || ''; - var dialog = new xq.controls.FormDialog( + var dialog = new xq.ui.FormDialog( this, xq.ui_templates.basicLinkDialog, function(dialog) { @@ -1796,6 +8064,7 @@ xq.Editor = xq.Class({ /** * Inserts link or apply link into selected area + * @TODO Add selenium test * * @param {boolean} autoSelection if set true and there's no selection, automatically select word to link(if possible) * @param {String} url url @@ -1809,7 +8078,7 @@ xq.Editor = xq.Class({ var marker = this.rdom.pushMarker(); var a = this.rdom.smartWrap(marker, "A", function(text) { var index = text.lastIndexOf(" "); - return index == -1 ? index : index + 1; + return index === -1 ? index : index + 1; }); a.href = url; a.title = title; @@ -1839,8 +8108,24 @@ xq.Editor = xq.Class({ return true; }, + /** + * @TODO Add selenium test + */ + handleSpace: function() { + // If it has selection, perform default action. + if(this.rdom.hasSelection()) return false; + + // Trident performs URL replacing automatically + if(!xq.Browser.isTrident) { + this.replaceUrlToLink(); + } + + return false; + }, + /** * Called when enter key pressed. + * @TODO Add selenium test * * @param {boolean} skipAutocorrection if set true, skips autocorrection * @param {boolean} forceInsertParagraph if set true, inserts paragraph @@ -1849,23 +8134,39 @@ xq.Editor = xq.Class({ // If it has selection, perform default action. if(this.rdom.hasSelection()) return false; + // @WORKAROUND: + // If caret is in HR, default action should be performed and + // this._handleEvent() will correct broken HTML + if(xq.Browser.isTrident && this.rdom.tree.isBlockOnlyContainer(this.rdom.getCurrentElement()) && this.rdom.recentHR) { + this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.recentHR, "before"); + this.rdom.recentHR = null; + return true; + } + // Perform autocorrection if(!skipAutocorrection && this.handleAutocorrection()) return true; + var block = this.rdom.getCurrentBlockElement(); + var info = this.rdom.collectStructureAndStyle(block); + + // Perform URL replacing. Trident performs URL replacing automatically + if(!xq.Browser.isTrident) { + this.replaceUrlToLink(); + } + var atEmptyBlock = this.rdom.isCaretAtEmptyBlock(); var atStart = atEmptyBlock || this.rdom.isCaretAtBlockStart(); var atEnd = atEmptyBlock || (!atStart && this.rdom.isCaretAtBlockEnd()); var atEdge = atEmptyBlock || atStart || atEnd; if(!atEdge) { - var 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)) { + } else if(block.nodeName !== "LI" && this.rdom.tree.isBlockContainer(block)) { block = this.rdom.wrapAllInlineOrTextNodesAs("P", block, true).first(); } this.rdom.splitElementUpto(marker, block); @@ -1873,8 +8174,18 @@ xq.Editor = xq.Class({ this.rdom.popMarker(true); } else if(atEmptyBlock) { this._handleEnterAtEmptyBlock(); + + if(!xq.Browser.isWebkit) { + if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize); + if(info.fontName) this.handleFontFace(info.fontName); + } } else { this._handleEnterAtEdge(atStart, forceInsertParagraph); + + if(!xq.Browser.isWebkit) { + if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize); + if(info.fontName) this.handleFontFace(info.fontName); + } } return true; @@ -1889,6 +8200,8 @@ xq.Editor = xq.Class({ var block = this.rdom.moveBlock(this.rdom.getCurrentBlockElement(), up); if(block) { this.rdom.selectElement(block, false); + if(this.rdom.isEmptyBlock(block)) this.rdom.collapseSelection(true); + block.scrollIntoView(false); var historyAdded = this.editHistory.onCommand(); @@ -1899,6 +8212,7 @@ xq.Editor = xq.Class({ /** * Called when tab key pressed + * @TODO: Add selenium test */ handleTab: function() { var hasSelection = this.rdom.hasSelection(); @@ -1906,7 +8220,7 @@ xq.Editor = xq.Class({ if(hasSelection) { this.handleIndent(); - } else if (table && table.className == "datatable") { + } else if (table && table.className === "datatable") { this.handleMoveToNextCell(); } else if (this.rdom.isCaretAtBlockStart()) { this.handleIndent(); @@ -1919,6 +8233,7 @@ xq.Editor = xq.Class({ /** * Called when shift+tab key pressed + * @TODO: Add selenium test */ handleShiftTab: function() { var hasSelection = this.rdom.hasSelection(); @@ -1926,7 +8241,7 @@ xq.Editor = xq.Class({ if(hasSelection) { this.handleOutdent(); - } else if (table && table.className == "datatable") { + } else if (table && table.className === "datatable") { this.handleMoveToPreviousCell(); } else { this.handleOutdent(); @@ -1937,6 +8252,7 @@ xq.Editor = xq.Class({ /** * Inserts three non-breaking spaces + * @TODO: Add selenium test */ handleInsertTab: function() { this.rdom.insertHtml(' '); @@ -1948,6 +8264,7 @@ xq.Editor = xq.Class({ /** * Called when delete key pressed + * @TODO: Add selenium test */ handleDelete: function() { if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockEnd()) return false; @@ -1956,6 +8273,7 @@ xq.Editor = xq.Class({ /** * Called when backspace key pressed + * @TODO: Add selenium test */ handleBackspace: function() { if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockStart()) return false; @@ -1965,25 +8283,36 @@ xq.Editor = xq.Class({ _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; + if(this.rdom.isEmptyBlock(block) && !this.rdom.tree.isBlockContainer(block.nextSibling) && withNext) { + var blockToMove = this.rdom.removeBlock(block); + this.rdom.placeCaretAtStartOf(blockToMove); + blockToMove.scrollIntoView(false); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + } else { + // save caret position; + var marker = this.rdom.pushMarker(); + + // perform merge + var merged = this.rdom.mergeElement(block, withNext, withNext); + if(!merged && !withNext) this.rdom.extractOutElementFromParent(block); + + // restore caret position + this.rdom.popMarker(true); + if(merged) this.rdom.correctEmptyElement(merged); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + return !!merged; + } }, /** * (in table) Moves caret to the next cell + * @TODO: Add selenium test */ handleMoveToNextCell: function() { this._handleMoveToCell("next"); @@ -1991,6 +8320,7 @@ xq.Editor = xq.Class({ /** * (in table) Moves caret to the previous cell + * @TODO: Add selenium test */ handleMoveToPreviousCell: function() { this._handleMoveToCell("prev"); @@ -1998,6 +8328,7 @@ xq.Editor = xq.Class({ /** * (in table) Moves caret to the above cell + * @TODO: Add selenium test */ handleMoveToAboveCell: function() { this._handleMoveToCell("above"); @@ -2005,6 +8336,7 @@ xq.Editor = xq.Class({ /** * (in table) Moves caret to the below cell + * @TODO: Add selenium test */ handleMoveToBelowCell: function() { this._handleMoveToCell("below"); @@ -2017,16 +8349,16 @@ xq.Editor = xq.Class({ var rtable = new xq.RichTable(this.rdom, table); var target = null; - if(["next", "prev"].indexOf(dir) != -1) { - var toNext = dir == "next"; + if(["next", "prev"].indexOf(dir) !== -1) { + var toNext = dir === "next"; target = toNext ? rtable.getNextCellOf(cell) : rtable.getPreviousCellOf(cell); } else { - var toBelow = dir == "below"; + 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 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) ? @@ -2039,10 +8371,11 @@ xq.Editor = xq.Class({ /** * Applies STRONG tag + * @TODO: Add selenium test */ handleStrongEmphasis: function() { this.rdom.applyStrongEmphasis(); - + var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); @@ -2051,6 +8384,7 @@ xq.Editor = xq.Class({ /** * Applies EM tag + * @TODO: Add selenium test */ handleEmphasis: function() { this.rdom.applyEmphasis(); @@ -2063,6 +8397,7 @@ xq.Editor = xq.Class({ /** * Applies EM.underline tag + * @TODO: Add selenium test */ handleUnderline: function() { this.rdom.applyUnderline(); @@ -2075,6 +8410,7 @@ xq.Editor = xq.Class({ /** * Applies SPAN.strike tag + * @TODO: Add selenium test */ handleStrike: function() { this.rdom.applyStrike(); @@ -2087,6 +8423,7 @@ xq.Editor = xq.Class({ /** * Removes all style + * @TODO: Add selenium test */ handleRemoveFormat: function() { this.rdom.applyRemoveFormat(); @@ -2097,8 +8434,22 @@ xq.Editor = xq.Class({ return true; }, + /** + * Remove link + * @TODO: Add selenium test + */ + handleRemoveLink: function() { + this.rdom.applyRemoveLink(); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; + }, + /** * Inserts table + * @TODO: Add selenium test * * @param {Number} cols number of columns * @param {Number} rows number of rows @@ -2136,6 +8487,10 @@ xq.Editor = xq.Class({ this.rdom.placeCaretAtStartOf(row.cells[0]); return true; }, + + /** + * @TODO: Add selenium test + */ handleInsertNewColumnAt: function(where) { var cur = this.rdom.getCurrentBlockElement(); var td = this.rdom.getParentElementOf(cur, ["TD"], true); @@ -2149,6 +8504,9 @@ xq.Editor = xq.Class({ return true; }, + /** + * @TODO: Add selenium test + */ handleDeleteRow: function() { var cur = this.rdom.getCurrentBlockElement(); var tr = this.rdom.getParentElementOf(cur, ["TR"]); @@ -2162,6 +8520,9 @@ xq.Editor = xq.Class({ return true; }, + /** + * @TODO: Add selenium test + */ handleDeleteColumn: function() { var cur = this.rdom.getCurrentBlockElement(); var td = this.rdom.getParentElementOf(cur, ["TD"], true); @@ -2171,16 +8532,18 @@ xq.Editor = xq.Class({ var rtable = new xq.RichTable(this.rdom, table); rtable.deleteCell(td); + //this.rdom.placeCaretAtStartOf(table); return true; }, /** * Performs block indentation + * @TODO: Add selenium test */ handleIndent: function() { if(this.rdom.hasSelection()) { var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); - if(blocks.first() != blocks.last()) { + if(blocks.first() !== blocks.last()) { var affected = this.rdom.indentElements(blocks.first(), blocks.last()); this.rdom.selectBlocksBetween(affected.first(), affected.last()); @@ -2203,14 +8566,15 @@ xq.Editor = xq.Class({ return true; }, - + /** * Performs block outdentation + * @TODO: Add selenium test */ handleOutdent: function() { if(this.rdom.hasSelection()) { var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); - if(blocks.first() != blocks.last()) { + if(blocks.first() !== blocks.last()) { var affected = this.rdom.outdentElements(blocks.first(), blocks.last()); this.rdom.selectBlocksBetween(affected.first(), affected.last()); @@ -2236,20 +8600,22 @@ xq.Editor = xq.Class({ /** * Applies list. + * @TODO: Add selenium test * - * @param {String} type "UL" or "OL" or "CODE". CODE generates OL.code + * @param {String} type "UL" or "OL" + * @param {String} CSS class name */ - handleList: function(type) { + handleList: function(type, className) { if(this.rdom.hasSelection()) { var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); - if(blocks.first() != blocks.last()) { - blocks = this.rdom.applyLists(blocks.first(), blocks.last(), type); + if(blocks.first() !== blocks.last()) { + blocks = this.rdom.applyLists(blocks.first(), blocks.last(), type, className); } else { - blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type); + blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type, className); } this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); } else { - var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type); + var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type, className); this.rdom.placeCaretAtStartOf(block); } var historyAdded = this.editHistory.onCommand(); @@ -2260,18 +8626,19 @@ xq.Editor = xq.Class({ /** * Applies justification + * @TODO: Add selenium test * * @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(); + var dir = (dir === "left" || dir === "both") && (blocks[0].style.textAlign === "left" || blocks[0].style.textAlign === "") ? "both" : dir; this.rdom.justifyBlocks(blocks, dir); this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); } else { + var block = this.rdom.getCurrentBlockElement(); + var dir = (dir === "left" || dir === "both") && (block.style.textAlign === "left" || block.style.textAlign === "") ? "both" : dir; this.rdom.justifyBlock(block, dir); } var historyAdded = this.editHistory.onCommand(); @@ -2282,16 +8649,23 @@ xq.Editor = xq.Class({ /** * Removes current block element + * @TODO: Add selenium test */ handleRemoveBlock: function() { var block = this.rdom.getCurrentBlockElement(); var blockToMove = this.rdom.removeBlock(block); this.rdom.placeCaretAtStartOf(blockToMove); blockToMove.scrollIntoView(false); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + + return true; }, /** * Applies background color + * @TODO: Add selenium test * * @param {String} color CSS color string */ @@ -2302,7 +8676,7 @@ xq.Editor = xq.Class({ var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } else { - var dialog = new xq.controls.FormDialog( + var dialog = new xq.ui.FormDialog( this, xq.ui_templates.basicColorPickerDialog, function(dialog) {}, @@ -2330,17 +8704,18 @@ xq.Editor = xq.Class({ /** * Applies foreground color + * @TODO: Add selenium test * * @param {String} color CSS color string */ handleForegroundColor: function(color) { if(color) { this.rdom.applyForegroundColor(color); - + var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } else { - var dialog = new xq.controls.FormDialog( + var dialog = new xq.ui.FormDialog( this, xq.ui_templates.basicColorPickerDialog, function(dialog) {}, @@ -2366,8 +8741,44 @@ xq.Editor = xq.Class({ return true; }, + /** + * Applies font face + * @TODO: Add selenium test + * + * @param {String} face font face + */ + handleFontFace: function(face) { + if(face) { + this.rdom.applyFontFace(face); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + //TODO: popup font dialog + } + return true; + }, + + /** + * Applies font size + * + * @param {Number} font size (1 to 6) + */ + handleFontSize: function(size) { + if(size) { + this.rdom.applyFontSize(size); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + //TODO: popup font dialog + } + return true; + }, + /** * Applies superscription + * @TODO: Add selenium test */ handleSuperscription: function() { this.rdom.applySuperscription(); @@ -2380,6 +8791,7 @@ xq.Editor = xq.Class({ /** * Applies subscription + * @TODO: Add selenium test */ handleSubscription: function() { this.rdom.applySubscription(); @@ -2389,15 +8801,22 @@ xq.Editor = xq.Class({ return true; }, - + /** - * Change of wrap current block's tag + * Change or wrap current block(or selected blocks)'s tag + * @TODO: Add selenium test + * + * @param {String} [tagName] Name of tag. If not provided, it does not modify current tag name + * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. */ - handleApplyBlock: function(tagName) { + handleApplyBlock: function(tagName, className) { + if(!tagName && !className) return true; + + // if current selection contains multi-blocks if(this.rdom.hasSelection()) { var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); - if(blocks.first() != blocks.last()) { - var applied = this.rdom.applyTagIntoElements(tagName, blocks.first(), blocks.last()); + if(blocks.first() !== blocks.last()) { + var applied = this.rdom.applyTagIntoElements(tagName, blocks.first(), blocks.last(), className); this.rdom.selectBlocksBetween(applied.first(), applied.last()); var historyAdded = this.editHistory.onCommand(); @@ -2407,13 +8826,14 @@ xq.Editor = xq.Class({ } } + // else var block = this.rdom.getCurrentBlockElement(); this.rdom.pushMarker(); var applied = - this.rdom.applyTagIntoElement(tagName, block) || + this.rdom.applyTagIntoElement(tagName, block, className) || block; this.rdom.popMarker(true); - + if(this.rdom.isEmptyBlock(applied)) { this.rdom.correctEmptyElement(applied); this.rdom.placeCaretAtStartOf(applied); @@ -2424,9 +8844,10 @@ xq.Editor = xq.Class({ return true; }, - + /** * Inserts seperator (HR) + * @TODO: Add selenium test */ handleSeparator: function() { this.rdom.collapseSelection(); @@ -2447,6 +8868,7 @@ xq.Editor = xq.Class({ /** * Performs UNDO + * @TODO: Add selenium test */ handleUndo: function() { var performed = this.editHistory.undo(); @@ -2461,6 +8883,7 @@ xq.Editor = xq.Class({ /** * Performs REDO + * @TODO: Add selenium test */ handleRedo: function() { var performed = this.editHistory.redo(); @@ -2485,20 +8908,12 @@ xq.Editor = xq.Class({ var point = xq.getEventPoint(e); var x = point.x; var y = point.y; - - var pos = xq.getCumulativeOffset(this.getFrame()); + + var pos = xq.getCumulativeOffset(this.wysiwygEditorDiv); 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(); @@ -2506,10 +8921,8 @@ xq.Editor = xq.Class({ x -= doc.documentElement.scrollLeft; y -= doc.documentElement.scrollTop; - if (doc != body) { - x -= body.scrollLeft; - y -= body.scrollTop; - } + x -= body.scrollLeft; + y -= body.scrollTop; } for(var cmh in this.config.contextMenuHandlers) { @@ -2526,48 +8939,48 @@ xq.Editor = xq.Class({ 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'; + 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); + this.body.appendChild(this.contextMenuContainer); } else { - while (this._contextMenuContainer.childNodes.length > 0) - this._contextMenuContainer.removeChild(this._contextMenuContainer.childNodes[0]); + 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.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; + this.contextMenuItems = menuItems; }, hideContextMenu: function() { - if (this._contextMenuContainer) - this._contextMenuContainer.style.display='none'; + if (this.contextMenuContainer) + this.contextMenuContainer.style.display='none'; }, _addContextMenuItem: function(item) { - if (!this._contextMenuContainer) throw "No conext menu container exists"; + if (!this.contextMenuContainer) throw "No conext menu container exists"; var node = this.doc.createElement('LI'); if (item.disabled) node.className += ' disabled'; - if (item.title == '----') { + if (item.title === '----') { node.innerHTML = ' '; node.className = 'separator'; } else { if(item.handler) { - node.innerHTML = ''+(item.title.toString().escapeHTML())+''; + node.innerHTML = ''+(item.title.toString().escapeHTML())+''; } else { node.innerHTML = (item.title.toString().escapeHTML()); } @@ -2575,7 +8988,7 @@ xq.Editor = xq.Class({ if(item.className) node.className = item.className; - this._contextMenuContainer.appendChild(node); + this.contextMenuContainer.appendChild(node); return node; }, @@ -2583,21 +8996,21 @@ xq.Editor = xq.Class({ _contextMenuClicked: function(e) { this.hideContextMenu(); - if (!this._contextMenuContainer) return; + if (!this.contextMenuContainer) return; var node = e.srcElement || e.target; - while(node && node.nodeName != "LI") { + while(node && node.nodeName !== "LI") { node = node.parentNode; } - if (!node || !this.rdom.tree.isDescendantOf(this._contextMenuContainer, node)) return; + 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) { + 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") { + if(typeof handler === "function") { handler(xed, element); } else { eval(handler); @@ -2610,6 +9023,7 @@ xq.Editor = xq.Class({ /** * Inserts HTML template + * @TODO: Add selenium test * * @param {String} html Template string. It should have single root element * @returns {Element} inserted element @@ -2620,6 +9034,7 @@ xq.Editor = xq.Class({ /** * Places given HTML template nearby target. + * @TODO: Add selenium test * * @param {String} html Template string. It should have single root element * @param {Node} target Target node. @@ -2640,7 +9055,7 @@ xq.Editor = xq.Class({ } // remove all whitespace characters between block tags - return html = this.removeUnnecessarySpaces(html); + return this.removeUnnecessarySpaces(html); }, @@ -2677,4792 +9092,32 @@ xq.Editor = xq.Class({ 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 tag during cleanup phase. - this.execCommand("styleWithCSS", "false"); - this.execCommand("italic"); - }, - applyStrongEmphasis: function() { - // Generate tag. It will be replaced with tag during cleanup phase. - this.execCommand("styleWithCSS", "false"); - this.execCommand("bold"); - }, - applyStrike: function() { - // Generate tag. It will be replaced with '); +this.editorFrame=this._createIFrame(C); +C.body.appendChild(this.editorFrame); +var D=this._createDoc(this.editorFrame,''+(!xq.Browser.isTrident?'':"")+(this.config.changeCursorOnLink?"":""),this.config.contentCssList,this.config.bodyId,this.config.bodyClass,""); +this.rdom.setWin(this.editorFrame.contentWindow); +this.editHistory=new xq.EditHistory(this.rdom); +this.rdom.getDoc().designMode="On"; +if(xq.Browser.isGecko){try{this.rdom.getDoc().execCommand("enableInlineTableEditing",false,"false") +}catch(A){}}this._registerEventHandlers(); +if(this.config.automaticallyHookSubmitEvent&&this.contentElement.form){var B=this.contentElement.form.onsubmit; +this.contentElement.form.onsubmit=function(){this.contentElement.value=this.getCurrentContent(); +return B?B.bind(this.contentElement.form)():true +}.bind(this) +}},_registerEventHandlers:function(){var B=[this.platformDepedentKeyEventType,"click","keyup","mouseup","contextmenu"]; +if(xq.Browser.isTrident&&this.config.changeCursorOnLink){B.push("mousemove") +}var C=this._handleEvent.bindAsEventListener(this); +for(var A=0; +A0){this.contextMenuContainer.removeChild(this.contextMenuContainer.childNodes[0]) +}}for(var B=0; +B" +}else{A.innerHTML=(B.title.toString().escapeHTML()) +}}if(B.className){A.className=B.className +}this.contextMenuContainer.appendChild(A); +return A +},_contextMenuClicked:function(e){this.hideContextMenu(); +if(!this.contextMenuContainer){return +}var node=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 + *
+ * Derived class may override this to make a plugin as a event listener.
+ * Whenever you override this function, you should also implement at least one event handler for xq.Editor. + */ + isEventListener: function() {return false}, + + /** + * Callback function. Derived class may override this. + */ + onBeforeLoad: function(editor) {}, + + /** + * Callback function. Derived class may override this. + */ + onAfterLoad: function(editor) {}, + + /** + * Callback function. Derived class may override this. + */ + onBeforeUnload: function(editor) {}, + + /** + * Callback function. Derived class may override this. + */ + onAfterUnload: function(editor) {}, + + /** + * Callback function. Derived class may override this. + */ + getShortcuts: function() {return [];}, + + /** + * Callback function. Derived class may override this. + */ + getAutocorrections: function() {return [];}, + + /** + * Callback function. Derived class may override this. + */ + getAutocompletions: function() {return [];}, + + /** + * Callback function. Derived class may override this. + */ + getTemplateProcessors: function() {return [];}, + + /** + * Callback function. Derived class may override this. + */ + getContextMenuHandlers: function() {return [];} +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/plugin/EditorResizePlugin.js b/modules/editor/skins/xquared/javascripts/plugin/EditorResizePlugin.js new file mode 100644 index 000000000..53711aba5 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/plugin/EditorResizePlugin.js @@ -0,0 +1,121 @@ +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * @requires plugin/Base.js + */ +xq.plugin.EditorResizePlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.EditorResizePlugin + * @lends xq.plugin.EditorResizePlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + isEventListener: function() {return true;}, + + onAfterLoad: function(xed) { + this.xed = xed; + this.bar = null; + this.screen = null; + this.active = false; + }, + + onEditorInitialized: function(xed) { + xed.registerEventFirer("Editor", "Resized"); + + var wrapper = this.xed.getOutmostWrapper(); + var doc = wrapper.ownerDocument; + + // create resize bar + this.bar = doc.createElement("DIV"); + if(xq.Browser.isIE6) this.bar.innerHTML = ""; + this.bar.style.height = "6px"; + this.bar.style.backgroundColor = "#ddd"; + this.bar.style.cursor = "n-resize"; + wrapper.appendChild(this.bar); + + // register event + xq.observe(this.bar, 'mousedown', this.onMousedown.bindAsEventListener(this)); + xq.observe(this.bar, 'mouseup', this.onMouseup.bindAsEventListener(this)); + xq.observe(this.bar, 'click', this.onMouseup.bindAsEventListener(this)); + this.mousemoveHandler = this.onMousemove.bindAsEventListener(this); + }, + + onMousedown: function(e) { + if(this.active) return; + + xq.observe(document, 'mousemove', this.mousemoveHandler); + this.last = e.screenY; + + var wrapper = this.xed.getOutmostWrapper(); + var doc = wrapper.ownerDocument; + var wysiwygDiv = this.xed.getWysiwygEditorDiv(); + var sourceDiv = this.xed.getSourceEditorDiv(); + var visibleDiv = this.xed.getCurrentEditMode() == "wysiwyg" ? wysiwygDiv : sourceDiv; + var location = xq.getCumulativeOffset(visibleDiv); + + // create screen + this.screen = doc.createElement("DIV"); + if(xq.Browser.isIE6) this.screen.innerHTML = ""; + + if(xq.Browser.isIE6) { + this.screen.style.backgroundColor = "#EEE"; + wysiwygDiv.style.display = "none"; + } else { + this.screen.style.position = "absolute"; + this.screen.style.left = location.left + "px"; + this.screen.style.top = location.top + "px"; + } + + this.screen.style.width = visibleDiv.clientWidth + "px"; + this.screen.style.height = visibleDiv.clientHeight + "px"; + wrapper.insertBefore(this.screen, visibleDiv); + + this.resize(e.screenY); + this.active = true; + + xq.stopEvent(e); + return true; + }, + onMouseup: function(e) { + if(!this.active) return; + + this.active = false; + + xq.stopObserving(document, 'mousemove', this.mousemoveHandler); + this.resize(e.screenY); + + if(xq.Browser.isIE6) { + var wysiwygDiv = this.xed.getWysiwygEditorDiv(); + var sourceDiv = this.xed.getSourceEditorDiv(); + var visibleDiv = this.xed.getCurrentEditMode() == "wysiwyg" ? wysiwygDiv : sourceDiv; + visibleDiv.style.display = "block"; + } + + this.screen.parentNode.removeChild(this.screen); + this.screen = null; + + this.xed._fireOnResized(this.xed); + + xq.stopEvent(e); + return true; + }, + onMousemove: function(e) { + this.resize(e.screenY); + + xq.stopEvent(e); + return true; + }, + resize: function(y) { + var delta = y - this.last; + + var wysiwygDiv = this.xed.getWysiwygEditorDiv(); + var sourceDiv = this.xed.getSourceEditorDiv(); + var newHeight = Math.max(0, this.screen.clientHeight + delta); + + sourceDiv.style.height = wysiwygDiv.style.height = this.screen.style.height = newHeight + "px"; + + this.last = y; + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/plugin/EventLogPlugin.js b/modules/editor/skins/xquared/javascripts/plugin/EventLogPlugin.js new file mode 100644 index 000000000..1314dc709 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/plugin/EventLogPlugin.js @@ -0,0 +1,77 @@ +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * @requires plugin/Base.js + */ +xq.plugin.EventLogPlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.EventLogPlugin + * @lends xq.plugin.EventLogPlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + isEventListener: function() {return true;}, + + onAfterLoad: function(xed) { + this.createLogWindow(); + }, + + onEditorStartInitialization: function(xed) { + this.log("Start initialization."); + }, + onEditorInitialized: function(xed) { + this.log("Initialized."); + }, + onEditorElementChanged: function(xed, from, to) { + this.log("Element changed from <" + (from ? from.nodeName : null) + "> to <" + (to ? to.nodeName : null) + ">."); + }, + onEditorBeforeEvent: function(xed, e) { + this.log("Before event [" + e.type + "]"); + }, + onEditorAfterEvent: function(xed, e) { + this.log("After event [" + e.type + "]"); + }, + onEditorCurrentContentChanged: function(xed) { + this.log("Current content changed."); + }, + onEditorStaticContentChanged: function(xed, content) { + this.log("Static content changed."); + }, + onEditorCurrentEditModeChanged: function(xed, from, to) { + this.log("Edit mode changed from <" + from + "> to <" + to + ">."); + }, + + + + createLogWindow: function() { + var wrapper = document.createElement("DIV"); + wrapper.innerHTML = "

Log

"; + wrapper.style.width = "500px"; + document.body.appendChild(wrapper); + + this.logWindow = document.createElement("PRE"); + this.logWindow.style.fontSize = "0.75em"; + this.logWindow.style.height = "200px"; + this.logWindow.style.overflow = "scroll"; + this.logWindow.style.border = "1px solid black"; + this.logWindow.style.padding = "2px"; + wrapper.appendChild(this.logWindow); + }, + + log: function(message) { + var line = document.createTextNode(this.getFormattedTime() + ": " + message); + this.logWindow.insertBefore(document.createElement("BR"), this.logWindow.firstChild); + this.logWindow.insertBefore(line, this.logWindow.firstChild); + }, + + getFormattedTime: function() { + var date = new Date(); + var time = date.toTimeString().split(" ")[0]; + var msec = "000" + date.getMilliseconds(); + msec = msec.substring(msec.length - 4); + + return time + "." + msec; + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/plugin/FlashMovieMacroPlugin.js b/modules/editor/skins/xquared/javascripts/plugin/FlashMovieMacroPlugin.js new file mode 100644 index 000000000..118750fd0 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/plugin/FlashMovieMacroPlugin.js @@ -0,0 +1,65 @@ +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * @requires plugin/Base.js + * @requires ui/Control.js + * @requires macro/Factory.js + * @requires macro/FlashMovieMacro.js + */ +xq.plugin.FlashMovieMacroPlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.FlashMovieMacroPlugin + * @lends xq.plugin.FlashMovieMacroPlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + onAfterLoad: function(xed) { + xed.config.macroIds.push("FlashMovie"); + xed.config.defaultToolbarButtonGroups.insert.push( + {className:"movie", title:"Movie", handler:"xed.handleMovie()"} + ) + + xed.handleInsertMovie = function(html) { + var macro = this.macroFactory.createMacroFromDefinition({id:"FlashMovie", params:{html:html}}); + if(macro) { + var placeHolder = macro.createPlaceHolderHtml(); + this.rdom.insertHtml(placeHolder); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + alert("Unknown URL pattern"); + } + return true; + }; + + xed.handleMovie = function() { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicMovieDialog, + function(dialog) {}, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + // cancel? + if(!data) return; + + this.handleInsertMovie(data.html); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + dialog.show({position: 'centerOfEditor'}); + + return true; + } + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/plugin/IFrameMacroPlugin.js b/modules/editor/skins/xquared/javascripts/plugin/IFrameMacroPlugin.js new file mode 100644 index 000000000..e1dcc2f43 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/plugin/IFrameMacroPlugin.js @@ -0,0 +1,50 @@ +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * @requires plugin/Base.js + * @requires ui/Control.js + * @requires macro/Factory.js + * @requires macro/IFrameMacro.js + */ +xq.plugin.IFrameMacroPlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.IFrameMacroPlugin + * @lends xq.plugin.IFrameMacroPlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + onAfterLoad: function(xed) { + xed.config.macroIds.push("IFrame"); + xed.config.defaultToolbarButtonGroups.insert.push( + {className:"iframe", title:"IFrame", handler:"xed.handleIFrame()"} + ) + + xed.handleIFrame = function() { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicIFrameDialog, + function(dialog) {}, + function(data) { + this.focus(); + + // cancel? + if(!data) return; + + var macro = this.macroFactory.createMacroFromDefinition({id:"IFrame", params:data}); + if(macro) { + var placeHolder = macro.createPlaceHolderHtml(); + this.rdom.insertHtml(placeHolder); + } else { + alert("Unknown error"); + } + }.bind(this) + ); + + dialog.show({position: 'centerOfEditor'}); + + return true; + } + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/plugin/JavascriptMacroPlugin.js b/modules/editor/skins/xquared/javascripts/plugin/JavascriptMacroPlugin.js new file mode 100644 index 000000000..ad7e15923 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/plugin/JavascriptMacroPlugin.js @@ -0,0 +1,66 @@ +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * @requires plugin/Base.js + * @requires ui/Control.js + * @requires macro/Factory.js + * @requires macro/JavascriptMacro.js + */ +xq.plugin.JavascriptMacroPlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.JavascriptMacroPlugin + * @lends xq.plugin.JavascriptMacroPlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + onAfterLoad: function(xed) { + xed.config.macroIds.push("Javascript"); + xed.config.defaultToolbarButtonGroups.insert.push( + {className:"script", title:"Script", handler:"xed.handleScript()"} + ) + + xed.handleInsertScript = function(url) { + var params = {url: url}; + var macro = this.macroFactory.createMacroFromDefinition({id:"Javascript", params:params}); + if(macro) { + var placeHolder = macro.createPlaceHolderHtml(); + this.rdom.insertHtml(placeHolder); + + var historyAdded = this.editHistory.onCommand(); + this._fireOnCurrentContentChanged(this); + } else { + alert("Unknown URL pattern"); + } + return true; + }; + + xed.handleScript = function() { + var dialog = new xq.ui.FormDialog( + this, + xq.ui_templates.basicScriptDialog, + function(dialog) {}, + function(data) { + this.focus(); + + if(xq.Browser.isTrident) { + var rng = this.rdom.rng(); + rng.moveToBookmark(bm); + rng.select(); + } + + // cancel? + if(!data) return; + + this.handleInsertScript(data.url); + }.bind(this) + ); + + if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); + dialog.show({position: 'centerOfEditor'}); + + return true; + } + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/plugin/MacroPlugin.js b/modules/editor/skins/xquared/javascripts/plugin/MacroPlugin.js new file mode 100644 index 000000000..1050ca04f --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/plugin/MacroPlugin.js @@ -0,0 +1,110 @@ +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * + * @requires macro/Factory.js + * @requires Layer.js + * @requires Json2.js + * + * @requires plugin/Base.js + */ +xq.plugin.MacroPlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.MacroPlugin + * @lends xq.plugin.MacroPlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + isEventListener: function() {return true;}, + + onAfterLoad: function(xed) { + this.xed = xed; + this.xed.config.macroIds = []; + this.layers = []; + }, + + onEditorStartInitialization: function(xed) { + this.xed.validator.addListener(this); + + this.xed.macroFactory = new xq.macro.Factory(this.xed.config.imagePathForContent + 'placeholder.gif'); + for(var i = 0; i < this.xed.config.macroIds.length; i++) { + this.xed.macroFactory.register(this.xed.config.macroIds[i]); + } + + xed.timer.register(this.updateLayers.bind(this), 100); + xed.timer.register(this.updateLayerList.bind(this), 2000); + }, + + /** + * @param {Element} [placeHolder] place holder element + */ + attachMacro: function(element) { + var longdesc = element.getAttribute("longdesc") || element.longdesc; + var def = JSON.parse(unescape(longdesc)); + var macro = this.xed.macroFactory.createMacroFromDefinition(def); + var layer = new xq.Layer(this.xed, element, macro.createHtml()); + macro.onLayerInitialzied(layer); + this.layers.push(layer); + }, + + isAttachedPlaceHolder: function(element) { + for(var i = 0; i < this.layers.length; i++) { + if(this.layers[i].element === element) return true; + } + return false; + }, + + updateLayerList: function() { + if(this.xed.getCurrentEditMode() !== 'wysiwyg') { + for(var i = 0; i < this.layers.length; i++) { + this.layers[i].detach(); + } + this.layers = []; + } else { + var placeHolders = xq.getElementsByClassName(this.xed.rdom.getRoot(), "xqlayer", xq.Browser.isTrident ? "img" : null); + for(var i = 0; i < placeHolders.length; i++) { + if(!this.isAttachedPlaceHolder(placeHolders[i])) { + this.attachMacro(placeHolders[i]); + } + } + } + }, + + /** + * Updates all layers immediately. If there're invalid layers, detachs and removes them. + */ + updateLayers: function() { + if(this.xed.getCurrentEditMode() !== 'wysiwyg') return; + + for(var i = 0; i < this.layers.length; i++) { + var layer = this.layers[i]; + if(layer.isValid()) { + layer.updatePosition(); + } else { + layer.detach(); + this.layers.splice(i, 1); + } + } + }, + + onValidatorPreprocessing: function(html) { + var p = xq.compilePattern("<(IFRAME|SCRIPT|OBJECT|EMBED)\\s+[^>]+(?:/>|>.*?)", "img"); + html.value = html.value.replace(p, function(str, tag) { + var macro = this.xed.macroFactory.createMacroFromHtml(str); + return macro ? macro.createPlaceHolderHtml() : ""; + }.bind(this)); + }, + + onValidatorAfterStringValidation: function(html) { + var p1 = /]*class="xqlayer"\s+[^>]*\/>/mg; + var p2 = /]*longdesc="(.+?)"\s+[^>]*\/>/m; + + html.value = html.value.replace(p1, function(img) { + var def = JSON.parse(unescape(img.match(p2)[1])); + var macro = this.xed.macroFactory.createMacroFromDefinition(def); + return macro.createHtml(); + }.bind(this)); + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/plugin/SpringnotePlugin.js b/modules/editor/skins/xquared/javascripts/plugin/SpringnotePlugin.js new file mode 100644 index 000000000..24ea20777 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/plugin/SpringnotePlugin.js @@ -0,0 +1,224 @@ +/** + * @requires Xquared.js + * @requires Browser.js + * @requires Editor.js + * @requires plugin/Base.js + */ +xq.plugin.SpringnotePlugin = xq.Class(xq.plugin.Base, + /** + * @name xq.plugin.SpringnotePlugin + * @lends xq.plugin.SpringnotePlugin.prototype + * @extends xq.plugin.Base + * @constructor + */ + { + getShortcuts: function() { + if(xq.Browser.isMac) { + // Mac FF & Safari + return [ + {event:"Ctrl+SPACE", handler:"xed.handleAutocompletion(); stop = true;"}, + {event:"Ctrl+Meta+0", handler:"xed.handleApplyBlock('P')"}, + {event:"Ctrl+Meta+1", handler:"xed.handleApplyBlock('H1')"}, + {event:"Ctrl+Meta+2", handler:"xed.handleApplyBlock('H2')"}, + {event:"Ctrl+Meta+3", handler:"xed.handleApplyBlock('H3')"}, + {event:"Ctrl+Meta+4", handler:"xed.handleApplyBlock('H4')"}, + {event:"Ctrl+Meta+5", handler:"xed.handleApplyBlock('H5')"}, + {event:"Ctrl+Meta+6", handler:"xed.handleApplyBlock('H6')"}, + + {event:"Ctrl+Meta+B", handler:"xed.handleApplyBlock('BLOCKQUOTE')"}, + {event:"Ctrl+Meta+D", handler:"xed.handleApplyBlock('DIV')"}, + {event:"Ctrl+Meta+EQUAL", handler:"xed.handleSeparator()"}, + + {event:"Ctrl+Meta+O", handler:"xed.handleList('OL')"}, + {event:"Ctrl+Meta+U", handler:"xed.handleList('UL')"}, + + {event:"Ctrl+Meta+E", handler:"xed.handleRemoveBlock()"}, + + {event:"Ctrl+(Meta)+COMMA", handler:"xed.handleJustify('left')"}, + {event:"Ctrl+(Meta)+PERIOD", handler:"xed.handleJustify('center')"}, + {event:"Ctrl+(Meta)+SLASH", handler:"xed.handleJustify('right')"}, + + {event:"Meta+UP", handler:"xed.handleMoveBlock(true)"}, + {event:"Meta+DOWN", handler:"xed.handleMoveBlock(false)"} + ]; + } else if(xq.Browser.isUbuntu) { + // Ubunto FF + return [ + {event:"Ctrl+SPACE", handler:"xed.handleAutocompletion(); stop = true;"}, + {event:"Ctrl+0", handler:"xed.handleApplyBlock('P')"}, + {event:"Ctrl+1", handler:"xed.handleApplyBlock('H1')"}, + {event:"Ctrl+2", handler:"xed.handleApplyBlock('H2')"}, + {event:"Ctrl+3", handler:"xed.handleApplyBlock('H3')"}, + {event:"Ctrl+4", handler:"xed.handleApplyBlock('H4')"}, + {event:"Ctrl+5", handler:"xed.handleApplyBlock('H5')"}, + {event:"Ctrl+6", handler:"xed.handleApplyBlock('H6')"}, + + {event:"Ctrl+Alt+B", handler:"xed.handleApplyBlock('BLOCKQUOTE')"}, + {event:"Ctrl+Alt+D", handler:"xed.handleApplyBlock('DIV')"}, + {event:"Alt+HYPHEN", handler:"xed.handleSeparator()"}, + + {event:"Ctrl+Alt+O", handler:"xed.handleList('OL')"}, + {event:"Ctrl+Alt+U", handler:"xed.handleList('UL')"}, + + {event:"Ctrl+Alt+E", handler:"xed.handleRemoveBlock()"}, + + {event:"Alt+COMMA", handler:"xed.handleJustify('left')"}, + {event:"Alt+PERIOD", handler:"xed.handleJustify('center')"}, + {event:"Alt+SLASH", handler:"xed.handleJustify('right')"}, + + {event:"Alt+UP", handler:"xed.handleMoveBlock(true)"}, + {event:"Alt+DOWN", handler:"xed.handleMoveBlock(false)"} + ]; + } else { + // Win IE & FF && Safari + return [ + {event:"Ctrl+SPACE", handler:"xed.handleAutocompletion(); stop = true;"}, + {event:"Alt+0", handler:"xed.handleApplyBlock('P')"}, + {event:"Alt+1", handler:"xed.handleApplyBlock('H1')"}, + {event:"Alt+2", handler:"xed.handleApplyBlock('H2')"}, + {event:"Alt+3", handler:"xed.handleApplyBlock('H3')"}, + {event:"Alt+4", handler:"xed.handleApplyBlock('H4')"}, + {event:"Alt+5", handler:"xed.handleApplyBlock('H5')"}, + {event:"Alt+6", handler:"xed.handleApplyBlock('H6')"}, + {event:"Alt+7", handler:"xed.handleInsertMacro('TableOfContents')"}, + {event:"Alt+8", handler:"xed.attachLayer()"}, + + {event:"Ctrl+Alt+B", handler:"xed.handleApplyBlock('BLOCKQUOTE')"}, + {event:"Ctrl+Alt+D", handler:"xed.handleApplyBlock('DIV')"}, + {event:"Alt+HYPHEN", handler:"xed.handleSeparator()"}, + + {event:"Ctrl+Alt+O", handler:"xed.handleList('OL')"}, + {event:"Ctrl+Alt+U", handler:"xed.handleList('UL')"}, + + {event:"Ctrl+Alt+E", handler:"xed.handleRemoveBlock()"}, + + {event:"Alt+COMMA", handler:"xed.handleJustify('left')"}, + {event:"Alt+PERIOD", handler:"xed.handleJustify('center')"}, + {event:"Alt+SLASH", handler:"xed.handleJustify('right')"}, + + {event:"Alt+UP", handler:"xed.handleMoveBlock(true)"}, + {event:"Alt+DOWN", handler:"xed.handleMoveBlock(false)"} + ]; + } + }, + + getAutocorrections: function() { + return [ + {id:'bullet', criteria: /^(\s|\ \;)*(\*|-)(\s|\ \;).+$/, handler: function(xed, rdom, block, text) { + rdom.pushMarker(); + rdom.removePlaceHoldersAndEmptyNodes(block); + block.innerHTML = block.innerHTML.replace(/((\s| )*(\*|\-)\s*)/, ""); + if(block.nodeName === "LI") xed.handleIndent(); + if(block.parentNode.nodeName !== "UL") xed.handleList('UL'); + rdom.popMarker(true); + }}, + {id:'numbering', criteria: /^(\s|\ \;)*(\d\.|#)(\s|\ \;).+$/, handler: function(xed, rdom, block, text) { + rdom.pushMarker(); + rdom.removePlaceHoldersAndEmptyNodes(block); + block.innerHTML = block.innerHTML.replace(/(\s| )*(\d\.|\#)\s*/, "") + if(block.nodeName === "LI") xed.handleIndent(); + if(block.parentNode.nodeName !== "OL") xed.handleList('OL'); + rdom.popMarker(true); + }}, + {id:'imageUrl', criteria: /https?:\/\/.*?\/(.*?\.(jpg|jpeg|gif|bmp|png))$/i, handler: function(xed, rdom, block, text) { + var fileName = text.match(/https?:\/\/.*?\/(.*?\.(jpg|jpeg|gif|bmp|png))$/i)[1]; + block.innerHTML = ""; + var img = rdom.createElement("img"); + img.src = text; + img.alt = fileName; + img.title = fileName; + block.appendChild(img); + rdom.selectElement(block); + rdom.collapseSelection(false); + }}, + {id:'separator', criteria: /^---+(\ |\s)*$/, handler: function(xed, rdom, block, text) { + if(rdom.tree.isBlockContainer(block)) block = rdom.wrapAllInlineOrTextNodesAs("P", block, true)[0]; + rdom.insertNodeAt(rdom.createElement("HR"), block, "before"); + block.innerHTML = ""; + rdom.placeCaretAtStartOf(block); + return true; + }}, + {id:'heading', criteria: /^\=+[^=]*\=+(\ |\s)*$/, handler: function(xed, rdom, block, text) { + var textWithoutEqualMarks = text.strip().replace(/=/g, ""); + var level = Math.min(6, parseInt((text.length - textWithoutEqualMarks.length) / 2)) + xed.handleApplyBlock('H' + level); + block = rdom.getCurrentBlockElement(); + block.innerHTML = textWithoutEqualMarks; + rdom.selectElement(block); + rdom.collapseSelection(); + }} + ]; + }, + + getAutocompletions: function() { + return [ + { + id:'isbn', + criteria: /@ISBN:\d+$/i, + handler: function(xed, rdom, block, wrapper, text) { + var isbn = text.split(":")[1] + var korean = isbn.indexOf("97889") === 0 || isbn.indexOf("89") === 0 + var href = korean ? + "http://www.aladdin.co.kr/shop/wproduct.aspx?ISBN=" : + "http://www.amazon.com/exec/obidos/ISBN=" + var node = rdom.createElement('A'); + node.innerHTML = 'ISBN:' + isbn; + node.href = href + isbn; + node.className = 'external'; + node.title = 'ISBN:' + isbn; + + wrapper.innerHTML = ""; + wrapper.appendChild(node); + } + }, + { + id:'anchor', + criteria: /@A(:(.+))?$/i, + handler: function(xed, rdom, block, wrapper, text) { + var m = text.match(/@A(:(.+))?$/i); + var anchorId = m[2] ? m[2] : function() { + var id = 0; + while(true) { + var element = rdom.$("a" + (id)); + if(!element) return "a" + id; + id++; + } + }(); + + var node = rdom.createElement('A'); + node.id = anchorId; + node.href = '#' + anchorId; + node.className = 'anchor'; + node.title = 'Anchor ' + anchorId; + node.innerHTML = '(' + anchorId + ')'; + + wrapper.innerHTML = ""; + wrapper.appendChild(node); + } + } + ]; + }, + + getTemplateProcessors: function() { + return [ + { + id:"datetime", + handler:function(html) { + var today = Date.get(); + var keywords = { + year: today.getFullYear(), + month: today.getMonth() + 1, + date: today.getDate(), + hour: today.getHours(), + min: today.getMinutes(), + sec: today.getSeconds() + }; + + return html.replace(/\{xq:(year|month|date|hour|min|sec)\}/img, function(text, keyword) { + return keywords[keyword] || keyword; + }); + } + } + ]; + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/rdom/Base.js b/modules/editor/skins/xquared/javascripts/rdom/Base.js new file mode 100644 index 000000000..4c99c7c3d --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/rdom/Base.js @@ -0,0 +1,2347 @@ +/** + * @namespace + */ +xq.rdom = {} + +/** + * @requires Xquared.js + * @requires DomTree.js + */ +xq.rdom.Base = xq.Class(/** @lends xq.rdom.Base.prototype */{ + /** + * Encapsulates browser incompatibility problem and provides rich set of DOM manipulation API.
+ *
+ * Base provides basic CRUD + Advanced DOM manipulation API, various query methods and caret/selection management API. + * + * @constructs + */ + initialize: function() { + xq.addToFinalizeQueue(this); + + /** + * Instance of DomTree + * @type xq.DomTree + */ + this.tree = new xq.DomTree(); + this.focused = false; + this._lastMarkerId = 0; + }, + + + + /** + * Initialize Base instance using window object. + * Reads document and body from window object and sets them as a property + * + * @param {Window} win Browser's window object + */ + setWin: function(win) { + if(!win) throw "[win] is null"; + this.win = win; + }, + + /** + * Initialize Base instance using root element. + * Reads window and document from root element and sets them as a property. + * + * @param {Element} root Root element + */ + setRoot: function(root) { + if(!root) throw "[root] is null"; + this.root = root; + }, + + /** + * @returns Browser's window object. + */ + getWin: function() { + return this.win || + (this.root ? (this.root.ownerDocument.defaultView || this.root.ownerDocument.parentWindow) : window); + }, + + /** + * @returns Root element. + */ + getRoot: function() { + return this.root || this.win.document.body; + }, + + /** + * @returns Document object of root element. + */ + getDoc: function() { + return this.getWin().document || this.getRoot().ownerDocument; + }, + + + + ///////////////////////////////////////////// + // CRUDs + + clearRoot: function() { + this.getRoot().innerHTML = ""; + this.getRoot().appendChild(this.makeEmptyParagraph()); + }, + + /** + * Removes place holders and empty text nodes of given element. + * + * @param {Element} element target element + */ + removePlaceHoldersAndEmptyNodes: function(element) { + if(!element.hasChildNodes()) return; + + var stopAt = this.getBottommostLastChild(element); + if(!stopAt) return; + stopAt = this.tree.walkForward(stopAt); + + while(element && element !== stopAt) { + if( + this.isPlaceHolder(element) || + (element.nodeType === 3 && (element.nodeValue === "" || (!element.nextSibling && element.nodeValue.isBlank()))) + ) { + 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.getDoc().createTextNode(value);}, + + /** + * Creates empty element by given tag name. + * + * @param {String} tagName name of tag + * @returns {Element} Created element + */ + createElement: function(tagName) {return this.getDoc().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; + if(node.nodeName === "BODY") throw "Cannot delete BODY"; + + 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". + * @param {String} className CSS class name. + * + * @return {Element} LI element + */ + turnElementIntoListItem: function(element, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var container = this.createElement(type); + if(className) container.className = className; + + 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.getRoot() || element.parentNode === this.getRoot() || !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. If not provided, it does not modify tag name. + * @param {Element} element Target element + * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. + * + * @return {Element} wrapper element or replaced element. + */ + applyTagIntoElement: function(tag, element, className) { + if(!tag && !className) return null; + + var result = element; + + if(tag) { + if(this.tree.isBlockOnlyContainer(tag)) { + result = this.wrapBlock(tag, element); + } else if(this.tree.isBlockContainer(element)) { + var wrapper = this.createElement(tag); + this.moveChildNodes(element, wrapper); + result = this.insertNodeAt(wrapper, element, "start"); + } else if(this.tree.isBlockContainer(tag) && this.hasImportantAttributes(element)) { + result = this.wrapBlock(tag, element); + } else { + result = this.replaceTag(tag, element); + } + } + + if(className) { + result.className = className; + } + + return result; + }, + + /** + * Wrap or replace elements with given tag name. + * + * @param {String} [tag] Tag name. If not provided, it does not modify tag name. + * @param {Element} from Start boundary (inclusive) + * @param {Element} to End boundary (inclusive) + * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. + * + * @returns {Array} Array of wrappers or replaced elements + */ + applyTagIntoElements: function(tagName, from, to, className) { + if(!tagName && !className) return [from, to]; + + var applied = []; + + if(tagName) { + 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]) || elements[i]); + } + } + } + } + + if(className) { + var elements = this.tree.collectNodesBetween(from, to, function(n) {return n.nodeType == 1;}); + for(var i = 0; i < elements.length; i++) { + elements[i].className = className; + } + } + + 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" + * @param {String} className CSS class name + * + * @returns {Element} changed element + */ + changeListTypeTo: function(element, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var li = this.getParentElementOf(element, ["LI"]); + if(!li) throw "IllegalArgumentException"; + + var container = li.parentNode; + + this.splitContainerOf(li); + + var newContainer = this.insertNodeAt(this.createElement(type), container, "before"); + if(className) newContainer.className = className; + + 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 && nextContainer.nodeName !== "LI" && this.outdentElement(next)) return element; + + // nextContainer is first li and next of it is list container ([I] represents caret position): + // + // * A[I] + // * B + // * C + if(nextContainer && nextContainer.nodeName === 'LI' && this.tree.isListContainer(next.nextSibling)) { + // move child nodes and... + this.moveChildNodes(nextContainer, prevContainer); + + // merge two paragraphs + this.removePlaceHoldersAndEmptyNodes(prev); + this.moveChildNodes(next, prev); + this.deleteNode(next); + + 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.includeElement(leaves[0])) { + var affected = this.indentElement(node, true); + if (affected) { + affect.push(affected); + return; + } + } + + if (blocks.includeElement(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.includeElement(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.includeElement(leaves[0]) && !this.outdentElementsCode(leaves[0])) { + var affected = this.outdentElement(node, true); + if (affected) { + affect.push(affected); + return; + } + } + + if (blocks.includeElement(node)) { + var children = xq.$A(node.parentNode.childNodes); + var isCode = this.outdentElementsCode(node); + var affected = this.outdentElement(node, true, isCode); + if (affected) { + if (children.includeElement(affected) && this.tree.isListContainer(node.parentNode) && !isCode) { + for (var i=0; i < children.length; i++) { + if (blocks.includeElement(children[i]) && !affect.includeElement(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.includeElement(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" + * @param {String} className CSS className + * @returns {Element} affected element + */ + applyList: function(element, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var containerTag = type; + + if(element.nodeName === "LI" || (element.parentNode.nodeName === "LI" && !element.previousSibling)) { + var element = this.getParentElementOf(element, ["LI"]); + var container = element.parentNode; + if(container.nodeName === containerTag && container.className === className) { + return this.extractOutElementFromParent(element); + } else { + return this.changeListTypeTo(element, type, className); + } + } else { + return this.turnElementIntoListItem(element, type, className); + } + }, + + applyLists: function(from, to, type, className) { + type = type.toUpperCase(); + className = className || ""; + + var containerTag = type; + 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, className); + } + + 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); + }, + + /** + * Applies font face to selected area + * + * @param {String} face font face + */ + applyFontFace: function(face) { + this.execCommand("fontname", face); + }, + + /** + * Applies font size to selected area + * + * @param {Number} size font size (px) + */ + applyFontSize: function(size) { + this.execCommand("fontsize", size); + }, + + 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() { + return this.focused; + }, + + /** + * 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 caret is place at start of the block + */ + isCaretAtBlockStart: function() { + if(this.isCaretAtEmptyBlock()) return true; + if(this.hasSelection()) return false; + var node = this.getCurrentBlockElement(); + var marker = this.pushMarker(); + + var isTrue = false; + while (node = this.getFirstChild(node)) { + if (node === marker) { + isTrue = true; + break; + } + } + + this.popMarker(); + + return isTrue; + }, + + /** + * Checks if the caret is place at end of the block + */ + isCaretAtBlockEnd: function() {throw "Not implemented"}, + + /** + * Checks if the node is empty-text-node or not + */ + isEmptyTextNode: function(node) { + return node.nodeType === 3 && (node.nodeValue.length === 0 || (node.nodeValue.length === 1 && (node.nodeValue.charAt(0) === 32 || node.nodeValue.charAt(0) === 160))); + }, + + /** + * Checks if the caret is place in empty block element + */ + isCaretAtEmptyBlock: function() { + return this.isEmptyBlock(this.getCurrentBlockElement()); + }, + + /** + * 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(); + if(this.isFirstLiWithNestedList(block)) block = block.parentNode; + + var found = this.tree.findBackward( + block, + function(node) { + return node === root || (this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)); + }.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); + + if(block === null || (xq.Browser.isTrident && ["ready", "complete"].indexOf(block.readyState) === -1)) 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"); + var foregroundColor = doc.queryCommandValue("forecolor"); + var fontName = doc.queryCommandValue("fontname"); + var fontSize = doc.queryCommandValue("fontsize"); + // @WORKAROUND: Trident's fontSize value is affected by CSS + if(xq.Browser.isTrident && fontSize === "5" && this.getParentElementOf(element, ["H1", "H2", "H3", "H4", "H5", "H6"])) fontSize = ""; + + // @TODO: remove conditional + var backgroundColor; + if(xq.Browser.isGecko) { + this.execCommand("styleWithCSS", "true"); + try { + backgroundColor = doc.queryCommandValue("hilitecolor"); + } catch(e) { + // if there's selection and the first element of the selection is + // an empty block... + backgroundColor = ""; + } + this.execCommand("styleWithCSS", "false"); + } else { + backgroundColor = doc.queryCommandValue("backcolor"); + } + + // 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"; + var hasClass = parent.className.length > 0; + if(isCode) { + list = "CODE"; + } else if(hasClass) { + list = false; + } else { + list = 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, + foregroundColor: foregroundColor, + backgroundColor: backgroundColor, + fontSize: fontSize, + fontName: fontName + }; + }, + + /** + * 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. Trident overrides this method. + */ + 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.getDoc().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.isBlank()) 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 + }; + } +}); \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/rdom/Factory.js b/modules/editor/skins/xquared/javascripts/rdom/Factory.js new file mode 100644 index 000000000..c9b7a53bd --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/rdom/Factory.js @@ -0,0 +1,18 @@ +/** + * Creates and returns instance of browser specific implementation. + * + * @requires Xquared.js + * @requires rdom/Base.js + * @requires rdom/Trident.js + * @requires rdom/Gecko.js + * @requires rdom/Webkit.js + */ +xq.rdom.Base.createInstance = function() { + if(xq.Browser.isTrident) { + return new xq.rdom.Trident(); + } else if(xq.Browser.isWebkit) { + return new xq.rdom.Webkit(); + } else { + return new xq.rdom.Gecko(); + } +} \ No newline at end of file diff --git a/modules/editor/skins/xquared/javascripts/rdom/Gecko.js b/modules/editor/skins/xquared/javascripts/rdom/Gecko.js new file mode 100644 index 000000000..3b9c932f6 --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/rdom/Gecko.js @@ -0,0 +1,48 @@ +/** + * @requires Xquared.js + * @requires rdom/W3.js + */ +xq.rdom.Gecko = xq.Class(xq.rdom.W3, + /** + * @name xq.rdom.Gecko + * @lends xq.rdom.Gecko.prototype + * @extends xq.rdom.W3 + * @constructor + */ + { + makePlaceHolder: function() { + var holder = this.createElement("BR"); + holder.setAttribute("type", "_moz"); + return holder; + }, + + makePlaceHolderString: function() { + return '
'; + }, + + makeEmptyParagraph: function() { + return this.createElementFromHtml('


'); + }, + + isPlaceHolder: function(node) { + return node.nodeName === "BR" && (node.getAttribute("type") === "_moz" || !this.getNextSibling(node)); + }, + + selectElement: function(element, entireElement) { + if(!element) throw "[element] is null"; + if(element.nodeType !== 1) throw "[element] is not an element"; + + // @WORKAROUND: required to avoid Windows FF selection bug. + try { + if(!xq.Browser.isMac) this.getDoc().execCommand("SelectAll", false, null); + } catch(ignored) {} + + var rng = this.rng() || this.getDoc().createRange(); + + if(entireElement) { + rng.selectNode(element); + } else { + rng.selectNodeContents(element); + } + } +}); diff --git a/modules/editor/skins/xquared/javascripts/rdom/Trident.js b/modules/editor/skins/xquared/javascripts/rdom/Trident.js new file mode 100644 index 000000000..9e1d632eb --- /dev/null +++ b/modules/editor/skins/xquared/javascripts/rdom/Trident.js @@ -0,0 +1,397 @@ +/** + * @requires Xquared.js + * @requires rdom/Base.js + */ +xq.rdom.Trident = xq.Class(xq.rdom.Base, + /** + * @name xq.rdom.Trident + * @lends xq.rdom.Trident.prototype + * @extends xq.rdom.Base + * @constructor + */ + { + makePlaceHolder: function() { + return this.createTextNode(" "); + }, + + makePlaceHolderString: function() { + return ' '; + }, + + makeEmptyParagraph: function() { + return this.createElementFromHtml("

 

"); + }, + + isPlaceHolder: function(node) { + return false; + }, + + getOuterHTML: function(element) { + return element.outerHTML; + }, + + getCurrentBlockElement: function() { + var cur = this.getCurrentElement(); + if(!cur) return null; + + var block = this.getParentBlockElementOf(cur); + if(!block) return null; + + if(block.nodeName === "BODY") { + // Atomic block such as HR + var newParagraph = this.insertNode(this.makeEmptyParagraph()); + var next = newParagraph.nextSibling; + if(this.tree.isAtomic(next)) { + this.deleteNode(newParagraph); + return next; + } + } else { + return block; + } + }, + + insertNode: function(node) { + if(this.hasSelection()) this.collapseSelection(true); + + this.rng().pasteHTML(''); + var marker = this.$('xquared_temp'); + if(node.id === 'xquared_temp') return marker; + + if(marker) marker.replaceNode(node); + return node; + }, + + removeTrailingWhitespace: function(block) { + if(!block) return; + + // @TODO: reimplement to handle atomic tags and so on. (use DomTree) + if(this.tree.isBlockOnlyContainer(block)) return; + if(this.isEmptyBlock(block)) return; + + var text = block.innerText; + var html = block.innerHTML; + var lastCharCode = text.charCodeAt(text.length - 1); + if(text.length <= 1 || [32,160].indexOf(lastCharCode) === -1) return; + + // shortcut for most common case + if(text == html.replace(/ /g, " ")) { + block.innerHTML = html.replace(/ $/, ""); + return; + } + + var node = block; + while(node && node.nodeType !== 3) node = node.lastChild; + if(!node) return; + + // DO NOT REMOVE OR MODIFY FOLLOWING CODE. Modifying following code will crash IE7 + var nodeValue = node.nodeValue; + if(nodeValue.length <= 1) { + this.deleteNode(node, true); + } else { + node.nodeValue = nodeValue.substring(0, nodeValue.length - 1); + } + }, + + correctEmptyElement: function(element) { + if(!element || element.nodeType !== 1 || this.tree.isAtomic(element)) return; + + if(element.firstChild) { + this.correctEmptyElement(element.firstChild); + } else { + element.innerHTML = " "; + } + }, + + copyAttributes: function(from, to, copyId) { + to.mergeAttributes(from, !copyId); + }, + + correctParagraph: function() { + if(!this.hasFocus()) return false; + if(this.hasSelection()) return false; + + var block = this.getCurrentElement(); + + // if caret is at + // * atomic block level elements(HR) or + // * ... + // then following is true + if(this.tree.isBlockOnlyContainer(block)) { + // check for atomic block element such as HR + block = this.insertNode(this.makeEmptyParagraph()); + if(this.tree.isAtomic(block.nextSibling)) { + // @WORKAROUND: + // At this point, HR has a caret but getCurrentElement() doesn't return the HR and + // I couldn't find a way to get this HR. So I have to keep this reference. + // I will be used in Editor._handleEnter. + this.recentHR = block.nextSibling; + this.deleteNode(block); + return false; + } else { + // I can't remember exactly when following is executed and what it does :-( + // * Case 1: Performing Ctrl+A and Ctrl+X repeatedly + // * ... + var nextBlock = this.tree.findForward( + block, + function(node) {return this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)}.bind(this) + ); + + if(nextBlock) { + this.deleteNode(block); + this.placeCaretAtStartOf(nextBlock); + } else { + this.placeCaretAtStartOf(block); + } + + return true; + } + } else { + block = this.getCurrentBlockElement(); + if(block.nodeType === 3) block = block.parentNode; + + if(this.tree.hasMixedContents(block)) { + var marker = this.pushMarker(); + this.wrapAllInlineOrTextNodesAs("P", block, true); + this.popMarker(true); + return true; + } else if((this.tree.isTextOrInlineNode(block.previousSibling) || this.tree.isTextOrInlineNode(block.nextSibling)) && this.tree.hasMixedContents(block.parentNode)) { + // @WORKAROUND: + // IE에서는 Block과 Inline/Text가 인접한 경우 getCurrentElement 등이 오작동한다. + // 따라서 현재 Block 주변까지 한번에 잡아주어야 한다. + this.wrapAllInlineOrTextNodesAs("P", block.parentNode, true); + return true; + } else { + return false; + } + } + }, + + + + ////// + // Commands + execCommand: function(commandId, param) { + return this.getDoc().execCommand(commandId, false, param); + }, + + applyBackgroundColor: function(color) { + this.execCommand("BackColor", color); + }, + + applyEmphasis: function() { + // Generate tag. It will be replaced with tag during cleanup phase. + this.execCommand("Italic"); + }, + applyStrongEmphasis: function() { + // Generate tag. It will be replaced with tag during cleanup phase. + this.execCommand("Bold"); + }, + applyStrike: function() { + // Generate tag. It will be replaced with ") -}B.write(""); -B.write("

"+this.rdom.makePlaceHolderString()+"

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


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


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

 

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

Link

\n\t\t
\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t
\n\t
" diff --git a/modules/editor/skins/xquared/css/default.css b/modules/editor/skins/xquared/stylesheets/default.css similarity index 100% rename from modules/editor/skins/xquared/css/default.css rename to modules/editor/skins/xquared/stylesheets/default.css diff --git a/modules/editor/skins/xquared/css/xq_contents.css b/modules/editor/skins/xquared/stylesheets/xq_contents.css similarity index 90% rename from modules/editor/skins/xquared/css/xq_contents.css rename to modules/editor/skins/xquared/stylesheets/xq_contents.css index 65e68904a..c2c77004f 100644 --- a/modules/editor/skins/xquared/css/xq_contents.css +++ b/modules/editor/skins/xquared/stylesheets/xq_contents.css @@ -69,15 +69,15 @@ font-family: monospace; list-style-type: none; border-color: #ffb781; - background: url('../images/content/code.gif') no-repeat 0 0; + background: url(../images/content/code.gif) no-repeat 0 0; } .xed div { border-color: #8ccfff; - background: url('../images/content/div.gif') no-repeat 0 0; + background: url(../images/content/div.gif) no-repeat 0 0; } .xed blockquote { border-color: #c9c9c9; - background: url('../images/content/blockquote.gif') no-repeat 0 0; + background: url(../images/content/blockquote.gif) no-repeat 0 0; } @@ -119,4 +119,4 @@ .xed table.datatable td { border-bottom: 1px solid #000; border-right: 1px solid #000; -} +} \ No newline at end of file diff --git a/modules/editor/skins/xquared/css/xq_ui.css b/modules/editor/skins/xquared/stylesheets/xq_ui.css similarity index 83% rename from modules/editor/skins/xquared/css/xq_ui.css rename to modules/editor/skins/xquared/stylesheets/xq_ui.css index abcc1f373..490a647b3 100644 --- a/modules/editor/skins/xquared/css/xq_ui.css +++ b/modules/editor/skins/xquared/stylesheets/xq_ui.css @@ -1,18 +1,25 @@ /** * Default Toolbar */ - .xquared { - border: 1px solid #c2c2c2; - } +.xquared .toolbar { + border: 1px solid #c2c2c2; + border-bottom: none; +} +.xquared .editor { + border: 1px solid #c2c2c2; + border-top: none; +} + .xquared div.toolbar { position: relative; background-color: #ebebeb; background-position: 0 0; background-repeat: repeat-x; - background-image: url('../images/toolbar/toolbarBg.gif'); + background-image: url(../images/toolbar/toolbarBg.gif); } .xquared ul.buttons { + position: relative; margin: 0; padding: 5px 4px 2px; list-style: none; @@ -26,14 +33,14 @@ padding-bottom: 3px; background-position: 0 0; background-repeat: repeat-x; - background-image: url('../images/toolbar/toolbarButtonBg.gif'); + background-image: url(../images/toolbar/toolbarButtonBg.gif); } .xquared ul.buttons li.xq_separator { padding-left: 8px; margin-left: 8px; background-position: 0 0; background-repeat: repeat-x; - background-image: url('../images/toolbar/toolbarSeparator.gif'); + background-image: url(../images/toolbar/toolbarSeparator.gif); } .xquared ul.buttons li a { display: block; @@ -47,6 +54,7 @@ z-index: 0; } .xquared ul.buttons li a img { + height: 15px; margin: 0; padding: 0; border: none; @@ -90,9 +98,8 @@ border: 1px solid #dbdbdb; } +/* editor */ .xquared .editor { - border: 0 none; - border-top:1px solid #c2c2c2; height:300px; } .xquared .editor textarea, @@ -105,10 +112,12 @@ } .xquared .editor textarea { - _height: expression(this.parentNode.clientHeight - 2); /* TODO remove IE6 hack */ + /* TODO remove IE6 hack */ + _height: expression(this.parentNode.clientHeight - 2 - parseInt(this.currentStyle.borderTopWidth) - parseInt(this.currentStyle.borderBottomWidth)); } *+html .xquared .editor textarea { - height: expression(this.parentNode.clientHeight - 1); /* TODO remove IE7 hack */ + /* TODO remove IE7 hack */ + height: expression(this.parentNode.clientHeight - 1 - parseInt(this.currentStyle.borderTopWidth) - parseInt(this.currentStyle.borderBottomWidth)); } .xquared .source_editor { @@ -230,4 +239,4 @@ .xqQuickSearch li.selected { background-color: #ffd; -} +} \ No newline at end of file diff --git a/modules/editor/tpl/js/editor.js b/modules/editor/tpl/js/editor.js index 50811c186..804cd33c5 100755 --- a/modules/editor/tpl/js/editor.js +++ b/modules/editor/tpl/js/editor.js @@ -67,9 +67,11 @@ function editorGetSelectedNode(editor_sequence) { /** * editor 시작 (editor_sequence로 iframe객체를 얻어서 쓰기 모드로 전환) **/ +var _editorFontColor = new Array(); function editorStart(editor_sequence, primary_key, content_key, editor_height, font_color) { if(typeof(font_color)=='undefined') font_color = '#000'; + _editorFontColor[editor_sequence] = font_color; // iframe obj를 찾음 var iframe_obj = editorGetIFrame(editor_sequence); @@ -125,7 +127,7 @@ function editorStart(editor_sequence, primary_key, content_key, editor_height, f ''+ ''+ ''+ ''+ content+ @@ -500,17 +502,18 @@ function showEditorHelp(e,editor_sequence){ } } -function showEditorExtension(e,editor_sequence){ +function showEditorExtension(evt,editor_sequence){ var oid = 'editorExtension_'+editor_sequence; + var e = new xEvent(evt); if(xGetElementById(oid).className =='extension2'){ xGetElementById(oid).className = 'extension2 open'; if(e.pageX <= xWidth('editor_component_'+editor_sequence)){ xGetElementById('editor_component_'+editor_sequence).style.right='auto'; - xGetElementById('editor_component_'+editor_sequence).style.left='0'; + xGetElementById('editor_component_'+editor_sequence).style.left='0px'; }else{ - xGetElementById('editor_component_'+editor_sequence).style.right='0'; - xGetElementById('editor_component_'+editor_sequence).style.left=''; + xGetElementById('editor_component_'+editor_sequence).style.right='0px'; + xGetElementById('editor_component_'+editor_sequence).style.left='auto'; } }else{ @@ -518,11 +521,11 @@ function showEditorExtension(e,editor_sequence){ } } -function showPreviewContent(ret_obj,response_tags, params, fo_obj) { - var preview_obj = editorGetPreviewArea(params.editor_sequence); - if(xGetElementById('fileUploader_'+editor_sequence)) xGetElementById('fileUploader_'+params.editor_sequence).style.display='none'; -// alert(ret_obj.content); - xInnerHtml(preview_obj, ret_obj.content); +function showPreviewContent(editor_sequence) { + if(typeof(editor_sequence)=='undefined') return; + if(typeof(_editorFontColor[editor_sequence])=='undefined') return; + var preview_obj = editorGetPreviewArea(editor_sequence); + preview_obj.contentWindow.document.body.style.color = _editorFontColor[editor_sequence]; } function setPreviewHeight(editor_sequence){ diff --git a/modules/editor/tpl/preview.html b/modules/editor/tpl/preview.html index ce0fc1b4a..15e26a14f 100644 --- a/modules/editor/tpl/preview.html +++ b/modules/editor/tpl/preview.html @@ -1 +1,4 @@ -{$content} \ No newline at end of file + +{$content} diff --git a/modules/homepage/homepage.class.php b/modules/homepage/homepage.class.php index 61f2e5cf6..624d9c303 100644 --- a/modules/homepage/homepage.class.php +++ b/modules/homepage/homepage.class.php @@ -17,13 +17,15 @@ $oModuleController->insertActionForward('homepage', 'view', 'dispHomepageAdminDelete'); // 신규 홈페이지 추가 + /* $oModuleModel = &getModel('module'); if(!$oModuleModel->getDefaultMid()) { $tmp_url = parse_url(Context::getRequestUri()); - $domain = sprintf('%s%s', $tmp_url['host'], $tmp_url['path']); + $domain = sprintf('%s%s%s', $tmp_url['host'], $tmp_url['port']&&$tmp_url['port']!=80?':'.$tmp_url['port']:'',$tmp_url['path']); $oHomepageAdminController = &getAdminController('homepage'); $oHomepageAdminController->insertHomepage('homepage', $domain); } + */ return new Object(); } diff --git a/modules/homepage/skins/xe_official/menu_manage.html b/modules/homepage/skins/xe_official/menu_manage.html index c90323d1e..9af9a9e1e 100644 --- a/modules/homepage/skins/xe_official/menu_manage.html +++ b/modules/homepage/skins/xe_official/menu_manage.html @@ -140,8 +140,14 @@ - - {$lang->cmd_close} + + + + +
+ + {$lang->cmd_close} +
diff --git a/modules/issuetracker/lang/zh-TW.lang.php b/modules/issuetracker/lang/zh-TW.lang.php index e27f7a0cf..f6af51060 100644 --- a/modules/issuetracker/lang/zh-TW.lang.php +++ b/modules/issuetracker/lang/zh-TW.lang.php @@ -49,7 +49,7 @@ $lang->completed_date = '結束日期'; $lang->order = '順序'; $lang->package = $lang->package_srl = '套裝包'; - $lang->release = $lang->release_srl = '發佈版本'; + $lang->release = $lang->release_srl = '發佈版'; $lang->release_note = '發佈記錄'; $lang->release_changes = '更新日誌'; $lang->occured_version = $lang->occured_version_srl = '目前版本'; @@ -70,7 +70,7 @@ $lang->about_milestone = '設置開發計劃。'; $lang->about_priority = '設置優先順序。'; - $lang->about_type = '設置問題種類。 (ex. 問題, 改善項目)'; + $lang->about_type = '設置問題種類。 (例如:問題,改善項目)'; $lang->about_component = '設置問題組件。'; $lang->project_menus = array( diff --git a/modules/issuetracker/skins/xe_issuetracker/download.html b/modules/issuetracker/skins/xe_issuetracker/download.html index a2c991d65..289d5cdaf 100644 --- a/modules/issuetracker/skins/xe_issuetracker/download.html +++ b/modules/issuetracker/skins/xe_issuetracker/download.html @@ -65,7 +65,7 @@     - comment)-->rel="{str_replace("\n","
",htmlspecialchars($file->comment))}">{$file->source_filename}
+ comment)-->rel="{str_replace("\n","
",htmlspecialchars($file->comment))}">{$file->source_filename}
{FileHandler::filesize($file->file_size)} {$file->download_count} diff --git a/modules/ldap/conf/info.xml b/modules/ldap/conf/info.xml index bde362a6e..747015c30 100644 --- a/modules/ldap/conf/info.xml +++ b/modules/ldap/conf/info.xml @@ -23,7 +23,7 @@ accessory zero - zero + zero zero diff --git a/modules/ldap/lang/zh-TW.lang.php b/modules/ldap/lang/zh-TW.lang.php index 7721b11d7..9fc63d793 100644 --- a/modules/ldap/lang/zh-TW.lang.php +++ b/modules/ldap/lang/zh-TW.lang.php @@ -17,10 +17,10 @@ $lang->ldap_username_entry = '請輸入使用者名稱'; $lang->ldap_group_entry = '請輸入群組'; - $lang->about_use_ldap = '請輸入 all necessary information first and then check this'; - $lang->about_ldap_server = '請輸入 LDAP server information for authirization and data request'; + $lang->about_use_ldap = '請輸入並檢查所有必填的資料。'; + $lang->about_ldap_server = '請輸入已授權的 LDAP主機資料。'; $lang->about_ldap_port = '請輸入 LDAP主機端口。'; - $lang->about_ldap_userdn_suffix = '請輸入 userdn suffux for authorization. 例) @abc.com'; + $lang->about_ldap_userdn_suffix = '請輸入已授權的 userdn後綴。例) @abc.com'; $lang->about_ldap_basedn = '請輸入 base DN資料夾。 例) dc=abc,dc=com'; $lang->about_ldap_email_entry = '請輸入 LDAP電子郵件資料。(禁止重複)'; diff --git a/modules/member/member.admin.controller.php b/modules/member/member.admin.controller.php index 2b872a1ff..1271910a1 100644 --- a/modules/member/member.admin.controller.php +++ b/modules/member/member.admin.controller.php @@ -83,9 +83,11 @@ **/ function procMemberAdminInsertConfig() { // 기본 정보를 받음 - $args = Context::gets('webmaster_name','webmaster_email','skin','colorset','enable_openid','enable_join','enable_confirm','enable_ssl','limit_day','after_login_url','after_logout_url','redirect_url','agreement','profile_image','profile_image_max_width','profile_image_max_height','image_name','image_mark', 'image_name_max_width', 'image_name_max_height','image_mark_max_width','image_mark_max_height','signature_max_height','editor_skin'); + $args = Context::gets('webmaster_name','webmaster_email','skin','colorset','enable_openid','enable_join','enable_confirm','enable_ssl','limit_day','after_login_url','after_logout_url','redirect_url','agreement','profile_image','profile_image_max_width','profile_image_max_height','image_name','image_mark', 'image_name_max_width', 'image_name_max_height','image_mark_max_width','image_mark_max_height','signature_max_height','editor_skin','editor_colorset'); if(!$args->skin) $args->skin = "default"; if(!$args->colorset) $args->colorset = "white"; + if(!$args->editor_skin) $args->editor_skin= "default"; + if(!$args->editor_colorset) $args->editor_colorset = "white"; if($args->enable_join!='Y') $args->enable_join = 'N'; if($args->enable_ssl!='Y') $args->enable_ssl = 'N'; if($args->enable_openid!='Y') $args->enable_openid= 'N'; diff --git a/modules/member/member.admin.view.php b/modules/member/member.admin.view.php index 0c55ca9c9..b07102d57 100644 --- a/modules/member/member.admin.view.php +++ b/modules/member/member.admin.view.php @@ -109,7 +109,13 @@ * @brief 회원 정보 출력 **/ function dispMemberAdminInfo() { - return $this->dispMemberAdminInsert(); + // 추가 가입폼 목록을 받음 + $oMemberModel = &getModel('member'); + $oModuleModel = &getModel('module'); + $member_config = $oModuleModel->getModuleConfig('member'); + Context::set('member_config', $member_config); + Context::set('extend_form_list', $oMemberModel->getCombineJoinForm($this->member_info)); + $this->setTemplateFile('member_info'); } /** diff --git a/modules/member/member.view.php b/modules/member/member.view.php index 838f9bd4b..eef077373 100644 --- a/modules/member/member.view.php +++ b/modules/member/member.view.php @@ -114,6 +114,7 @@ $option->disable_html = true; $option->height = 200; $option->skin = $this->member_config->editor_skin; + $option->colorset = $this->member_config->editor_colorset; $editor = $oEditorModel->getEditor($member_info->member_srl, $option); Context::set('editor', $editor); } diff --git a/modules/member/skins/default/css/black.css b/modules/member/skins/default/css/black.css index 779f7e158..4ce4c6b67 100644 --- a/modules/member/skins/default/css/black.css +++ b/modules/member/skins/default/css/black.css @@ -4,18 +4,11 @@ .memberSmallBox .header h3 { margin:0; border:none; float:left; clear:both; font-size:1.2em; padding:.8em 2em .6em 1.2em; border:none; border-bottom:3px solid #fe3614; background:url(../images/common/lineH3.gif) no-repeat right bottom;} /* button */ -a.button, span.button, del.button, -a.button span, span.button button, span.button input, del.button span{ - background-image:url(../images/black/form_buttons.png); - _background-image:url(../images/black/form_buttons.gif); -} - - -del.button span, -a.button, span.button, del.button, -a.button span, span.button button, span.button input, del.button span{ - color:#FFFFFF; -} +a.button, +a.button span, +span.button, +span.button button, +span.button input { background-image:url(../../../../../common/tpl/images/buttonBlack.gif); color:#fff;} .inputTypeText { background-color:transparent !important; color:#AAAAAA !important; } .inputTypeText:hover, .inputTypeText:focus { background-color:transparent; color:#AAAAAA; } @@ -27,8 +20,10 @@ a.button span, span.button button, span.button input, del.button span{ .memberSmallBox .openid_user_id { color:#AAAAAA; background:transparent url("../images/black/openid_input_bg.gif") no-repeat left 50% !important; } .memberSmallBox .help { background-color:transparent; } +.memberInfoTable { border:1px solid #444; } .memberInfoTable tr.first-child th, .memberInfoTable tr.first-child td { background-color:transparent; color:#AAAAAA; } -.memberInfoTable tr th, .memberInfoTable tr td { background-color:transparent; color:#AAAAAA; } +.memberInfoTable tr th, .memberInfoTable tr td { border-top:1px solid #444; background-color:transparent; color:#AAAAAA; } +.memberInfoTable td { border-left:1px solid #444; } .memberInfoTable td input { background-color:transparent; color:#AAAAAA; } .memberInfoTable caption { color:#AAAAAA; } div.checkValue { color:#BBBBBB !important; } @@ -45,8 +40,15 @@ table.list tr td.title a { color:#BBBBBB !important; } table.list tr td { border-top:1px solid #888888 !important; } table .list tr td, table.list tr td * { color:#BBBBBB; } -.pagenation a { border:none !important; } -.pagenation strong { border:none !important; color:#EEEEEE; } +/* Pagination*/ +.pagination a, .pagination strong{ color:#888; } +.pagination a:hover, .pagination a:active, .pagination a:focus{ background-color:#555 !important; } +.pagination strong{ color:#ff6600 !important;} +.pagination.a1 a, .pagination.a1 strong{ background:#000; border-left:1px solid #555; border-right:1px solid #666; color:#888;} +.pagination.a1 a.prev, .pagination.a1 a.prevEnd, .pagination.a1 a.next, .pagination.a1 a.nextEnd { background:#000; } + +.pagination.a2 a, .pagination.a2 strong{ background:#000; border-left:1px solid #555; border-right:1px solid #666; color:#888; } +.pagination.a2 a.prev, .pagination.a2 a.prevEnd, .pagination.a2 a.next, .pagination.a2 a.nextEnd { background:#000; } .memberSmallBox .leftHeaderType th { background:transparent; color:#AAAAAA; } .memberSmallBox .leftHeaderType td { color:#AAAAAA; } diff --git a/modules/member/tpl/insert_member.html b/modules/member/tpl/insert_member.html index 17dc1205f..39d1214b5 100644 --- a/modules/member/tpl/insert_member.html +++ b/modules/member/tpl/insert_member.html @@ -145,7 +145,7 @@
{$lang->description}
- +

{$lang->about_member_description}

@@ -197,7 +197,7 @@ - + diff --git a/modules/member/tpl/member_config.html b/modules/member/tpl/member_config.html index 510047c21..203330d89 100644 --- a/modules/member/tpl/member_config.html +++ b/modules/member/tpl/member_config.html @@ -1,4 +1,5 @@ + @@ -99,11 +100,16 @@
{$lang->editor_skin}
- + + diff --git a/modules/module/module.model.php b/modules/module/module.model.php index fe0cb5282..218942e01 100644 --- a/modules/module/module.model.php +++ b/modules/module/module.model.php @@ -32,7 +32,7 @@ $url_info = parse_url(Context::getRequestUri()); $hostname = $url_info['host']; $path = preg_replace('/\/$/','',$url_info['path']); - $sites_args->domain = sprintf('%s%s', $hostname, $path); + $sites_args->domain = sprintf('%s%s%s', $hostname, $url_info['port']&&$url_info['port']!=80?':'.$url_info['port']:'',$path); $output = executeQuery('module.getSiteDefaultInfo', $sites_args); if(!$output->toBool() || !$output->data) $output = executeQuery('module.getDefaultMidInfo'); $module_info = $output->data; diff --git a/modules/planet/conf/module.xml b/modules/planet/conf/module.xml index 5e87f4011..12bf7045f 100644 --- a/modules/planet/conf/module.xml +++ b/modules/planet/conf/module.xml @@ -3,26 +3,26 @@ 접근 - Access - 访问 + Access + 访问 訪問 플래닛 생성 - Create a planet - 生成微博 + Create a planet + 生成微博 建立 글쓰기 - Write a document - 发布主题 + Write a document + 发布主题 發表 관리 - Manager - 管理 + Manager + 管理 管理 diff --git a/modules/planet/lang/en.lang.php b/modules/planet/lang/en.lang.php index d874c8a52..9b46df93d 100644 --- a/modules/planet/lang/en.lang.php +++ b/modules/planet/lang/en.lang.php @@ -161,6 +161,8 @@ $lang->planet_tagtab = "Tags for Main Tab"; $lang->about_planet_tagtab = "You can set multiple tags seperated by comma(,), These tags displayed as tabs in the main page."; + $lang->planet_tagtab_after = "뒤 추가 Tag 탭"; + $lang->about_planet_tagtab_after = "기본 태그탭 이후에 여러개의 Tag 탭을 추가할 수 있습니다. 콤마(,)로 여러개를 지정할 수 있습니다"; $lang->planet_smstag = "SMS Tag"; $lang->about_planet_smstag = "You can set multiple tags seperated by comma(,), These tags automatically added if the posting is registered via SMS"; @@ -168,5 +170,5 @@ $lang->about_use_mobile = "Enable to write posting via mobile SMS"; $lang->planet_use_me2day = "Use Me2day"; $lang->about_use_me2day = "When writing a message, users can send it to me2day(http://me2day.net)."; - + $lang->msg_search_thisplanet = "이 플래닛에서 검색"; ?> diff --git a/modules/planet/lang/jp.lang.php b/modules/planet/lang/jp.lang.php index 3dd110b25..bd1d6658f 100644 --- a/modules/planet/lang/jp.lang.php +++ b/modules/planet/lang/jp.lang.php @@ -163,6 +163,8 @@ $lang->planet_tagtab = "메인 추가 Tag 탭"; $lang->about_planet_tagtab = "콤마(,)로 여러개의 Tag를 지정할 수 있으며, 지정한 Tag는 메인에 탭으로 출력 됩니다. 글쓰기 창에도 출력됩니다."; + $lang->planet_tagtab_after = "뒤 추가 Tag 탭"; + $lang->about_planet_tagtab_after = "기본 태그탭 이후에 여러개의 Tag 탭을 추가할 수 있습니다. 콤마(,)로 여러개를 지정할 수 있습니다"; $lang->planet_smstag = "SMS 등록 Tag"; $lang->about_planet_smstag = "콤마(,)로 여러개의 Tag를 지정할 수 있으며, SMS로 등록시에 등록 되는 Tag"; @@ -170,5 +172,5 @@ $lang->about_use_mobile = "핸드폰 SMS로 글을 작성할 수 있게 합니다."; $lang->planet_use_me2day = "미투데이 연동"; $lang->about_use_me2day = "글 작성시 me2day에 보내게 됩니다."; - -?> \ No newline at end of file + $lang->msg_search_thisplanet = "이 플래닛에서 검색"; +?> diff --git a/modules/planet/lang/ko.lang.php b/modules/planet/lang/ko.lang.php index fd1c22c1f..cc8900617 100644 --- a/modules/planet/lang/ko.lang.php +++ b/modules/planet/lang/ko.lang.php @@ -162,8 +162,10 @@ $lang->planet_mobile_number = '전화번호'; $lang->msg_success_set_phone_number = '전화번호가 등록되었습니다.'; - $lang->planet_tagtab = "메인 추가 Tag 탭"; - $lang->about_planet_tagtab = "콤마(,)로 여러개의 Tag를 지정할 수 있으며, 지정한 Tag는 메인에 탭으로 출력 됩니다. 글쓰기 창에도 출력됩니다."; + $lang->planet_tagtab = "추가 Tag 탭"; + $lang->about_planet_tagtab = "콤마(,)로 여러개의 Tag를 지정할 수 있으며, 지정한 Tag는 메인에 탭으로 출력 됩니다."; + $lang->planet_tagtab_after = "뒤 추가 Tag 탭"; + $lang->about_planet_tagtab_after = "기본 태그탭 이후에 여러개의 Tag 탭을 추가할 수 있습니다. 콤마(,)로 여러개를 지정할 수 있습니다"; $lang->planet_smstag = "SMS 등록 Tag"; $lang->about_planet_smstag = "콤마(,)로 여러개의 Tag를 지정할 수 있으며, SMS로 등록시에 등록 되는 Tag"; @@ -172,4 +174,8 @@ $lang->planet_use_me2day = "미투데이 연동"; $lang->about_use_me2day = "글 작성시 me2day에 보내게 됩니다."; + + $lang->msg_search_thisplanet = "이 플래닛에서 검색"; + + ?> diff --git a/modules/planet/lang/zh-CN.lang.php b/modules/planet/lang/zh-CN.lang.php index 9446dca1f..c5fc27da1 100644 --- a/modules/planet/lang/zh-CN.lang.php +++ b/modules/planet/lang/zh-CN.lang.php @@ -17,7 +17,7 @@ $lang->planet_reply_content = "微博评论内容"; - $lang->about_planet = + $lang->about_planet = "XE的微博模块。用户可以创建自己的微博并使用。 注意:主站使用的域名可能无法链接微博。 注意:要想把微博设置为默认首页,你要链接的域名应是唯一的,不能同时用在其他站点。"; @@ -106,8 +106,8 @@ $lang->planet_nowhot_tag = "微博实时人气标签"; $lang->cmd_planet_close_nowhot_tag = "关闭实时人气标签"; - $lang->about_planet_whats_textSearch_in_planet = "%s님의 플래닛에서 검색한 '%s' 에 대한 결과 입니다."; - $lang->about_planet_whats_textSearch = "'%s' 에 대한 전체 검색결과 입니다."; + $lang->about_planet_whats_textSearch_in_planet = "在%s的微博搜索 '%s' 的结果。"; + $lang->about_planet_whats_textSearch = "'%s' 的搜索结果。"; $lang->planet_acticle = "主题"; $lang->planet_persontag = "形象标签"; @@ -164,6 +164,8 @@ $lang->planet_tagtab = "添加首页Tag标签页"; $lang->about_planet_tagtab = "用逗号(,)来区分而难多个Tag标签页。"; + $lang->planet_tagtab_after = "뒤 추가 Tag 탭"; + $lang->about_planet_tagtab_after = "기본 태그탭 이후에 여러개의 Tag 탭을 추가할 수 있습니다. 콤마(,)로 여러개를 지정할 수 있습니다"; $lang->planet_smstag = "添加SMS标签"; $lang->about_planet_smstag = "用SMS发送时自动添加的标签,用逗号(,)来区分多个标签。"; @@ -171,5 +173,5 @@ $lang->about_use_mobile = "可以用手机SMS发送。"; $lang->planet_use_me2day = "绑定me2day"; $lang->about_use_me2day = "发布主题的同时发送到me2day。"; - + $lang->msg_search_thisplanet = "이 플래닛에서 검색"; ?> diff --git a/modules/planet/lang/zh-TW.lang.php b/modules/planet/lang/zh-TW.lang.php index 49805161f..b037474f7 100644 --- a/modules/planet/lang/zh-TW.lang.php +++ b/modules/planet/lang/zh-TW.lang.php @@ -11,22 +11,22 @@ $lang->planet_myplanet = "我的微型部落格"; $lang->planet_whos_planet = "%s 的微型部落格"; $lang->planet_whos_favorite = "%s 的最愛"; - $lang->planet_whos_favorite_list = "經常拜訪 '%s'的微型部落格"; + $lang->planet_whos_favorite_list = "'%s'的微型部落格清單"; - $lang->planet_welcome = "歡迎!"; + $lang->planet_welcome = "歡迎光臨!"; $lang->planet_reply_content = "微型部落格的回覆內容"; $lang->about_planet = "XE 微型部落格模組。每個會員都能建立自己的微型部落格。 - Planet may not be accessible with the domain name used in the homepage package. + 網站模組所使用的網域名稱可能無法用於微型部落格。 如果想將微型部落格當作首頁,請先在網站模組中設定其他的網域名稱。"; $lang->planet_mid = "微型部落格連結網址"; - $lang->about_planet_mid = "可以直接輸入網址訪問部落格,微型部落格連結網址是「http://主程式網址/planet連結網址」。"; + $lang->about_planet_mid = "可以直接輸入網址訪問部落格,微型部落格連結網址是「http://主程式網址/微型部落格網址」。"; - $lang->planet_default_skin = "微型部落格基本面板"; - $lang->about_planet_default_skin = "建立和設定微型部落格首頁。"; + $lang->planet_default_skin = "微型部落格面板"; + $lang->about_planet_default_skin = "選擇微型部落格面板。"; $lang->planet_comment = "微型部落格簡介"; $lang->about_planet_comment = "微型部落格的簡介,同時也會顯示在瀏覽器的標題。"; @@ -35,8 +35,8 @@ $lang->about_use_signup = "選擇此項,會在微型部落格首頁顯示『新會員註冊』按鈕。"; $lang->cmd_create_planet = "建立我的微型部落格"; - $lang->create_message = "我的微型部落格簡介"; - $lang->about_create_message = "可簡單地輸入簡介。"; + $lang->create_message = "微型部落格簡介"; + $lang->about_create_message = "簡單地輸入關於部落格的簡介。"; $lang->cmd_planet_setup = "基本設定"; $lang->cmd_planet_list = "微型部落格列表"; @@ -55,19 +55,19 @@ $lang->cmd_planet_addfavorite = "新增至最愛"; $lang->planet_hot_tag = "熱門標籤"; - $lang->planet_home = "TOP"; + $lang->planet_home = "首頁"; $lang->cmd_planet_more_tag = "更多"; - $lang->planet_memo = "memo"; - $lang->cmd_planet_show_memo_write_form = "發表 memo"; - $lang->cmd_planet_delete_memo = "刪除 memo"; + $lang->planet_memo = "留言"; + $lang->cmd_planet_show_memo_write_form = "發表留言"; + $lang->cmd_planet_delete_memo = "刪除留言"; $lang->cmd_planet_memo_write_ok = "輸入完畢"; $lang->planet_interest_tag = "喜愛標籤"; $lang->planet_interest_content = "喜愛內容"; $lang->cmd_planet_show_interest_tag = "顯示喜愛標籤"; $lang->cmd_planet_close_interest_tag = "關閉喜愛標籤"; - $lang->msg_planet_already_added_interest_tag = "以新增的喜愛標籤。"; + $lang->msg_planet_already_added_interest_tag = "已新增的喜愛標籤。"; $lang->cmd_planet_edit_subject = "修改標題"; $lang->cmd_planet_edit_intro = "修改簡介"; @@ -76,18 +76,18 @@ $lang->cmd_planet_openclose_memo = "展開或關閉 memo"; $lang->cmd_planet_del_tag = "刪除標籤"; - $lang->cmd_planet_openclose_recommend_search = "推薦検索を開く/關閉"; + $lang->cmd_planet_openclose_recommend_search = "開啟或關閉推薦關鍵字"; $lang->about_planet_input_search_text = "輸入關鍵字"; - $lang->about_planet_make_planet = "建立微型部落格。プラネットの上段に表示される「マイインフォ」を入力してください。"; - $lang->about_planet_make_planet_info = "マイプラネットの上段に表示されるマイインフォです。各項目を入力して設定を変更してください。"; + $lang->about_planet_make_planet = "建立微型部落格。請在微型部落格的頂端輸入「我的個人資料」。"; + $lang->about_planet_make_planet_info = "顯示在微型部落格頂端的個人資料。當所有項目都輸入完畢後,請儲存您的設定。"; $lang->planet_input_personalinfo = "輸入個人資料"; $lang->planet_photo = "照片"; $lang->planet_myintro = "自我介紹"; $lang->about_planet_url = "設定完後無法再修改。"; $lang->planet_mytag = "個人標籤"; - $lang->about_planet_mytag = "自分を表現するタグです。以「,」區分多個標籤。"; + $lang->about_planet_mytag = "個人專屬標籤。以「,」區分多個標籤。"; $lang->about_planet_tag = "以「,」區分多個標籤。"; @@ -103,7 +103,7 @@ $lang->planet_nowhot_tag = "熱門標籤"; $lang->cmd_planet_close_nowhot_tag = "關閉熱門標籤"; - $lang->about_planet_whats_textSearch_in_planet = "%sさんのプラネットから検索した'%s'的搜尋結果。"; + $lang->about_planet_whats_textSearch_in_planet = "搜尋%s的微型部落格'%s'的搜尋結果。"; $lang->about_planet_whats_textSearch = "'%s'的全部搜尋結果。"; $lang->planet_acticle = "文章"; @@ -119,21 +119,21 @@ $lang->planet_article_preview = "預覽"; - $lang->planet_notice_title = "%sさん、プラネットにようこそ!
%sさんの考え、意見、情報、知識などを自由にたくさんの人々と共有できるプラネットです。簡単な使い方を案内します。"; + $lang->planet_notice_title = "%s,歡迎來到微型部落格!
%s,您可以在微型部落格中分享您的想法、意見、資訊以及其他知識。以下是簡單的使用說明。^^"; $lang->planet_notice_list = array( - "「書き込み」の「開く」ボタンを利用してみてください。記事を作成するウィンドウが開きます。", - "作成された記事は %sさんのプラネットに訪問する人々に公開されます。", - "「お気に入り追加」と「関心タグ追加」機能を利用すれば%sさんの関心記事を見ることができます。", - "%sさんの「写真、ネックネーム、タグ」などの情報は現在ページから変更できます。", - "對其他 planet有興趣?請利用『熱門標籤』或『搜尋』。", + "底下的內容輸入區域可以開啟與關閉。", + "所有瀏覽者都能夠觀看 %s的微型部落格,而且也能夠回覆。", + "利用「新增最愛」與「喜愛標籤」功能,隨時都能看到 %s的喜愛內容。", + "%s的「照片、暱稱、標籤」等資料都可在此頁面中進行修改。", + "對其他微型部落格有興趣?請利用『熱門標籤』或『搜尋』。", "如果您還有其他問題。請搜尋『問題』標籤。", ); $lang->planet_notice_disable = "不要再顯示。"; - $lang->msg_planet_about_postscript = "追加で書きたいことがありましたら作成してください。"; + $lang->msg_planet_about_postscript = "請輸入附註。"; $lang->msg_planet_about_tag = "請輸入標籤。(以「,」區隔)"; - $lang->msg_planet_already_added_favorite = "以新增的最愛。"; - $lang->msg_planet_no_memo = "沒有 memo。"; + $lang->msg_planet_already_added_favorite = "已新增的最愛。"; + $lang->msg_planet_no_memo = "目前沒有留言。"; $lang->msg_planet_rss_enabled = "RSS使用中。"; $lang->msg_planet_rss_disabled = "RSS暫停使用。"; @@ -155,18 +155,20 @@ $lang->cmd_send_me2day = "me2day傳送"; $lang->msg_already_have_phone_number = '已註冊的手機號碼。'; - $lang->planet_mobile_receive = '모바일 연동'; + $lang->planet_mobile_receive = 'Mobile work'; $lang->planet_mobile_number = '手機號碼'; $lang->msg_success_set_phone_number = '手機號碼註冊成功。'; - $lang->planet_tagtab = "Tag標籤"; + $lang->planet_tagtab = "標籤頁"; $lang->about_planet_tagtab = "以「,」區分多個標籤,這些標籤將顯示在標籤頁。"; - $lang->planet_smstag = "SMS Tag"; + $lang->planet_tagtab_after = "뒤 추가 Tag 탭"; + $lang->about_planet_tagtab_after = "기본 태그탭 이후에 여러개의 Tag 탭을 추가할 수 있습니다. 콤마(,)로 여러개를 지정할 수 있습니다"; + $lang->planet_smstag = "SMS標籤"; $lang->about_planet_smstag = "以「,」區分多個標籤, 以 SMS傳送時將自動新增標籤。"; $lang->planet_use_mobile = "開啟 SMS"; $lang->about_use_mobile = "可用手機發送 SMS"; - $lang->planet_use_me2day = "使用 Me2day"; + $lang->planet_use_me2day = "Me2day"; $lang->about_use_me2day = "發表主題的同時也傳送到 me2day(http://me2day.net)。"; - + $lang->msg_search_thisplanet = "搜尋微型部落格"; ?> diff --git a/modules/planet/planet.admin.controller.php b/modules/planet/planet.admin.controller.php index 2645ce935..b0c6ef926 100644 --- a/modules/planet/planet.admin.controller.php +++ b/modules/planet/planet.admin.controller.php @@ -19,21 +19,32 @@ $oPlanetModel = &getModel('planet'); $config = $oPlanetModel->getPlanetConfig(); + // 이미 등록된 플래닛의 유무 체크 + if($config->mid && $oModuleModel->getModuleInfoByMid($config->mid)) { + $is_registed = true; + } else { + $is_registed = false; + } + // mid, browser_title, is_default 값이 바뀌면 처리 $config->mid = $args->mid = Context::get('planet_mid'); $args->browser_title = Context::get('browser_title'); $args->is_default = Context::get('is_default'); $args->skin = Context::get('planet_default_skin'); - - $args->module = 'planet'; - $args->module_srl = $config->module_srl; + $args->module_srl = $is_registed?$config->module_srl:getNextSequence(); + if($args->is_default == 'Y') { $output = $oModuleController->clearDefaultModule(); if(!$output->toBool()) return $output; } - $output = $oModuleController->updateModule($args); + + if($is_registed) { + $output = $oModuleController->updateModule($args); + } else { + $output = $oModuleController->insertModule($args); + } if(!$output->toBool()) return $output; // 그외 정보 처리 @@ -41,7 +52,6 @@ $config->use_mobile = Context::get('use_mobile'); $config->use_me2day = Context::get('use_me2day'); - $tagtab = explode(',',Context::get('planet_tagtab')); for($i=0,$c=count($tagtab);$i<$c;$i++){ if(trim($tagtab[$i])) continue; @@ -50,6 +60,14 @@ $tagtab = array_unique($tagtab); $config->tagtab = $tagtab; + $tagtab_after = explode(',',Context::get('planet_tagtab_after')); + for($i=0,$c=count($tagtab_after);$i<$c;$i++){ + if(trim($tagtab_after[$i])) continue; + $tagtab_after[$i] = trim($tagtab_after[$i]); + } + $tagtab_after = array_unique($tagtab_after); + $config->tagtab_after = $tagtab_after; + $smstag = explode(',',Context::get('planet_smstag')); for($i=0,$c=count($smstag);$i<$c;$i++){ diff --git a/modules/planet/planet.admin.view.php b/modules/planet/planet.admin.view.php index dadbe323c..b41aa6c64 100644 --- a/modules/planet/planet.admin.view.php +++ b/modules/planet/planet.admin.view.php @@ -34,6 +34,7 @@ $group_list = $oMemberModel->getGroups(); Context::set('group_list', $group_list); if(is_array($this->config->tagtab)) Context::set('tagtab', join(',',$this->config->tagtab)); + if(is_array($this->config->tagtab_after)) Context::set('tagtab_after', join(',',$this->config->tagtab_after)); if(is_array($this->config->smstag)) Context::set('smstag', join(',',$this->config->smstag)); diff --git a/modules/planet/planet.class.php b/modules/planet/planet.class.php index ac3d552b4..d896cb11c 100644 --- a/modules/planet/planet.class.php +++ b/modules/planet/planet.class.php @@ -37,12 +37,6 @@ $args->skin = 'xe_planet'; $args->is_default = 'N'; $args->mid = 'planet'; - $idx = 0; - while(true) { - $_o = executeQuery('module.getMidInfo', $args); - if(!$_o->data) break; - $idx = $idx + 1; - } $args->module_srl = getNextSequence(); $output = $oModuleController->insertModule($args); diff --git a/modules/planet/planet.view.php b/modules/planet/planet.view.php index 07f965719..8809f9fd4 100644 --- a/modules/planet/planet.view.php +++ b/modules/planet/planet.view.php @@ -93,7 +93,6 @@ } } - $tagtab = null; if($type == 'tagtab'){ $tagtab = Context::get('tagtab'); @@ -154,6 +153,17 @@ Context::set('tagtab_list', $tagtab_list); } + // tagtab_after을 만든다 + if(is_array($this->config->tagtab_after) && $this->config->tagtab_after[0]){ + $tagtab_after_list = array(); + foreach($this->config->tagtab_after as $key => $val){ + $args->tag = $val; + $output = executeQuery('planet.getTotalTagSearchContents', $args); + $tagtab_after_list[$val] = $output->data->count; + } + Context::set('tagtab_after_list', $tagtab_after_list); + } + // 템플릿 지정 $this->setTemplateFile('main'); } diff --git a/modules/planet/skins/xe_planet/content_list.html b/modules/planet/skins/xe_planet/content_list.html index 507e0683f..7a4f173d9 100644 --- a/modules/planet/skins/xe_planet/content_list.html +++ b/modules/planet/skins/xe_planet/content_list.html @@ -1,3 +1,15 @@ + + + + +
diff --git a/modules/planet/skins/xe_planet/css/blue.css b/modules/planet/skins/xe_planet/css/blue.css index e58521b59..6496e1837 100644 --- a/modules/planet/skins/xe_planet/css/blue.css +++ b/modules/planet/skins/xe_planet/css/blue.css @@ -10,7 +10,9 @@ .myTab li, .exTab li, .myTab li a, -.exTab li a{ background-color:#aed1e4;} +.exTab li a, +.tab .tagRank .buttonTag, +.tab .tagRank .buttonTag button{ background-color:#aed1e4;} .myTab li.active, .exTab li.active, .myTab li.active a, diff --git a/modules/planet/skins/xe_planet/css/button.css b/modules/planet/skins/xe_planet/css/button.css deleted file mode 100644 index ae09422c0..000000000 --- a/modules/planet/skins/xe_planet/css/button.css +++ /dev/null @@ -1,116 +0,0 @@ -@charset "utf-8"; -/* Anchor Button */ -a.button, -a.button span { position:relative; display:inline-block; text-decoration:none !important; background:url(../images/buttonWhite.gif) no-repeat; cursor:pointer; white-space:nowrap; vertical-align:middle; *vertical-align:top;} -a.button { padding:0; background-position:left top; overflow:visible;} -a.button span { left:2px; padding:6px 10px 5px 8px; color:#000; font:12px/12px Sans-serif; background-position:right top;} -a.button, x:-moz-any-link { font:0/23px Sans-serif; padding:12px 0; top:1px;} /* Firefox 2 Fix */ -a.button, x:-moz-any-link, x:default { padding:0; top:0;} /* Firefox 2 Fix */ -/* Large Size */ -a.button.large { background-position:left -30px; } -a.button.large span { padding:7px 10px 6px 8px; font:16px/16px Sans-serif; background-position:right -30px;} -a.button.large, x:-moz-any-link { font:0/29px Sans-serif; padding:15px 0;} /* Firefox 2 Fix */ -a.button.large, x:-moz-any-link, x:default { padding:0;} /* Firefox 2 Fix */ -/* xLarge Size */ -a.button.xLarge { background-position:left -65px; } -a.button.xLarge span { padding:8px 10px 7px 8px; font:20px/20px Sans-serif; background-position:right -65px;} -a.button.xLarge, x:-moz-any-link { font:0/35px Sans-serif; padding:18px 0;} /* Firefox 2 Fix */ -a.button.xLarge, x:-moz-any-link, x:default { padding:0;} /* Firefox 2 Fix */ -/* Small Size */ -a.button.small { background-position:left -107px; } -a.button.small span { padding:4px 6px 3px 4px; font:11px/11px Sans-serif; background-position:right -107px;} -a.button.small, x:-moz-any-link { font:0/18px Sans-serif; padding:9px 0;} /* Firefox 2 Fix */ -a.button.small, x:-moz-any-link, x:default { padding:0;} /* Firefox 2 Fix */ - -/* Control Button + Submit Button */ -span.button, -span.button button, -span.button input { position:relative; margin:0; display:inline-block; border:0; font:12px Sans-serif; white-space:nowrap; background:url(../images/buttonWhite.gif) no-repeat; vertical-align:middle;} -span.button { padding:0; background-position:left top;} -span.button, x:-moz-any-link{ font:0/23px Sans-serif; padding:11px 0;} /* Firefox 2 Fix */ -span.button, x:-moz-any-link, x:default{ padding:0;} /* Firefox 2 Fix */ -span.button button, -span.button input { height:23px; left:2px; *top:-1px; _top:0; padding:0 10px 0 8px; *padding:0 5px 0 3px; line-height:24px; background-position:right top; cursor:pointer;} -/* Large Size */ -span.button.large { background-position:left -30px; *top:-1px;} -span.button.large, x:-moz-any-link{ font:0/29px Sans-serif; padding:14px 0;} /* Firefox 2 Fix */ -span.button.large, x:-moz-any-link, x:default{ padding:0;} /* Firefox 2 Fix */ -span.button.large button, -span.button.large input { height:30px; *top:0; _top:-1px; padding:0 10px 0 8px; *padding:0 5px 0 3px; font:16px/30px Sans-serif; background-position:right -30px;} -/* xLarge Size */ -span.button.xLarge { background-position:left -65px;} -span.button.xLarge, x:-moz-any-link{ font:0/35px Sans-serif; padding:17px 0;} /* Firefox 2 Fix */ -span.button.xLarge, x:-moz-any-link, x:default{ padding:0;} /* Firefox 2 Fix */ -span.button.xLarge button, -span.button.xLarge input { height:35px; padding:0 10px 0 8px; *padding:0 5px 0 3px; font:20px/36px Sans-serif; background-position:right -65px;} -/* Large Size */ -span.button.small { background-position:left -107px;} -span.button.small, x:-moz-any-link{ font:0/18px Sans-serif; padding:9px 0;} /* Firefox 2 Fix */ -span.button.small, x:-moz-any-link, x:default{ padding:0;} /* Firefox 2 Fix */ -span.button.small button, -span.button.small input { height:18px; *top:0; _top:-1px; padding:0 6px 0 4px; *padding:0 3px 0 2px; font:11px/18px Sans-serif; background-position:right -107px;} -span.button.small button, x:-moz-any-link, -span.button.small input, x:-moz-any-link{ top:-1px;} /* Firefox 2 Fix */ -span.button.small button, x:-moz-any-link, x:default, -span.button.small input, x:-moz-any-link, x:default{ top:0;} /* Firefox 2 Fix */ - -/* Strong Button */ -a.button.strong *, -span.button.strong * { font-weight:bold !important;} - -/* Icon Add */ -a.button .icon { position:relative; border:0; vertical-align:middle;} -span.button .icon { position:relative; left:10px; margin-right:8px; vertical-align:middle;} - -/* Color Preset */ -a.button.green, -a.button.green span, -span.button.green, -span.button.green button, -span.button.green input { background-image:url(../images/buttonGreen.gif); color:#fff;} -a.button.black, -a.button.black span, -span.button.black, -span.button.black button, -span.button.black input { background-image:url(../images/buttonBlack.gif); color:#fff;} -a.button.red, -a.button.red span, -span.button.red, -span.button.red button, -span.button.red input { background-image:url(../images/buttonRed.gif); color:#fff;} -a.button.blue, -a.button.blue span, -span.button.blue, -span.button.blue button, -span.button.blue input { background-image:url(../images/buttonBlue.gif); color:#fff;} - -/* Offset Debug */ -a.button, -span.button{ margin-right:2px;} - -/* Planet Style */ -a.button.planetBlue, -a.button.planetGreen, -span.button.planetBlue, -span.button.planetGreen{ background-position:left top;} -a.button.planetBlue span, -a.button.planetGreen span, -span.button.planetBlue button, -span.button.planetGreen button{ background-position:right top;} -a.button.planetBlue, -a.button.planetBlue span, -span.button.planetBlue, -span.button.planetBlue button, -span.button.planetBlue input { background-image:url(../images/buttonPlanetBlue.gif); color:#fff;} -a.button.planetGreen, -a.button.planetGreen span, -span.button.planetGreen, -span.button.planetGreen button, -span.button.planetGreen input { background-image:url(../images/buttonPlanetGreen.gif); color:#fff;} - -/* Font Family */ -.button.large, -.button.large *, -.button.xLarge, -.button.xLarge * { font-family:AppleGothic, "돋움", Dotum, "굴림", Gulim, Sans-serif !important; letter-spacing:-1px;} - diff --git a/modules/planet/skins/xe_planet/css/cyan.css b/modules/planet/skins/xe_planet/css/cyan.css index 21092ad2d..c3d59f4a6 100644 --- a/modules/planet/skins/xe_planet/css/cyan.css +++ b/modules/planet/skins/xe_planet/css/cyan.css @@ -10,7 +10,9 @@ .myTab li, .exTab li, .myTab li a, -.exTab li a{ background-color:#bac8c9;} +.exTab li a, +.tab .tagRank .buttonTag, +.tab .tagRank .buttonTag button{ background-color:#bac8c9;} .myTab li.active, .exTab li.active, .myTab li.active a, diff --git a/modules/planet/skins/xe_planet/css/default.css b/modules/planet/skins/xe_planet/css/default.css index dea03fdf5..ca504123d 100644 --- a/modules/planet/skins/xe_planet/css/default.css +++ b/modules/planet/skins/xe_planet/css/default.css @@ -24,25 +24,34 @@ button *{position:relative;} button span{ position:absolute; width:0; height:0; font:0/0 Sans-serif; visibility:hidden;} .inputText, textarea{*margin:-1px 0; border:1px solid;} .inputCheck, .inputRadio{width:13px; height:13px;} -.blind, legend {position:absolute; top:0; left:0; width:0; height:0; overflow:hidden; visibility:hidden; font-size:0; line-height:0;} +.blind, legend {position:absolute; top:0; left:0; width:0; height:0; overflow:hidden; visibility:hidden; font-size:0 !important; line-height:0;} /* Layout */ #container {} -#header{ position:relative; z-index:30; height:65px; float:left; clear:both;} +#header{ position:relative; z-index:30; clear:both; margin:0 0 20px 0;} #header *{ margin:0; padding:0;} #header li{ list-style:none;} #body{ clear:both; z-index:20;} -.nTop #navigation{ top:-65px !important; z-index:100; } +.nTop #navigation{ right:0; top:0 !important; z-index:100; } .nTop #navigation .gnb ul{ margin:0;} .nTop #navigation li{ list-style:none;} .buttonArea{ text-align:center;} -.languageSelect { position:absolute; right:30px; top:10px; } +.languageSelect { position:absolute; right:0; top:-30px; } +.languageSelect select{ font-size:12px;} /* Heading */ -h1 { position:relative; top:-10px;} +h1 { position:relative; top:-10px; white-space:nowrap; font-family:"Malgun Gothic", AppleGothic, Dotum, Sans-serif;} h1, x:-moz-any-link{ top:0;} h1, x:-moz-any-link, x:default{ top:-10px;} -h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000; letter-spacing:-3px; text-decoration:none !important;} +h1 a{ font-weight:bold; font-size:30px; color:#000; letter-spacing:-3px; text-decoration:none !important;} + +/* Time Navigation */ +.timeNav{ font:11px Dotum, Tahoma; white-space:nowrap; color:#999;} +.timeNav *{ color:#000;} +.timeNav strong{ font-family:Arial; font-size:18px;} +.timeNav em{ font-style:normal; font-weight:bold; font-size:18px; font-family:"Malgun Gothic", AppleGothic, Dotum, Tahoma, Sans-serif;} +.timeNav .prev, +.timeNav .next{ display:inline-block; font-size:18px; font-family:Arial; font-weight:bold; color:#000; padding:0 5px !important; text-decoration:none !important;} /* Footer */ #footer{margin:0; clear:both; text-align:center; padding:30px 0;} @@ -54,82 +63,81 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 /* ---------- Navigation ---------- */ /* Search */ -.search{ position:relative; top:2px; float:right; margin:0; padding:0; white-space:nowrap; text-align:right; *zoom:1;} -.search .inputText{ padding:4px 5px 2px 5px; height:21px; width:160px; border:1px solid #e7e7e7; vertical-align:top; font:bold 14px AppleGothic, Dotum, Gulim, Sans-serif;} -.search .inputText.focus{ border:3px solid #4ba300; height:17px; width:156px;} -.search .button{ top:0;} -.search .suggestion{ position:absolute; top:3px; left:174px; /* border:1px solid; */} -.search .suggestion .autoComplete{ position:absolute; top:0; left:0; width:23px; height:23px; background:#fff url(../images/buttonAutoComplete.gif) no-repeat center -10px;} -.search .suggestion ul.recommendation{ display:none; position:absolute; top:26px; left:-174px; width:198px; border:1px solid #e7e7e7; border-top:none; background:#fff;} -.search .suggestion li button{ width:100%; text-align:left; padding:5px 4px 2px 4px; font:.75em AppleGothic, Gulim, Dotum, Sans-serif} -.search .suggestion li button:hover, -.search .suggestion li button:active, -.search .suggestion li button:focus{ background:#eee;} -.search .suggestion.open .autoComplete{ background-position:center 10px;} -.search .suggestion.open ul.recommendation{ display:block;} +#header .search{ position:relative; top:2px; float:right; margin:0; padding:0; white-space:nowrap; text-align:right; *zoom:1;} +#header .search .inputText{ padding:4px 5px 2px 5px; height:21px; width:160px; border:1px solid #e7e7e7; vertical-align:top; font:bold 14px AppleGothic, Dotum, Gulim, Sans-serif;} +#header .search .inputText.focus{ border:3px solid #4ba300; height:17px; width:156px;} +#header .search .button{ top:1px;} +#header .search .button button{ padding:0 10px 0 8px; font-family:Dotum;} +#header .search .suggestion{ position:absolute; top:3px; left:174px; /* border:1px solid; */} +#header .search .suggestion .autoComplete{ position:absolute; top:0; left:0; width:23px; height:23px; background:#fff url(../images/buttonAutoComplete.gif) no-repeat center -10px;} +#header .search .suggestion ul.recommendation{ display:none; position:absolute; top:26px; left:-174px; width:198px; border:1px solid #e7e7e7; border-top:none; background:#fff;} +#header .search .suggestion li button{ width:100%; text-align:left; padding:5px 4px 2px 4px; font:.75em AppleGothic, Gulim, Dotum, Sans-serif;} +#header .search .suggestion li button:hover, +#header .search .suggestion li button:active, +#header .search .suggestion li button:focus{ background:#eee;} +#header .search .suggestion.open .autoComplete{ background-position:center 10px;} +#header .search .suggestion.open ul.recommendation{ display:block;} /* GNB */ -.gnb{ position:relative; float:right; margin-left:20px; } -.gnb ul{ margin:0; padding:0; position:relative; float:left; overflow:hidden;} -.gnb ul li{ position:relative; float:left;} -.gnb ul li a{ position:relative; display:block; float:left; background:url(../images/buttonGnb.gif) no-repeat center top; padding:11px 25px 11px 25px; font:11px AppleGothic, Dotum, Gulim, Sans-serif; color:#fff !important; text-decoration:none !important; letter-spacing:-1px; white-space:nowrap;} -.gnb ul li.active a{ opacity:0.7; filter:alpha(opacity=70);} -.gnb ul li a:hover, -.gnb ul li a:active, -.gnb ul li a:focus{ opacity:0.9; filter:alpha(opacity=90);} -.gnb ul li.first a{ background-position:left top;} -.gnb ul li.last a{ background-position:right top;} +#header .gnb{ position:relative; float:right; margin-left:20px; } +#header .gnb ul{ margin:0; padding:0; position:relative; float:left; overflow:hidden;} +#header .gnb ul li{ position:relative; float:left;} +#header .gnb ul li a{ position:relative; display:block; float:left; background:url(../images/buttonGnb.gif) no-repeat center top; padding:11px 25px 11px 25px; font:12px AppleGothic, Dotum, Gulim, Sans-serif; color:#fff !important; text-decoration:none !important; letter-spacing:-1px; white-space:nowrap;} +#header .gnb ul li.active a{ opacity:0.7; filter:alpha(opacity=70); font-weight:bold;} +#header .gnb ul li a:hover, +#header .gnb ul li a:active, +#header .gnb ul li a:focus{ opacity:0.9; filter:alpha(opacity=90);} +#header .gnb ul li.first a{ background-position:left top;} +#header .gnb ul li.last a{ background-position:right top;} + +/* ---------- Content ---------- */ + +/* Tab */ +.generalTab{ float:left; margin:0; padding:0 0 0 16px; position:relative; *zoom:1;} +.generalTab *{ margin:0; padding:0; list-style:none;} +.generalTab li{ position:relative; top:2px; float:left; margin-right:6px; text-align:center; background:#848f97 url(../images/cr3pxLeft.gif) no-repeat left top;} +.generalTab li a{ position:relative; left:3px; display:block; float:left; height:22px; padding:5px 23px 0 20px; font-size:12px; color:#fff; text-decoration:none !important; background:#848f97 url(../images/cr3pxRight.gif) no-repeat right top;} +.generalTab li a sup{ font:9px Tahoma, Sans-serif;} +.generalTab li.active{ top:0; background-color:#50606B;} +.generalTab li.active a{ padding:7px 23px 0 20px; background-color:#50606B; font-weight:bold;} + +.tab{ *zoom:1;} +.tab:after{ content:""; display:block; clear:both;} + +.myTab{ float:left; margin:0; padding:0 0 0 16px; position:relative; *zoom:1;} +.myTab:after{ content:""; display:block; clear:both;} +.myTab *{ margin:0; padding:0; list-style:none;} +.myTab li{ position:relative; top:2px; float:left; margin-right:6px; text-align:center; background:url(../images/cr3pxLeft.gif) no-repeat left top;} +.myTab li a{ position:relative; left:3px; display:block; float:left; height:22px; padding:5px 23px 0 20px; font-size:12px; color:#fff; text-decoration:none !important; background:url(../images/cr3pxRight.gif) no-repeat right top;} +.myTab li a sup{ font:9px Tahoma, Sans-serif;} +.myTab li.active{ top:0;} +.myTab li.active a{ padding:7px 23px 0 20px; font-weight:bold;} + +.exTab{ float:right; margin:0; padding:0; position:relative; *zoom:1;} +.exTab:after{ content:""; display:block; clear:both;} +.exTab *{ margin:0; padding:0; list-style:none;} +.exTab li{ position:relative; top:2px; float:left; margin-right:6px; _margin-right:3px; text-align:center; background:url(../images/cr3pxLeft.gif) no-repeat left top;} +.exTab li a{ position:relative; left:3px; display:block; float:left; height:22px; padding:5px 23px 0 20px; font-size:12px; color:#fff; text-decoration:none !important; background:url(../images/cr3pxRight.gif) no-repeat right top;} +.exTab li a sup{ font:9px Tahoma, Sans-serif;} +.exTab li.active{ top:0;} +.exTab li.active a{ padding:7px 23px 0 20px; font-weight:bold;} /* Tag */ -.tagRank{ position:relative; top:2px; float:right; margin-left:15px; } +.tagRank{ float:right; position:relative; top:2px; padding:0 19px 0 0; z-index:20; margin:0; *zoom:1;} .tagRank *{ margin:0; padding:0;} -.tagRank .buttonTag{ display:inline-block; position:relative; background:url(../images/buttonTagNavigation.gif) no-repeat left top;} -.tagRank .buttonTag, x:-moz-any-link{ padding:3px 0 7px 0;} /* Firefox 2 Fix */ -.tagRank .buttonTag, x:-moz-any-link, x:default{ padding:0;} /* Firefox 2 Fix */ -.tagRank .buttonTag button{ position:relative; left:3px; width:197px; padding:6px 10px 9px 10px; background:url(../images/buttonTagNavigation.gif) no-repeat right top; text-align:left;} -.tagRank .buttonTag button span{ display:block; width:177px; height:auto; visibility:visible; position:relative; font:bold 12px NanumGothic,"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serif; color:#fff; background:url(../images/arrowTagNavigation.gif) no-repeat right -14px;} -.tagRank .layer{ display:none; position:absolute; width:200px; top:27px; left:0; background:#50606b; background:url(../images/boxTagNavigation.gif) no-repeat left bottom;} +.tagRank .buttonTag{ width:197px; display:block; position:relative; background:#848f97 url(../images/cr3pxLeft.gif) no-repeat left top;} +.tagRank .buttonTag button{ width:197px; display:block; position:relative; left:3px; height:27px; padding:5px 10px 0 10px; background:#848f97 url(../images/cr3pxRight.gif) no-repeat right top; text-align:left; vertical-align:middle;} +.tagRank .buttonTag button span{ padding:0 15px 0 0; display:block; visibility:visible; width:auto; height:auto; white-space:nowrap; position:relative; font-size:12px; line-height:12px; color:#fff; background:url(../images/arrowTagNavigation.gif) no-repeat right -14px; text-align:center;} +.tagRank .layer{ display:none; position:absolute; width:200px; top:27px; left:0px; background:#50606b; background:url(../images/boxTagNavigation.gif) no-repeat left bottom;} .tagRank .layer a{ font:11px Dotum, Sans-serif; white-space:nowrap; text-decoration:none; line-height:normal;} .tagRank .layer ul{ padding:1px 13px 30px 13px; text-align:justify;} .tagRank .layer ul *{ line-height:16px;} .tagRank .layer ul li{ display:inline; margin-bottom:4px;} .tagRank .layer ul li a{ color:#cbd0d3; zoom:1;} -.tagRank .layer .more{ display:block; padding:5px 13px 7px 10px; _margin:-1px 0; position:absolute; bottom:0; right:0; font-weight:bold; color:#fff;} +.tagRank .layer .more{ display:block; padding:5px 13px 7px 10px; _margin:-1px 0; position:absolute; bottom:0; right:0; color:#fff;} .tagRank.open .buttonTag button span{ background-position:right 6px;} .tagRank.open .layer{ display:block;} -/* ---------- Content ---------- */ - -/* Tab */ -.generalTab{ margin:0 0 0 10px; padding:0; position:relative; clear:both; *zoom:1;} -.generalTab *{ margin:0; padding:0; list-style:none;} -.generalTab li{ position:relative; top:4px; float:left; margin-right:6px; text-align:center; background:#848f97 url(../images/cr3pxLeft.gif) no-repeat left top;} -.generalTab li a{ position:relative; left:3px; display:block; padding:3px 23px 3px 20px; font:bold 1em NanumGothic,"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serif; color:#fff; text-decoration:none !important; background:#848f97 url(../images/cr3pxRight.gif) no-repeat right top;} -.generalTab li a sup{ font:11px Tahoma, Sans-serif;} -.generalTab li.active{ top:0; background-color:#50606B;} -.generalTab li.active a{ padding:5px 23px 5px 20px; background-color:#50606B;} - -.tab{ *zoom:1;} -.tab:after{ content:""; display:block; clear:both;} - -.myTab{ float:left; margin:0 0 0 10px; padding:0; position:relative; *zoom:1;} -.myTab:after{ content:""; display:block; clear:both;} -.myTab *{ margin:0; padding:0; list-style:none;} -.myTab li{ position:relative; top:4px; float:left; margin-right:6px; text-align:center; background:url(../images/cr3pxLeft.gif) no-repeat left top;} -.myTab li a{ position:relative; left:3px; display:block; padding:3px 23px 3px 20px; font:bold 1em NanumGothic,"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serif; color:#fff; text-decoration:none !important; background:url(../images/cr3pxRight.gif) no-repeat right top;} -.myTab li a sup{ font:11px Tahoma, Sans-serif;} -.myTab li.active{ top:0;} -.myTab li.active a{ padding:5px 23px 5px 20px;} - -.exTab{ float:right; margin:0; padding:0; position:relative; right:7px; *zoom:1;} -.exTab:after{ content:""; display:block; clear:both;} -.exTab *{ margin:0; padding:0; list-style:none;} -.exTab li{ position:relative; top:4px; float:left; margin-right:6px; text-align:center; background:url(../images/cr3pxLeft.gif) no-repeat left top;} -.exTab li a{ position:relative; left:3px; display:block; padding:3px 23px 3px 20px; font:bold 1em NanumGothic,"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serif; color:#fff; text-decoration:none !important; background:url(../images/cr3pxRight.gif) no-repeat right top;} -.exTab li a sup{ font:11px Tahoma, Sans-serif;} -.exTab li.active{ top:0;} -.exTab li.active a{ padding:5px 23px 5px 20px;} - /* Try Planet */ .tryPlanet{ position:relative; height:90px; background:url(../images/boxTryPlanet.gif) repeat-x 0 0; margin-bottom:15px;} .tryPlanet *{ margin:0; padding:0;} @@ -212,8 +220,6 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 /* Comment List */ .commentList{ position:relative; padding:15px; border:1px solid #dedede; *zoom:1; z-index:10; clear:both; background:#eee;} -.commentList .commentHeader{ float:left; position:relative; clear:both; margin-bottom:10px; z-index:10;} -.commentList .commentHeader *{ margin:0;} .commentList h3{ position:relative; display:inline-block; *display:inline; margin-right:20px; background:url(../images/boxH2Comment.gif) no-repeat 0 0; vertical-align:middle; *zoom:1; line-height:18px;} .commentList h3, x:-moz-any-link { display:inline; padding-top:3px;} .commentList h3, x:-moz-any-link, x:default { display:inline-block; *display:inline; padding-top:0;} @@ -222,21 +228,6 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .commentList h3 span, x:-moz-any-link{ padding:8px 20px 5px 17px; } .commentList h3 span, x:-moz-any-link, x:default{ padding:5px 20px 0 17px} -.commentList .timeNav{ left:20px; top:3px; padding:0 30px; font-family:NanumGothic,"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serif; letter-spacing:-1px; *zoom:1; text-align:center;} -.commentList .timeNav strong{ font:bold 18px/18px NanumGothic,"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serif;} -.commentList .timeNav .prev, -.commentList .timeNav .next{ position:absolute; top:2px; width:20px; height:20px; } -.commentList .timeNav .prev{ left:0; } -.commentList .timeNav .next{ right:0; } -.commentList .timeNav a:hover, -.commentList .timeNav a:activfe, -.commentList .timeNav a:focus{ border:1px solid #fff !important; width:18px; height:18px;} -.commentList .timeNav a img{ position:relative; top:3px;} - -.commentList h3.timeNav{ position:relative; float:left; vertical-align:top; clear:both; left:0; top:0; background:none; font-size:1em; *zoom:1; *line-height:24px; margin-bottom:10px; *margin-bottom:0;} -.commentList h3.timeNav .prev, -.commentList h3.timeNav .next{ *top:5px;} - .commentList .commentHeader .button *{ font-size:12px;} .commentList .planetBlue{ top:-3px;} .commentList span.planetBlue{ margin-right:10px;} @@ -251,13 +242,13 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .commentList .tagChoiceAfter button{ width:45px; height:23px; text-align:center; background:url(../images/buttonFixedBlack45.png) no-repeat; _behavior:url(./common/js/iePngFix.htc);} .commentList .tagChoiceAfter a{ width:94px; height:18px; _height /**/:23px; padding-top:5px; text-align:center; background:url(../images/buttonFixedBlack94.png) no-repeat; _behavior:url(./common/js/iePngFix.htc);} -.commentBody.myPlanet .comment .tongue{ margin:0;} +.commentBody.myPlanet .comment .tongue{ margin:0 !important;} .commentBody.myPlanet .comment .tongue .post, .commentBody.myPlanet .comment .tongue .postScript, .commentBody.myPlanet .comment .tongue .postExtra{ *zoom:1;} /* Comment Body */ -.commentBody{ position:relative; margin-top:10px; border:1px solid #d7dfe2; background:#fff; *zoom:1; clear:both;} +.commentBody{ position:relative; margin:10px 0; border:1px solid #d7dfe2; background:#fff; *zoom:1; clear:both;} .commentBody .comment{ position:relative; margin:0 15px; padding:15px 0 15px 0; clear:both; *zoom:1;} .commentBody .comment:after { content:""; display:block; width:0; height:0; font:0/0 Sans-serif; overflow:hidden; visibility:hidden; clear:both; float:none;} .commentBody .comment .identity{ position:relative; width:98px; float:left; margin:0 -98px 0 0; font-size:1em; background:none; *zoom:1;} @@ -297,8 +288,8 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .commentBody .comment .tongue .postExtra .sympathy a{ position:relative; *top:-2px; margin-right:1px; color:#999; padding-left:8px; font:.7em AppleGothic, Dotum, Gulim, Sans-serif; letter-spacing:-1px; background:url(../images/arrowReplyOpen.gif) no-repeat left 3px;} /* Todo Manager */ -.commentList .commentHeader h3.exTitle{ font-family:"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serif; line-height:normal; background:none;} -.commentList .commentHeader .todoWrite{ position:absolute; top:0; right:3px;} +.commentList .commentHeader h3.exTitle{ font-family:"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serif; line-height:normal; background:none; float:left;} +.commentList .commentHeader .todoWrite{ float:right; } .todoManager ul{ position:relative; margin:0 0 0 40px; padding:20px 0;} .todoManager li{ position:relative; font-family:"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serif;} .todoManager li .button{ margin-left:5px;} @@ -391,21 +382,23 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .authorHeader .authorTag.edit dd.tagModify{ display:inline;} .authorHeader .authorTag dd.tagModify .inputText{ border:1px solid #ccc; padding:2px 4px; height:13px; width:290px;} .authorHeader .authorTag dd.tagModify button{ height:19px; font-size:.7em; background:#eee; border:1px solid #ccc; margin-left:5px; vertical-align:top;} -.authorHeader .authorTag .planetUri{ position:relative; right:20px; float:right; color:#fff; font-size:.7em; font-family:Tahoma Sans-serif;} -.authorHeader .authorTag .rssFeed{ position:absolute; top:10px; right:0; width:12px; height:12px; cursor:pointer; background:url(../images/rssFeed.png) no-repeat; _behavior:url(./common/js/iePngFix.htc);} +.authorHeader .authorTag .planetUri{ position:relative; float:right; color:#fff; font-size:.7em; font-family:Tahoma Sans-serif;} +.authorHeader .authorTag .rssFeed{ float:right; display:block; margin:0 0 0 5px; width:12px; height:12px; cursor:pointer; background:url(../images/rssFeed.png) no-repeat; _behavior:url(./common/js/iePngFix.htc);} .authorHeader .authorTag .rssFeed span{ position:absolute; width:0; height:0; overflow:hidden;} .authorHeader .tl, .authorHeader .tr{position:absolute; top:0; display:block; width:4px; height:4px; overflow:hidden; background:url(../images/boxAuthorHeader.png) no-repeat; _background:url(../images/boxAuthorHeader.gif) no-repeat;} .authorHeader .tl{ left:0; background-position:left top;} .authorHeader .tr{ right:0; background-position:right top;} -.authorHeader .rss{ position:absolute; display:block; top:0; right:58px; width:15px; height:15px; background-repeat:no-repeat; background-position:center; cursor:pointer;} +.authorHeader .myOption{ position:absolute; top:20px; right:15px;} + +.authorHeader .rss{ float:left; margin:0 5px 0 0; position:relative; display:block; width:15px; height:15px; background-repeat:no-repeat; background-position:center; cursor:pointer;} .authorHeader .rss.off{ background-image:url(../images/rssOff.png); _behavior:url(./common/js/iePngFix.htc);} .authorHeader .rss.on{ background-image:url(../images/rssOn.png); _behavior:url(./common/js/iePngFix.htc);} .authorHeader .rss span{ display:block; width:0; height:0; overflow:hidden;} -.authorHeader .color{ position:absolute; display:block; top:0; right:18px; border:1px solid #ccc; width:16px; height:16px;} -.authorHeader .colorPreset{ position:absolute; display:none; top:25px; right:0;} +.authorHeader .color{ float:left; margin:0 5px 0 0; display:block; border:1px solid #ccc; width:16px; height:16px;} +.authorHeader .colorPreset{ position:absolute; display:none; top:25px; right:0; width:131px;} .authorHeader .colorPreset.open{ display:block;} .authorHeader .colorPreset.close{ display:none;} .authorHeader .colorPreset li{ float:left; border:1px solid #ccc; margin-left:5px;} @@ -418,9 +411,10 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .authorHeader .colorPreset li a{ display:block; width:14px; height:14px;} .authorHeader .colorPreset li a span{ display:block; width:0; height:0; overflow:hidden; visibility:hidden;} -.authorHeader .apiMe2day{ position:absolute; top:20px; right:58px;} +.authorHeader .apiMe2day{ position:relative; float:left; width:16px; margin:0 5px 0 0;} +.authorHeader .apiMe2day fieldset{ width:16px;} .authorHeader .apiMe2day *{ font-size:12px;} -.authorHeader .apiMe2day .setupButton{ position:absolute; top:0; right:0; width:16px; height:16px; background:url(../images/iconMe2day.png) no-repeat center; _behavior:url(./common/js/iePngFix.htc);} +.authorHeader .apiMe2day .setupButton{ width:16px; height:16px; background:url(../images/iconMe2day.png) no-repeat center; _behavior:url(./common/js/iePngFix.htc);} .authorHeader .apiMe2day .apiProtocol{ position:relative; width:240px; border:2px solid #eee; background:#fff; padding:10px 15px 10px 20px; position:absolute; top:25px; right:0;} .authorHeader .apiMe2day .apiProtocol.open{ display:block;} .authorHeader .apiMe2day .apiProtocol.close{ display:none;} @@ -434,8 +428,9 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .authorHeader .apiMe2day .apiProtocol .autoPush label{} .authorHeader .apiMe2day .apiProtocol .buttonArea button{ border:1px solid #ccc; background:#eee; font-size:11px; *margin-right:4px;} -.authorHeader .mobile{ position:absolute; top:20px; right:20px;} -.authorHeader .mobile .setupButton{ position:absolute; top:-3px; right:0; width:12px; height:19px; background:url(../images/iconMobile.gif) no-repeat;} +.authorHeader .mobile{ float:left; width:12px; margin:0 5px 0 0; position:relative;} +.authorHeader .mobile fieldset{ width:12px;} +.authorHeader .mobile .setupButton{ position:relative; top:-3px; width:12px; height:19px; background:url(../images/iconMobile.gif) no-repeat;} .authorHeader .mobile .address{ display:none; position:absolute; border:2px solid #eee; background:#fff; padding:10px 15px 10px 20px; position:absolute; top:25px; right:0; white-space:nowrap;} .authorHeader .mobile .address.open{ display:block;} .authorHeader .mobile .address *{ font-size:12px;} @@ -464,7 +459,7 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .authorFooter .memo li span{ position:relative; right:40px; float:right; color:#fff; font:11px AppleGothic, Dotum, Gulim, Sans-serif;} .authorFooter .memo p span .delete, .authorFooter .memo li span .delete{ position:relative; left:5px; vertical-align:top; width:12px; height:11px; background:url(../images/buttonX.gif) no-repeat center;} -.authorFooter .memo .pagination{ position:relative; top:-10px; display:none; padding:0 0 10px 0; clear:both; background:url(../images/boxMemo.gif) no-repeat left bottom;} +.authorFooter .memo .pagination{ position:relative; top:-14px; display:none; padding:0 0 10px 0; clear:both; background:url(../images/boxMemo.gif) no-repeat left bottom;} .authorFooter .memo .pagination a, .authorFooter .memo .pagination strong{ background:#333; color:#ccc; border-color:#666;} .authorFooter .memo .bottomCap{ clear:both; position:relative; top:2px; height:2px; overflow:hidden; background:url(../images/boxMemo.gif) no-repeat left -27px;} @@ -473,7 +468,7 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .authorFooter .memo.open p .toggle, .authorFooter .memo.open li.first .toggle{ display:block; background-position: center 10px;} .authorFooter .memo.open ul, -.authorFooter .memo.open .pagination{ display:block; padding-top:2px; } +.authorFooter .memo.open .pagination{ display:block; padding-top:3px; } .authorFooter .memoWrite{ position:relative; float:left; width:600px;} .authorFooter .memoWrite.close{ display:none;} .authorFooter .memoWrite .form.close{ display:none; } @@ -488,6 +483,7 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .authorFooter .nav .button, x:-moz-any-link, x:default { vertical-align:top; } .authorFooter .nav a.button, x:-moz-any-link{ padding:4px 0 6px 0;} .authorFooter .nav a.button, x:-moz-any-link, x:default { padding:0;} +.authorFooter .nav span.button{ top:0;} .authorFooter .tl, .authorFooter .tr{position:absolute; bottom:-1px; display:block; width:4px; height:4px; overflow:hidden; background:url(../images/boxCommentOut.gif) no-repeat;} .authorFooter .tl{ left:-1px; background-position:left bottom;} @@ -545,10 +541,10 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .writePost .writeBody dd .inputText{ padding:3px 10px;} .writePost .writeBody dd.preview{ display:block; clear:both;} .writePost .writeBody dd.preview.off{ display:none;} -.writePost .writeBody dd.preview p{ padding:5px 10px; border:1px solid #f4f4f4; width:96%; background:#f4f4f4; color:#000;} +.writePost .writeBody dd.preview p{ padding:5px 10px; border:1px solid #ccc; width:96%; background:#fff; color:#000; opacity:.5; filter:alpha(opacity=50);} .writePost .writeBody dd.preview p a{ color:#00f;} -.writePost .writeBody .writeOptions{ position:absolute; top:0; left:100%; margin-left:-105px;} -.writePost .writeBody .writeOptions p{ white-space:nowrap;} +.writePost .writeBody .writeOptions{ position:absolute; top:-8px; left:100%; margin-left:-105px;} +.writePost .writeBody .writeOptions p{ white-space:nowrap; margin:0; padding:0;} .writePost .writeBody .writeOptions input{ vertical-align:middle; width:13px; height:13px; margin-right:4px;} .writePost .writeBody button{ width:90px; height:68px; position:absolute; bottom:5px; right:15px; background-repeat:no-repeat;} .writePost .writeBody button span{ position:relative; display:block; *padding-top:20px; visibility:visible; width:auto; height:auto; text-align:center; font:bold 1.2em NanumGothic,"Malgun Gothic", AppleGothic, Dotum, Gulim, Sans-serifl;} @@ -604,15 +600,16 @@ h1 a{ font:bold 30px "Malgun Gothic", AppleGothic, Dotum, Sans-serif; color:#000 .welcome .br{ bottom:0; right:0; background-position:right -4px;} /* Pagination */ -.pagination{ padding:15px 0; text-align:center;} +.pagination{ padding:5px 0; text-align:center;} .pagination *{ margin:0; padding:0;} .pagination a, -.pagination strong{ _position:relative; font-weight:bold; display:inline-block; padding:5px 5px 3px 5px; color:#313031; text-decoration:none; line-height:normal; font:bold 12px Tahoma, Sans-serif;} +.pagination strong{ _position:relative; font-weight:bold; display:inline-block; padding:5px 5px 3px 5px; color:#313031; text-decoration:none; line-height:normal; font:bold 12px Tahoma, Sans-serif !important;} .pagination strong{ color:#ff8600 !important;} .pagination a:hover{ background:#eee; } -.pagination a.prev{ padding:5px 10px 3px 18px; background:url(../images/paginationPrev.gif) no-repeat 8px 8px !important; border:none; font-weight:normal; } -.pagination a.prevEnd{ padding:5px 10px 3px 18px; background:url(../images/paginationPrevEnd.gif) no-repeat 4px 8px !important; border:none; border-right:1px solid #ccc; font-weight:normal;} -.pagination a.next{ padding:5px 18px 3px 10px; background:url(../images/paginationNext.gif) no-repeat 40px 8px !important; border:none; font-weight:normal; } -.pagination a.nextEnd{ padding:5px 18px 3px 10px; background:url(../images/paginationNextEnd.gif) no-repeat 40px 8px !important; border:none; border-left:1px solid #ccc; font-weight:normal;} +.pagination a.prev{ padding:5px 10px 3px 10px; background:url(../images/paginationPrev.gif) no-repeat left center !important; border:none; font-weight:normal; } +.pagination a.prevEnd{ padding:5px 10px 3px 10px; background:url(../images/paginationPrevEnd.gif) no-repeat left center !important; border:none; border-right:1px solid #ccc;} +.pagination a.next{ padding:5px 10px 3px 10px; background:url(../images/paginationNext.gif) no-repeat right center !important; border:none; font-weight:normal; } +.pagination a.nextEnd{ padding:5px 10px 3px 10px; background:url(../images/paginationNextEnd.gif) no-repeat right center !important; border:none; border-left:1px solid #ccc;} + diff --git a/modules/planet/skins/xe_planet/css/gray.css b/modules/planet/skins/xe_planet/css/gray.css index 8844338f2..a5cee5b0e 100644 --- a/modules/planet/skins/xe_planet/css/gray.css +++ b/modules/planet/skins/xe_planet/css/gray.css @@ -10,7 +10,9 @@ .myTab li, .exTab li, .myTab li a, -.exTab li a{ background-color:#b7b7b7;} +.exTab li a, +.tab .tagRank .buttonTag, +.tab .tagRank .buttonTag button{ background-color:#b7b7b7;} .myTab li.active, .exTab li.active, .myTab li.active a, diff --git a/modules/planet/skins/xe_planet/css/green.css b/modules/planet/skins/xe_planet/css/green.css index 3ce968d19..93d38abbd 100644 --- a/modules/planet/skins/xe_planet/css/green.css +++ b/modules/planet/skins/xe_planet/css/green.css @@ -10,7 +10,9 @@ .myTab li, .exTab li, .myTab li a, -.exTab li a{ background-color:#9ecb9b;} +.exTab li a, +.tab .tagRank .buttonTag, +.tab .tagRank .buttonTag button{ background-color:#9ecb9b;} .myTab li.active, .exTab li.active, .myTab li.active a, diff --git a/modules/planet/skins/xe_planet/css/orange.css b/modules/planet/skins/xe_planet/css/orange.css index b2fd40d2e..37d3b7d30 100644 --- a/modules/planet/skins/xe_planet/css/orange.css +++ b/modules/planet/skins/xe_planet/css/orange.css @@ -10,7 +10,9 @@ .myTab li, .exTab li, .myTab li a, -.exTab li a{ background-color:#efbd8e;} +.exTab li a, +.tab .tagRank .buttonTag, +.tab .tagRank .buttonTag button{ background-color:#efbd8e;} .myTab li.active, .exTab li.active, .myTab li.active a, diff --git a/modules/planet/skins/xe_planet/css/red.css b/modules/planet/skins/xe_planet/css/red.css index 7d5d17ff1..425e0fd9f 100644 --- a/modules/planet/skins/xe_planet/css/red.css +++ b/modules/planet/skins/xe_planet/css/red.css @@ -10,7 +10,9 @@ .myTab li, .exTab li, .myTab li a, -.exTab li a{ background-color:#dfaa9e;} +.exTab li a, +.tab .tagRank .buttonTag, +.tab .tagRank .buttonTag button{ background-color:#dfaa9e;} .myTab li.active, .exTab li.active, .myTab li.active a, diff --git a/modules/planet/skins/xe_planet/footer.html b/modules/planet/skins/xe_planet/footer.html index cbadd3b96..64d09e3c9 100644 --- a/modules/planet/skins/xe_planet/footer.html +++ b/modules/planet/skins/xe_planet/footer.html @@ -1,59 +1,8 @@
-
- -
-
- -
diff --git a/modules/planet/skins/xe_planet/header.html b/modules/planet/skins/xe_planet/header.html index 5bb558df3..d9ded4d04 100644 --- a/modules/planet/skins/xe_planet/header.html +++ b/modules/planet/skins/xe_planet/header.html @@ -1,7 +1,5 @@ - - @@ -34,6 +32,65 @@

 

+ + + + +
+ + {zdate($date,'Y')}{$lang->unit_year} + {zdate($date,'m')}{$lang->unit_month} + {zdate($date,'d')}{$lang->unit_day} + ({zdate($date,'l')}) + +
+
@@ -67,4 +124,4 @@

{$config->notice}

- \ No newline at end of file + diff --git a/modules/planet/skins/xe_planet/images/boxMemo.gif b/modules/planet/skins/xe_planet/images/boxMemo.gif index 668a85425..6133c62f0 100644 Binary files a/modules/planet/skins/xe_planet/images/boxMemo.gif and b/modules/planet/skins/xe_planet/images/boxMemo.gif differ diff --git a/modules/planet/skins/xe_planet/images/boxTagNavigation.gif b/modules/planet/skins/xe_planet/images/boxTagNavigation.gif index c52645744..9c6802eea 100644 Binary files a/modules/planet/skins/xe_planet/images/boxTagNavigation.gif and b/modules/planet/skins/xe_planet/images/boxTagNavigation.gif differ diff --git a/modules/planet/skins/xe_planet/images/buttonTagNavigation.gif b/modules/planet/skins/xe_planet/images/buttonTagNavigation.gif index beb1eaf73..b9af79469 100644 Binary files a/modules/planet/skins/xe_planet/images/buttonTagNavigation.gif and b/modules/planet/skins/xe_planet/images/buttonTagNavigation.gif differ diff --git a/modules/planet/skins/xe_planet/js/planet.js b/modules/planet/skins/xe_planet/js/planet.js index 23667bc9f..a0b3f05d1 100644 --- a/modules/planet/skins/xe_planet/js/planet.js +++ b/modules/planet/skins/xe_planet/js/planet.js @@ -176,6 +176,8 @@ function planetPreview(obj) { var text = obj.value; text = text.replace(//ig,'>'); + text = text.replace(/\.\.\./g, '…'); + text = text.replace(/--/g, '—'); $('preview_text').innerHTML = text.replace(/"([^"]*)":(mms|http|ftp|https)([^ ]+)/ig,'$1'); }else{ $Element('preview').addClass('off'); diff --git a/modules/planet/skins/xe_planet/main.html b/modules/planet/skins/xe_planet/main.html index 63490a3ee..01871dacc 100644 --- a/modules/planet/skins/xe_planet/main.html +++ b/modules/planet/skins/xe_planet/main.html @@ -11,19 +11,30 @@
  • class="active">{$lang->planet_total_articles}{number_format($total_content)}
  • class="active">{$lang->planet_wantyou}{number_format($total_wantyou)}
  • class="active">{$lang->planet_best}{number_format($total_best)}
  • + + + +
  • class="active">{$key}{number_format($val)}
  • + + + +
    + + + +
    + +
    -
    -

    - {zdate($date,'Y')}{$lang->unit_year} - {zdate($date,'m')}{$lang->unit_month} - {zdate($date,'d')}{$lang->unit_day} - ({zdate($date,'l')}) - - -

    -
    diff --git a/modules/planet/skins/xe_planet/myPlanet.html b/modules/planet/skins/xe_planet/myPlanet.html index 27c0927c2..1e739bc50 100644 --- a/modules/planet/skins/xe_planet/myPlanet.html +++ b/modules/planet/skins/xe_planet/myPlanet.html @@ -13,24 +13,25 @@ class="active">{$lang->planet_fish} 
  • class="active">{$lang->planet_bigfish} 
  • --> - + + +
    + + + +
    + +
    -
    -

    - {zdate($date,'Y')}{$lang->unit_year} - {zdate($date,'m')}{$lang->unit_month} - {zdate($date,'d')}{$lang->unit_day} - ({zdate($date,'l')}) - - -

    -
    diff --git a/modules/planet/skins/xe_planet/planet_info.include.html b/modules/planet/skins/xe_planet/planet_info.include.html index 0a7e2f802..317919d14 100644 --- a/modules/planet/skins/xe_planet/planet_info.include.html +++ b/modules/planet/skins/xe_planet/planet_info.include.html @@ -1,7 +1,7 @@ -
    +
    @@ -58,75 +58,78 @@ - {$planet->getPermanentUrl()} RSS Feed -
    - - - - - - - - - - - - - - - -
    -
    - {$lang->msg_me2day_sync} - -
    -

    {$lang->msg_me2day_sync_q}

    -
    -
    :
    -
    -
    :
    -
    -
    - -

    getMe2dayAuthPush())-->checked="checked" />

    - -
    - - -
    + {$planet->getPermanentUrl()}
    + + +
    + + + + + + + + + + + +
    +
    + {$lang->msg_me2day_sync} + +
    +

    {$lang->msg_me2day_sync_q}

    +
    +
    :
    +
    +
    :
    +
    +
    + +

    getMe2dayAuthPush())-->checked="checked" />

    + +
    + + +
    +
    +
    +
    + + + + {@$phone_number=$myplanet->getPhoneNumber()} +
    +
    + {$lang->planet_mobile_receive} + +
    +
    {$lang->planet_mobile_number} :
    +
    - -
    +
    +
    +
    +
    + +
    - - {@$phone_number=$myplanet->getPhoneNumber()} -
    -
    - {$lang->planet_mobile_receive} - -
    -
    {$lang->planet_mobile_number} :
    -
    - -
    -
    -
    -
    -
    - - -
    +
    @@ -196,14 +199,14 @@
    +
    {$lang->planet_postscript}
    +
    +
    {$lang->tag}
    +

     

    -
    {$lang->planet_postscript}
    -
    -
    {$lang->tag}
    -

    @@ -218,3 +221,4 @@
    + diff --git a/modules/planet/tpl/filter/insert_config.xml b/modules/planet/tpl/filter/insert_config.xml index bc28ee9d8..e477e1a97 100644 --- a/modules/planet/tpl/filter/insert_config.xml +++ b/modules/planet/tpl/filter/insert_config.xml @@ -16,6 +16,7 @@ + diff --git a/modules/planet/tpl/setup.html b/modules/planet/tpl/setup.html index 913e8b3e8..c561925b2 100644 --- a/modules/planet/tpl/setup.html +++ b/modules/planet/tpl/setup.html @@ -45,14 +45,21 @@
    {$lang->planet_tagtab}
    - +

    {$lang->about_planet_tagtab}

    + +
    {$lang->planet_tagtab_after}
    + + +

    {$lang->about_planet_tagtab_after}

    + +
    {$lang->planet_smstag}
    - +

    {$lang->about_planet_smstag}

    diff --git a/modules/rss/tpl/rss20.html b/modules/rss/tpl/rss20.html index 081be5589..c8c545aa1 100644 --- a/modules/rss/tpl/rss20.html +++ b/modules/rss/tpl/rss20.html @@ -13,7 +13,7 @@ <![CDATA[{$oDocument->getTitleText()}]]> getNickName()}]]> getPermanentUrl()}]]> - + getContent(false,false,true)}]]> getContentText(100)}]]>