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