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