      1 /**
      2  * @fileOverview xq.Editor manages configurations such as autocompletion and autocorrection, edit mode/normal mode switching, handles editing commands, keyboard shortcuts and other events.
      3  */
      4 xq.Editor = Class.create({
      5 	/**
      6 	 * Initialize editor but it doesn't automatically start designMode. setEditMode should be called after initialization.
      7 	 *
      8      * @constructor
      9 	 * @param {Element} contentElement HTML element(TEXTAREA or normal block element such as DIV) to be replaced with editable area
     10 	 * @param {Element} toolbarContainer HTML element which contains toolbar icons
     11 	 */
     12 	initialize: function(contentElement, toolbarContainer) {
     13 		if(!contentElement) throw "[contentElement] is null";
     14 		if(contentElement.nodeType != 1) throw "[contentElement] is not an element";
     15
     16 		xq.asEventSource(this, "Editor", ["ElementChanged", "BeforeEvent", "AfterEvent", "CurrentContentChanged", "StaticContentChanged", "CurrentEditModeChanged"]);
     17
     18 		/**
     19 		 * Editor's configuration
     20 		 * @type object
     21 		 */
     22 		this.config = {};
     23 		this.config.enableLinkClick = false;
     24 		this.config.changeCursorOnLink = false;
     25 		this.config.generateDefaultToolbar = true;
     26 		this.config.defaultToolbarButtonMap = [
     27 			[
     28 				{className:"foregroundColor", title:"Foreground color", handler:"xed.handleForegroundColor()"},
     29 				{className:"backgroundColor", title:"Background color", handler:"xed.handleBackgroundColor()"}
     30 			],
     31 			[
     32 				{className:"link", title:"Link", handler:"xed.handleLink()"},
     33 				{className:"strongEmphasis", title:"Strong emphasis", handler:"xed.handleStrongEmphasis()"},
     34 				{className:"emphasis", title:"Emphasis", handler:"xed.handleEmphasis()"},
     35 				{className:"underline", title:"Underline", handler:"xed.handleUnderline()"},
     36 				{className:"strike", title:"Strike", handler:"xed.handleStrike()"},
     37 				{className:"superscription", title:"Superscription", handler:"xed.handleSuperscription()"},
     38 				{className:"subscription", title:"Subscription", handler:"xed.handleSubscription()"}
     39 			],
     40 			[
     41 				{className:"removeFormat", title:"Remove format", handler:"xed.handleRemoveFormat()"}
     42 			],
     43 			[
     44 				{className:"justifyLeft", title:"Justify left", handler:"xed.handleJustify('left')"},
     45 				{className:"justifyCenter", title:"Justify center", handler:"xed.handleJustify('center')"},
     46 				{className:"justifyRight", title:"Justify right", handler:"xed.handleJustify('right')"},
     47 				{className:"justifyBoth", title:"Justify both", handler:"xed.handleJustify('both')"}
     48 			],
     49 			[
     50 				{className:"indent", title:"Indent", handler:"xed.handleIndent()"},
     51 				{className:"outdent", title:"Outdent", handler:"xed.handleOutdent()"}
     52 			],
     53 			[
     54 				{className:"unorderedList", title:"Unordered list", handler:"xed.handleList('UL')"},
     55 				{className:"orderedList", title:"Ordered list", handler:"xed.handleList('OL')"}
     56 			],
     57 			[
     58 				{className:"paragraph", title:"Paragraph", handler:"xed.handleApplyBlock('P')"},
     59 				{className:"heading1", title:"Heading 1", handler:"xed.handleApplyBlock('H1')"},
     60 				{className:"blockquote", title:"Blockquote", handler:"xed.handleApplyBlock('BLOCKQUOTE')"},
     61 				{className:"code", title:"Code", handler:"xed.handleList('CODE')"},
     62 				{className:"division", title:"Division", handler:"xed.handleApplyBlock('DIV')"}
     63 			],
     64 			[
     65 				{className:"table", title:"Table", handler:"xed.handleTable(3,3,'tl')"},
     66 				{className:"separator", title:"Separator", handler:"xed.handleSeparator()"}
     67 			],
     68 			[
     69 				{className:"html", title:"Edit source", handler:"xed.toggleSourceAndWysiwygMode()"}
     70 			],
     71 			[
     72 				{className:"undo", title:"Undo", handler:"xed.handleUndo()"},
     73 				{className:"redo", title:"Redo", handler:"xed.handleRedo()"}
     74 			]
     75 		];
     76
     77 		this.config.imagePathForDefaultToobar = 'img/toolbar/';
     78
     79 		// relative | host_relative | absolute | browser_default
     80 		this.config.urlValidationMode = 'absolute';
     81
     82 		this.config.automaticallyHookSubmitEvent = true;
     83
     84 		this.config.allowedTags = ['a', 'abbr', 'acronym', 'address', 'blockquote', 'br', 'caption', 'cite', 'code', 'dd', 'dfn', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'img', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'span', 'sup', 'sub', 'strong', 'table', 'thead', 'tbody', 'td', 'th', 'tr', 'ul', 'var'];
     85 		this.config.allowedAttributes = ['alt', 'cite', 'class', 'datetime', 'height', 'href', 'id', 'rel', 'rev', 'src', 'style', 'title', 'width'];
     86
     87 		this.config.shortcuts = {};
     88 		this.config.autocorrections = {};
     89 		this.config.autocompletions = {};
     90 		this.config.templateProcessors = {};
     91 		this.config.contextMenuHandlers = {};
     92
     93 		/**
     94 		 * Original content element
     95 		 * @type Element
     96 		 */
     97 		this.contentElement = contentElement;
     98
     99 		/**
    100 		 * Owner document of content element
    101 		 * @type Document
    102 		 */
    103 		this.doc = this.contentElement.ownerDocument;
    104
    105 		/**
    106 		 * Body of content element
    107 		 * @type Element
    108 		 */
    109 		this.body = this.doc.body;
    110
    111 		/**
    112 		 * False or 'readonly' means read-only mode, true or 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode.
    113 		 * @type Object
    114 		 */
    115 		this.currentEditMode = 'readonly';
    116
    117 		/**
    118 		 * RichDom instance
    119 		 * @type xq.RichDom
    120 		 */
    121 		this.rdom = xq.RichDom.createInstance();
    122
    123 		/**
    124 		 * Validator instance
    125 		 * @type xq.Validator
    126 		 */
    127 		this.validator = null;
    128
    129 		/**
    130 		 * Outmost wrapper div
    131 		 * @type Element
    132 		 */
    133 		this.outmostWrapper = null;
    134
    135 		/**
    136 		 * Source editor container
    137 		 * @type Element
    138 		 */
    139 		this.sourceEditorDiv = null;
    140
    141 		/**
    142 		 * Source editor textarea
    143 		 * @type Element
    144 		 */
    145 		this.sourceEditorTextarea = null;
    146
    147 		/**
    148 		 * WYSIWYG editor container
    149 		 * @type Element
    150 		 */
    151 		this.wysiwygEditorDiv = null;
    152
    153 		/**
    154 		 * Design mode iframe
    155 		 * @type IFrame
    156 		 */
    157 		this.editorFrame = null;
    158
    159 		/**
    160 		 * Window that contains design mode iframe
    161 		 * @type Window
    162 		 */
    163 		this.editorWin = null;
    164
    165 		/**
    166 		 * Document that contained by design mode iframe
    167 		 * @type Document
    168 		 */
    169 		this.editorDoc = null;
    170
    171 		/**
    172 		 * Body that contained by design mode iframe
    173 		 * @type Element
    174 		 */
    175 		this.editorBody = null;
    176
    177 		/**
    178 		 * Toolbar container
    179 		 * @type Element
    180 		 */
    181 		this.toolbarContainer = toolbarContainer;
    182
    183 		/**
    184 		 * Toolbar buttons
    185 		 * @type Array
    186 		 */
    187 		this.toolbarButtons = null;
    188
    189 		/**
    190 		 * Undo/redo manager
    191 		 * @type xq.EditHistory
    192 		 */
    193 		this.editHistory = null;
    194
    195 		this._contextMenuContainer = null;
    196 		this._contextMenuItems = null;
    197
    198 		this._validContentCache = null;
    199 		this._lastModified = null;
    200
    201 		this.addShortcuts(this._getDefaultShortcuts());
    202 		this.addTemplateProcessors(this._getDefaultTemplateProcessors());
    203
    204 		this.addListener({
    205 			onEditorCurrentContentChanged: function(xed) {
    206 				var curFocusElement = xed.rdom.getCurrentElement();
    207 				if(!curFocusElement) return;
    208
    209 				if(xed._lastFocusElement != curFocusElement) {
    210 					if(!xed.rdom.tree.isBlockOnlyContainer(xed._lastFocusElement) && xed.rdom.tree.isBlock(xed._lastFocusElement)) {
    211 						xed.rdom.removeTrailingWhitespace(xed._lastFocusElement);
    212 					}
    213 					xed._fireOnElementChanged(xed._lastFocusElement, curFocusElement);
    214 					xed._lastFocusElement = curFocusElement;
    215 				}
    216
    217 				xed.updateAllToolbarButtonsStatus(curFocusElement);
    218 			}
    219 		});
    220 	},
    221
    222
    223
    224 	/////////////////////////////////////////////
    225 	// Configuration Management
    226
    227 	_getDefaultShortcuts: function() {
    228 		if(xq.Browser.isMac) {
    229 			// Mac FF & Safari
    230 			return [
    231 				{event:"Ctrl+Shift+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
    232 				{event:"ENTER", handler:"this.handleEnter(false, false)"},
    233 				{event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
    234 				{event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
    235 				{event:"TAB", handler:"this.handleTab()"},
    236 				{event:"Shift+TAB", handler:"this.handleShiftTab()"},
    237 				{event:"DELETE", handler:"this.handleDelete()"},
    238 				{event:"BACKSPACE", handler:"this.handleBackspace()"},
    239
    240 				{event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
    241 				{event:"Ctrl+I", handler:"this.handleEmphasis()"},
    242 				{event:"Ctrl+U", handler:"this.handleUnderline()"},
    243 				{event:"Ctrl+K", handler:"this.handleStrike()"},
    244 				{event:"Meta+Z", handler:"this.handleUndo()"},
    245 				{event:"Meta+Shift+Z", handler:"this.handleRedo()"},
    246 				{event:"Meta+Y", handler:"this.handleRedo()"}
    247 			];
    248 		} else if(xq.Browser.isUbuntu) {
    249 			//  Ubunto FF
    250 			return [
    251 				{event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
    252 				{event:"ENTER", handler:"this.handleEnter(false, false)"},
    253 				{event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
    254 				{event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
    255 				{event:"TAB", handler:"this.handleTab()"},
    256 				{event:"Shift+TAB", handler:"this.handleShiftTab()"},
    257 				{event:"DELETE", handler:"this.handleDelete()"},
    258 				{event:"BACKSPACE", handler:"this.handleBackspace()"},
    259
    260 				{event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
    261 				{event:"Ctrl+I", handler:"this.handleEmphasis()"},
    262 				{event:"Ctrl+U", handler:"this.handleUnderline()"},
    263 				{event:"Ctrl+K", handler:"this.handleStrike()"},
    264 				{event:"Ctrl+Z", handler:"this.handleUndo()"},
    265 				{event:"Ctrl+Y", handler:"this.handleRedo()"}
    266 			];
    267 		} else {
    268 			// Win IE & FF
    269 			return [
    270 				{event:"Ctrl+SPACE", handler:"this.handleAutocompletion(); stop = true;"},
    271 				{event:"ENTER", handler:"this.handleEnter(false, false)"},
    272 				{event:"Ctrl+ENTER", handler:"this.handleEnter(true, false)"},
    273 				{event:"Ctrl+Shift+ENTER", handler:"this.handleEnter(true, true)"},
    274 				{event:"TAB", handler:"this.handleTab()"},
    275 				{event:"Shift+TAB", handler:"this.handleShiftTab()"},
    276 				{event:"DELETE", handler:"this.handleDelete()"},
    277 				{event:"BACKSPACE", handler:"this.handleBackspace()"},
    278
    279 				{event:"Ctrl+B", handler:"this.handleStrongEmphasis()"},
    280 				{event:"Ctrl+I", handler:"this.handleEmphasis()"},
    281 				{event:"Ctrl+U", handler:"this.handleUnderline()"},
    282 				{event:"Ctrl+K", handler:"this.handleStrike()"},
    283 				{event:"Ctrl+Z", handler:"this.handleUndo()"},
    284 				{event:"Ctrl+Y", handler:"this.handleRedo()"}
    285 			];
    286 		}
    287 	},
    288
    289 	_getDefaultTemplateProcessors: function() {
    290 		return [
    291 			{
    292 				id:"predefinedKeywordProcessor",
    293 				handler:function(html) {
    294 					var today = Date.get();
    295 					var keywords = {
    296 						year: today.getFullYear(),
    297 						month: today.getMonth() + 1,
    298 						date: today.getDate(),
    299 						hour: today.getHours(),
    300 						min: today.getMinutes(),
    301 						sec: today.getSeconds()
    302 					};
    303
    304 					return html.replace(/\{xq:(year|month|date|hour|min|sec)\}/img, function(text, keyword) {
    305 						return keywords[keyword] || keyword;
    306 					});
    307 				}
    308 			}
    309 		];
    310 	},
    311
    312 	/**
    313 	 * Adds or replaces keyboard shortcut.
    314 	 *
    315 	 * @param {String} shortcut keymap expression like "CTRL+Space"
    316 	 * @param {Object} handler string or function to be evaluated or called
    317 	 */
    318 	addShortcut: function(shortcut, handler) {
    319 		this.config.shortcuts[shortcut] = {"event":new xq.Shortcut(shortcut), "handler":handler};
    320 	},
    321
    322 	/**
    323 	 * Adds several keyboard shortcuts at once.
    324 	 *
    325 	 * @param {Array} list of shortcuts. each element should have following structure: {event:"keymap expression", handler:handler}
    326 	 */
    327 	addShortcuts: function(list) {
    328 		list.each(function(shortcut) {
    329 			this.addShortcut(shortcut.event, shortcut.handler);
    330 		}.bind(this));
    331 	},
    332
    333 	/**
    334 	 * Returns keyboard shortcut matches with given keymap expression.
    335 	 *
    336 	 * @param {String} shortcut keymap expression like "CTRL+Space"
    337 	 */
    338 	getShortcut: function(shortcut) {return this.config.shortcuts[shortcut];},
    339
    340 	/**
    341 	 * Returns entire keyboard shortcuts' map
    342 	 */
    343 	getShortcuts: function() {return this.config.shortcuts;},
    344
    345 	/**
    346 	 * Remove keyboard shortcut matches with given keymap expression.
    347 	 *
    348 	 * @param {String} shortcut keymap expression like "CTRL+Space"
    349 	 */
    350 	removeShortcut: function(shortcut) {delete this.config.shortcuts[shortcut];},
    351
    352 	/**
    353 	 * Adds or replaces autocorrection handler.
    354 	 *
    355 	 * @param {String} id unique identifier
    356 	 * @param {Object} criteria regex pattern or function to be used as a criterion for match
    357 	 * @param {Object} handler string or function to be evaluated or called when criteria met
    358 	 */
    359 	addAutocorrection: function(id, criteria, handler) {
    360 		if(criteria.exec) {
    361 			var pattern = criteria;
    362 			criteria = function(text) {return text.match(pattern)};
    363 		}
    364 		this.config.autocorrections[id] = {"criteria":criteria, "handler":handler};
    365 	},
    366
    367 	/**
    368 	 * Adds several autocorrection handlers at once.
    369 	 *
    370 	 * @param {Array} list of autocorrection. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler}
    371 	 */
    372 	addAutocorrections: function(list) {
    373 		list.each(function(ac) {
    374 			this.addAutocorrection(ac.id, ac.criteria, ac.handler);
    375 		}.bind(this));
    376 	},
    377
    378 	/**
    379 	 * Returns autocorrection handler matches with given id
    380 	 *
    381 	 * @param {String} id unique identifier
    382 	 */
    383 	getAutocorrection: function(id) {return this.config.autocorrection[id];},
    384
    385 	/**
    386 	 * Returns entire autocorrections' map
    387 	 */
    388 	getAutocorrections: function() {return this.config.autocorrections;},
    389
    390 	/**
    391 	 * Removes autocorrection handler matches with given id
    392 	 *
    393 	 * @param {String} id unique identifier
    394 	 */
    395 	removeAutocorrection: function(id) {delete this.config.autocorrections[id];},
    396
    397 	/**
    398 	 * Adds or replaces autocompletion handler.
    399 	 *
    400 	 * @param {String} id unique identifier
    401 	 * @param {Object} criteria regex pattern or function to be used as a criterion for match
    402 	 * @param {Object} handler string or function to be evaluated or called when criteria met
    403 	 */
    404 	addAutocompletion: function(id, criteria, handler) {
    405 		if(criteria.exec) {
    406 			var pattern = criteria;
    407 			criteria = function(text) {
    408 				var m = pattern.exec(text);
    409 				return m ? m.index : -1;
    410 			};
    411 		}
    412 		this.config.autocompletions[id] = {"criteria":criteria, "handler":handler};
    413 	},
    414
    415 	/**
    416 	 * Adds several autocompletion handlers at once.
    417 	 *
    418 	 * @param {Array} list of autocompletion. each element should have following structure: {id:"identifier", criteria:criteria, handler:handler}
    419 	 */
    420 	addAutocompletions: function(list) {
    421 		list.each(function(ac) {
    422 			this.addAutocompletion(ac.id, ac.criteria, ac.handler);
    423 		}.bind(this));
    424 	},
    425
    426 	/**
    427 	 * Returns autocompletion handler matches with given id
    428 	 *
    429 	 * @param {String} id unique identifier
    430 	 */
    431 	getAutocompletion: function(id) {return this.config.autocompletions[id];},
    432
    433 	/**
    434 	 * Returns entire autocompletions' map
    435 	 */
    436 	getAutocompletions: function() {return this.config.autocompletions;},
    437
    438 	/**
    439 	 * Removes autocompletion handler matches with given id
    440 	 *
    441 	 * @param {String} id unique identifier
    442 	 */
    443 	removeAutocompletion: function(id) {delete this.config.autocompletions[id];},
    444
    445 	/**
    446 	 * Adds or replaces template processor.
    447 	 *
    448 	 * @param {String} id unique identifier
    449 	 * @param {Object} handler string or function to be evaluated or called when template inserted
    450 	 */
    451 	addTemplateProcessor: function(id, handler) {
    452 		this.config.templateProcessors[id] = {"handler":handler};
    453 	},
    454
    455 	/**
    456 	 * Adds several template processors at once.
    457 	 *
    458 	 * @param {Array} list of template processors. Each element should have following structure: {id:"identifier", handler:handler}
    459 	 */
    460 	addTemplateProcessors: function(list) {
    461 		list.each(function(tp) {
    462 			this.addTemplateProcessor(tp.id, tp.handler);
    463 		}.bind(this));
    464 	},
    465
    466 	/**
    467 	 * Returns template processor matches with given id
    468 	 *
    469 	 * @param {String} id unique identifier
    470 	 */
    471 	getTemplateProcessor: function(id) {return this.config.templateProcessors[id];},
    472
    473 	/**
    474 	 * Returns entire template processors' map
    475 	 */
    476 	getTemplateProcessors: function() {return this.config.templateProcessors;},
    477
    478 	/**
    479 	 * Removes template processor matches with given id
    480 	 *
    481 	 * @param {String} id unique identifier
    482 	 */
    483 	removeTemplateProcessor: function(id) {delete this.config.templateProcessors[id];},
    484
    485
    486
    487 	/**
    488 	 * Adds or replaces context menu handler.
    489 	 *
    490 	 * @param {String} id unique identifier
    491 	 * @param {Object} handler string or function to be evaluated or called when onContextMenu occured
    492 	 */
    493 	addContextMenuHandler: function(id, handler) {
    494 		this.config.contextMenuHandlers[id] = {"handler":handler};
    495 	},
    496
    497 	/**
    498 	 * Adds several context menu handlers at once.
    499 	 *
    500 	 * @param {Array} list of handlers. Each element should have following structure: {id:"identifier", handler:handler}
    501 	 */
    502 	addContextMenuHandlers: function(list) {
    503 		list.each(function(mh) {
    504 			this.addContextMenuHandler(mh.id, mh.handler);
    505 		}.bind(this));
    506 	},
    507
    508 	/**
    509 	 * Returns context menu handler matches with given id
    510 	 *
    511 	 * @param {String} id unique identifier
    512 	 */
    513 	getContextMenuHandler: function(id) {return this.config.contextMenuHandlers[id];},
    514
    515 	/**
    516 	 * Returns entire context menu handlers' map
    517 	 */
    518 	getContextMenuHandlers: function() {return this.config.contextMenuHandlers;},
    519
    520 	/**
    521 	 * Removes context menu handler matches with given id
    522 	 *
    523 	 * @param {String} id unique identifier
    524 	 */
    525 	removeContextMenuHandler: function(id) {delete this.config.contextMenuHandlers[id];},
    526
    527
    528
    529 	/////////////////////////////////////////////
    530 	// Edit mode management
    531
    532 	/**
    533 	 * Returns current edit mode - readonly, wysiwyg, source
    534 	 */
    535 	getCurrentEditMode: function() {
    536 		return this.currentEditMode;
    537 	},
    538
    539 	toggleSourceAndWysiwygMode: function() {
    540 		var mode = this.getCurrentEditMode();
    541 		if(mode == 'readonly') return;
    542 		this.setEditMode(mode == 'wysiwyg' ? 'source' : 'wysiwyg');
    543
    544 		return true;
    545 	},
    546
    547 	/**
    548 	 * Switches between edit-mode/normal mode.
    549 	 *
    550 	 * @param {Object} mode false or 'readonly' means read-only mode, true or 'wysiwyg' means WYSIWYG editing mode, and 'source' means source editing mode.
    551 	 */
    552 	setEditMode: function(mode) {
    553 		if(this.currentEditMode == mode) return;
    554
    555 		var firstCall = mode != false && mode != 'readonly' && !this.outmostWrapper;
    556 		if(firstCall) {
    557 			// Create editor element if needed
    558 			this._createEditorFrame();
    559 			this._registerEventHandlers();
    560
    561 			this.loadCurrentContentFromStaticContent();
    562 			this.editHistory = new xq.EditHistory(this.rdom);
    563 		}
    564
    565 		if(mode == 'wysiwyg') {
    566 			// Update contents
    567 			if(this.currentEditMode == 'source') this.setStaticContent(this.getSourceContent());
    568 			this.loadCurrentContentFromStaticContent();
    569
    570 			// Make static content invisible
    571 			this.contentElement.style.display = "none";
    572
    573 			// Make WYSIWYG editor visible
    574 			this.sourceEditorDiv.style.display = "none";
    575 			this.wysiwygEditorDiv.style.display = "block";
    576 			this.outmostWrapper.style.display = "block";
    577
    578 			this.currentEditMode = mode;
    579
    580 			if(!xq.Browser.isTrident) {
    581 				window.setTimeout(function() {
    582 					if(this.getDoc().designMode == 'On') return;
    583
    584 					// Without it, Firefox doesn't display embedded SWF
    585 					this.getDoc().designMode = 'On';
    586
    587 					// turn off Firefox's table editing feature
    588 					try {this.getDoc().execCommand("enableInlineTableEditing", false, "false")} catch(ignored) {}
    589 				}.bind(this), 0);
    590 			}
    591
    592 			this.enableToolbarButtons();
    593 			if(!firstCall) this.focus();
    594 		} else if(mode == 'source') {
    595 			// Update contents
    596 			if(this.currentEditMode == 'wysiwyg') this.setStaticContent(this.getWysiwygContent());
    597 			this.loadCurrentContentFromStaticContent();
    598
    599 			// Make static content invisible
    600 			this.contentElement.style.display = "none";
    601
    602 			// Make source editor visible
    603 			this.sourceEditorDiv.style.display = "block";
    604 			this.wysiwygEditorDiv.style.display = "none";
    605 			this.outmostWrapper.style.display = "block";
    606
    607 			this.currentEditMode = mode;
    608
    609 			this.disableToolbarButtons(['html']);
    610 			if(!firstCall) this.focus();
    611 		} else {
    612 			// Update contents
    613 			this.setStaticContent(this.getCurrentContent());
    614 			this.loadCurrentContentFromStaticContent();
    615
    616 			// Make editor and toolbar invisible
    617 			this.outmostWrapper.style.display = "none";
    618
    619 			// Make static content visible
    620 			this.contentElement.style.display = "block";
    621
    622 			this.currentEditMode = mode;
    623 		}
    624
    625 		this._fireOnCurrentEditModeChanged(this, mode);
    626 	},
    627
    628 	/**
    629 	 * Load CSS into editing-mode document
    630 	 *
    631 	 * @param {string} path URL
    632 	 */
    633 	loadStylesheet: function(path) {
    634 		var head = this.editorDoc.getElementsByTagName("HEAD")[0];
    635 		var link = this.editorDoc.createElement("LINK");
    636 		link.rel = "Stylesheet";
    637 		link.type = "text/css";
    638 		link.href = path;
    639 		head.appendChild(link);
    640 	},
    641
    642 	/**
    643 	 * Sets editor's dynamic content from static content
    644 	 */
    645 	loadCurrentContentFromStaticContent: function() {
    646 		// update WYSIWYG editor
    647 		var html = this.validator.invalidate(this.getStaticContentAsDOM());
    648 		html = this.removeUnnecessarySpaces(html);
    649
    650 		if(html.blank()) {
    651 			this.rdom.clearRoot();
    652 		} else {
    653 			this.rdom.getRoot().innerHTML = html;
    654 		}
    655 		this.rdom.wrapAllInlineOrTextNodesAs("P", this.rdom.getRoot(), true);
    656
    657 		// update source editor
    658 		var source = this.getWysiwygContent(true, true);
    659
    660 		this.sourceEditorTextarea.value = source;
    661 		if(xq.Browser.isWebkit) {
    662 			this.sourceEditorTextarea.innerHTML = source;
    663 		}
    664
    665 		this._fireOnCurrentContentChanged(this);
    666 	},
    667
    668 	/**
    669 	 * Enables all toolbar buttons
    670 	 *
    671 	 * @param {Array} [exceptions] array of string containing classnames to exclude
    672 	 */
    673 	enableToolbarButtons: function(exceptions) {
    674 		if(!this.toolbarContainer) return;
    675
    676 		this._execForAllToolbarButtons(exceptions, function(li, exception) {
    677 			li.firstChild.className = !exception ? '' : 'disabled';
    678 		});
    679
    680 		// Toolbar image icon disappears without following code:
    681 		if(xq.Browser.isIE6) {
    682 			this.toolbarContainer.style.display = 'none';
    683 			setTimeout(function() {this.toolbarContainer.style.display = 'block';}.bind(this), 0);
    684 		}
    685 	},
    686
    687 	/**
    688 	 * Disables all toolbar buttons
    689 	 *
    690 	 * @param {Array} [exceptions] array of string containing classnames to exclude
    691 	 */
    692 	disableToolbarButtons: function(exceptions) {
    693 		this._execForAllToolbarButtons(exceptions, function(li, exception) {
    694 			li.firstChild.className = exception ? '' : 'disabled';
    695 		});
    696 	},
    697
    698 	_execForAllToolbarButtons: function(exceptions, exec) {
    699 		if(!this.toolbarContainer) return;
    700 		exceptions = exceptions || [];
    701
    702 		$(this.toolbarContainer).select('li').each(function(li) {
    703 			var buttonsClassName = li.classNames().find(function(name) {return name != 'xq_separator'});
    704 			var exception = exceptions.include(buttonsClassName);
    705 			exec(li, exception);
    706 		});
    707 	},
    708
    709 	_updateToolbarButtonStatus: function(buttonClassName, selected) {
    710 		var button = this.toolbarButtons.get(buttonClassName);
    711 		if(button) button.firstChild.firstChild.className = selected ? 'selected' : '';
    712 	},
    713
    714 	updateAllToolbarButtonsStatus: function(element) {
    715 		if(!this.toolbarContainer) return;
    716 		if(!this.toolbarButtons) {
    717 			var classNames = [
    718 				"emphasis", "strongEmphasis", "underline", "strike", "superscription", "subscription",
    719 				"justifyLeft", "justifyCenter", "justifyRight", "justifyBoth",
    720 				"unorderedList", "orderedList", "code",
    721 				"paragraph", "heading1", "heading2", "heading3", "heading4", "heading5", "heading6"
    722 			];
    723
    724 			this.toolbarButtons = $H({});
    725
    726 			classNames.each(function(className) {
    727 				var found = $(this.toolbarContainer).getElementsBySelector("." + className);
    728 				var button = found && found.length > 0 ? found[0] : null;
    729 				if(button) this.toolbarButtons.set(className, button);
    730 			}.bind(this));
    731 		}
    732
    733 		var buttons = this.toolbarButtons;
    734
    735 		var info = this.rdom.collectStructureAndStyle(element);
    736
    737 		this._updateToolbarButtonStatus('emphasis', info.em);
    738 		this._updateToolbarButtonStatus('strongEmphasis', info.strong);
    739 		this._updateToolbarButtonStatus('underline', info.underline);
    740 		this._updateToolbarButtonStatus('strike', info.strike);
    741 		this._updateToolbarButtonStatus('superscription', info.superscription);
    742 		this._updateToolbarButtonStatus('subscription', info.subscription);
    743
    744 		this._updateToolbarButtonStatus('justifyLeft', info.justification == 'left');
    745 		this._updateToolbarButtonStatus('justifyCenter', info.justification == 'center');
    746 		this._updateToolbarButtonStatus('justifyRight', info.justification == 'right');
    747 		this._updateToolbarButtonStatus('justifyBoth', info.justification == 'justify');
    748
    749 		this._updateToolbarButtonStatus('orderedList', info.list == 'OL');
    750 		this._updateToolbarButtonStatus('unorderedList', info.list == 'UL');
    751 		this._updateToolbarButtonStatus('code', info.list == 'CODE');
    752
    753 		this._updateToolbarButtonStatus('paragraph', info.block == 'P');
    754 		this._updateToolbarButtonStatus('heading1', info.block == 'H1');
    755 		this._updateToolbarButtonStatus('heading2', info.block == 'H2');
    756 		this._updateToolbarButtonStatus('heading3', info.block == 'H3');
    757 		this._updateToolbarButtonStatus('heading4', info.block == 'H4');
    758 		this._updateToolbarButtonStatus('heading5', info.block == 'H5');
    759 		this._updateToolbarButtonStatus('heading6', info.block == 'H6');
    760 	},
    761
    762 	removeUnnecessarySpaces: function(html) {
    763 		var blocks = this.rdom.tree.getBlockTags().join("|");
    764 		var regex = new RegExp("\\s*<(/?)(" + blocks + ")>\\s*", "img");
    765 		return html.replace(regex, '<$1$2>');
    766 	},
    767
    768 	/**
    769 	 * Gets editor's dynamic content from current editor(source or WYSIWYG)
    770 	 *
    771 	 * @return {Object} HTML String
    772 	 */
    773 	getCurrentContent: function(performFullValidation) {
    774 		if(this.getCurrentEditMode() == 'source') {
    775 			return this.getSourceContent(performFullValidation);
    776 		} else {
    777 			return this.getWysiwygContent(performFullValidation);
    778 		}
    779 	},
    780
    781 	/**
    782 	 * Gets editor's dynamic content from WYSIWYG editor
    783 	 *
    784 	 * @return {Object} HTML String
    785 	 */
    786 	getWysiwygContent: function(performFullValidation, dontUseCache) {
    787 		if(dontUseCache || !performFullValidation) return this.validator.validate(this.rdom.getRoot(), performFullValidation);
    788
    789 		var lastModified = this.editHistory.getLastModifiedDate();
    790 		if(this._lastModified != lastModified) {
    791 			this._validContentCache = this.validator.validate(this.rdom.getRoot(), performFullValidation);
    792 			this._lastModified = lastModified;
    793 		}
    794 		return this._validContentCache;
    795 	},
    796
    797 	/**
    798 	 * Gets editor's dynamic content from source editor
    799 	 *
    800 	 * @return {Object} HTML String
    801 	 */
    802 	getSourceContent: function(performFullValidation) {
    803 		var raw = this.sourceEditorTextarea[xq.Browser.isWebkit ? 'innerHTML' : 'value'];
    804 		var tempDiv = document.createElement('div');
    805 		tempDiv.innerHTML = this.removeUnnecessarySpaces(raw);
    806
    807 		var rdom = xq.RichDom.createInstance();
    808 		rdom.setRoot(document.body);
    809 		rdom.wrapAllInlineOrTextNodesAs("P", tempDiv, true);
    810
    811 		return this.validator.validate(tempDiv, performFullValidation);
    812 	},
    813
    814 	/**
    815 	 * Sets editor's original content
    816 	 *
    817 	 * @param {Object} content HTML String
    818 	 */
    819 	setStaticContent: function(content) {
    820 		if(this.contentElement.nodeName == 'TEXTAREA') {
    821 			this.contentElement.value = content;
    822 			if(xq.Browser.isWebkit) {
    823 				this.contentElement.innerHTML = content;
    824 			}
    825 		} else {
    826 			this.contentElement.innerHTML = content;
    827 		}
    828 		this._fireOnStaticContentChanged(this, content);
    829 	},
    830
    831 	/**
    832 	 * Gets editor's original content
    833 	 *
    834 	 * @return {Object} HTML String
    835 	 */
    836 	getStaticContent: function() {
    837 		var content;
    838 		if(this.contentElement.nodeName == 'TEXTAREA') {
    839 			content = this.contentElement[xq.Browser.isWebkit ? 'innerHTML' : 'value'];
    840 		} else {
    841 			content = this.contentElement.innerHTML;
    842 		}
    843 		return content;
    844 	},
    845
    846 	/**
    847 	 * Gets editor's original content as DOM node
    848 	 *
    849 	 * @return {Object} HTML String
    850 	 */
    851 	getStaticContentAsDOM: function() {
    852 		if(this.contentElement.nodeName == 'TEXTAREA') {
    853 			var div = this.doc.createElement('DIV');
    854 			div.innerHTML = this.contentElement[xq.Browser.isWebkit ? 'innerHTML' : 'value'];
    855 			return div;
    856 		} else {
    857 			return this.contentElement;
    858 		}
    859 	},
    860
    861 	/**
    862 	 * Gives focus to editor
    863 	 */
    864 	focus: function() {
    865 		if(this.getCurrentEditMode() == 'wysiwyg') {
    866 			this.rdom.focus();
    867 			window.setTimeout(function() {
    868 				this.updateAllToolbarButtonsStatus(this.rdom.getCurrentElement());
    869 			}.bind(this), 0);
    870 		} else if(this.getCurrentEditMode() == 'source') {
    871 			this.sourceEditorTextarea.focus();
    872 		}
    873 	},
    874
    875 	/**
    876 	 * Returns designmode iframe object
    877 	 */
    878 	getFrame: function() {
    879 		return this.editorFrame;
    880 	},
    881
    882 	/**
    883 	 * Returns designmode window object
    884 	 */
    885 	getWin: function() {
    886 		return this.editorWin;
    887 	},
    888
    889 	/**
    890 	 * Returns designmode document object
    891 	 */
    892 	getDoc: function() {
    893 		return this.editorDoc;
    894 	},
    895
    896 	/**
    897 	 * Returns outmost wrapper element
    898 	 */
    899 	getOutmostWrapper: function() {
    900 		return this.outmostWrapper;
    901 	},
    902
    903 	/**
    904 	 * Returns designmode body object
    905 	 */
    906 	getBody: function() {
    907 		return this.editorBody;
    908 	},
    909
    910 	_createEditorFrame: function() {
    911 		// create outer DIV
    912 		this.outmostWrapper = this.doc.createElement('div');
    913 		this.outmostWrapper.className = "xquared";
    914
    915 		this.contentElement.parentNode.insertBefore(this.outmostWrapper, this.contentElement);
    916
    917 		// create toolbar is needed
    918 		if(!this.toolbarContainer && this.config.generateDefaultToolbar) {
    919 			this.toolbarContainer = this._generateDefaultToolbar();
    920 			this.outmostWrapper.appendChild(this.toolbarContainer);
    921 		}
    922
    923 		// create source editor div
    924 		this.sourceEditorDiv = this.doc.createElement('div');
    925 		this.sourceEditorDiv.className = "editor source_editor"; //TODO: remove editor
    926 		this.sourceEditorDiv.style.display = "none";
    927 		this.outmostWrapper.appendChild(this.sourceEditorDiv);
    928
    929 		// create TEXTAREA for source editor
    930 		this.sourceEditorTextarea = this.doc.createElement('textarea');
    931 		this.sourceEditorDiv.appendChild(this.sourceEditorTextarea);
    932
    933 		// create WYSIWYG editor div
    934 		this.wysiwygEditorDiv = this.doc.createElement('div');
    935 		this.wysiwygEditorDiv.className = "editor wysiwyg_editor"; //TODO: remove editor
    936 		this.wysiwygEditorDiv.style.display = "none";
    937 		this.outmostWrapper.appendChild(this.wysiwygEditorDiv);
    938
    939 		// create designmode iframe for WYSIWYG editor
    940 		this.editorFrame = this.doc.createElement('iframe');
    941 		this.rdom.setAttributes(this.editorFrame, {
    942 			"frameBorder": "0",
    943 			"marginWidth": "0",
    944 			"marginHeight": "0",
    945 			"leftMargin": "0",
    946 			"topMargin": "0",
    947 			"allowTransparency": "true"
    948 		});
    949 		this.wysiwygEditorDiv.appendChild(this.editorFrame);
    950
    951 		var doc = this.editorFrame.contentWindow.document;
    952 		if(xq.Browser.isTrident) doc.designMode = 'On';
    953
    954 		doc.open();
    955 		doc.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">');
    956 		doc.write('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ko">');
    957 		doc.write('<head>');
    958
    959 		// it is needed to force href of pasted content to be an absolute url
    960 		if(!xq.Browser.isTrident) doc.write('<base href="./" />');
    961
    962 		doc.write('<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />');
    963 		doc.write('<title>XQuared</title>');
    964 		if(this.config.changeCursorOnLink) doc.write('<style>.xed a {cursor: pointer !important;}</style>');
    965 		doc.write('</head>');
    966 		doc.write('<body><p>' + this.rdom.makePlaceHolderString() + '</p></body>');
    967 		doc.write('</html>');
    968 		doc.close();
    969
    970 		this.editorWin = this.editorFrame.contentWindow;
    971 		this.editorDoc = this.editorWin.document;
    972 		this.editorBody = this.editorDoc.body;
    973 		this.editorBody.className = "xed";
    974
    975 		// it is needed to fix IE6 horizontal scrollbar problem
    976 		if(xq.Browser.isIE6) {
    977 			this.editorDoc.documentElement.style.overflowY='auto';
    978 			this.editorDoc.documentElement.style.overflowX='hidden';
    979 		}
    980
    981 		this.rdom.setWin(this.editorWin);
    982 		this.rdom.setRoot(this.editorBody);
    983 		this.validator = xq.Validator.createInstance(this.doc.location.href, this.config.urlValidationMode, this.config.allowedTags, this.config.allowedAttributes);
    984
    985 		// hook onsubmit of form
    986 		if(this.config.automaticallyHookSubmitEvent && this.contentElement.nodeName == 'TEXTAREA' && this.contentElement.form) {
    987 			var original = this.contentElement.form.onsubmit;
    988
    989 			this.contentElement.form.onsubmit = function() {
    990 				this.contentElement.value = this.getCurrentContent(true);
    991 				if(original) {
    992 					return original();
    993 				} else {
    994 					return true;
    995 				}
    996 			}.bind(this);
    997 		}
    998 	},
    999
    1000 	_addStyleRule: function(selector, rule) {
    1001 		if(!this.dynamicStyle) {
    1002 			if(xq.Browser.isTrident) {
    1003 			    this.dynamicStyle = this.doc.createStyleSheet();
    1004 			} else {
    1005 	    		var style = this.doc.createElement('style');
    1006 	    		this.doc.body.appendChild(style);
    1007 		    	this.dynamicStyle = $A(this.doc.styleSheets).last();
    1008 			}
    1009 		}
    1010
    1011 		if(xq.Browser.isTrident) {
    1012 			this.dynamicStyle.addRule(selector, rule);
    1013 		} else {
    1014 	    	this.dynamicStyle.insertRule(selector + " {" + rule + "}", this.dynamicStyle.cssRules.length);
    1015     	}
    1016 	},
    1017
    1018 	_generateDefaultToolbar: function() {
    1019 		// override image path
    1020 		this._addStyleRule(".xquared div.toolbar", "background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarBg.gif)");
    1021 		this._addStyleRule(".xquared ul.buttons li", "background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarButtonBg.gif)");
    1022 		this._addStyleRule(".xquared ul.buttons li.xq_separator", "background-image: url(" + this.config.imagePathForDefaultToobar + "toolbarSeparator.gif)");
    1023
    1024 		// outmost container
    1025 		var container = this.doc.createElement('div');
    1026 		container.className = 'toolbar';
    1027
    1028 		// button container
    1029 		var buttons = this.doc.createElement('ul');
    1030 		buttons.className = 'buttons';
    1031 		container.appendChild(buttons);
    1032
    1033 		// Generate buttons from map and append it to button container
    1034 		var cancelMousedown = function(e) {Event.stop(e); return false};
    1035 		var map = this.config.defaultToolbarButtonMap;
    1036 		for(var i = 0; i < map.length; i++) {
    1037 			for(var j = 0; j < map[i].length; j++) {
    1038 				var buttonConfig = map[i][j];
    1039
    1040 				var li = this.doc.createElement('li');
    1041 				buttons.appendChild(li);
    1042 				li.className = buttonConfig.className;
    1043
    1044 				var span = this.doc.createElement('span');
    1045 				li.appendChild(span);
    1046
    1047 				var a = this.doc.createElement('a');
    1048 				span.appendChild(a);
    1049 				a.href = '#';
    1050 				a.title = buttonConfig.title;
    1051 				a.handler = buttonConfig.handler;
    1052 				a.xed = this;
    1053 				Event.observe(a, 'mousedown', cancelMousedown);
    1054 				Event.observe(a, 'click', function(e) {
    1055 					var xed = this.xed;
    1056
    1057 					if($(this.parentNode).hasClassName('disabled') || xed.toolbarContainer.hasClassName('disabled')) {
    1058 						Event.stop(e);
    1059 						return false;
    1060 					}
    1061
    1062 					if(xq.Browser.isTrident) xed.focus();
    1063
    1064 					var handler = this.handler;
    1065 					var stop = (typeof handler == "function") ? handler(xed) : eval(handler);
    1066 					if(stop) {
    1067 						Event.stop(e);
    1068 						return false;
    1069 					} else {
    1070 						return true;
    1071 					}
    1072 				}.bind(a));
    1073
    1074 				var img = this.doc.createElement('img');
    1075 				a.appendChild(img);
    1076 				img.src = this.config.imagePathForDefaultToobar + buttonConfig.className + '.gif';
    1077
    1078 				if(j == 0 && i != 0) li.className += ' xq_separator';
    1079 			}
    1080 		}
    1081
    1082 		return container;
    1083 	},
    1084
    1085
    1086
    1087 	/////////////////////////////////////////////
    1088 	// Event Management
    1089
    1090 	_registerEventHandlers: function() {
    1091 		var events = ['keydown', 'click', 'keyup', 'mouseup', 'contextmenu', 'scroll'];
    1092
    1093 		if(xq.Browser.isTrident && this.config.changeCursorOnLink) events.push('mousemove');
    1094 		if(xq.Browser.isMac && xq.Browser.isGecko) events.push('keypress');
    1095
    1096 		for(var i = 0; i < events.length; i++) {
    1097 			Event.observe(this.getDoc(), events[i], this._handleEvent.bindAsEventListener(this));
    1098 		}
    1099 	},
    1100
    1101 	_handleEvent: function(e) {
    1102 		this._fireOnBeforeEvent(this, e);
    1103
    1104 		var stop = false;
    1105
    1106 		var modifiedByCorrection = false;
    1107
    1108 		if(e.type == 'mousemove' && this.config.changeCursorOnLink) {
    1109 			// Trident only
    1110 			var link = !!this.rdom.getParentElementOf(e.srcElement, ["A"]);
    1111 			if(this.editorBody.contentEditable != link && !this.rdom.hasSelection()) this.editorBody.contentEditable = !link;
    1112 		} else if(e.type == 'click' && e.button == 0 && this.config.enableLinkClick) {
    1113 			var a = this.rdom.getParentElementOf(e.target || e.srcElement, ["A"]);
    1114 			if(a) stop = this.handleClick(e, a);
    1115 		} else if(e.type == (xq.Browser.isMac && xq.Browser.isGecko ? "keypress" : "keydown")) {
    1116 			var undoPerformed = false;
    1117
    1118 			modifiedByCorrection = this.rdom.correctParagraph();
    1119 			for(var key in this.config.shortcuts) {
    1120 				if(!this.config.shortcuts[key].event.matches(e)) continue;
    1121
    1122 				var handler = this.config.shortcuts[key].handler;
    1123 				var xed = this;
    1124 				stop = (typeof handler == "function") ? handler(this) : eval(handler);
    1125
    1126 				if(key == "undo") undoPerformed = true;
    1127 			}
    1128 		} else if(["mouseup", "keyup"].include(e.type)) {
    1129 			modifiedByCorrection = this.rdom.correctParagraph();
    1130 		} else if(["contextmenu"].include(e.type)) {
    1131 			this._handleContextMenu(e);
    1132 		}
    1133
    1134 		if(stop) Event.stop(e);
    1135
    1136 		this._fireOnCurrentContentChanged(this);
    1137 		this._fireOnAfterEvent(this, e);
    1138
    1139 		if(!undoPerformed && !modifiedByCorrection) this.editHistory.onEvent(e);
    1140
    1141 		return !stop;
    1142 	},
    1143
    1144 	/**
    1145 	 * TODO: remove dup with handleAutocompletion
    1146 	 */
    1147 	handleAutocorrection: function() {
    1148 		var block = this.rdom.getCurrentBlockElement();
    1149
    1150 		// TODO: use complete unescape algorithm
    1151 		var text = this.rdom.getInnerText(block).replace(/ /gi, " ");
    1152
    1153 		var acs = this.config.autocorrections;
    1154 		var performed = false;
    1155
    1156 		var stop = false;
    1157 		for(var key in acs) {
    1158 			var ac = acs[key];
    1159 			if(ac.criteria(text)) {
    1160 				try {
    1161 					this.editHistory.onCommand();
    1162 					this.editHistory.disable();
    1163 					if(typeof ac.handler == "String") {
    1164 						var xed = this;
    1165 						var rdom = this.rdom;
    1166 						eval(ac.handler);
    1167 					} else {
    1168 						stop = ac.handler(this, this.rdom, block, text);
    1169 					}
    1170 					this.editHistory.enable();
    1171 				} catch(ignored) {}
    1172
    1173 				block = this.rdom.getCurrentBlockElement();
    1174 				text = this.rdom.getInnerText(block);
    1175
    1176 				performed = true;
    1177 				if(stop) break;
    1178 			}
    1179 		}
    1180
    1181 		return stop;
    1182 	},
    1183
    1184 	/**
    1185 	 * TODO: remove dup with handleAutocorrection
    1186 	 */
    1187 	handleAutocompletion: function() {
    1188 		var acs = $H(this.config.autocompletions);
    1189 		if(acs.size() == 0) return;
    1190
    1191 		if(this.rdom.hasSelection()) {
    1192 			var text = this.rdom.getSelectionAsText();
    1193 			this.rdom.deleteSelection();
    1194 			var wrapper = this.rdom.insertNode(this.rdom.createElement("SPAN"));
    1195 			wrapper.innerHTML = text;
    1196
    1197 			var marker = this.rdom.pushMarker();
    1198
    1199 			var filtered =
    1200 				acs.map(function(pair) {
    1201 					return [pair.key, pair.value.criteria(text)];
    1202 				}.bind(this)).findAll(function(elem) {
    1203 					return elem[1] != -1;
    1204 				}).sortBy(function(elem) {
    1205 					return elem[1];
    1206 				});
    1207
    1208 			if(filtered.length == 0) {
    1209 				this.rdom.popMarker(true);
    1210 				return;
    1211 			}
    1212 			var ac = acs.get(filtered[0][0]);
    1213
    1214 			this.editHistory.disable();
    1215 		} else {
    1216 			var marker = this.rdom.pushMarker();
    1217
    1218 			var filtered =
    1219 				acs.map(function(pair) {
    1220 					return [pair.key, this.rdom.testSmartWrap(marker, pair.value.criteria).textIndex];
    1221 				}.bind(this)).findAll(function(elem) {
    1222 					return elem[1] != -1;
    1223 				}).sortBy(function(elem) {
    1224 					return elem[1];
    1225 				});
    1226
    1227 			if(filtered.length == 0) {
    1228 				this.rdom.popMarker(true);
    1229 				return;
    1230 			}
    1231
    1232 			var ac = acs.get(filtered[0][0]);
    1233
    1234 			this.editHistory.disable();
    1235
    1236 			var wrapper = this.rdom.smartWrap(marker, "SPAN", ac.criteria);
    1237 		}
    1238
    1239 		var block = this.rdom.getCurrentBlockElement();
    1240
    1241 		// TODO: use complete unescape algorithm
    1242 		var text = this.rdom.getInnerText(wrapper).replace(/ /gi, " ");
    1243
    1244 		try {
    1245 			// call handler
    1246 			if(typeof ac.handler == "String") {
    1247 				var xed = this;
    1248 				var rdom = this.rdom;
    1249 				eval(ac.handler);
    1250 			} else {
    1251 				ac.handler(this, this.rdom, block, wrapper, text);
    1252 			}
    1253 		} catch(ignored) {}
    1254
    1255 		try {
    1256 			this.rdom.unwrapElement(wrapper);
    1257 		} catch(ignored) {}
    1258
    1259
    1260 		if(this.rdom.isEmptyBlock(block)) this.rdom.correctEmptyElement(block);
    1261
    1262 		this.editHistory.enable();
    1263 		this.editHistory.onCommand();
    1264
    1265 		this.rdom.popMarker(true);
    1266 	},
    1267
    1268 	/**
    1269 	 * Handles click event
    1270 	 *
    1271 	 * @param {Event} e click event
    1272 	 * @param {Element} target target element(usually has A tag)
    1273 	 */
    1274 	handleClick: function(e, target) {
    1275 		var href = decodeURI(target.href);
    1276 		if(!xq.Browser.isTrident) {
    1277 			if(!e.ctrlKey && !e.shiftKey && e.button != 1) {
    1278 				window.location.href = href;
    1279 				return true;
    1280 			}
    1281 		} else {
    1282 			if(e.shiftKey) {
    1283 				window.open(href, "_blank");
    1284 			} else {
    1285 				window.location.href = href;
    1286 			}
    1287 			return true;
    1288 		}
    1289
    1290 		return false;
    1291 	},
    1292
    1293 	/**
    1294 	 * Show link dialog
    1295 	 *
    1296 	 * TODO: should support modify/unlink
    1297 	 */
    1298 	handleLink: function() {
    1299 		var text = this.rdom.getSelectionAsText() || '';
    1300 		var dialog = new xq.controls.FormDialog(
    1301 			this,
    1302 			xq.ui_templates.basicLinkDialog,
    1303 			function(dialog) {
    1304 				if(text) {
    1305 					dialog.form.text.value = text;
    1306 					dialog.form.url.focus();
    1307 					dialog.form.url.select();
    1308 				}
    1309 			},
    1310 			function(data) {
    1311 				this.focus();
    1312
    1313 				if(xq.Browser.isTrident) {
    1314 					var rng = this.rdom.rng();
    1315 					rng.moveToBookmark(bm);
    1316 					rng.select();
    1317 				}
    1318
    1319 				if(!data) return;
    1320 				this.handleInsertLink(false, data.url, data.text, data.text);
    1321 			}.bind(this)
    1322 		);
    1323
    1324 		if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
    1325
    1326 		dialog.show({position: 'centerOfEditor'});
    1327
    1328 		return true;
    1329 	},
    1330
    1331 	/**
    1332 	 * Inserts link or apply link into selected area
    1333 	 *
    1334 	 * @param {boolean} autoSelection if set true and there's no selection, automatically select word to link(if possible)
    1335 	 * @param {String} url url
    1336 	 * @param {String} title title of link
    1337 	 * @param {String} text text of link. If there's a selection(manually or automatically), it will be replaced with this text
    1338 	 *
    1339 	 * @returns {Element} created element
    1340 	 */
    1341 	handleInsertLink: function(autoSelection, url, title, text) {
    1342 		if(autoSelection && !this.rdom.hasSelection()) {
    1343 			var marker = this.rdom.pushMarker();
    1344 			var a = this.rdom.smartWrap(marker, "A", function(text) {
    1345 				var index = text.lastIndexOf(" ");
    1346 				return index == -1 ? index : index + 1;
    1347 			});
    1348 			a.href = url;
    1349 			a.title = title;
    1350 			if(text) {
    1351 				a.innerHTML = ""
    1352 				a.appendChild(this.rdom.createTextNode(text));
    1353 			} else if(!a.hasChildNodes()) {
    1354 				this.rdom.deleteNode(a);
    1355 			}
    1356 			this.rdom.popMarker(true);
    1357 		} else {
    1358 			text = text || (this.rdom.hasSelection() ? this.rdom.getSelectionAsText() : null);
    1359 			if(!text) return;
    1360
    1361 			this.rdom.deleteSelection();
    1362
    1363 			var a = this.rdom.createElement('A');
    1364 			a.href = url;
    1365 			a.title = title;
    1366 			a.appendChild(this.rdom.createTextNode(text));
    1367 			this.rdom.insertNode(a);
    1368 		}
    1369
    1370 		var historyAdded = this.editHistory.onCommand();
    1371 		this._fireOnCurrentContentChanged(this);
    1372
    1373 		return true;
    1374 	},
    1375
    1376 	/**
    1377 	 * Called when enter key pressed.
    1378 	 *
    1379 	 * @param {boolean} skipAutocorrection if set true, skips autocorrection
    1380 	 * @param {boolean} forceInsertParagraph if set true, inserts paragraph
    1381 	 */
    1382 	handleEnter: function(skipAutocorrection, forceInsertParagraph) {
    1383 		// If it has selection, perform default action.
    1384 		if(this.rdom.hasSelection()) return false;
    1385
    1386 		// Perform autocorrection
    1387 		if(!skipAutocorrection && this.handleAutocorrection()) return true;
    1388
    1389 		var atEmptyBlock = this.rdom.isCaretAtEmptyBlock();
    1390 		var atStart = atEmptyBlock || this.rdom.isCaretAtBlockStart();
    1391 		var atEnd = atEmptyBlock || (!atStart && this.rdom.isCaretAtBlockEnd());
    1392 		var atEdge = atEmptyBlock || atStart || atEnd;
    1393
    1394 		if(!atEdge) {
    1395 			var block = this.rdom.getCurrentBlockElement();
    1396 			var marker = this.rdom.pushMarker();
    1397
    1398 			if(this.rdom.isFirstLiWithNestedList(block) && !forceInsertParagraph) {
    1399 				var parent = block.parentNode;
    1400 				this.rdom.unwrapElement(block);
    1401 				block = parent;
    1402 			} else if(block.nodeName != "LI" && this.rdom.tree.isBlockContainer(block)) {
    1403 				block = this.rdom.wrapAllInlineOrTextNodesAs("P", block, true).first();
    1404 			}
    1405 			this.rdom.splitElementUpto(marker, block);
    1406
    1407 			this.rdom.popMarker(true);
    1408 		} else if(atEmptyBlock) {
    1409 			this._handleEnterAtEmptyBlock();
    1410 		} else {
    1411 			this._handleEnterAtEdge(atStart, forceInsertParagraph);
    1412 		}
    1413
    1414 		return true;
    1415 	},
    1416
    1417 	/**
    1418 	 * Moves current block upward or downward
    1419 	 *
    1420 	 * @param {boolean} up moves current block upward
    1421 	 */
    1422 	handleMoveBlock: function(up) {
    1423 		var block = this.rdom.moveBlock(this.rdom.getCurrentBlockElement(), up);
    1424 		if(block) {
    1425 			this.rdom.selectElement(block, false);
    1426 			block.scrollIntoView(false);
    1427
    1428 			var historyAdded = this.editHistory.onCommand();
    1429 			this._fireOnCurrentContentChanged(this);
    1430 		}
    1431 		return true;
    1432 	},
    1433
    1434 	/**
    1435 	 * Called when tab key pressed
    1436 	 */
    1437 	handleTab: function() {
    1438 		var hasSelection = this.rdom.hasSelection();
    1439 		var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]);
    1440
    1441 		if(hasSelection) {
    1442 			this.handleIndent();
    1443 		} else if (table && table.className == "datatable") {
    1444 			this.handleMoveToNextCell();
    1445 		} else if (this.rdom.isCaretAtBlockStart()) {
    1446 			this.handleIndent();
    1447 		} else {
    1448 			this.handleInsertTab();
    1449 		}
    1450
    1451 		return true;
    1452 	},
    1453
    1454 	/**
    1455 	 * Called when shift+tab key pressed
    1456 	 */
    1457 	handleShiftTab: function() {
    1458 		var hasSelection = this.rdom.hasSelection();
    1459 		var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]);
    1460
    1461 		if(hasSelection) {
    1462 			this.handleOutdent();
    1463 		} else if (table && table.className == "datatable") {
    1464 			this.handleMoveToPreviousCell();
    1465 		} else {
    1466 			this.handleOutdent();
    1467 		}
    1468
    1469 		return true;
    1470 	},
    1471
    1472 	/**
    1473 	 * Inserts three non-breaking spaces
    1474 	 */
    1475 	handleInsertTab: function() {
    1476 		this.rdom.insertHtml(' ');
    1477 		this.rdom.insertHtml(' ');
    1478 		this.rdom.insertHtml(' ');
    1479
    1480 		return true;
    1481 	},
    1482
    1483 	/**
    1484 	 * Called when delete key pressed
    1485 	 */
    1486 	handleDelete: function() {
    1487 		if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockEnd()) return false;
    1488 		return this._handleMerge(true);
    1489 	},
    1490
    1491 	/**
    1492 	 * Called when backspace key pressed
    1493 	 */
    1494 	handleBackspace: function() {
    1495 		if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockStart()) return false;
    1496 		return this._handleMerge(false);
    1497 	},
    1498
    1499 	_handleMerge: function(withNext) {
    1500 		var block = this.rdom.getCurrentBlockElement();
    1501
    1502 		// save caret position;
    1503 		var marker = this.rdom.pushMarker();
    1504
    1505 		// perform merge
    1506 		var merged = this.rdom.mergeElement(block, withNext, withNext);
    1507 		if(!merged && !withNext) this.rdom.extractOutElementFromParent(block);
    1508
    1509 		// restore caret position
    1510 		this.rdom.popMarker(true);
    1511 		if(merged) this.rdom.correctEmptyElement(merged);
    1512
    1513 		var historyAdded = this.editHistory.onCommand();
    1514 		this._fireOnCurrentContentChanged(this);
    1515
    1516 		return !!merged;
    1517 	},
    1518
    1519 	/**
    1520 	 * (in table) Moves caret to the next cell
    1521 	 */
    1522 	handleMoveToNextCell: function() {
    1523 		this._handleMoveToCell("next");
    1524 	},
    1525
    1526 	/**
    1527 	 * (in table) Moves caret to the previous cell
    1528 	 */
    1529 	handleMoveToPreviousCell: function() {
    1530 		this._handleMoveToCell("prev");
    1531 	},
    1532
    1533 	/**
    1534 	 * (in table) Moves caret to the above cell
    1535 	 */
    1536 	handleMoveToAboveCell: function() {
    1537 		this._handleMoveToCell("above");
    1538 	},
    1539
    1540 	/**
    1541 	 * (in table) Moves caret to the below cell
    1542 	 */
    1543 	handleMoveToBelowCell: function() {
    1544 		this._handleMoveToCell("below");
    1545 	},
    1546
    1547 	_handleMoveToCell: function(dir) {
    1548 		var block = this.rdom.getCurrentBlockElement();
    1549 		var cell = this.rdom.getParentElementOf(block, ["TD", "TH"]);
    1550 		var table = this.rdom.getParentElementOf(cell, ["TABLE"]);
    1551 		var rtable = new xq.RichTable(this.rdom, table);
    1552 		var target = null;
    1553
    1554 		if(["next", "prev"].include(dir)) {
    1555 			var toNext = dir == "next";
    1556 			target = toNext ? rtable.getNextCellOf(cell) : rtable.getPreviousCellOf(cell);
    1557 		} else {
    1558 			var toBelow = dir == "below";
    1559 			target = toBelow ? rtable.getBelowCellOf(cell) : rtable.getAboveCellOf(cell);
    1560 		}
    1561
    1562 		if(!target) {
    1563 			var finder = function(node) {return !['TD', 'TH'].include(node.nodeName) && this.tree.isBlock(node) && !this.tree.hasBlocks(node);}.bind(this.rdom);
    1564 			var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this.rdom);
    1565
    1566 			target = (toNext || toBelow) ?
    1567 				this.rdom.tree.findForward(cell, finder, exitCondition) :
    1568 				this.rdom.tree.findBackward(table, finder, exitCondition);
    1569 		}
    1570
    1571 		if(target) this.rdom.placeCaretAtStartOf(target);
    1572 	},
    1573
    1574 	/**
    1575 	 * Applies STRONG tag
    1576 	 */
    1577 	handleStrongEmphasis: function() {
    1578 		this.rdom.applyStrongEmphasis();
    1579
    1580 		var historyAdded = this.editHistory.onCommand();
    1581 		this._fireOnCurrentContentChanged(this);
    1582
    1583 		return true;
    1584 	},
    1585
    1586 	/**
    1587 	 * Applies EM tag
    1588 	 */
    1589 	handleEmphasis: function() {
    1590 		this.rdom.applyEmphasis();
    1591
    1592 		var historyAdded = this.editHistory.onCommand();
    1593 		this._fireOnCurrentContentChanged(this);
    1594
    1595 		return true;
    1596 	},
    1597
    1598 	/**
    1599 	 * Applies EM.underline tag
    1600 	 */
    1601 	handleUnderline: function() {
    1602 		this.rdom.applyUnderline();
    1603
    1604 		var historyAdded = this.editHistory.onCommand();
    1605 		this._fireOnCurrentContentChanged(this);
    1606
    1607 		return true;
    1608 	},
    1609
    1610 	/**
    1611 	 * Applies SPAN.strike tag
    1612 	 */
    1613 	handleStrike: function() {
    1614 		this.rdom.applyStrike();
    1615
    1616 		var historyAdded = this.editHistory.onCommand();
    1617 		this._fireOnCurrentContentChanged(this);
    1618
    1619 		return true;
    1620 	},
    1621
    1622 	/**
    1623 	 * Removes all style
    1624 	 */
    1625 	handleRemoveFormat: function() {
    1626 		this.rdom.applyRemoveFormat();
    1627
    1628 		var historyAdded = this.editHistory.onCommand();
    1629 		this._fireOnCurrentContentChanged(this);
    1630
    1631 		return true;
    1632 	},
    1633
    1634 	/**
    1635 	 * Inserts table
    1636 	 *
    1637 	 * @param {Number} cols number of columns
    1638 	 * @param {Number} rows number of rows
    1639 	 * @param {String} headerPosition position of THs. "T" or "L" or "TL". "T" means top, "L" means left.
    1640 	 */
    1641 	handleTable: function(cols, rows, headerPositions) {
    1642 		var cur = this.rdom.getCurrentBlockElement();
    1643 		if(this.rdom.getParentElementOf(cur, ["TABLE"])) return true;
    1644
    1645 		var rtable = xq.RichTable.create(this.rdom, cols, rows, headerPositions);
    1646 		if(this.rdom.tree.isBlockContainer(cur)) {
    1647 			var wrappers = this.rdom.wrapAllInlineOrTextNodesAs("P", cur, true);
    1648 			cur = wrappers.last();
    1649 		}
    1650 		var tableDom = this.rdom.insertNodeAt(rtable.getDom(), cur, "after");
    1651 		this.rdom.placeCaretAtStartOf(rtable.getCellAt(0, 0));
    1652
    1653 		if(this.rdom.isEmptyBlock(cur)) this.rdom.deleteNode(cur, true);
    1654
    1655 		var historyAdded = this.editHistory.onCommand();
    1656 		this._fireOnCurrentContentChanged(this);
    1657
    1658 		return true;
    1659 	},
    1660
    1661 	handleInsertNewRowAt: function(where) {
    1662 		var cur = this.rdom.getCurrentBlockElement();
    1663 		var tr = this.rdom.getParentElementOf(cur, ["TR"]);
    1664 		if(!tr) return true;
    1665
    1666 		var table = this.rdom.getParentElementOf(tr, ["TABLE"]);
    1667 		var rtable = new xq.RichTable(this.rdom, table);
    1668 		var row = rtable.insertNewRowAt(tr, where);
    1669
    1670 		this.rdom.placeCaretAtStartOf(row.cells[0]);
    1671 		return true;
    1672 	},
    1673 	handleInsertNewColumnAt: function(where) {
    1674 		var cur = this.rdom.getCurrentBlockElement();
    1675 		var td = this.rdom.getParentElementOf(cur, ["TD"], true);
    1676 		if(!td) return true;
    1677
    1678 		var table = this.rdom.getParentElementOf(td, ["TABLE"]);
    1679 		var rtable = new xq.RichTable(this.rdom, table);
    1680 		rtable.insertNewCellAt(td, where);
    1681
    1682 		this.rdom.placeCaretAtStartOf(cur);
    1683 		return true;
    1684 	},
    1685
    1686 	handleDeleteRow: function() {
    1687 		var cur = this.rdom.getCurrentBlockElement();
    1688 		var tr = this.rdom.getParentElementOf(cur, ["TR"]);
    1689 		if(!tr) return true;
    1690
    1691 		var table = this.rdom.getParentElementOf(tr, ["TABLE"]);
    1692 		var rtable = new xq.RichTable(this.rdom, table);
    1693 		var blockToMove = rtable.deleteRow(tr);
    1694
    1695 		this.rdom.placeCaretAtStartOf(blockToMove);
    1696 		return true;
    1697 	},
    1698
    1699 	handleDeleteColumn: function() {
    1700 		var cur = this.rdom.getCurrentBlockElement();
    1701 		var td = this.rdom.getParentElementOf(cur, ["TD"], true);
    1702 		if(!td) return true;
    1703
    1704 		var table = this.rdom.getParentElementOf(td, ["TABLE"]);
    1705 		var rtable = new xq.RichTable(this.rdom, table);
    1706 		rtable.deleteCell(td);
    1707
    1708 		return true;
    1709 	},
    1710
    1711 	/**
    1712 	 * Performs block indentation
    1713 	 */
    1714 	handleIndent: function() {
    1715 		if(this.rdom.hasSelection()) {
    1716 			var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
    1717 			if(blocks.first() != blocks.last()) {
    1718 				var affected = this.rdom.indentElements(blocks.first(), blocks.last());
    1719 				this.rdom.selectBlocksBetween(affected.first(), affected.last());
    1720
    1721 				var historyAdded = this.editHistory.onCommand();
    1722 				this._fireOnCurrentContentChanged(this);
    1723
    1724 				return true;
    1725 			}
    1726 		}
    1727
    1728 		var block = this.rdom.getCurrentBlockElement();
    1729 		var affected = this.rdom.indentElement(block);
    1730
    1731 		if(affected) {
    1732 			this.rdom.placeCaretAtStartOf(affected);
    1733
    1734 			var historyAdded = this.editHistory.onCommand();
    1735 			this._fireOnCurrentContentChanged(this);
    1736 		}
    1737
    1738 		return true;
    1739 	},
    1740
    1741 	/**
    1742 	 * Performs block outdentation
    1743 	 */
    1744 	handleOutdent: function() {
    1745 		if(this.rdom.hasSelection()) {
    1746 			var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
    1747 			if(blocks.first() != blocks.last()) {
    1748 				var affected = this.rdom.outdentElements(blocks.first(), blocks.last());
    1749 				this.rdom.selectBlocksBetween(affected.first(), affected.last());
    1750
    1751 				var historyAdded = this.editHistory.onCommand();
    1752 				this._fireOnCurrentContentChanged(this);
    1753
    1754 				return true;
    1755 			}
    1756 		}
    1757
    1758 		var block = this.rdom.getCurrentBlockElement();
    1759 		var affected = this.rdom.outdentElement(block);
    1760
    1761 		if(affected) {
    1762 			this.rdom.placeCaretAtStartOf(affected);
    1763
    1764 			var historyAdded = this.editHistory.onCommand();
    1765 			this._fireOnCurrentContentChanged(this);
    1766 		}
    1767
    1768 		return true;
    1769 	},
    1770
    1771 	/**
    1772 	 * Applies list.
    1773 	 *
    1774 	 * @param {String} type "UL" or "OL" or "CODE". CODE generates OL.code
    1775 	 */
    1776 	handleList: function(type) {
    1777 		if(this.rdom.hasSelection()) {
    1778 			var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
    1779 			if(blocks.first() != blocks.last()) {
    1780 				blocks = this.rdom.applyLists(blocks.first(), blocks.last(), type);
    1781 			} else {
    1782 				blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type);
    1783 			}
    1784 			this.rdom.selectBlocksBetween(blocks.first(), blocks.last());
    1785 		} else {
    1786 			var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type);
    1787 			this.rdom.placeCaretAtStartOf(block);
    1788 		}
    1789 		var historyAdded = this.editHistory.onCommand();
    1790 		this._fireOnCurrentContentChanged(this);
    1791
    1792 		return true;
    1793 	},
    1794
    1795 	/**
    1796 	 * Applies justification
    1797 	 *
    1798 	 * @param {String} dir "left", "center", "right" or "both"
    1799 	 */
    1800 	handleJustify: function(dir) {
    1801 		var block = this.rdom.getCurrentBlockElement();
    1802 		var dir = (dir == "left" || dir == "both") && (block.style.textAlign == "left" || block.style.textAlign == "") ? "both" : dir;
    1803
    1804 		if(this.rdom.hasSelection()) {
    1805 			var blocks = this.rdom.getSelectedBlockElements();
    1806 			this.rdom.justifyBlocks(blocks, dir);
    1807 			this.rdom.selectBlocksBetween(blocks.first(), blocks.last());
    1808 		} else {
    1809 			this.rdom.justifyBlock(block, dir);
    1810 		}
    1811 		var historyAdded = this.editHistory.onCommand();
    1812 		this._fireOnCurrentContentChanged(this);
    1813
    1814 		return true;
    1815 	},
    1816
    1817 	/**
    1818 	 * Removes current block element
    1819 	 */
    1820 	handleRemoveBlock: function() {
    1821 		var block = this.rdom.getCurrentBlockElement();
    1822 		var blockToMove = this.rdom.removeBlock(block);
    1823 		this.rdom.placeCaretAtStartOf(blockToMove);
    1824 		blockToMove.scrollIntoView(false);
    1825 	},
    1826
    1827 	/**
    1828 	 * Applies background color
    1829 	 *
    1830 	 * @param {String} color CSS color string
    1831 	 */
    1832 	handleBackgroundColor: function(color) {
    1833 		if(color) {
    1834 			this.rdom.applyBackgroundColor(color);
    1835
    1836 			var historyAdded = this.editHistory.onCommand();
    1837 			this._fireOnCurrentContentChanged(this);
    1838 		} else {
    1839 			var dialog = new xq.controls.FormDialog(
    1840 				this,
    1841 				xq.ui_templates.basicColorPickerDialog,
    1842 				function(dialog) {},
    1843 				function(data) {
    1844 					this.focus();
    1845
    1846 					if(xq.Browser.isTrident) {
    1847 						var rng = this.rdom.rng();
    1848 						rng.moveToBookmark(bm);
    1849 						rng.select();
    1850 					}
    1851
    1852 					if(!data) return;
    1853
    1854 					this.handleBackgroundColor(data.color);
    1855 				}.bind(this)
    1856 			);
    1857
    1858 			if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
    1859
    1860 			dialog.show({position: 'centerOfEditor'});
    1861 		}
    1862 		return true;
    1863 	},
    1864
    1865 	/**
    1866 	 * Applies foreground color
    1867 	 *
    1868 	 * @param {String} color CSS color string
    1869 	 */
    1870 	handleForegroundColor: function(color) {
    1871 		if(color) {
    1872 			this.rdom.applyForegroundColor(color);
    1873
    1874 			var historyAdded = this.editHistory.onCommand();
    1875 			this._fireOnCurrentContentChanged(this);
    1876 		} else {
    1877 			var dialog = new xq.controls.FormDialog(
    1878 				this,
    1879 				xq.ui_templates.basicColorPickerDialog,
    1880 				function(dialog) {},
    1881 				function(data) {
    1882 					this.focus();
    1883
    1884 					if(xq.Browser.isTrident) {
    1885 						var rng = this.rdom.rng();
    1886 						rng.moveToBookmark(bm);
    1887 						rng.select();
    1888 					}
    1889
    1890 					if(!data) return;
    1891
    1892 					this.handleForegroundColor(data.color);
    1893 				}.bind(this)
    1894 			);
    1895
    1896 			if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark();
    1897
    1898 			dialog.show({position: 'centerOfEditor'});
    1899 		}
    1900 		return true;
    1901 	},
    1902
    1903 	/**
    1904 	 * Applies superscription
    1905 	 */
    1906 	handleSuperscription: function() {
    1907 		this.rdom.applySuperscription();
    1908
    1909 		var historyAdded = this.editHistory.onCommand();
    1910 		this._fireOnCurrentContentChanged(this);
    1911
    1912 		return true;
    1913 	},
    1914
    1915 	/**
    1916 	 * Applies subscription
    1917 	 */
    1918 	handleSubscription: function() {
    1919 		this.rdom.applySubscription();
    1920
    1921 		var historyAdded = this.editHistory.onCommand();
    1922 		this._fireOnCurrentContentChanged(this);
    1923
    1924 		return true;
    1925 	},
    1926
    1927 	/**
    1928 	 * Change of wrap current block's tag
    1929 	 */
    1930 	handleApplyBlock: function(tagName) {
    1931 		if(this.rdom.hasSelection()) {
    1932 			var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true);
    1933 			if(blocks.first() != blocks.last()) {
    1934 				var applied = this.rdom.applyTagIntoElements(tagName, blocks.first(), blocks.last());
    1935 				this.rdom.selectBlocksBetween(applied.first(), applied.last());
    1936
    1937 				var historyAdded = this.editHistory.onCommand();
    1938 				this._fireOnCurrentContentChanged(this);
    1939
    1940 				return true;
    1941 			}
    1942 		}
    1943
    1944 		var block = this.rdom.getCurrentBlockElement();
    1945 		this.rdom.pushMarker();
    1946 		var applied =
    1947 			this.rdom.applyTagIntoElement(tagName, block) ||
    1948 			block;
    1949 		this.rdom.popMarker(true);
    1950
    1951 		if(this.rdom.isEmptyBlock(applied)) {
    1952 			this.rdom.correctEmptyElement(applied);
    1953 			this.rdom.placeCaretAtStartOf(applied);
    1954 		}
    1955
    1956 		var historyAdded = this.editHistory.onCommand();
    1957 		this._fireOnCurrentContentChanged(this);
    1958
    1959 		return true;
    1960 	},
    1961
    1962 	/**
    1963 	 * Inserts seperator (HR)
    1964 	 */
    1965 	handleSeparator: function() {
    1966 		this.rdom.collapseSelection();
    1967
    1968 		var curBlock = this.rdom.getCurrentBlockElement();
    1969 		var atStart = this.rdom.isCaretAtBlockStart();
    1970 		if(this.rdom.tree.isBlockContainer(curBlock)) curBlock = this.rdom.wrapAllInlineOrTextNodesAs("P", curBlock, true)[0];
    1971
    1972 		this.rdom.insertNodeAt(this.rdom.createElement("HR"), curBlock, atStart ? "before" : "after");
    1973 		this.rdom.placeCaretAtStartOf(curBlock);
    1974
    1975 		// add undo history
    1976 		var historyAdded = this.editHistory.onCommand();
    1977 		this._fireOnCurrentContentChanged(this);
    1978
    1979 		return true;
    1980 	},
    1981
    1982 	/**
    1983 	 * Performs UNDO
    1984 	 */
    1985 	handleUndo: function() {
    1986 		var performed = this.editHistory.undo();
    1987 		this._fireOnCurrentContentChanged(this);
    1988
    1989 		var curBlock = this.rdom.getCurrentBlockElement();
    1990 		if(!xq.Browser.isTrident && curBlock) {
    1991 			curBlock.scrollIntoView(false);
    1992 		}
    1993 		return true;
    1994 	},
    1995
    1996 	/**
    1997 	 * Performs REDO
    1998 	 */
    1999 	handleRedo: function() {
    2000 		var performed = this.editHistory.redo();
    2001 		this._fireOnCurrentContentChanged(this);
    2002
    2003 		var curBlock = this.rdom.getCurrentBlockElement();
    2004 		if(!xq.Browser.isTrident && curBlock) {
    2005 			curBlock.scrollIntoView(false);
    2006 		}
    2007 		return true;
    2008 	},
    2009
    2010
    2011
    2012 	_handleContextMenu: function(e) {
    2013 		if (xq.Browser.isWebkit) {
    2014 			if (e.metaKey || Event.isLeftClick(e)) return false;
    2015 		} else if (e.shiftKey || e.ctrlKey || e.altKey) {
    2016 			return false;
    2017 		}
    2018
    2019 		var x=Event.pointerX(e);
    2020 		var y=Event.pointerY(e);
    2021 		var pos=Position.cumulativeOffset(this.getFrame());
    2022 		x+=pos[0];
    2023 		y+=pos[1];
    2024 		this._contextMenuTargetElement = e.target || e.srcElement;
    2025
    2026 		//TODO: Safari on Windows doesn't work with context key(app key)
    2027 		if (!x || !y || xq.Browser.isTrident) {
    2028 			var pos = Position.cumulativeOffset(this._contextMenuTargetElement);
    2029 			var posFrame = Position.cumulativeOffset(this.getFrame());
    2030 			x = pos[0] + posFrame[0] - this.getDoc().documentElement.scrollLeft;
    2031 			y = pos[1] + posFrame[1] - this.getDoc().documentElement.scrollTop;
    2032 		}
    2033
    2034 		if (!xq.Browser.isTrident) {
    2035 			var doc = this.getDoc();
    2036 			var body = this.getBody();
    2037
    2038 			x -= doc.documentElement.scrollLeft;
    2039 			y -= doc.documentElement.scrollTop;
    2040
    2041 			if (doc != body) {
    2042 				x -= body.scrollLeft;
    2043 				y -= body.scrollTop;
    2044 			}
    2045 		}
    2046
    2047 		for(var cmh in this.config.contextMenuHandlers) {
    2048 			var stop = this.config.contextMenuHandlers[cmh].handler(this, this._contextMenuTargetElement, x, y);
    2049 			if(stop) {
    2050 				Event.stop(e);
    2051 				return true;
    2052 			}
    2053 		}
    2054
    2055 		return false;
    2056 	},
    2057
    2058 	showContextMenu: function(menuItems, x, y) {
    2059 		if (!menuItems || menuItems.length <= 0) return;
    2060
    2061 		if (!this._contextMenuContainer) {
    2062 			this._contextMenuContainer = this.doc.createElement('UL');
    2063 			this._contextMenuContainer.className = 'xqContextMenu';
    2064 			this._contextMenuContainer.style.display='none';
    2065
    2066 			Event.observe(this.doc, 'click', this._contextMenuClicked.bindAsEventListener(this));
    2067 			Event.observe(this.rdom.getDoc(), 'click', this.hideContextMenu.bindAsEventListener(this));
    2068
    2069 			this.body.appendChild(this._contextMenuContainer);
    2070 		} else {
    2071 			while (this._contextMenuContainer.childNodes.length > 0)
    2072 				this._contextMenuContainer.removeChild(this._contextMenuContainer.childNodes[0]);
    2073 		}
    2074
    2075 		for (var i=0; i < menuItems.length; i++) {
    2076 			menuItems[i]._node = this._addContextMenuItem(menuItems[i]);
    2077 		}
    2078
    2079 		this._contextMenuContainer.style.display='block';
    2080 		this._contextMenuContainer.style.left=Math.min(Math.max(this.doc.body.scrollWidth, this.doc.documentElement.clientWidth)-this._contextMenuContainer.offsetWidth, x)+'px';
    2081 		this._contextMenuContainer.style.top=Math.min(Math.max(this.doc.body.scrollHeight, this.doc.documentElement.clientHeight)-this._contextMenuContainer.offsetHeight, y)+'px';
    2082
    2083 		this._contextMenuItems = menuItems;
    2084 	},
    2085
    2086 	hideContextMenu: function() {
    2087 		if (this._contextMenuContainer)
    2088 			this._contextMenuContainer.style.display='none';
    2089 	},
    2090
    2091 	_addContextMenuItem: function(item) {
    2092 		if (!this._contextMenuContainer) throw "No conext menu container exists";
    2093
    2094 		var node = this.doc.createElement('LI');
    2095 		if (item.disabled) node.className += ' disabled';
    2096
    2097 		if (item.title == '----') {
    2098 			node.innerHTML = ' ';
    2099 			node.className = 'separator';
    2100 		} else {
    2101 			if(item.handler) {
    2102 				node.innerHTML = '<a href="javascript:;" onclick="return false;">'+(item.title.toString().escapeHTML())+'</a>';
    2103 			} else {
    2104 				node.innerHTML = (item.title.toString().escapeHTML());
    2105 			}
    2106 		}
    2107
    2108 		if(item.className) node.className = item.className;
    2109
    2110 		this._contextMenuContainer.appendChild(node);
    2111
    2112 		return node;
    2113 	},
    2114
    2115 	_contextMenuClicked: function(e) {
    2116 		this.hideContextMenu();
    2117
    2118 		if (!this._contextMenuContainer) return;
    2119
    2120 		var node = Event.findElement(e, 'LI');
    2121 		if (!node || !this.rdom.tree.isDescendantOf(this._contextMenuContainer, node)) return;
    2122
    2123 		for (var i=0; i < this._contextMenuItems.length; i++) {
    2124 			if (this._contextMenuItems[i]._node == node) {
    2125 				var handler = this._contextMenuItems[i].handler;
    2126 				if (!this._contextMenuItems[i].disabled && handler) {
    2127 					var xed = this;
    2128 					var element = this._contextMenuTargetElement;
    2129 					if(typeof handler == "function") {
    2130 						handler(xed, element);
    2131 					} else {
    2132 						eval(handler);
    2133 					}
    2134 				}
    2135 				break;
    2136 			}
    2137 		}
    2138 	},
    2139
    2140 	/**
    2141 	 * Inserts HTML template
    2142 	 *
    2143 	 * @param {String} html Template string. It should have single root element
    2144 	 * @returns {Element} inserted element
    2145 	 */
    2146 	insertTemplate: function(html) {
    2147 		return this.rdom.insertHtml(this._processTemplate(html));
    2148 	},
    2149
    2150 	/**
    2151 	 * Places given HTML template nearby target.
    2152 	 *
    2153 	 * @param {String} html Template string. It should have single root element
    2154 	 * @param {Node} target Target node.
    2155 	 * @param {String} where Possible values: "before", "start", "end", "after"
    2156 	 *
    2157 	 * @returns {Element} Inserted element.
    2158 	 */
    2159 	insertTemplateAt: function(html, target, where) {
    2160 		return this.rdom.insertHtmlAt(this._processTemplate(html), target, where);
    2161 	},
    2162
    2163 	_processTemplate: function(html) {
    2164 		// apply template processors
    2165 		var tps = $H(this.getTemplateProcessors()).values();
    2166 		for(var i = 0; i < tps.length; i++) {
    2167 			html = tps[i].handler(html);
    2168 		}
    2169
    2170 		// remove all whitespace characters between block tags
    2171 		return html = this.removeUnnecessarySpaces(html);
    2172 	},
    2173
    2174
    2175
    2176 	/** @private */
    2177 	_handleEnterAtEmptyBlock: function() {
    2178 		var block = this.rdom.getCurrentBlockElement();
    2179 		if(this.rdom.tree.isTableCell(block) && this.rdom.isFirstBlockOfBody(block)) {
    2180 			block = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start");
    2181 		} else {
    2182 			block =
    2183 				this.rdom.outdentElement(block) ||
    2184 				this.rdom.extractOutElementFromParent(block) ||
    2185 				this.rdom.replaceTag("P", block) ||
    2186 				this.rdom.insertNewBlockAround(block);
    2187 		}
    2188
    2189 		this.rdom.placeCaretAtStartOf(block);
    2190 		if(!xq.Browser.isTrident) block.scrollIntoView(false);
    2191 	},
    2192
    2193 	/** @private */
    2194 	_handleEnterAtEdge: function(atStart, forceInsertParagraph) {
    2195 		var block = this.rdom.getCurrentBlockElement();
    2196 		var blockToPlaceCaret;
    2197
    2198 		if(atStart && this.rdom.isFirstBlockOfBody(block)) {
    2199 			blockToPlaceCaret = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start");
    2200 		} else {
    2201 			if(this.rdom.tree.isTableCell(block)) forceInsertParagraph = true;
    2202 			var newBlock = this.rdom.insertNewBlockAround(block, atStart, forceInsertParagraph ? "P" : null);
    2203 			blockToPlaceCaret = !atStart ? newBlock : newBlock.nextSibling;
    2204 		}
    2205
    2206 		this.rdom.placeCaretAtStartOf(blockToPlaceCaret);
    2207 		if(!xq.Browser.isTrident) blockToPlaceCaret.scrollIntoView(false);
    2208 	}
    2209 });