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