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 });