1/** 2 * Bitrix HTML Editor 3.0 3 * Date: 24.04.13 4 * Time: 4:23 5 * 6 * Views class 7 */ 8(function() 9{ 10 11function BXEditorView(editor, element, container) 12{ 13 this.editor = editor; 14 this.element = element; 15 this.container = container; 16 this.config = editor.config || {}; 17 this.isShown = null; 18 this.bbCode = editor.bbCode; 19 BX.addCustomEvent(this.editor, "OnClickBefore", BX.proxy(this.OnClick, this)); 20} 21 22BXEditorView.prototype = { 23 Focus: function() 24 { 25 if (!document.querySelector || this.element.ownerDocument.querySelector(":focus") === this.element) 26 return; 27 28 try{this.element.focus();}catch(e){} 29 }, 30 31 Hide: function() 32 { 33 this.isShown = false; 34 this.container.style.display = "none"; 35 }, 36 37 Show: function() 38 { 39 this.isShown = true; 40 this.container.style.display = ""; 41 }, 42 43 Disable: function() 44 { 45 this.element.setAttribute("disabled", "disabled"); 46 }, 47 48 Enable: function() 49 { 50 this.element.removeAttribute("disabled"); 51 }, 52 53 OnClick: function(params) 54 { 55 56 }, 57 58 IsShown: function() 59 { 60 return !!this.isShown; 61 } 62}; 63 64function BXEditorTextareaView(parent, textareaElement, container) 65{ 66 // Call parrent constructor 67 BXEditorIframeView.superclass.constructor.apply(this, arguments); 68 this.name = "textarea"; 69 this.InitEventHandlers(); 70 71 if (!this.element.value && this.editor.config.content) 72 this.SetValue(this.editor.config.content, false); 73} 74 75BX.extend(BXEditorTextareaView, BXEditorView); 76 77BXEditorTextareaView.prototype.Clear = function() 78{ 79 this.element.value = ""; 80}; 81 82BXEditorTextareaView.prototype.GetValue = function(bParse) 83{ 84 var value = this.IsEmpty() ? "" : this.element.value; 85 86 if (bParse) 87 { 88 value = this.parent.parse(value); 89 } 90 91 return value; 92}; 93 94BXEditorTextareaView.prototype.SetValue = function(html, bParse, bFormat) 95{ 96 if (bParse) 97 { 98 html = this.editor.Parse(html, true, bFormat); 99 } 100 this.editor.dom.pValueInput.value = this.element.value = html; 101}; 102 103 104BXEditorTextareaView.prototype.SaveValue = function() 105{ 106 if (this.editor.inited) 107 { 108 this.editor.dom.pValueInput.value = this.element.value; 109 } 110}; 111 112BXEditorTextareaView.prototype.HasPlaceholderSet = function() 113{ 114 var 115 placeholderText = this.element.getAttribute("placeholder") || null, 116 value = this.element.value; 117 return !value || (value === placeholderText); 118}; 119 120BXEditorTextareaView.prototype.IsEmpty = function() 121{ 122 var value = BX.util.trim(this.element.value); 123 return value === '' || this.HasPlaceholderSet(); 124}; 125 126BXEditorTextareaView.prototype.InitEventHandlers = function() 127{ 128 var _this = this; 129 BX.bind(this.element, "focus", function() 130 { 131 _this.editor.On("OnTextareaFocus"); 132 _this.isFocused = true; 133 }); 134 135 BX.bind(this.element, "blur", function() 136 { 137 _this.editor.On("OnTextareaBlur"); 138 _this.isFocused = false; 139 }); 140 141 BX.bind(this.element, "keydown", function(e) 142 { 143 _this.editor.textareaKeyDownPreventDefault = false;
144 145 // Handle Ctrl+Enter 146 if ((e.ctrlKey || e.metaKey) && !e.altKey && e.keyCode === _this.editor.KEY_CODES["enter"]) 147 { 148 _this.editor.On('OnCtrlEnter', [e, _this.editor.GetViewMode()]); 149 return BX.PreventDefault(e); 150 } 151 _this.editor.On('OnTextareaKeydown', [e]); 152 153 if (_this.editor.textareaKeyDownPreventDefault) 154 return BX.PreventDefault(e); 155 }); 156 157 BX.bind(this.element, "keyup", function(e) 158 { 159 _this.editor.On('OnTextareaKeyup', [e]); 160 }); 161}; 162 163BXEditorTextareaView.prototype.IsFocused = function() 164{ 165 return this.isFocused; 166}; 167 168BXEditorTextareaView.prototype.ScrollToSelectedText = function(searchText) 169{ 170// http://blog.blupixelit.eu/scroll-textarea-to-selected-word-using-javascript-jquery/ 171// var parola_cercata = "parola"; // the searched word 172// var posi = jQuery('#my_textarea').val().indexOf(parola_cercata); // take the position of the word in the text 173// if (posi != -1) { 174// var target = document.getElementById("my_textarea"); 175// // select the textarea and the word 176// target.focus(); 177// if (target.setSelectionRange) 178// target.setSelectionRange(posi, posi+parola_cercata.length); 179// else { 180// var r = target.createTextRange(); 181// r.collapse(true); 182// r.moveEnd('character', posi+parola_cercata); 183// r.moveStart('character', posi); 184// r.select(); 185// } 186// var objDiv = document.getElementById("my_textarea"); 187// var sh = objDiv.scrollHeight; //height in pixel of the textarea (n_rows*line_height) 188// var line_ht = jQuery('#my_textarea').css('line-height').replace('px',''); //height in pixel of each row 189// var n_lines = sh/line_ht; // the total amount of lines 190// var char_in_line = jQuery('#insert_textarea').val().length / n_lines; // amount of chars for each line 191// var height = Math.floor(posi/char_in_line); // amount of lines in the textarea 192// jQuery('#my_textarea').scrollTop(height*line_ht); // scroll to the selected line 193// } else { 194// alert('parola '+parola_cercata+' non trovata'); // alert word not found 195// } 196}; 197 198BXEditorTextareaView.prototype.SelectText = function(searchText) 199{ 200 var 201 value = this.element.value, 202 ind = value.indexOf(searchText); 203 204 if(ind != -1) 205 { 206 this.element.focus(); 207 this.element.setSelectionRange(ind, ind + searchText.length); 208 } 209}; 210 211BXEditorTextareaView.prototype.GetTextSelection = function() 212{ 213 var res = false; 214 if (this.element.selectionStart != undefined) 215 { 216 res = this.element.value.substr(this.element.selectionStart, this.element.selectionEnd - this.element.selectionStart); 217 } 218 else if (document.selection && document.selection.createRange) 219 { 220 res = document.selection.createRange().text; 221 } 222 else if (window.getSelection) 223 { 224 res = window.getSelection(); 225 res = res.toString(); 226 } 227 228 return res; 229}; 230 231BXEditorTextareaView.prototype.WrapWith = function (tagBegin, tagEnd, postText) 232{ 233 if (!tagBegin) 234 tagBegin = ""; 235 if (!tagEnd) 236 tagEnd = ""; 237 if (!postText) 238 postText = ""; 239 240 if (tagBegin.length <= 0 && tagEnd.length <= 0 && postText.length <= 0) 241 return true; 242 243 var 244 bReplaceText = !!postText, 245 selectedText = this.GetTextSelection(), 246 mode = (selectedText ? 'select' : (bReplaceText ? 'after' : 'in')); 247 248 //if (!this.bTextareaFocus) 249 // this.pTextarea.focus(); // BUG IN IE 250 251 if (bReplaceText) 252 { 253 postText = tagBegin + postText + tagEnd; 254 } 255 else if (selectedText) 256 { 257 postText = tagBegin + selectedText + tagEnd; 258 } 259 else 260 { 261 postText = tagBegin + tagEnd; 262 } 263 264 if (this.element.selectionStart != undefined) 265 { 266 var 267 currentScroll = this.element.scrollTop, 268 start = this.element.selectionStart, 269 end = this.element.selectionEnd; 270 271 this.element.value = this.element.value.substr(0, start) + postText + this.element.value.substr(end); 272 273 if (mode == 'select') 274 { 275 this.element.selectionStart = start; 276 this.element.selectionEnd = start + postText.length; 277 } 278 else if (mode == 'in') 279 { 280 this.element.selectionStart = this.element.selectionEnd = start + tagBegin.length; 281 } 282 else 283 { 284 this.element.selectionStart = this.element.selectionEnd = start + postText.length; 285 } 286 this.element.scrollTop = currentScroll; 287 } 288 else if (document.selection && document.selection.createRange) 289 { 290 var sel = document.selection.createRange(); 291 var selection_copy = sel.duplicate(); 292 postText = postText.replace(/\r?\n/g, '\n'); 293 sel.text = postText; 294 sel.setEndPoint('StartToStart', selection_copy); 295 sel.setEndPoint('EndToEnd', selection_copy); 296 297 if (mode == 'select') 298 { 299 sel.collapse(true); 300 postText = postText.replace(/\r\n/g, '1'); 301 sel.moveEnd('character', postText.length); 302 } 303 else if (mode == 'in') 304 { 305 sel.collapse(false); 306 sel.moveEnd('character', tagBegin.length); 307 sel.collapse(false); 308 } 309 else 310 { 311 sel.collapse(false); 312 sel.moveEnd('character', postText.length); 313 sel.collapse(false); 314 } 315 sel.select(); 316 } 317 else 318 { 319 // failed - just stuff it at the end of the message 320 this.element.value += postText; 321 } 322 return true; 323}; 324 325 326BXEditorTextareaView.prototype.GetCursorPosition = function() 327{ 328 return this.element.selectionStart; 329}; 330 331class BXEditorIframeCopilot 332{ 333 copilotParams = {}; 334 copilotLoaded = false;
335 selectionTypeCaret = 'Caret'; 336 resultNodeAttr = 'bxhtmled-copilot-result-node'; 337 resultColor = '#8d52ec'; 338 responseFormatSupported = false; 339 340 invitationLineModes = { 341 NONE: 'none', 342 LAST_LINE: 'lastLine', 343 EACH_LINE: 'eachLine', 344 }; 345 346 invitationLineMode = this.invitationLineModes.LAST_LINE; 347 348 /** 349 * @param iframeView BXEditorIframeView 350 * @param copilotParams {{moduleId, contextId, category, contextParameters, invitationLineMode, isMentionUnavailable, responseFormat}} 351 */ 352 constructor(iframeView, copilotParams = {}) 353 { 354 this.iframeView = iframeView; 355 this.iframeContainer = iframeView.container; 356 this.contentEditable = iframeView.element; 357 358 this.copilotParams = copilotParams; 359 this.responseFormatSupported = BX.Extension?.getSettings('ai.copilot')?.get('isSupportResponseFormatting') === true; 360 361 this.invitationLineMode = copilotParams.invitationLineMode ?? this.invitationLineModes.LAST_LINE; 362 this.invitationLine = this.renderInvitationLine(); 363 this.invitationLineAbsolute = this.renderInvitationLine(); 364 this.insertResultNode = this.renderInsertResultNode(); 365 366 const copilotOptions = { 367 moduleId: copilotParams.moduleId, 368 contextId: copilotParams.contextId, 369 category: copilotParams.category, 370 contextParameters: copilotParams.contextParameters, 371 autoHide: true, 372 preventAutoHide: (event) =>
372 this.iframeContainer.contains(event.target), 373 }; 374 375 if (this.responseFormatSupported) 376 { 377 copilotOptions.responseFormat = copilotParams.responseFormat ?? 'html'; 378 } 379 380 this.copilot = new BX.AI.Copilot(copilotOptions); 381 382 this.bindHandlers(); 383 384 BX.Event.EventEmitter.emit('onHtmlEditorCopilotInit', { copilot: this.copilot }); 385 this.copilot.init(); 386 } 387 388 renderInvitationLine() 389 { 390 let placeHolder = BX.message('BXEdCopilotPlaceholder_MSGVER_2'); 391 if (this.copilotParams.isMentionUnavailable) 392 { 393 placeHolder = BX.message('BXEdCopilotPlaceholderWithoutMention_MSGVER_1'); 394 } 395 396 return BX.Tag.render` 397 <div class="bxhtmled-copilot" placeholder="${placeHolder}"></div> 398 `; 399 } 400 401 renderInsertResultNode() 402 { 403 return BX.Tag.render` 404 <span class="bxhtmled-insert-result"></span> 405 `; 406 } 407 408 bindHandlers() 409 { 410 this.copilot.subscribe('finish-init', this.finishInitHandler.bind(this)); 411 this.copilot.subscribe('aiResult', this.aiResultHandler.bind(this)); 412 this.copilot.subscribe('save', this.saveHandler.bind(this)); 413 this.copilot.subscribe('add_below', this.addBelowHandler.bind(this));
414 this.copilot.subscribe('cancel', this.cancelHandler.bind(this)); 415 this.copilot.subscribe('hide', this.saveResultNodes.bind(this)); 416 document.addEventListener('click', this.documentClickHandler.bind(this)); 417 document.addEventListener('keydown', this.onWindowKeyDownHandler.bind(this)); 418 this.contentEditable.addEventListener('input', this.onContentEditableKeyDown.bind(this)); 419 window.addEventListener('scroll', this.onScrollHandler.bind(this), true); 420 window.addEventListener('resize', this.handleResizeWindow.bind(this)); 421 BX.addCustomEvent(window, "onPullEvent-unicomments", this.startAdjustAnimation.bind(this)); 422 this.hideObserver = new MutationObserver(() => { 423 if (this.iframeContainer.offsetHeight <= 0) { 424 this.hideInvitationLine(); 425 this.hideObserver.disconnect(); 426 } 427 }); 428 this.hideObserver.observe(document.body, {childList: true}); 429 } 430 431 documentClickHandler(event) 432 { 433 if (!this.iframeContainer.contains(event.target)) 434 { 435 this.hideInvitationLine(); 436 } 437 } 438 439 startAdjustAnimation() 440 { 441 this.animation?.stop(); 442 this.animation = new BX.easing({ 443 duration: 1000, 444 start: {}, 445 finish: {}, 446 transition : BX.easing.makeEaseOut(BX.easing.transitions.linear), 447 step: () => { 448 if (this.copilot.isShown()) 449 { 450 this.copilot.adjust(this.getAdjustOptionsForRect(this.adjustmentRect)); 451 } 452 if (this.copilotBtnPopup?.isShown()) 453 { 454 this.adjustCopilotButton(this.getAdjustOptionsForRectSpaced(this.adjustmentRect, true)); 455 } 456 }, 457 complete: () => this.animation = null, 458 }); 459 this.animation.animate(); 460 } 461 462 finishInitHandler() 463 { 464 this.copilotLoaded = true; 465 this.updateInvitationLine(); 466 } 467 468 aiResultHandler(event) 469 { 470 const resultNodes = this.getResultNodes(); 471 const isHtmlFormat = this.responseFormatSupported 472 && (this.copilotParams.responseFormat ?? 'html') === 'html'; 473 474 let lastResultSpan; 475 if (resultNodes.length !== 0) 476 { 477 lastResultSpan = [...resultNodes].pop(); 478 if (isHtmlFormat) 479 { 480 lastResultSpan.innerHTML = event.data.result; 481 } 482 else 483 { 484 lastResultSpan.innerText = event.data.result; 485 } 486 } 487 else 488 { 489 if (isHtmlFormat) 490 { 491 lastResultSpan = this.getSpanWithHtml(event.data.result); 492 } 493 else 494 { 495 lastResultSpan = this.getSpanWithText(event.data.result); 496 } 497 BX.Dom.style(lastResultSpan, 'color', this.resultColor); 498 BX.Dom.attr(lastResultSpan, this.resultNodeAttr, 'true'); 499 500 if (this.invitationLineMode === this.invitationLineModes.LAST_LINE) 501 { 502 this.insertResultNode.after(BX.Tag.render`<br>`); 503 } 504 505 this.insertResultNode.after(lastResultSpan); 506 this.insertResultNode.remove(); 507 } 508 509 this.iframeView.ScrollToInsertedText(); 510 this.getSelection().removeAllRanges(); 511 512 this.copilot.adjust(this.getAdjustOptions(lastResultSpan)); 513 } 514 515 saveHandler(event) 516 { 517 if (this.getResultNodes().length === 0) 518 { 519 const selection = this.getSelection().getRangeAt(0); 520 const isHtmlFormat = this.responseFormatSupported 521 && (this.copilotParams.responseFormat ?? 'html') === 'html'; 522 const span = isHtmlFormat
523 ? this.getSpanWithHtml(event.data.result) 524 : this.convertTextToHtmlSpan(event.data.result); 525 selection.deleteContents(); 526 selection.insertNode(span); 527 this.iframeView.UpdateHeight(); 528 } 529 530 this.saveResultNodes(); 531 532 this.iframeView.editor.synchro.FromIframeToTextarea(true, true); 533 this.iframeView.editor.synchro.FromTextareaToIframe(true); 534 535 this.copilot.hide(); 536 } 537 538 addBelowHandler(event) 539 { 540 const isHtmlFormat = this.responseFormatSupported 541 && (this.copilotParams.responseFormat ?? 'html') === 'html'; 542 const span = isHtmlFormat 543 ? this.getSpanWithHtml(event.data.result) 544 : this.getSpanWithText(event.data.result); 545 546 let lastSelectedElement = null; 547 if (this.getSelectionText() !== '') 548 { 549 lastSelectedElement = this.getSelection().getRangeAt(0).endContainer; 550 } 551 552 const isSelectionEndOutOfBounds = lastSelectedElement === this.iframeView.element; 553 if (lastSelectedElement && !isSelectionEndOutOfBounds) 554 { 555 lastSelectedElement.after(span); 556 lastSelectedElement.after(BX.Tag.render`<br>`); 557 } 558 else 559 { 560 BX.Dom.append(BX.Tag.render`<br>`, this.contentEditable); 561 BX.Dom.append(span, this.contentEditable); 562 BX.Dom.append(BX.Tag.render`<br>`, this.contentEditable); 563 } 564 565 this.iframeView.ScrollToInsertedText(); 566 this.getSelection().removeAllRanges(); 567 568 this.iframeView.editor.synchro.FromIframeToTextarea(true, true); 569 this.iframeView.editor.synchro.FromTextareaToIframe(true); 570 571 this.copilot.hide(); 572 } 573 574 cancelHandler() 575 { 576 [...this.getResultNodes()][0]?.before(this.insertResultNode); 577 this.removeResultNodes(); 578 if (this.showRect) 579 { 580 this.copilot.adjust(this.getAdjustOptionsForRect(this.showRect)); 581 } 582 } 583 584 convertTextToHtmlSpan(text) 585 { 586 return BX.Tag.render`<span>${text}</span>`; 587 } 588 589 getSpanWithText(text) 590 { 591 const span = BX.Dom.create('span'); 592 span.innerText = text; 593 594 return span; 595 } 596 597 getSpanWithHtml(html) 598 { 599 const span = BX.Dom.create('span'); 600 span.innerHTML = html; 601 602 return span; 603 } 604 605 onWindowKeyDownHandler(event) 606 { 607 if (event.key === "Escape") 608 { 609 this.copilot.hide(); 610 } 611 } 612 613 handleResizeWindow() 614 { 615 if (this.copilot.isShown()) 616 { 617 this.copilot.adjustWidth(this.getCopilotWidth()); 618 } 619 } 620 621 removeResultNodes() 622 { 623 this.getResultNodes().forEach(resultNode => resultNode.remove()); 624 } 625 626 saveResultNodes() 627 { 628 for (const resultNode of this.getResultNodes()) 629 { 630 for (const childNode of resultNode.childNodes) 631 { 632 resultNode.before(childNode.cloneNode(true)); 633 } 634 635 resultNode.remove(); 636 } 637 } 638 639 onContentEditableMouseDown() 640 { 641 this.insertResultNode.remove(); 642 this.hideInvitationLine(); 643 this.hideCopilotButton(); 644 } 645 646 onIframeWindowClick() 647 { 648 this.update(); 649 } 650 651 onContentEditableKeyDown() 652 { 653 this.insertResultNode.remove(); 654 this.updatePopup(); 655 this.hideInvitationLine(); 656 this.hideCopilotButton(); 657 } 658 659 onContentEditableKeyUp(e) 660 { 661 if (e.key === ' ') 662 { 663 return; 664 } 665 666 this.update(); 667 } 668 669 onScrollHandler() 670 { 671 if (this.contentEditable.offsetHeight <= 0) 672 { 673 return; 674 } 675 676 if (this.invitationLineAbsolute.parentNode) 677 { 678 this.showInvitationLine(); 679 } 680
681 if (this.copilotBtnPopup?.isShown() && this.getSelectionText() !== '') 682 { 683 const range = this.getSelection().getRangeAt(0); 684 this.adjustCopilotButton(this.getAdjustOptions(range, true)); 685 } 686 687 if (this.copilot.isShown() && this.getSelectionText() !== '') 688 { 689 const range = this.getSelection().getRangeAt(0); 690 this.copilot.adjust(this.getAdjustOptions(range)); 691 692 return; 693 } 694 695 if (this.getResultNodes().pop()) 696 { 697 this.updatePopup(); 698 } 699 else if (this.adjustmentRect) 700 { 701 this.copilot.adjust(this.getAdjustOptionsForRect(this.adjustmentRect)); 702 } 703 } 704 705 onContentEditableBlur() 706 { 707 setTimeout(() => { 708 this.hideInvitationLine(); 709 }, 0); 710 } 711 712 onIframeFocus() 713 { 714 setTimeout(() => { 715 this.updateInvitationLine(); 716 }, 0); 717 } 718 719 shouldBeShown() 720 { 721 return this.invitationLineAbsolute.offsetWidth !== 0 && !this.copilot.isShown(); 722 } 723 724 show(showFromSpace = false) 725 { 726 this.copilot.setContext(this.contentEditable.innerText); 727 BX.Event.EventEmitter.emit('onHtmlEditorCopilotShow', { contexText: this.contentEditable.innerText }); 728 729 this.insertResultNode.remove(); 730 this.getSelection().getRangeAt(0).insertNode(this.insertResultNode); 731 732 const bindElement = { 733 top: parseInt(this.invitationLineAbsolute.style.top), 734 left: parseInt(this.invitationLineAbsolute.style.left), 735 }; 736 737 if (showFromSpace) 738 { 739 bindElement.top += 28; 740 } 741
742 const containerRect = this.iframeContainer.getBoundingClientRect(); 743 this.adjustmentRect = { 744 bottom: bindElement.top - containerRect.y - window.scrollY, 745 x: bindElement.left - containerRect.x - window.scrollX, 746 width: 0, 747 }; 748 this.showRect = this.adjustmentRect; 749 750 this.copilot.show({ 751 bindElement, 752 showFromSpace, 753 width: this.getCopilotWidth(), 754 }); 755 756 this.hideInvitationLine(); 757 } 758 759 showAtTheBottom() 760 { 761 this.copilot.setContext(this.contentEditable.innerText); 762 BX.Event.EventEmitter.emit('onHtmlEditorCopilotShow', { contexText: this.contentEditable.innerText }); 763 764 this.insertResultNode.remove(); 765 766 let emptyBr; 767 let emptyLine; 768 if (this.needToAppendEmptyElement()) 769 { 770 emptyBr = BX.Tag.render`<br>`; 771 emptyLine = BX.Tag.render`<div></div>`; 772 const lastNode = this.getFilteredChildNodes(this.contentEditable).pop(); 773 if (this.contentEditable.innerText !== '' && lastNode?.tagName !== 'BR') 774 { 775 this.contentEditable.append(BX.Tag.render`<br>`); 776 } 777 this.contentEditable.append(emptyBr); 778 this.contentEditable.append(emptyLine); 779 } 780 781 const lastNode = this.getFilteredChildNodes(this.contentEditable).pop(); 782 const bindElement = this.getAdjustOptions(lastNode).position; 783 784 emptyLine?.remove(); 785 786 this.showRect = this.adjustmentRect; 787 788 if (emptyBr) 789 { 790 emptyBr.before(this.insertResultNode); 791 } 792 else 793 { 794 this.contentEditable.append(this.insertResultNode); 795 } 796 797 this.copilot.show({ 798 bindElement, 799 width: this.getCopilotWidth(), 800 }); 801 802 this.hideInvitationLine(); 803 } 804 805 needToAppendEmptyElement() 806 { 807 const innerText = this.contentEditable.innerText; 808 return !innerText 809 || (innerText.at(-1) !== '\n') 810 || (innerText.at(-2) && innerText.at(-2) !== '\n'); 811 } 812 813 update() 814 { 815 this.updateCopilotButton(); 816 this.updatePopup(); 817 this.updateInvitationLine(); 818 } 819 820 updateCopilotButton() 821 { 822 const shouldShowCopilotButton = !this.copilot.isShown() && this.get
822SelectionText() !== ''; 823 824 if (shouldShowCopilotButton && !this.copilotBtnPopup?.isShown() && this.copilotLoaded) 825 { 826 this.showCopilotButton(); 827 } 828 829 if (!shouldShowCopilotButton) 830 { 831 this.hideCopilotButton(); 832 } 833 } 834 835 showCopilotButton() 836 { 837 if (!this.copilotBtnPopup) 838 { 839 this.copilotBtnPopup = new BX.Main.Popup({ 840 bindElement: this.contentEditable, 841 padding: 6, 842 borderRadius: '6px', 843 content: this.renderCopilotButton(), 844 autoHide: true, 845 }); 846 } 847 848 this.copilotBtnPopup.setMaxWidth(null); 849 this.copilotBtnPopup.setMinWidth(null); 850 this.copilotBtnPopup.setPadding(6); 851 this.adjustCopilotButton(this.getAdjustOptions(this.getSelection().getRangeAt(0), true)); 852 this.copilotBtnPopup.adjustPosition(); 853 854 this.copilotBtnPopup.show(); 855 setTimeout(() => this.updateCopilotButton(), 0); 856 } 857 858 renderCopilotButton() 859 { 860 BX.Runtime.loadExtension('ui.icon-set.main'); 861 862 this.copilotButton = BX.Tag.render` 863 <button class="bxhtmled-copilot-btn" onclick="${this.copilotButtonClickHandler.bind(this)}"> 864 <div class="bxhtmled-copilot-btn-icon ui-icon-set --copilot-ai"></div> 865 ${ BX.message('BXEdCopilotButtonText') } 866 </button> 867 `; 868 869 return this.copilotButton; 870 } 871 872 copilotButtonClickHandler() 873 { 874 BX.Event.EventEmitter.emit('onHtmlEditorCopilotShow', { contexText: this.contentEditable.innerText }); 875 const adjustOptions = this.getAdjustOptions(this.getSelection().getRangeAt(0)); 876 877 this.copilot.setSelectedText(this.getSelection().toString()); 878 this.copilot.show({ 879 bindElement: adjustOptions.position, 880 showFromPopup: true, 881 width: this.getCopilotWidth(), 882 }); 883 this.hideCopilotButton(); 884 } 885 886 adjustCopilotButton(options) 887 { 888 if (options.hide) 889 { 890 this.copilotBtnPopup.setMaxWidth(0); 891 this.copilotBtnPopup.setMinWidth(0); 892 this.copilotBtnPopup.setPadding(0); 893 } 894 else 895 { 896 this.copilotBtnPopup.setMaxWidth(null); 897 this.copilotBtnPopup.setMinWidth(null); 898 this.copilotBtnPopup.setPadding(6); 899 this.copilotBtnPopup.setBindElement(options.position); 900 this.copilotBtnPopup.adjustPosition(); 901 } 902 } 903 904 getCopilotWidth() 905 { 906 return this.contentEditable.offsetWidth - 30; 907 } 908 909 hideCopilotButton() 910 { 911 this.copilotBtnPopup?.close(); 912 } 913 914 updatePopup() 915 { 916 if (!this.copilot.isShown()) 917 { 918 return; 919 } 920 921 const lastResultSpan = this.getResultNodes().pop(); 922 if (!this.isNotFocusedOrCursorAtResultNode(this.getSelection()) || !lastResultSpan) 923 { 924 this.copilot.hide(); 925 926 return; 927 } 928 929 this.copilot.adjust(this.getAdjustOptions(lastResultSpan)); 930 } 931 932 getAdjustOptions(pivot, isCentered = false) 933 { 934 const pivotRect = pivot.getBoundingClientRect(); 935 936 return this.getAdjustOptionsForRectSpaced(pivotRect, isCentered); 937 } 938 939 getAdjustOptionsForRectSpaced(pivotRect, isCentered = false) 940 { 941 const adjustment = this.getAdjustOptionsForRect(pivotRect, isCentered); 942 adjustment.position.top += 10; 943 944 return adjustment; 945 } 946 947 getAdjustOptionsForRect(pivotRect, isCentered = false) 948 { 949 this.adjustmentRect = pivotRect;
950 const containerRect = this.iframeContainer.getBoundingClientRect(); 951 952 return { 953 hide: pivotRect.bottom > this.iframeView.document.documentElement.offsetHeight + 13 || pivotRect.bottom + 13 < 0, 954 position: { 955 top: pivotRect.bottom + containerRect.y + window.scrollY, 956 left: pivotRect.x + containerRect.x + isCentered * (pivotRect.width / 2 - 55) + window.scrollX, 957 }, 958 }; 959 } 960 961 isNotFocusedOrCursorAtResultNode(selection) 962 { 963 if (!selection.focusNode) 964 { 965 return true; 966 } 967 968 return this.getResultNodes().filter(node => selection.focusNode === node || selection.focusNode.parentElement === node).length; 969 } 970 971 getResultNodes() 972 { 973 return [...this.contentEditable.querySelectorAll(`[${this.resultNodeAttr}=true]`)]; 974 } 975 976 updateInvitationLine() 977 { 978 this.insertResultNode.remove(); 979 980 if (this.shouldShowInvitationLine() && this.copilotLoaded) 981 { 982 this.showInvitationLine(); 983 } 984 else 985 { 986 this.hideInvitationLine(); 987 } 988 } 989 990 showInvitationLine() 991 { 992 const selection = this.getSelection(); 993 if (selection.rangeCount === 0) 994 { 995 return false; 996 } 997 998 const lastNode = this.getFilteredChildNodes(this.contentEditable).pop(); 999 if (this.isZwnbspNode(lastNode)) 1000 { 1001 lastNode.replaceWith(BX.Tag.render`<br>`); 1002 } 1003 1004 const range = selection.getRangeAt(0); 1005 range.insertNode(this.invitationLine); 1006
1007 const container = this.iframeContainer.getBoundingClientRect(); 1008 const invitationLineRect = { 1009 top: this.invitationLine.offsetTop + container.y - this.contentEditable.parentElement.scrollTop + window.scrollY, 1010 left: this.invitationLine.offsetLeft + container.x + window.scrollX, 1011 width: this.invitationLine.offsetWidth, 1012 }; 1013 this.invitationLineAbsolute.style.top = `${invitationLineRect.top}px`; 1014 this.invitationLineAbsolute.style.left = `${invitationLineRect.left}px`; 1015 this.invitationLineAbsolute.style.width = `${invitationLineRect.width}px`; 1016 this.invitationLineAbsolute.style.display = ''; 1017 1018 this.hideInvitationLine(); 1019 document.body.append(this.invitationLineAbsolute); 1020 1021 if (!this.invitationLineAbsolute.registered) 1022 { 1023 BX.ZIndexManager.register(this.invitationLineAbsolute); 1024 BX.ZIndexManager.bringToFront(this.invitationLineAbsolute); 1025 this.invitationLineAbsolute.registered = true; 1026 } 1027 1028 if (!this.shouldDisplayInvitationLine()) 1029 { 1030 this.invitationLineAbsolute.style.display = 'none'; 1031 } 1032 } 1033 1034 shouldDisplayInvitationLine() 1035 {
1036 const contentEditableRect = this.iframeContainer.getBoundingClientRect(); 1037 const invitationLineRect = this.invitationLineAbsolute.getBoundingClientRect(); 1038 return invitationLineRect.bottom + 20 < contentEditableRect.bottom && invitationLineRect.top > contentEditableRect.top; 1039 } 1040 1041 hideInvitationLine() 1042 { 1043 this.invitationLine.remove(); 1044 this.invitationLineAbsolute.remove(); 1045 } 1046 1047 getSelectionText() 1048 { 1049 return this.getSelection().toString().replaceAll('\n', '').trim(); 1050 } 1051 1052 getSelection() 1053 { 1054 return this.iframeView.GetSelection(); 1055 } 1056 1057 shouldShowInvitationLine() 1058 { 1059 if (!this.iframeContainer.contains(document.activeElement)) 1060 { 1061 return false; 1062 } 1063 1064 if (this.invitationLineMode === this.invitationLineModes.NONE) 1065 { 1066 return false; 1067 } 1068 1069 if (this.invitationLineMode === this.invitationLineModes.EACH_LINE) 1070 { 1071 return this.isCursorAtStartOfLine(this.getSelection()); 1072 } 1073 1074 return this.isCursorAtNewLine(this.getSelection()); 1075 } 1076 1077 isCursorAtStartOfLine(selection) 1078 { 1079 if (selection.type !== this.selectionTypeCaret) 1080 { 1081 return false; 1082 } 1083 1084 this.removeZwnbspSequence(); 1085 1086 const range = selection.getRangeAt(0); 1087 const contentEditableOffset = parseInt(getComputedStyle(this.contentEditable).paddingLeft); 1088 if (range.getBoundingClientRect().x > contentEditableOffset) 1089 { 1090 return false; 1091 } 1092 1093 const tmpNode = BX.Tag.render`<span style="width: 10px; height: 10px;"></span>`; 1094 range.insertNode(tmpNode); 1095 1096 const previousNode = tmpNode.previousSibling; 1097 const nextNode = tmpNode.nextSibling; 1098 const offset = tmpNode.getBoundingClientRect().x - contentEditableOffset; 1099 1100 if ( 1101 selection.focusNode === this.contentEditable 1102 && nextNode === null 1103 && ( 1104 previousNode?.tagName === 'DIV' || this.isZwnbspNode(previousNode) 1105 ) 1106 ) 1107 { 1108 if (this.isZwnbspNode(previousNode)) 1109 { 1110 if (!previousNode.nextSibling) 1111 { 1112 previousNode.replaceWith(BX.Tag.render`<br>`); 1113 } 1114 else 1115 { 1116 previousNode.remove(); 1117 } 1118 } 1119 1120 tmpNode.remove(); 1121 return false; 1122 } 1123 1124 const afterBr = !previousNode || this.doesNodeMakeLine(previousNode) || tmpNode.nodeName !== '#text' && tmpNode.textContent === ''; 1125 const beforeBr = !nextNode || this.doesNodeMakeLine(nextNode) && nextNode.tagName !== 'TABLE'; 1126 const atStart = offset <= 0; 1127 tmpNode.remove(); 1128 1129 return ((afterBr && beforeBr) || (this.isZwnbspNode(previousNode) && nextNode?.nodeName === '#text')) && atStart; 1130 } 1131 1132 isCursorAtNewLine(selection) 1133 { 1134 if (selection.toString() !== '') 1135 { 1136 return false; 1137 } 1138 1139 this.removeZwnbspSequence(); 1140 1141 const range = selection.getRangeAt(0); 1142 const contentEditableOffset = parseInt(getComputedStyle(this.contentEditable).paddingLeft); 1143 if (range.getBoundingClientRect().x > contentEditableOffset) 1144 { 1145 return false; 1146 } 1147 1148 if (selection.focusNode.outerHTML === '<span><br></span>' || selection.focusNode.outerHTML === '<div><br></div>') 1149 { 1150 selection.focusNode.replaceWith(BX.Tag.render`<br>`); 1151 } 1152 1153 const nodes = this.getFilteredChildNodes(this.contentEditable); 1154 const isEmptyOrOnlyLine = (nodes.length === 0) || (nodes.length === 1 && nodes[0].tagName === 'BR'); 1155 const lastNode = nodes.pop(); 1156 1157 let doesLastNodeMakeLine = false; 1158 if (lastNode?.tagName === 'SPAN') 1159 { 1160 const spanNodes = this.getFilteredChildNodes(lastNode); 1161 const lastSpanNode = spanNodes.pop(); 1162 const oneSpanNodeBeforeLast = spanNodes.pop(); 1163 doesLastNodeMakeLine = this.doesNodeMakeLine(oneSpanNodeBeforeLast) && this.doesNodeMakeLine(lastSpanNode); 1164 } 1165 1166 if (!this.isZwnbspNode(selection.focusNode) && selection.focusNode !== this.contentEditable && lastNode?.tagName !== 'SPAN') 1167 { 1168 return false; 1169 } 1170 1171 const oneNodeBeforeLast = nodes.pop(); 1172 const doLastTwoNodesMakeLine = this.doesNodeMakeLine(oneNodeBeforeLast) && this.doesNodeMakeLine(lastNode) && lastNode?.tagName !== 'BLOCKQUOTE'; 1173 1174 return (isEmptyOrOnlyLine || doLastTwoNodesMakeLine || doesLastNodeMakeLine) && this.isCursorAtTheEndOfDocument(selection); 1175 } 1176 1177 removeZwnbspSequence() 1178 { 1179 const nodesToRemove = []; 1180 this.contentEditable.childNodes.forEach((node) => { 1181 if (this.isZwnbspNode(node) && this.isZwnbspNode(node.nextSibling)) 1182 { 1183 nodesToRemove.push(node); 1184 } 1185 }); 1186 1187 nodesToRemove.forEach((node) => node.remove()); 1188 if (this.contentEditable.innerHTML === this.createZwnbspNode().innerHTML) 1189 { 1190 this.contentEditable.innerHTML = ''; 1191 } 1192 } 1193 1194 getFilteredChildNodes(element) 1195 { 1196 return [...element.childNodes].filter((node) => { 1197 return node.nodeName !== '#text' || this.isZwnbspNode(node) || node.textContent !== '' 1198 }); 1199 } 1200 1201 doesNodeMakeLine(node) 1202 { 1203 if (!node) 1204 { 1205 return false;
1206 } 1207 1208 if (node.outerHTML === '<span><br></span>') 1209 { 1210 node.replaceWith(BX.Tag.render`<br>`); 1211 return true; 1212 } 1213 1214 if (this.isZwnbspNode(node)) 1215 { 1216 if (!node.nextSibling) 1217 { 1218 node.replaceWith(BX.Tag.render`<br>`); 1219 } 1220 else 1221 { 1222 node.remove(); 1223 } 1224 1225 return true; 1226 } 1227 1228 const makingLineNodes = ['BR', 'UL', 'OL', 'PRE', 'TABLE', 'DIV', 'P', 'BLOCKQUOTE']; 1229 1230 if (node.tagName === 'SPAN') 1231 { 1232 return this.doesNodeMakeLine(this.getFilteredChildNodes(node).pop()); 1233 } 1234 1235 return makingLineNodes.includes(node.tagName) || makingLineNodes.includes([...node.childNodes].pop()?.tagName); 1236 } 1237 1238 isZwnbspNode(node) 1239 { 1240 if (!node) 1241 { 1242 return false; 1243 } 1244 1245 return BX.Tag.render`<div>${node.cloneNode()}</div>`.innerHTML === this.createZwnbspNode().innerHTML; 1246 } 1247 1248 createZwnbspNode() 1249 { 1250 return BX.Tag.render`<div></div>`; 1251 } 1252 1253 isCursorAtTheEndOfDocument(selection) 1254 { 1255 const offset = selection.focusOffset; 1256 const node = selection.focusNode; 1257 1258 selection.modify("move", "forward", "character"); 1259 if (offset === selection.focusOffset && node === selection.focusNode) 1260 { 1261 return true; 1262 } 1263 else 1264 { 1265 selection.modify("move", "backward", "character"); 1266 1267 return false; 1268 } 1269 } 1270} 1271 1272function BXEditorIframeView(editor, textarea, container) 1273{ 1274 // Call parrent constructor 1275 BXEditorIframeView.superclass.constructor.apply(this, arguments); 1276 this.name = "wysiwyg"; 1277 this.caretNode = "<br>"; 1278} 1279 1280BX.extend(BXEditorIframeView, BXEditorView); 1281 1282BXEditorIframeView.prototype.OnCreateIframe = function() 1283{ 1284 this.document = this.editor.sandbox.GetDocument(); 1285 this.element = this.document.body; 1286 this.editor.document = this.document; 1287 this.textarea = this.editor.dom.textarea; 1288 this.isFocused = false; 1289 this.InitEventHandlers(); 1290 1291 // Check and init external range library 1292 window.rangy.init(); 1293 1294 if (this.config.isCopilotEnabled && BX.AI?.Copilot) 1295 { 1296 this.copilot = new BXEditorIframeCopilot(this, { 1297 ...this.config.copilotParams, 1298 isMentionUnavailable: this.config.isMentionUnavailable, 1299 }); 1300 } 1301 1302 this.Enable(); 1303}; 1304 1305BXEditorIframeView.prototype.setCopilotContextParameters = function(formData) 1306{ 1307 this.config.copilotParams.contextParameters = { 1308 xmlId: formData[0], 1309 entityId: formData[1], 1310 }; 1311}; 1312 1313BXEditorIframeView.prototype.ScrollToInsertedText = function() 1314{ 1315 this.document.documentElement.scrollTop = this.document.documentElement.scrollHeight; 1316 this.UpdateHeight(); 1317}; 1318 1319BXEditorIframeView.prototype.UpdateHeight = function() 1320{ 1321 this.editor.UpdateHeight(); 1322}; 1323 1324BXEditorIframeView.prototype.GetSelection = function() 1325{ 1326 return this.document.getSelection(); 1327}; 1328 1329BXEditorIframeView.prototype.isCopilotInitialized = function() 1330{ 1331 return this.copilot && this.copilot.copilotLoaded; 1332}; 1333 1334BXEditorIframeView.prototype.Clear = function() 1335{ 1336 //this.element.innerHTML = BX.browser.IsFirefox() ? this.caretNode : ""; 1337 this.element.innerHTML = this.caretNode; 1338}; 1339 1340BXEditorIframeView.prototype.GetValue = function(bParse, bFormat) 1341{ 1342 this.iframeValue = this.IsEmpty() ? "" : this.editor.GetInnerHtml(this.element); 1343 this.iframeValue = this.iframeValue.replaceAll(/<span style="font-family: var\(--ui-font-family-primary, var\(--ui-font-family-helvetica\)\);">(.*?)<\/span>/g, '$1'); 1344 this.iframeValue = this.iframeValue.replace(/<span class="bxhtmled-insert-result"><\/span>/g, ''); 1345 this.editor.On('OnIframeBeforeGetValue', [this.iframeValue]); 1346 if (bParse) 1347 { 1348 this.iframeValue = this.editor.Parse(this.iframeValue, false, bFormat); 1349 } 1350 return this.iframeValue; 1351}; 1352 1353BXEditorIframeView.prototype.SetValue = function(html, bParse) 1354{ 1355 if (bParse) 1356 { 1357 html = this.editor.Parse(html); 1358 } 1359 this.element.innerHTML = html; 1360 // Check last child - if it's block node in the end - add <br> tag there 1361 this.CheckContentLastChild(this.element); 1362 this.editor.On('OnIframeSetValue', [html]); 1363 this.editor.textareaView?.SaveValue(); 1364}; 1365 1366BXEditorIframeView.prototype.Show = function() 1367{ 1368 this.isShown = true; 1369 this.container.style.display = ""; 1370 this.ReInit(); 1371}; 1372 1373BXEditorIframeView.prototype.ReInit = function() 1374{ 1375 // Firefox needs this, otherwise contentEditable becomes uneditable 1376 this.Disable(); 1377 this.Enable(); 1378 1379 this.editor.On('OnIframeReInit'); 1380}; 1381 1382BXEditorIframeView.prototype.Hide = function() 1383{ 1384 this.isShown = false;
1385 this.container.style.display = "none"; 1386}; 1387 1388BXEditorIframeView.prototype.Disable = function() 1389{ 1390 this.element.removeAttribute("contentEditable"); 1391}; 1392 1393BXEditorIframeView.prototype.Enable = function() 1394{ 1395 this.element.setAttribute("contentEditable", "true"); 1396}; 1397 1398BXEditorIframeView.prototype.Focus = function(setToEnd) 1399{ 1400 if (BX.browser.IsIE() && this.HasPlaceholderSet()) 1401 { 1402 this.Clear(); 1403 } 1404 1405 if (!document.querySelector 1406 || this.element.ownerDocument.querySelector(":focus") !== this.element 1407 || !this.IsFocused()) 1408 { 1409 if (BX.browser.IsIOS()) 1410 { 1411 var _this = this; 1412 if (this.focusTimeout) 1413 clearTimeout(this.focusTimeout); 1414 1415 this.focusTimeout = setTimeout(function() 1416 { 1417 var 1418 orScrollTop = document.documentElement.scrollTop || document.body.scrollTop, 1419 orScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft; 1420 BX.focus(_this.element); 1421 window.scrollTo(orScrollLeft, orScrollTop); 1422 }, 200); 1423 } 1424 else 1425 { 1426 BX.focus(this.element); 1427 } 1428 } 1429 1430 if (setToEnd && this.element.lastChild) 1431 { 1432 if (this.element.lastChild.nodeName === "BR") 1433 { 1434 this.editor.selection.SetBefore(this.element.lastChild); 1435 } 1436 else 1437 { 1438 this.editor.selection.SetAfter(this.element.lastChild); 1439 } 1440 } 1441}; 1442 1443BXEditorIframeView.prototype.SetFocusedFlag = function(isFocused) 1444{ 1445 this.isFocused = isFocused; 1446}; 1447 1448BXEditorIframeView.prototype.IsFocused = function() 1449{ 1450 return this.isFocused; 1451}; 1452 1453BXEditorIframeView.prototype.GetTextContent = function(clearInvisibleSpace) 1454{ 1455 var txt = this.editor.util.GetTextContent(this.element); 1456 return clearInvisibleSpace === true ? txt.replace(/\uFEFF/ig, '') : txt; 1457}; 1458 1459BXEditorIframeView.prototype.HasPlaceholderSet = function() 1460{ 1461 return this.textarea && this.GetTextContent() == this.textarea.getAttribute("placeholder"); 1462}; 1463 1464BXEditorIframeView.prototype.IsEmpty = function(clearInvisibleSpace) 1465{ 1466 if (!document.querySelector) 1467 return false; 1468 1469 var 1470 innerHTML = this.element.innerHTML, 1471 elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea"; 1472 1473 return innerHTML === "" || 1474 innerHTML === this.caretNode || 1475 this.HasPlaceholderSet() ||
1476 (this.GetTextContent(clearInvisibleSpace) === "" && !this.element.querySelector(elementsWithVisualValue)); 1477}; 1478 1479BXEditorIframeView.prototype._initObjectResizing = function() 1480{ 1481 var properties = ["width", "height"], 1482 propertiesLength = properties.length, 1483 element = this.element; 1484 1485 this.commands.exec("enableObjectResizing", this.config.allowObjectResizing); 1486 1487 if (this.config.allowObjectResizing) { 1488 // IE sets inline styles after resizing objects 1489 // The following lines make sure _this the width/height css properties 1490 // are copied over to the width/height attributes 1491 if (browser.supportsEvent("resizeend")) { 1492 dom.observe(element, "resizeend", function(event) { 1493 var target = event.target || event.srcElement, 1494 style = target.style, 1495 i = 0, 1496 property; 1497 for(; i<propertiesLength; i++) { 1498 property = properties[i]; 1499 if (style[property]) { 1500 target.setAttribute(property, parseInt(style[property], 10)); 1501 style[property] = ""; 1502 } 1503 } 1504 // After resizing IE sometimes forgets to remove the old resize handles 1505 redraw(element); 1506 }); 1507 } 1508 } else { 1509 if (browser.supportsEvent("resizestart")) { 1510 dom.observe(element, "resizestart", function(event) { event.preventDefault(); }); 1511 } 1512 } 1513}; 1514 1515/** 1516 * With "setActive" IE offers a smart way of focusing elements without scrolling them into view: 1517 * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx 1518 * 1519 * Other browsers need a more hacky way: (pssst don't tell my mama) 1520 * In order to prevent the element being scrolled into view when focusing it, we simply 1521 * move it out of the scrollable area, focus it, and reset it's position 1522 */ 1523 1524var focusWithoutScrolling = function(element) 1525{ 1526 if (element.setActive) { 1527 // Following line could cause a js error when the textarea is invisible 1528 // See https://github.com/xing/wysihtml5/issues/9 1529 try { element.setActive(); } catch(e) {} 1530 } else { 1531 var elementStyle = element.style, 1532 originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop, 1533 originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft, 1534 originalStyles = { 1535 position: elementStyle.position, 1536 top: elementStyle.top, 1537 left: elementStyle.left, 1538 WebkitUserSelect: elementStyle.WebkitUserSelect 1539 }; 1540 1541 dom.setStyles({ 1542 position: "absolute", 1543 top: "-99999px", 1544 left: "-99999px", 1545 // Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother 1546 WebkitUserSelect: "none" 1547 }).on(element); 1548 1549 element.focus(); 1550 1551 dom.setStyles(originalStyles).on(element); 1552 1553 if (win.scrollTo) { 1554 // Some browser extensions unset this method to prevent annoyances 1555 // "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100 1556 // Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1 1557 win.scrollTo(originalScrollLeft, originalScrollTop); 1558 } 1559 } 1560}; 1561 1562 1563/** 1564 * Taking care of events 1565 * - Simulating 'change' event on contentEditable element 1566 * - Handling drag & drop logic 1567 * - Catch paste events 1568 * - Dispatch proprietary newword:composer event 1569 * - Keyboard shortcuts 1570 */ 1571 BXEditorIframeView.prototype.InitEventHandlers = function() 1572 { 1573 var 1574 _this = this, 1575 editor = this.editor, 1576 value = this.GetValue(), 1577 element = this.element, 1578 iframeWindow = this.editor.sandbox.GetWindow(), 1579 _element = !BX.browser.IsOpera() ? element : this.editor.sandbox.GetWindow(); 1580 1581 if (this._eventsInitedObject && this._eventsInitedObject === _element) 1582 return; 1583 1584 this._eventsInitedObject = _element; 1585 1586 BX.bind(_element, "focus", function() 1587 { 1588 editor.On("OnIframeFocus"); 1589 _this.isFocused = true; 1590 if (value !== _this.GetValue()) 1591 { 1592 BX.onCustomEvent(editor, "OnIframeChange"); 1593 } 1594 _this.copilot?.onIframeFocus(); 1595 }); 1596 1597 BX.bind(_element, "blur", function() 1598 { 1599 editor.On("OnIframeBlur"); 1600 _this.isFocused = false; 1601 setTimeout(function(){value = _this.GetValue();}, 0); 1602 _this.copilot?.onContentEditableBlur(); 1603 }); 1604 1605 BX.bind(_element, "contextmenu", function(e) 1606 { 1607 if(e && !e.ctrlKey && !e.shiftKey && (BX.getEventButton(e) & BX.MSRIGHT)) 1608 { 1609 editor.On("OnIframeContextMenu", [e, e.target || e.srcElement, _this.contMenuRangeCollapsed]); 1610 } 1611 }); 1612 1613 BX.bind(_element, "mousedown", function(e) 1614 { 1615 var 1616 range = editor.selection.GetRange(), 1617 target = e.target || e.srcElement, 1618 bxTag = editor.GetBxTag(target); 1619 1620 //mantis: 71174 1621 _this.contMenuRangeCollapsed = range && range.collapsed; 1622 if (editor.synchro.IsSyncOn()) 1623 { 1624 editor.synchro.StopSync(); 1625 } 1626 1627 if (BX.browser.IsIE10() || BX.browser.IsIE11()) 1628 { 1629 editor.phpParser.RedrawSurrogates(); 1630 } 1631 1632 if (target.nodeName == 'BODY' || !editor.phpParser.CheckParentSurrogate(target)) 1633 { 1634 setTimeout(function() 1635 { 1636 range = editor.selection.GetRange(); 1637 if (range && range.collapsed && range.startContainer && range.startContainer == range.endContainer) 1638 { 1639 var surr = editor.phpParser.CheckParentSurrogate(range.startContainer); 1640 if (surr) 1641 { 1642 editor.selection.SetInvisibleTextAfterNode(surr); 1643 editor.selection.SetInvisibleTextBeforeNode(surr); 1644 } 1645 } 1646 }, 10); 1647 } 1648 1649 editor.action.actions.quote.checkSpaceAfterQuotes(target); 1650 1651 editor.selection.SaveRange(false); 1652 setTimeout(function(){editor.selection.SaveRange(false);}, 10); 1653 editor.On("OnIframeMouseDown", [e, target, bxTag]); 1654 }); 1655 1656 BX.bind(_element, "touchend", function(){_this.Focus();}); 1657 BX.bind(_element, "touchstart", function(){_this.Focus();}); 1658 1659 BX.bind(_element, "click", function(e) 1660 { 1661 var 1662 target = e.target || e.srcElement; 1663 editor.On("OnIframeClick", [e, target]); 1664 }); 1665 1666 BX.bind(_element, "dblclick", function(e) 1667 { 1668 var 1669 target = e.target || e.srcElement; 1670 editor.On("OnIframeDblClick", [e, target]); 1671 }); 1672 1673 BX.bind(_element, "mouseup", function(e) 1674 { 1675 var target = e.target || e.srcElement; 1676 if (!editor.synchro.IsSyncOn()) 1677 { 1678 editor.synchro.StartSync(); 1679 } 1680 1681 editor.On("OnIframeMouseUp", [e, target]); 1682 }); 1683 1684 BX.bind(iframeWindow, 'click', () => this.copilot?.onIframeWindowClick()); 1685 1686 // Mantis: 90137 1687 //if (BX.browser.IsIOS()) 1688 //{ 1689 // // When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus 1690 // // but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible)
1691 // // We prevent _this by focusing a temporary input element which immediately loses focus 1692 // BX.bind(iframeWindow, "blur", function() 1693 // { 1694 // var 1695 // orScrollTop = document.documentElement.scrollTop || document.body.scrollTop, 1696 // orScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft, 1697 // input = BX.create('INPUT', { 1698 // props:{type: 'text', value: ''} 1699 // }, iframeWindow.ownerDocument); 1700 // 1701 // try 1702 // { 1703 // editor.selection.InsertNode(input); 1704 // } 1705 // catch(e) 1706 // { 1707 // iframeWindow.appendChild(input); 1708 // } 1709 // 1710 // BX.focus(input); 1711 // BX.remove(input); 1712 // window.scrollTo(orScrollLeft, orScrollTop); 1713 // }); 1714 //} 1715 1716 // --------- Drag & Drop events --------- 1717 BX.bind(element, "dragover", function(){editor.On("OnIframeDragOver", arguments);}); 1718 BX.bind(element, "dragenter", function(){editor.On("OnIframeDragEnter", arguments);}); 1719 BX.bind(element, "dragleave", function(){editor.On("OnIframeDragLeave", arguments);}); 1720 BX.bind(element, "dragexit", function(){editor.On("OnIframeDragExit", arguments);}); 1721 BX.bind(element, "drop", function(){editor.On("OnIframeDrop", arguments);}); 1722 1723 // Chrome & Safari & Firefox only fire the ondrop/ondragend/... events when the ondragover event is cancelled 1724 //if (BX.browser.IsChrome() || BX.browser.IsFirefox()) 1725 // TODO: Truobles with firefox during selections http://jabber.bx/view.php?id=49370 1726 if (BX.browser.IsFirefox()) 1727 { 1728 BX.bind(element, "dragover", function(e) 1729 { 1730 e.preventDefault(); 1731 }); 1732 BX.bind(element, "dragenter", function(e) 1733 { 1734 e.preventDefault(); 1735 }); 1736 } 1737 1738 BX.bind(element, 'drop', BX.delegate(this.OnPasteHandler, this)); 1739 1740 BX.bind(element, 'paste', (clipboardEvent) => { 1741 const event = new BX.Event.BaseEvent({ data: { clipboardEvent: clipboardEvent } }); 1742 BX.Event.EventEmitter.emitAsync(this.editor, 'BXEditor:onBeforePasteAsync', event).then(() => { 1743 if (!event.isDefaultPrevented()) 1744 { 1745 this.OnPasteHandler(clipboardEvent); 1746 } 1747 }); 1748 }); 1749 1750 BX.bind(element, "keyup", function(e) 1751 { 1752 var 1753 keyCode = e.keyCode, 1754 target = editor.selection.GetSelectedNode(true); 1755 1756 _this.SetFocusedFlag(true); 1757 if (keyCode === editor.KEY_CODES['space'] || keyCode === editor.KEY_CODES['enter']) 1758 { 1759 if (keyCode === editor.KEY_CODES['enter']) 1760 { 1761 _this.OnEnterHandlerKeyUp(e, keyCode, target); 1762 } 1763 editor.On("OnIframeNewWord"); 1764 } 1765 else 1766 { 1767 _this.OnKeyUpArrowsHandler(e, keyCode); 1768 } 1769 1770 editor.selection.SaveRange(); 1771 editor.On('OnIframeKeyup', [e, keyCode, target]); 1772 1773 // Mantis:#67998 1774 if (keyCode === editor.KEY_CODES['backspace'] && BX.browser.IsChrome() 1775 && target && target.nodeType == '3' && target.nextSibling && target.nextSibling.nodeType == '3') 1776 { 1777 _this.editor.selection.ExecuteAndRestoreSimple(function() 1778 { 1779 _this.editor.util.SetTextContent(target, _this.editor.util.GetTextContent(target) + _this.editor.util.GetTextContent(target.nextSibling)); 1780 target.nextSibling.parentNode.removeChild(target.nextSibling); 1781 }); 1782 } 1783 1784 if (!editor.util.FirstLetterSupported() && _this.editor.parser.firstNodeCheck) 1785 { 1786 _this.editor.parser.FirstLetterCheckNodes('', '', true); 1787 } 1788 1789 //mantis:91555, mantis:93629 1790 if (BX.browser.IsChrome()) 1791 { 1792 if (_this.stopBugusScrollTimeout) 1793 clearTimeout(_this.stopBugusScrollTimeout); 1794 _this.stopBugusScrollTimeout = setTimeout(function(){_this.stopBugusScroll = false;}, 200); 1795 } 1796 1797 _this.copilot?.onContentEditableKeyUp(e); 1798 }); 1799 1800 BX.bind(this.document, 'scroll', () => this.copilot?.onScrollHandler()); 1801 1802 BX.bind(element, "mousedown", function(e) 1803 { 1804 var target = e.target || e.srcElement; 1805 if (!editor.util.CheckImageSelectSupport() && target.nodeName === 'IMG') 1806 { 1807 editor.selection.SelectNode(target); 1808 } 1809 1810 // Handle mousedown for "code" element in IE 1811 if (!editor.util.CheckPreCursorSupport() && target.nodeName === 'PRE') 1812 { 1813 var selectedNode = editor.selection.GetSelectedNode(true); 1814 if (selectedNode && selectedNode != target) 1815 { 1816 _this.FocusPreElement(target, true); 1817 } 1818 } 1819 1820 _this.copilot?.onContentEditableMouseDown(); 1821 }); 1822 1823 BX.bind(element, "keydown", BX.proxy(this.KeyDown, this)); 1824 1825 // Workaround for chrome bug with bugus scrolling to the top of the page (mantis:91555, mantis:93629) 1826 if (BX.browser.IsChrome()) 1827 { 1828 BX.bind(window, "scroll", BX.proxy(function(e) 1829 { 1830 if (_this.stopBugusScroll) 1831 { 1832 if ((!_this.savedScroll || !_this.savedScroll.scrollTop) && _this.lastSavedScroll) 1833 _this.savedScroll = _this.lastSavedScroll; 1834 1835 if (_this.savedScroll && !_this.lastSavedScroll) 1836 _this.lastSavedScroll = _this.savedScroll; 1837 _this._RestoreScrollTop(); 1838 } 1839 }, this)); 1840 } 1841 1842 // Show urls and srcs in tooltip when hovering links or images 1843 var nodeTitles = { 1844 IMG: BX.message.SrcTitle + ": ", 1845 A: BX.message.UrlTitle + ": " 1846 }; 1847 1848 BX.bind(element, "mouseover", function(e) 1849 { 1850 var 1851 target = e.target || e.srcElement, 1852 value = (target.getAttribute("href") || target.getAttribute("src")), 1853 nodeName = target.nodeName; 1854 1855 if (nodeTitles[nodeName] 1856 && !target.hasAttribute("title") 1857 && value 1858 && value.indexOf('data:image/') === -1 1859 ) 1860 { 1861 target.setAttribute("title", nodeTitles[nodeName] + value); 1862 target.setAttribute("data-bx-clean-attribute", "title"); 1863 } 1864 }); 1865 1866 this.editor.InitClipboardHandler(); 1867 }; 1868 1869 BXEditorIframeView.prototype.KeyDown = function(e) 1870 { 1871 var 1872 _this = this, 1873 keyCode = e.keyCode, 1874 KEY_CODES = this.editor.KEY_CODES; 1875 1876 this.SetFocusedFlag(true); 1877 this.editor.iframeKeyDownPreventDefault = false;
1878 1879 // Workaround for chrome bug with bugus scrollint to the top of the page (mantis:91555, mantis:93629) 1880 if (BX.browser.IsChrome()) 1881 { 1882 this.stopBugusScroll = true; 1883 this.savedScroll = BX.GetWindowScrollPos(document); 1884 } 1885 1886 var 1887 command = this.editor.SHORTCUTS[keyCode], 1888 selectedNode = this.editor.selection.GetSelectedNode(true), 1889 range = this.editor.selection.GetRange(), 1890 body = this.document.body, 1891 parent; 1892 1893 if (e.key === ' ' && this.copilot?.shouldBeShown()) 1894 { 1895 this.copilot.show(true); 1896 e.preventDefault(); 1897 1898 return; 1899 } 1900 1901 this.copilot?.onContentEditableKeyDown(); 1902 1903 if ((BX.browser.IsIE() || BX.browser.IsIE10() || BX.browser.IsIE11()) && 1904 !BX.util.in_array(keyCode, [16, 17, 18, 20, 65, 144, 37, 38, 39, 40])) 1905 { 1906 if (selectedNode && selectedNode.nodeName == "BODY" 1907 || 1908 range.startContainer && range.startContainer.nodeName == "BODY" 1909 || 1910 (range.startContainer == body.firstChild && 1911 range.endContainer == body.lastChild && 1912 range.startOffset == 0 && 1913 range.endOffset == body.lastChild.length)) 1914 { 1915 BX.addCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._IEBodyClearHandler, this)); 1916 } 1917 } 1918 1919 // Last symbol in iframe and new paragraph in IE 1920 if ((BX.browser.IsIE() || BX.browser.IsIE10() || BX.browser.IsIE11()) && 1921 keyCode == KEY_CODES['backspace']) 1922 { 1923 BX.addCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._IEBodyClearHandlerEx, this)); 1924 } 1925 1926 this.isUserTyping = true; 1927 if (this.typingTimeout) 1928 { 1929 this.typingTimeout = clearTimeout(this.typingTimeout); 1930 } 1931 this.typingTimeout = setTimeout(function() 1932 { 1933 _this.isUserTyping = false; 1934 }, 1000); 1935 1936 this.editor.synchro.StartSync(200); 1937 1938 this.editor.On('OnIframeKeydown', [e, keyCode, command, selectedNode]); 1939 1940 if (this.editor.iframeKeyDownPreventDefault) 1941 return BX.PreventDefault(e); 1942 1943 // Handle Shortcuts 1944 if ((e.ctrlKey || e.metaKey) && !e.altKey && command) 1945 { 1946 this.editor.action.Exec(command); 1947 return BX.PreventDefault(e); 1948 } 1949 1950 // Bug mantis: #59759 workaround *Chrome only 1951 // Begin 1952 if ( 1953 keyCode === KEY_CODES['backspace'] && 1954 range.startOffset == 0 && 1955 range.startContainer.nodeType == 3 && 1956 range.startContainer.parentNode.firstChild == range.startContainer && 1957 range.startContainer.parentNode && 1958 range.startContainer.parentNode.nodeName == 'BLOCKQUOTE' && 1959 range.startContainer.parentNode.className 1960 ) 1961 { 1962 range.startContainer.parentNode.className = ''; 1963 } 1964 1965 if ( 1966 !BX.browser.IsSafari() 1967 && (keyCode === KEY_CODES['backspace'] || keyCode === KEY_CODES['delete']) 1968 && range.startContainer.nodeName === '#text' 1969 && range.startContainer.previousSibling?.nodeName === 'BLOCKQUOTE' 1970 && range.startContainer.nextSibling?.nodeName !== 'BR' 1971 && range.startContainer.textContent.length === 1 1972 ) 1973 { 1974 range.startContainer.before(BX.Tag.render`<span></span>`.innerHTML); 1975 } 1976 1977 if ( 1978 keyCode === KEY_CODES['delete'] && 1979 range.collapsed && 1980 range.endContainer.nodeType == 3 && 1981 range.endOffset == range.endContainer.length 1982 ) 1983 { 1984 var next = this.editor.util.GetNextNotEmptySibling(range.endContainer); 1985 if (next) 1986 { 1987 if(next.nodeName == 'BR') 1988 { 1989 next = this.editor.util.GetNextNotEmptySibling(next); 1990 } 1991 1992 if (next && next.nodeName == 'BLOCKQUOTE' && next.className) 1993 { 1994 next.className = ''; 1995 } 1996 } 1997 } 1998 // END: Bug mantis: #59759 1999 2000 // Clear link with image 2001 if (selectedNode && selectedNode.nodeName === "IMG" && 2002 (keyCode === KEY_CODES['backspace'] || keyCode === KEY_CODES['delete'])) 2003 { 2004 parent = selectedNode.parentNode; 2005 parent.removeChild(selectedNode); // delete image 2006 2007 // Parent - is LINK, and it's hasn't got any other childs 2008 if (parent.nodeName === "A" && !parent.firstChild) 2009 { 2010 parent.parentNode.removeChild(parent); 2011 } 2012 2013 setTimeout(function(){_this.editor.util.Refresh(_this.element);}, 0); 2014 BX.PreventDefault(e); 2015 } 2016 2017 if (range.collapsed && this.OnKeyDownArrowsHandler(e, keyCode, range) === false) 2018 { 2019 return false; 2020 } 2021 2022 // Handle Ctrl+Enter 2023 if ((e.ctrlKey || e.metaKey) && !e.altKey && keyCode === KEY_CODES["enter"]) 2024 { 2025 if (this.IsFocused()) 2026 this.editor.On("OnIframeBlur"); 2027 2028 this.editor.On('OnCtrlEnter', [e, this.editor.GetViewMode()]); 2029 return BX.PreventDefault(e); 2030 } 2031 2032 // Firefox's bug it remove first node for customized lists 2033 if (BX.browser.IsFirefox() && selectedNode && (keyCode === KEY_CODES["delete"] || keyCode === KEY_CODES["backspace"])) 2034 { 2035 var li = selectedNode.nodeName == 'LI' ? selectedNode : BX.findParent(selectedNode, {tag: 'LI'}, body); 2036 if (li && li.firstChild && li.firstChild.nodeName == 'I') 2037 { 2038 var ul = BX.findParent(li, {tag: 'UL'}, body); 2039 if (ul) 2040 { 2041 var customBullitClass = this.editor.action.actions.insertUnorderedList.getCustomBullitClass(ul); 2042 if (customBullitClass) 2043 { 2044 setTimeout(function() 2045 { 2046 if (ul && li && li.innerHTML !== '') 2047 { 2048 _this.editor.action.actions.insertUnorderedList.checkCustomBullitList(ul, customBullitClass); 2049 } 2050 }, 0); 2051 } 2052 } 2053 } 2054 } 2055 2056 if (!this.editor.util.FirstLetterSupported() && 2057 _this.editor.parser.firstNodeCheck && 2058 keyCode === this.editor.KEY_CODES['backspace']) 2059 { 2060 _this.editor.parser.FirstLetterBackspaceHandler(range); 2061 } 2062 2063 // Handle "Enter" 2064 if (!e.shiftKey && (keyCode === KEY_CODES["enter"] || keyCode === KEY_CODES["backspace"])) 2065 { 2066 return this.OnEnterHandler(e, keyCode, selectedNode, range); 2067 } 2068 2069 if (keyCode === KEY_CODES["pageUp"] || keyCode === KEY_CODES["pageDown"]) 2070 { 2071 this.savedScroll = BX.GetWindowScrollPos(document); 2072 BX.addCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._RestoreScrollTop, this)); 2073 setTimeout(BX.proxy(this._RestoreScrollTop, this), 0); 2074 } 2075 }; 2076 2077 BXEditorIframeView.prototype._RestoreScrollTop = function(e) 2078 { 2079 if (this.savedScroll) 2080 { 2081 window.scrollTo(this.savedScroll.scrollLeft, this.savedScroll.scrollTop); 2082 this.savedScroll = null; 2083 } 2084 BX.removeCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._RestoreScrollTop, this)); 2085 }; 2086 2087 BXEditorIframeView.prototype._IEBodyClearHandler = function(e) 2088 { 2089 var 2090 p = this.document.body.firstChild; 2091 2092 if (e.keyCode == this.editor.KEY_CODES['enter'] && p.nodeName == "P" && p != this.document.body.lastChild) 2093 { 2094 if (p.innerHTML && p.innerHTML.toLowerCase() == '<br>') 2095 { 2096 var newPar = p.nextSibling; 2097 this.editor.util.ReplaceWithOwnChildren(p); 2098 p = newPar; 2099 } 2100 } 2101 2102 if (p && p.nodeName == "P" && p == this.document.body.lastChild) 2103 { 2104 this.editor.util.ReplaceWithOwnChildren(p); 2105 } 2106 BX.removeCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._IEBodyClearHandler, this)); 2107 }; 2108 2109 BXEditorIframeView.prototype._IEBodyClearHandlerEx = function(e) 2110 { 2111 var p = this.document.body.firstChild; 2112 2113 if (e.keyCode == this.editor.KEY_CODES['backspace'] && 2114 p && p.nodeName == "P" && p == this.document.body.lastChild && 2115 (this.editor.util.IsEmptyNode(p, true, true) || p.innerHTML && p.innerHTML.toLowerCase() == '<br>')) 2116 { 2117 this.editor.util.ReplaceWithOwnChildren(p); 2118 } 2119 2120 BX.removeCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._IEBodyClearHandlerEx, this)); 2121 }; 2122 2123 BXEditorIframeView.prototype.OnEnterHandler = function(e, keyCode, selectedNode, range) 2124 { 2125 // TODO: check it again later maybe chrome will fix it 2126 // mantis: 55872. Chrome 38 rendering bug workaround 2127 if (BX.browser.IsChrome()) 2128 { 2129 this.document.body.style.minHeight = (parseInt(this.document.body.style.minHeight) + 1) + 'px'; 2130 // mantis: 60033 2131 this.document.body.style.minHeight = (parseInt(this.document.body.style.minHeight) - 1) + 'px'; 2132 } 2133
2134 // Check selectedNode 2135 if (!selectedNode) 2136 { 2137 return; 2138 } 2139 2140 var _this = this; 2141 function unwrap(node) 2142 { 2143 if (node) 2144 { 2145 if (node.nodeName !== "P" && node.nodeName !== "DIV") 2146 { 2147 node = BX.findParent(node, function(n) 2148 { 2149 return n.nodeName === "P" || n.nodeName === "DIV"; 2150 }, _this.document.body); 2151 } 2152 2153 var emptyNode = _this.editor.util.GetInvisibleTextNode(); 2154 if (node) 2155 { 2156 node.parentNode.insertBefore(emptyNode, node); 2157 _this.editor.util.ReplaceWithOwnChildren(node); 2158 _this.editor.selection.SelectNode(emptyNode); 2159 } 2160 } 2161 } 2162 2163 var 2164 list, br, blockElement, 2165 blockTags = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"], 2166 listTags = ["UL", "OL", "MENU"]; 2167 2168 if (BX.util.in_array(selectedNode.nodeName, blockTags)) 2169 { 2170 blockElement = selectedNode; 2171 } 2172 else 2173 { 2174 blockElement = BX.findParent(selectedNode, function(n) 2175 { 2176 return BX.util.in_array(n.nodeName, blockTags); 2177 }, this.document.body); 2178 } 2179 2180 if (blockElement) 2181 { 2182 if (blockElement.nodeName === "LI") 2183 { 2184 if (keyCode === _this.editor.KEY_CODES["enter"] && blockElement && blockElement.parentNode) 2185 { 2186 var bullitClass = _this.editor.action.actions.insertUnorderedList.getCustomBullitClass(blockElement.parentNode); 2187 } 2188 2189 // Some browsers create <p> elements after leaving a list 2190 // check after keydown of backspace and return whether a <p> got inserted and unwrap it 2191 setTimeout(function() 2192 { 2193 var node = _this.editor.selection.GetSelectedNode(true); 2194 if (node) 2195 { 2196 list = BX.findParent(node, function(n) 2197 { 2198 return BX.util.in_array(n.nodeName, listTags); 2199 }, _this.document.body); 2200 2201 // Check if it's list with custom styled bullits - we have to check it all items have same style 2202 if (keyCode === _this.editor.KEY_CODES["enter"] && blockElement && blockElement.parentNode) 2203 { 2204 _this.editor.action.actions.insertUnorderedList.checkCustomBullitList(blockElement.parentNode, bullitClass, true); 2205 } 2206 2207 // mantis: 82028, 83178 2208 if (list && BX.browser.IsChrome() && keyCode === _this.editor.KEY_CODES["enter"]) 2209 { 2210 var i, li = list.getElementsByTagName('LI'); 2211 2212 for (i = 0; i < li.length; i++) 2213 { 2214 //&65279; === \uFEFF - invisible space 2215 li[i].innerHTML = li[i].innerHTML.replace(/\uFEFF/ig, ''); 2216 } 2217 } 2218 2219 if (!list) 2220 { 2221 unwrap(node); 2222 } 2223 } 2224 }, 0); 2225 } 2226 else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === this.editor.KEY_CODES["enter"]) 2227 { 2228 setTimeout(function() 2229 { 2230 unwrap(_this.editor.selection.GetSelectedNode()); 2231 }, 0); 2232 } 2233 2234 return true; 2235 } 2236 2237 if (keyCode === this.editor.KEY_CODES["enter"] && this.editor.action.IsSupported('insertLineBreak')) 2238 { 2239 if (BX.browser.IsIE10() || BX.browser.IsIE11()) 2240 { 2241 this.editor.action.Exec('insertHTML', '<br>' + this.editor.INVISIBLE_SPACE); 2242 } 2243 else if(BX.browser.IsChrome()) 2244 { 2245 this.editor.action.Exec('insertLineBreak'); 2246 2247 const getChromeVersion = () => { 2248 const raw = navigator?.userAgent?.match(/Chrom(e|ium)\/(\d+)\./); 2249 2250 return raw ? parseInt(raw[2], 10) : 0; 2251 }; 2252 2253 // Bug in Chrome - when you press enter but it put carret on the prev string 2254 // Chrome 43.0.2357 in Mac puts visible space instead of invisible 2255 if (BX.browser.IsMac() || getChromeVersion() === 0 || getChromeVersion() >= 134) 2256 { 2257 return BX.PreventDefault(e); 2258 } 2259 else 2260 { 2261 this.editor.action.Exec('insertHTML', this.editor.INVISIBLE_SPACE); 2262 } 2263 } 2264 else 2265 { 2266 this.editor.action.Exec('insertLineBreak'); 2267 } 2268 return BX.PreventDefault(e); 2269 } 2270 2271 if ((BX.browser.IsChrome() || BX.browser.IsIE10() || BX.browser.IsIE11()) && keyCode == this.editor.KEY_CODES['backspace'] && range.collapsed) 2272 { 2273 var checkNode = BX.create('SPAN', false, this.document); 2274 this.editor.selection.InsertNode(checkNode); 2275 var prev = checkNode.previousSibling; 2276 if (prev && prev.nodeType == 3 && this.editor.util.IsEmptyNode(prev, false, false)) 2277 { 2278 BX.remove(prev); 2279 } 2280 this.editor.selection.SetBefore(checkNode); 2281 BX.remove(checkNode); 2282 } 2283 }; 2284 2285 BXEditorIframeView.prototype.OnEnterHandlerKeyUp = function(e, keyCode, node) 2286 { 2287 // Clean class of all block nodes when they created after Enter pressing 2288 // All new Ps and DIVs should be without classNames 2289 if (node) 2290 { 2291 var _this = this; 2292 if (!BX.util.in_array(node.nodeName, this.editor.GetBlockTags())) 2293 { 2294 node = BX.findParent(node, function(n) 2295 { 2296 return BX.util.in_array(n.nodeName, _this.editor.GetBlockTags()); 2297 }, this.document.body); 2298 } 2299 2300 if (node && BX.util.in_array(node.nodeName, this.editor.GetBlockTags())) 2301 { 2302 var html = BX.util.trim(node.innerHTML).toLowerCase(); 2303 if (this.editor.util.IsEmptyNode(node, true, true) || html == '' || html == '<br>') 2304 { 2305 node.removeAttribute("class"); 2306 } 2307 } 2308 } 2309 }; 2310 2311 BXEditorIframeView.prototype.OnKeyDownArrowsHandler = function(e, keyCode, range) 2312 { 2313 var 2314 node, parentNode, nextNode, prevNode, 2315 KC = this.editor.KEY_CODES; 2316 2317 this.keyDownRange = range; 2318 2319 if (keyCode === KC['right'] || keyCode === KC['down']) 2320 { 2321 node = range.endContainer; 2322 nextNode = node ? node.nextSibling : false;
2323 parentNode = node ? node.parentNode : false; 2324 2325 if ( 2326 node.nodeType == 3 && node.length == range.endOffset 2327 && parentNode && parentNode.nodeName !== 'BODY' 2328 && (!nextNode || (nextNode && nextNode.nodeName == 'BR' && !nextNode.nextSibling)) 2329 && (this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode)) 2330 ) 2331 { 2332 this.editor.selection.SetInvisibleTextAfterNode(parentNode, true); 2333 return BX.PreventDefault(e); 2334 } 2335 else if( 2336 node.nodeType == 3 && this.editor.util.IsEmptyNode(node) 2337 && nextNode 2338 && (this.editor.util.IsBlockElement(nextNode) || this.editor.util.IsBlockNode(nextNode)) 2339 ) 2340 { 2341 BX.remove(node); 2342 if (nextNode.firstChild) 2343 { 2344 this.editor.selection.SetBefore(nextNode.firstChild); 2345 } 2346 else 2347 { 2348 this.editor.selection.SetAfter(nextNode); 2349 } 2350 return BX.PreventDefault(e); 2351 } 2352 else if ( 2353 node.nodeType == 3 && node.length == range.endOffset 2354 && parentNode && parentNode.nodeName !== 'BODY' 2355 && nextNode && nextNode.nodeName == 'BR' 2356 && !nextNode.nextSibling 2357 && (this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode)) 2358 ) 2359 { 2360 this.editor.selection.SetInvisibleTextAfterNode(parentNode, true); 2361 return BX.PreventDefault(e); 2362 } 2363 2364 } 2365 else if (keyCode === KC['left'] || keyCode === KC['up']) 2366 { 2367 node = range.startContainer; 2368 parentNode = node ? node.parentNode : false; 2369 prevNode = node ? node.previousSibling : false; 2370 2371 if ( 2372 node.nodeType == 3 && range.endOffset === 0 2373 && parentNode && parentNode.nodeName !== 'BODY' 2374 && !prevNode 2375 && (this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode)) 2376 ) 2377 { 2378 this.editor.selection.SetInvisibleTextBeforeNode(parentNode); 2379 return BX.PreventDefault(e); 2380 } 2381 else if( 2382 node.nodeType == 3 && this.editor.util.IsEmptyNode(node) 2383 && prevNode 2384 && (this.editor.util.IsBlockElement(prevNode) || this.editor.util.IsBlockNode(prevNode)) 2385 ) 2386 { 2387 BX.remove(node); 2388 if (prevNode.lastChild) 2389 { 2390 this.editor.selection.SetAfter(prevNode.lastChild); 2391 } 2392 else 2393 { 2394 this.editor.selection.SetBefore(prevNode); 2395 } 2396 return BX.PreventDefault(e); 2397 } 2398 } 2399 2400 return true; 2401 }; 2402 2403 BXEditorIframeView.prototype.OnKeyUpArrowsHandler = function(e, keyCode) 2404 { 2405 var 2406 _this = this, 2407 pre, prevToSur, nextToSur, 2408 keyDownNode, keyDownPre, 2409 range = this.editor.selection.GetRange(), 2410 node, parentNode, nextNode, prevNode, isEmpty, isSur, sameLastRange, 2411 startCont, endCont, startIsSur, endIsSur, 2412 KC = this.editor.KEY_CODES; 2413 2414 // Arrows right or down 2415 if (keyCode === KC['right'] || keyCode === KC['down']) 2416 { 2417 this.editor.selection.GetStructuralTags(); 2418 // Moving cursor by arrows (right & down) 2419 if (range.collapsed) 2420 { 2421 node = range.endContainer; 2422 2423 isEmpty = this.editor.util.IsEmptyNode(node); 2424 // We check if last range was the same - it means that cursor doesn't 2425 // moved when user tried to move it 2426 sameLastRange = this.editor.selection.CheckLastRange(range); 2427 nextNode = node.nextSibling; 2428 2429 if (!this.editor.util.CheckPreCursorSupport()) 2430 { 2431 if (node.nodeName === 'PRE') 2432 { 2433 pre = node; 2434 } 2435 else if (node.nodeType == 3) 2436 { 2437 pre = BX.findParent(node, {tag: 'PRE'}, this.element); 2438 } 2439 2440 if(pre) 2441 { 2442 if (this.keyDownRange) 2443 { 2444 keyDownNode = this.keyDownRange.endContainer; 2445 keyDownPre = keyDownNode == pre ? pre : BX.findParent(keyDownNode, function(n){return n == pre;}, this.element); 2446 } 2447 2448 _this.FocusPreElement(pre, false, keyDownPre ? null : 'start'); 2449 } 2450 } 2451 2452 // If cursor in the invisible node - we take next node 2453 if (node.nodeType == 3 && isEmpty && nextNode) 2454 { 2455 node = nextNode; 2456 isEmpty = this.editor.util.IsEmptyNode(node); 2457 } 2458 2459 isSur = this.editor.util.CheckSurrogateNode(node); 2460 2461 // It's surrogate 2462 if (isSur) 2463 { 2464 nextToSur = node.nextSibling; 2465 if (nextToSur && nextToSur.nodeType == 3 && this.editor.util.IsEmptyNode(nextToSur)) 2466 this.editor.selection._MoveCursorAfterNode(nextToSur); 2467 else 2468 this.editor.selection._MoveCursorAfterNode(node); 2469 2470 BX.PreventDefault(e); 2471 } 2472 // If it's element 2473 else if (node.nodeType == 1 && node.nodeName != "BODY" && !isEmpty) 2474 { 2475 if (sameLastRange) 2476 { 2477 this.editor.selection._MoveCursorAfterNode(node); 2478 BX.PreventDefault(e); 2479 } 2480 } 2481 else if (sameLastRange && node.nodeType == 3 && /*node.length == range.endOffset &&*/ !isEmpty) 2482 { 2483 parentNode = node.parentNode; 2484 if (parentNode && node === parentNode.lastChild && parentNode.nodeName != "BODY") 2485 { 2486 this.editor.selection._MoveCursorAfterNode(parentNode); 2487 } 2488 } 2489 else if (node.nodeType == 3 && node.parentNode) 2490 { 2491 parentNode = node.parentNode; 2492 prevNode = parentNode.previousSibling; 2493 2494 // It's empty invisible node before block element which was put there by us. 2495 // So we should remove it. 2496 if ( 2497 (this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode)) 2498 && prevNode && prevNode.nodeType == 3 && this.editor.util.IsEmptyNode(prevNode) 2499 ) 2500 { 2501 BX.remove(prevNode); 2502 } 2503 } 2504 } 2505 else // Selection Shift + Right & Shift + down 2506 { 2507 startCont = range.startContainer; 2508 endCont = range.endContainer; 2509 startIsSur = this.editor.util.CheckSurrogateNode(startCont); 2510 endIsSur = this.editor.util.CheckSurrogateNode(endCont); 2511 2512 if (startIsSur) 2513 { 2514 prevToSur = startCont.previousSibling; 2515 if (prevToSur && prevToSur.nodeType == 3 && this.editor.util.IsEmptyNode(prevToSur)) 2516 range.setStartBefore(prevToSur); 2517 else 2518 range.setStartBefore(startCont); 2519 2520 this.editor.selection.SetSelection(range); 2521 } 2522 2523 if (endIsSur) 2524 { 2525 nextToSur = endCont.nextSibling; 2526 if (nextToSur && nextToSur.nodeType == 3 && this.editor.util.IsEmptyNode(nextToSur)) 2527 range.setEndAfter(nextToSur); 2528 else 2529 range.setEndAfter(endCont); 2530 2531 this.editor.selection.SetSelection(range); 2532 } 2533 } 2534 } 2535 // Arrows left or up 2536 else if (keyCode === KC['left'] || keyCode === KC['up']) 2537 { 2538 this.editor.selection.GetStructuralTags(); 2539 2540 // Moving cursor by arrows (left & up) 2541 if (range.collapsed) 2542 { 2543 node = range.startContainer; 2544 isEmpty = this.editor.util.IsEmptyNode(node); 2545 // We check if last range was the same - it means that cursor doesn't 2546 // moved when user tried to move it 2547 sameLastRange = this.editor.selection.CheckLastRange(range); 2548 2549 // If cursor in the invisible node - we take next node 2550 if (node.nodeType == 3 && isEmpty && node.previousSibling) 2551 { 2552 node = node.previousSibling; 2553 isEmpty = this.editor.util.IsEmptyNode(node); 2554 } 2555 2556 if (!this.editor.util.CheckPreCursorSupport()) 2557 { 2558 if (node.nodeName === 'PRE') 2559 { 2560 pre = node; 2561 } 2562 else if (node.nodeType == 3) 2563 { 2564 pre = BX.findParent(node, {tag: 'PRE'}, this.element); 2565 } 2566 2567 if(pre) 2568 { 2569 if (this.keyDownRange) 2570 { 2571 keyDownNode = this.keyDownRange.startContainer; 2572 keyDownPre = keyDownNode == pre ? pre : BX.findParent(keyDownNode, function(n){return n == pre;}, this.element); 2573 } 2574 _this.FocusPreElement(pre, false, keyDownPre ? null : 'end'); 2575 } 2576 } 2577 2578 isSur = this.editor.util.CheckSurrogateNode(node); 2579 // It's surrogate 2580 if (isSur) 2581 { 2582 prevToSur = node.previousSibling; 2583 if (prevToSur && prevToSur.nodeType == 3 && this.editor.util.IsEmptyNode(prevToSur)) 2584 this.editor.selection._MoveCursorBeforeNode(prevToSur); 2585 else 2586 this.editor.selection._MoveCursorBeforeNode(node); 2587 2588 BX.PreventDefault(e); 2589 } 2590 // If it's element 2591 else if (node.nodeType == 1 && node.nodeName != "BODY" && !isEmpty) 2592 { 2593 if (sameLastRange) 2594 { 2595 this.editor.selection._MoveCursorBeforeNode(node); 2596 BX.PreventDefault(e); 2597 } 2598 } 2599 //else if (sameLastRange && node.nodeType == 3 && range.startOffset == 0 && !isEmpty) 2600 else if (sameLastRange && node.nodeType == 3 && !isEmpty) 2601 { 2602 parentNode = node.parentNode; 2603 if (parentNode && node === parentNode.firstChild && parentNode.nodeName != "BODY") 2604 { 2605 this.editor.selection._MoveCursorBeforeNode(parentNode); 2606 } 2607 } 2608 else if (node.nodeType == 3 && node.parentNode) 2609 { 2610 parentNode = node.parentNode; 2611 prevNode = parentNode.nextSibling; 2612 2613 // It's empty invisible node after block element which was put there by us. 2614 // So we should remove it. 2615 if ( 2616 (this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode)) 2617 && prevNode && prevNode.nodeType == 3 && this.editor.util.IsEmptyNode(prevNode) 2618 ) 2619 { 2620 BX.remove(prevNode); 2621 } 2622 } 2623 2624 } 2625 else // Selection Shift + left & Shift + up 2626 { 2627 startCont = range.startContainer; 2628 endCont = range.endContainer; 2629 startIsSur = this.editor.util.CheckSurrogateNode(startCont); 2630 endIsSur = this.editor.util.CheckSurrogateNode(endCont); 2631 2632 if (startIsSur) 2633 { 2634 prevToSur = startCont.previousSibling; 2635 if (prevToSur && prevToSur.nodeType == 3 && this.editor.util.IsEmptyNode(prevToSur)) 2636 range.setStartBefore(prevToSur); 2637 else 2638 range.setStartBefore(startCont); 2639 this.editor.selection.SetSelection(range); 2640 } 2641 2642 if (endIsSur) 2643 { 2644 nextToSur = endCont.nextSibling; 2645 if (nextToSur && nextToSur.nodeType == 3 && this.editor.util.IsEmptyNode(nextToSur)) 2646 range.setEndAfter(nextToSur); 2647 else 2648 range.setEndAfter(endCont); 2649 this.SetSelection(range); 2650 } 2651 } 2652 } 2653 2654 this.keyDownRange = null; 2655 }; 2656 2657 BXEditorIframeView.prototype.FocusPreElement = function(preNode, timeout, mode) 2658 { 2659 var _this = this; 2660 2661 if (this._focusPreElementTimeout) 2662 this._focusPreElementTimeout = clearTimeout(this._focusPreElementTimeout); 2663 2664 if (timeout) 2665 { 2666 this._focusPreElementTimeout = setTimeout(function(){ 2667 _this.FocusPreElement(preNode, false, mode); 2668 }, 100); 2669 return; 2670 } 2671 BX.focus(preNode); 2672 if (mode == 'end' && preNode.lastChild) 2673 { 2674 this.editor.selection.SetAfter(preNode.lastChild); 2675 } 2676 else if (mode == 'start' && preNode.firstChild) 2677 { 2678 this.editor.selection.SetBefore(preNode.firstChild); 2679 } 2680 }; 2681 2682 BXEditorIframeView.prototype.OnPasteHandler = function(e) 2683 { 2684 if (!this.editor.skipPasteHandler) 2685 { 2686 this.editor.skipPasteHandler = true; 2687 this.editor.pasteNodeIndex = {}; 2688 var 2689 originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop, 2690 originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft, 2691 _this = this, 2692 arNodes = [], 2693 curNode, i, node, qnodes; 2694 2695 function markGoodNode(n) 2696 { 2697 if (n && n.setAttribute) 2698 { 2699 var randValue = Math.round(Math.random() * 1000000); 2700 _this.editor.pasteNodeIndex[randValue] = true; 2701 n.setAttribute('data-bx-paste-flag', randValue); 2702 } 2703 } 2704 2705 curNode = this.document.body; 2706 if (curNode) 2707 { 2708 qnodes = curNode.querySelectorAll("*"); 2709 for (i = 0; i < qnodes.length; i++) 2710 { 2711 if (qnodes[i].nodeType == 1 && qnodes[i].nodeName != 'BODY' && qnodes[i].nodeName != 'HEAD') 2712 { 2713 arNodes.push(qnodes[i]); 2714 } 2715 } 2716 2717 for (i = 0; i < curNode.parentNode.childNodes.length; i++) 2718 { 2719 node = curNode.parentNode.childNodes[i]; 2720 if (node.nodeType == 1 && node.nodeName != 'BODY' && node.nodeName != 'HEAD') 2721 { 2722 arNodes.push(node); 2723 } 2724 } 2725 } 2726 2727 for (i = 0; i < arNodes.length; i++) 2728 { 2729 markGoodNode(arNodes[i]); 2730 } 2731 2732 var sync = this.editor.synchro.IsSyncOn(); 2733 if (sync) 2734 { 2735 this.editor.synchro.StopSync(); 2736 } 2737 2738 if (this.editor.iframeView.pasteHandlerTimeout) 2739 { 2740 clearTimeout(this.editor.iframeView.pasteHandlerTimeout); 2741 } 2742 2743 this.pasteHandlerTimeout = setTimeout(function() 2744 { 2745 _this.editor.SetCursorNode(); 2746 2747 _this.editor.pasteHandleMode = true; 2748 _this.editor.bbParseContentMode = true; 2749 2750 _this.editor.synchro.lastIframeValue = false;
2751 2752 // Paste control: show menu after pasting content 2753 // to let user select weather insert rich content or plain text 2754 if (!_this.editor.skipPasteControl) 2755 { 2756 _this.editor.pasteControl.SaveIframeContent(_this.GetValue()); 2757 _this.editor.pasteControl.CheckAndShow(); 2758 } 2759 2760 _this.editor.synchro.FromIframeToTextarea(true, true); 2761 2762 _this.editor.pasteHandleMode = false; 2763 _this.editor.bbParseContentMode = false; 2764 2765 _this.editor.synchro.lastTextareaValue = false; 2766 _this.editor.synchro.FromTextareaToIframe(true); 2767 2768 _this.editor.RestoreCursor(); 2769 2770 _this.editor.On("OnIframePaste"); 2771 _this.editor.On("OnIframeNewWord"); 2772 _this.editor.skipPasteHandler = false; 2773 2774 if (sync) 2775 { 2776 _this.editor.synchro.StartSync(); 2777 } 2778 2779 if (window.scrollTo) 2780 { 2781 window.scrollTo(originalScrollLeft, originalScrollTop); 2782 } 2783 }, 0); 2784 } 2785 }; 2786 2787 BXEditorIframeView.prototype.InitAutoLinking = function() 2788 { 2789 var 2790 _this = this, 2791 editor = this.editor, 2792 nativeAutolinkCanBeDisabled = editor.action.IsSupportedByBrowser("autoUrlDetect"), 2793 nativeAutoLink = BX.browser.IsIE() || BX.browser.IsIE9() || BX.browser.IsIE10(); 2794 2795 if (nativeAutolinkCanBeDisabled) 2796 editor.action.Exec("autoUrlDetect", false); 2797 2798 if (editor.config.autoLink === false) 2799 return; 2800 2801 // Init Autolink system 2802 var 2803 ignorableParents = {"CODE" : 1, "PRE" : 1, "A" : 1, "SCRIPT" : 1, "HEAD" : 1, "TITLE" : 1, "STYLE" : 1}, 2804 urlRegExp = /(((?:https?|ftp):\/\/|www\.)[^\s'"<]{3,500})/gi, 2805 emailRegExp = /[\.a-z0-9_\-]+@[\.a-z0-9_\-]+\.[\.a-z0-9_\-]+/gi, 2806 MAX_LENGTH = 100, 2807 BRACKETS = { 2808 ")": "(", 2809 "]": "[", 2810 "}": "{" 2811 }; 2812 this.editor.autolinkUrlRegExp = urlRegExp; 2813 this.editor.autolinkEmailRegExp = emailRegExp; 2814 2815 function autoLinkHandler() 2816 { 2817 try 2818 { 2819 if (checkAutoLink()) 2820 { 2821 editor.selection.ExecuteAndRestore(function(startContainer, endContainer) 2822 { 2823 if (endContainer && endContainer.parentNode) 2824 autoLink(endContainer.parentNode); 2825 }); 2826 } 2827 } 2828 catch(e){} 2829 } 2830 2831 function checkAutoLink() 2832 { 2833 var 2834 node, nodeValue, 2835 result = false, 2836 doc = editor.GetIframeDoc(), 2837 walker = doc.createTreeWalker( 2838 doc.body, 2839 NodeFilter.SHOW_TEXT, 2840 null, 2841 false 2842 ); 2843 2844 while(node = walker.nextNode()) 2845 { 2846 nodeValue = node.nodeValue || ''; 2847 if ((nodeValue.match(emailRegExp) || nodeValue.match(urlRegExp)) && 2848 node.parentNode && node.parentNode.nodeName != 'A') 2849 { 2850 result = true; 2851 break; 2852 } 2853 } 2854 2855 return result; 2856 } 2857 2858 function autoLink(element) 2859 { 2860 if (element && !ignorableParents[element.nodeName]) 2861 { 2862 var ignorableParent = BX.findParent(element, function(node) 2863 { 2864 return !!ignorableParents[node.nodeName]; 2865 }, element.ownerDocument.body); 2866 2867 if (ignorableParent) 2868 return element; 2869 2870 if (element === element.ownerDocument.documentElement) 2871 element = element.ownerDocument.body; 2872 2873 return parseNode(element); 2874 } 2875 } 2876 2877 function convertUrlToLink(str) 2878 { 2879 str = BX.util.htmlspecialchars(str); 2880 return str.replace(urlRegExp, function(match, url) 2881 { 2882 var 2883 punctuation = (url.match(/([^\w\u0430-\u0456\u0451\/\-#](,?))$/i) || [])[1] || "", 2884 opening = BRACKETS[punctuation]; 2885 2886 url = url.replace(/([^\w\u0430-\u0456\u0451\/\-#](,?))$/i, ""); 2887 2888 if (url.split(opening).length > url.split(punctuation).length) 2889 { 2890 url = url + punctuation; 2891 punctuation = ""; 2892 } 2893 var 2894 realUrl = url, 2895 displayUrl = BX.util.htmlspecialchars(url); 2896 2897 if (url.length > MAX_LENGTH) 2898 displayUrl = displayUrl.substr(0, MAX_LENGTH) + "..."; 2899 2900 if (realUrl.substr(0, 4) === "www.") 2901 realUrl = "http://" + realUrl; 2902 2903 BX.onCustomEvent(_this.editor, 'OnAfterUrlConvert', [realUrl]); 2904 return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation; 2905 }); 2906 } 2907 2908 function convertEmailToLink(str) 2909 { 2910 str = BX.util.htmlspecialchars(str); 2911 return str.replace(emailRegExp, function(email) 2912 { 2913 var 2914 punctuation = (email.match(/([^\w\/\-](,?))$/i) || [])[1] || "", 2915 opening = BRACKETS[punctuation]; 2916 email = email.replace(/([^\w\/\-](,?))$/i, ""); 2917 if (email.split(opening).length > email.split(punctuation).length) 2918 { 2919 email = email + punctuation; 2920 punctuation = ""; 2921 } 2922 2923 var realUrl = "mailto:" + email; 2924 2925 return '<a href="' + realUrl + '">' + email + '</a>' + punctuation; 2926 }); 2927 } 2928 2929 function getTmpDiv(doc) 2930 { 2931 var tmp = doc._bx_autolink_temp_div; 2932 if (!tmp) 2933 tmp = doc._bx_autolink_temp_div = doc.createElement("div"); 2934 return tmp; 2935 } 2936 2937 function parseNode(element) 2938 { 2939 var res, parentNode, tmpDiv; 2940 if (element && !ignorableParents[element.nodeName]) 2941 { 2942 // Replaces the content of the text node by link 2943 if (element.nodeType === 3 && 2944 element.data.match(urlRegExp) && element.parentNode) 2945 { 2946 parentNode = element.parentNode; 2947 tmpDiv = getTmpDiv(parentNode.ownerDocument); 2948 tmpDiv.innerHTML = "<span></span>" + convertUrlToLink(element.data); 2949 tmpDiv.removeChild(tmpDiv.firstChild); 2950 2951 while (tmpDiv.firstChild) 2952 parentNode.insertBefore(tmpDiv.firstChild, element); 2953 2954 parentNode.removeChild(element); 2955 } 2956 else if (element.nodeType === 3 && 2957 element.data.match(emailRegExp) && element.parentNode) 2958 { 2959 parentNode = element.parentNode; 2960 tmpDiv = getTmpDiv(parentNode.ownerDocument); 2961 tmpDiv.innerHTML = "<span></span>" + convertEmailToLink(element.data); 2962 tmpDiv.removeChild(tmpDiv.firstChild); 2963 2964 while (tmpDiv.firstChild) 2965 parentNode.insertBefore(tmpDiv.firstChild, element); 2966 2967 parentNode.removeChild(element); 2968 } 2969 else if (element.nodeType === 1) 2970 { 2971 var 2972 childNodes = element.childNodes, 2973 i; 2974 2975 for (i = 0; i < childNodes.length; i++) 2976 parseNode(childNodes[i]); 2977 2978 res = element; 2979 } 2980 } 2981 return res; 2982 } 2983 2984 if (!nativeAutoLink || (nativeAutoLink && nativeAutolinkCanBeDisabled)) 2985 { 2986 BX.addCustomEvent(editor, "OnIframeNewWord", function() 2987 { 2988 if (editor.autolinkTimeout) 2989 editor.autolinkTimeout = clearTimeout(editor.autolinkTimeout); 2990 2991 editor.autolinkTimeout = setTimeout(autoLinkHandler, 500); 2992 }); 2993 2994 BX.addCustomEvent(editor, "OnSubmit", function() 2995 { 2996 try 2997 { 2998 autoLink(editor.GetIframeDoc().body); 2999 } 3000 catch(e){} 3001 }); 3002 } 3003 3004 var 3005 links = editor.sandbox.GetDocument().getElementsByTagName("a"), 3006 getTextContent = function(element) 3007 { 3008 var textContent = BX.util.trim(editor.util.GetTextContent(element)); 3009 if (textContent.substr(0, 4) === "www.") 3010 textContent = "http://" + textContent; 3011 return textContent; 3012 }; 3013 3014 BX.addCustomEvent(editor, "OnIframeKeydown", function(e, keyCode, command, selectedNode) 3015 { 3016 if (links.length > 0 && selectedNode) 3017 { 3018 var link = BX.findParent(selectedNode, {tag: 'A'}, selectedNode.ownerDocument.body); 3019 if (link) 3020 { 3021 var textContent = getTextContent(link); 3022 setTimeout(function() 3023 { 3024 var newTextContent = getTextContent(link); 3025 if (newTextContent === textContent) 3026 return; 3027 3028 // Only set href when new href looks like a valid url 3029 if (newTextContent.match(urlRegExp)) 3030 link.setAttribute("href", newTextContent); 3031 }, 0); 3032 } 3033 } 3034 }); 3035 }; 3036 3037 BXEditorIframeView.prototype.IsUserTypingNow = function(e) 3038 { 3039 return this.isFocused && this.isShown && this.isUserTyping; 3040 }; 3041 3042 BXEditorIframeView.prototype.CheckContentLastChild = function(element) 3043 { 3044 if (!element) 3045 { 3046 element = this.element; 3047 } 3048 3049 var lastChild = element.lastChild; 3050 if (lastChild && (this.editor.util.IsEmptyNode(lastChild, true) && this.editor.util.IsBlockNode(lastChild.previousSibling) || this.editor.phpParser.IsSurrogate(lastChild))) 3051 { 3052 element.appendChild(BX.create('BR', {}, element.ownerDocument)); 3053 element.appendChild(this.editor.util.GetInvisibleTextNode()); 3054 } 3055 }; 3056 3057/** 3058 * Class _this takes care that the value of the composer and the textarea is always in sync 3059 */ 3060 function BXEditorViewsSynchro(editor, textareaView, iframeView) 3061 { 3062 this.INTERVAL = 500; 3063 3064 this.editor = editor; 3065 this.textareaView = textareaView; 3066 this.iframeView = iframeView; 3067 this.lastFocused = 'wysiwyg'; 3068 3069 this.InitEventHandlers(); 3070 } 3071 3072 /** 3073 * Sync html from composer to textarea 3074 * Takes care of placeholders 3075 * @param {Boolean} bParseHtml Whether the html should be sanitized before inserting it into the textarea 3076 */ 3077 BXEditorViewsSynchro.prototype = 3078 { 3079 FromIframeToTextarea: function(bParseHtml, bFormat) 3080 { 3081 var value; 3082 if (this.editor.bbCode) 3083 { 3084 value = this.iframeView.GetValue(this.editor.bbParseContentMode, false); 3085 3086 value = BX.util.trim(value); 3087 if (value !== this.lastIframeValue) 3088 { 3089 var bbCodes = this.editor.bbParser.Unparse(value); 3090 this.textareaView.SetValue(bbCodes, false, bFormat || this.editor.bbParseContentMode); 3091 3092 if (typeof this.lastSavedIframeValue !== 'undefined' && this.lastSavedIframeValue != value) 3093 { 3094 this.editor.On("OnContentChanged", [bbCodes, value]); 3095 } 3096 this.lastSavedIframeValue = value; 3097 this.lastIframeValue = value; 3098 } 3099 } 3100 else 3101 {
3102 value = this.iframeView.GetValue(); 3103 value = BX.util.trim(value); 3104 if (value !== this.lastIframeValue) 3105 { 3106 this.textareaView.SetValue(value, true, bFormat); 3107 if (typeof this.lastSavedIframeValue !== 'undefined' && this.lastSavedIframeValue != value) 3108 { 3109 this.editor.On("OnContentChanged", [this.textareaView.GetValue() || '', value || '']); 3110 } 3111 this.lastSavedIframeValue = value; 3112 this.lastIframeValue = value; 3113 } 3114 } 3115 }, 3116 3117 /** 3118 * Sync value of textarea to composer 3119 * Takes care of placeholders 3120 * @param {Boolean} bParseHtml Whether the html should be sanitized before inserting it into the composer 3121 */ 3122 FromTextareaToIframe: function(bParseHtml) 3123 { 3124 var value = this.textareaView.GetValue(); 3125 3126 if (value !== this.lastTextareaValue) 3127 { 3128 if (value) 3129 { 3130 if (this.editor.bbCode) 3131 { 3132 var htmlFromBbCode = this.editor.bbParser.Parse(value); 3133 // INVISIBLE_CURSOR 3134 htmlFromBbCode = htmlFromBbCode.replace(/\u2060/ig, '<span id="bx-cursor-node"> </span>'); 3135 this.iframeView.SetValue(htmlFromBbCode, bParseHtml); 3136 } 3137 else 3138 { 3139 // INVISIBLE_CURSOR 3140 value = value.replace(/\u2060/ig, '<span id="bx-cursor-node"> </span>'); 3141 this.iframeView.SetValue(value, bParseHtml); 3142 } 3143 } 3144 else 3145 { 3146 this.iframeView.Clear(); 3147 } 3148 this.lastTextareaValue = value; 3149 this.editor.On("OnContentChanged", [value || '', this.iframeView.GetValue() || '']); 3150 } 3151 }, 3152 3153 FullSyncFromIframe: function() 3154 { 3155 this.lastIframeValue = false; 3156 this.FromIframeToTextarea(true, true); 3157 this.lastTextareaValue = false; 3158 this.FromTextareaToIframe(true); 3159 }, 3160 3161 Sync: function() 3162 { 3163 var bParseHtml = true; 3164 var view = this.editor.currentViewName; 3165 3166 if (view === "split") 3167 { 3168 if (this.GetSplitMode() === "code") 3169 { 3170 this.FromTextareaToIframe(bParseHtml); 3171 } 3172 else // wysiwyg 3173 { 3174 this.FromIframeToTextarea(bParseHtml); 3175 } 3176 } 3177 else if (view === "code") 3178 { 3179 this.FromTextareaToIframe(bParseHtml); 3180 } 3181 else // wysiwyg 3182 { 3183 this.FromIframeToTextarea(bParseHtml); 3184 } 3185 }, 3186 3187 GetSplitMode: function() 3188 { 3189 var mode = false;
3190 if (this.editor.currentViewName == "split") 3191 { 3192 if (this.editor.iframeView.IsFocused()) 3193 { 3194 mode = "wysiwyg"; 3195 } 3196 else if(this.editor.textareaView.IsFocused()) 3197 { 3198 mode = "code"; 3199 } 3200 else 3201 { 3202 mode = this.lastFocused; 3203 } 3204 } 3205 return mode; 3206 }, 3207 3208 InitEventHandlers: function() 3209 { 3210 var _this = this; 3211 BX.addCustomEvent(this.editor, "OnTextareaFocus", function() 3212 { 3213 _this.lastFocused = 'code'; 3214 _this.StartSync(); 3215 }); 3216 BX.addCustomEvent(this.editor, "OnIframeFocus", function() 3217 { 3218 _this.lastFocused = 'wysiwyg'; 3219 _this.StartSync(); 3220 }); 3221 3222 BX.addCustomEvent(this.editor, "OnTextareaBlur", BX.delegate(this.StopSync, this)); 3223 BX.addCustomEvent(this.editor, "OnIframeBlur", BX.delegate(this.StopSync, this)); 3224 }, 3225 3226 StartSync: function(delay) 3227 { 3228 var _this = this; 3229 3230 if (this.interval) 3231 { 3232 this.interval = clearTimeout(this.interval); 3233 } 3234 3235 this.delay = delay || this.INTERVAL; // it can reduce or increase initial timeout 3236 function sync() 3237 { 3238 // set delay to normal value 3239 _this.delay = _this.INTERVAL; 3240 _this.Sync(); 3241 _this.interval = setTimeout(sync, _this.delay); 3242 } 3243 this.interval = setTimeout(sync, _this.delay); 3244 }, 3245 3246 StopSync: function() 3247 { 3248 if (this.interval) 3249 { 3250 this.interval = clearTimeout(this.interval); 3251 } 3252 }, 3253 3254 IsSyncOn: function() 3255 { 3256 return !!this.interval; 3257 }, 3258 3259 OnIframeMousedown: function(e, target, bxTag) 3260 { 3261 }, 3262 3263 IsFocusedOnTextarea: function() 3264 { 3265 return this.editor.currentViewName === "code" || this.editor.currentViewName === "split" && this.GetSplitMode() === "code"; 3266 } 3267 } 3268 3269 // global interface 3270 window.BXEditorTextareaView = BXEditorTextareaView; 3271 window.BXEditorIframeView = BXEditorIframeView; 3272 window.BXEditorViewsSynchro = BXEditorViewsSynchro; 3273})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.