1 /** 2 * Bitrix HTML Editor 3.0 3 * Date: 24.04.13 4 * Time: 4:23 5 */ 6 (function(window) { 7 var __BXHtmlEditorParserRules; 8 9 function BXEditor(config) 10 { 11 // Container contains links to dom elements 12 this.InitUtil(); 13 this.dom = { 14 // cont - 15 // iframeCont - 16 // textareaCont - 17 // iframe - 18 // textarea - 19 }; 20 21 this.bxTags = {}; 22 23 this.EMPTY_IMAGE_SRC = '/bitrix/images/1.gif'; 24 this.HTML5_TAGS = [ 25 "abbr", "article", "aside", "audio", "bdi", "canvas", "command", "datalist", "details", "figcaption", 26 "figure", "footer", "header", "hgroup", "keygen", "mark", "meter", "nav", "output", "progress", 27 "rp", "rt", "ruby", "svg", "section", "source", "summary", "time", "track", "video", "wbr" 28 ]; 29 this.BLOCK_TAGS = ["H1", "H2", "H3", "H4", "H5", "H6", "P", "BLOCKQUOTE", "DIV", "SECTION", "PRE"]; 30 this.NESTED_BLOCK_TAGS = ["BLOCKQUOTE", "DIV"]; 31 this.TABLE_TAGS = ["TD", "TR", "TH", "TABLE", "TBODY", "CAPTION", "COL", "COLGROUP", "TFOOT", "THEAD"]; 32 this.BBCODE_TAGS = ['P', 'U', 'TABLE', 'TR', 'TD', 'TH', 'IMG', 'A', 'CENTER', 'LEFT', 'RIGHT', 'JUSTIFY']; 33 34 this.HTML_ENTITIES = ['¡','¢','£','¤','¥','¦','§','¨','©','ª','«','¬','®','¯','°','±','²','³','´','µ','¶','·','¸','¹','º','»','¼','½','¾','¿','À','Á','Â','Ã','Ä','Å','Æ','Ç','È','É','Ê','Ë','Ì','Í','Î','Ï','Ð','Ñ','Ò','Ó','Ô','Õ','Ö','×','Ø','Ù','Ú','Û','Ü','Ý','Þ','ß','à','á','â','ã','ä','å','æ','ç','è','é','ê','ë','ì','í','î','ï','ð','ñ','ò','ó','ô','õ','ö','÷','ø','ù','ú','û','ü','ý','þ','ÿ','Œ','œ','Š','š','Ÿ','ˆ','˜','–','—','‘','’','‚','“','”','„','†','‡','‰','‹','›','€','Α','Β','Γ','Δ','Ε','Ζ','Η','Θ','Ι','Κ','Λ','Μ','Ν','Ξ','Ο','Π','Ρ','Σ','Τ','Υ','Φ','Χ','Ψ','Ω','α','β','γ','δ','ε','ζ','η','θ','ι','κ','λ','μ','ν','ξ','ο','π','ρ','ς','σ','τ','υ','φ','χ','ψ','ω','•','…','′','″','‾','⁄','™','←','↑','→','↓','↔','∂','∑','−','√','∞','∫','≈','≠','≡','≤','≥','◊','♠','♣','♥']; 35 36 if(!BX.browser.IsIE()) 37 { 38 this.HTML_ENTITIES = this.HTML_ENTITIES.concat(['ϑ','ϒ','ϖ','℘','ℑ','ℜ','ℵ','↵','⇐','⇑','⇒','⇓','⇔','∀','∃','∅','∇','∈','∉','∋','∏','∗','∝','∠','∧','∨','∩','∪','∴','∼','≅','⊂','⊃','⊄','⊆','⊇','⊕','⊗','⊥','⋅','⌈','⌉','⌊','⌋','⟨','⟩','♦']); 39 } 40 41 this.SHORTCUTS = { 42 "66": "bold", // B 43 "73": "italic", // I 44 "85": "underline" // U 45 }; 46 47 this.KEY_CODES = { 48 'backspace': 8, 49 'enter': 13, 50 'escape': 27, 51 'space': 32, 52 'delete': 46, 53 'left': 37, 54 'right': 39, 55 'up': 38, 56 'down': 40, 57 'z': 90, 58 'y': 89, 59 'shift': 16, 60 'ctrl': 17, 61 'alt': 18, 62 'cmd': 91, // 93, 224, 17 Browser dependent 63 'cmdRight': 93, // 93, 224, 17 Browser dependent? 64 'pageUp': 33, 65 'pageDown': 34 66 }; 67 this.INVISIBLE_SPACE = "\uFEFF"; 68 this.INVISIBLE_CURSOR = "\u2060"; 69 this.NORMAL_WIDTH = 1020; 70 this.MIN_WIDTH = 700; 71 this.MIN_HEIGHT = 100; 72 73 this.MAX_HANDLED_FORMAT_LENGTH = 50000; // Max length of code which will be formated 74 this.MAX_HANDLED_FORMAT_TIME = 500; 75 this.iframeCssText = ''; // Here some controls can add additional css 76 77 this.InitConfig(this.CheckConfig(config)); 78 79 if (window.LHEPostForm) 80 {
81 const editorHandler = window.LHEPostForm.getHandler(this.config.id); 82 if (editorHandler) 83 { 84 BX.addCustomEvent( 85 editorHandler.eventNode, 86 'OnShowLHE', 87 (show, setFocus, FCFormId) => { 88 if (FCFormId) 89 { 90 this.iframeView.setCopilotContextParameters(FCFormId); 91 } 92 }, 93 ); 94 } 95 } 96 97 if (!config.lazyLoad) 98 { 99 this.Init(); 100 } 101 } 102 103 BXEditor.prototype = { 104 Init: function() 105 { 106 if (this.inited) 107 return; 108 109 // Parser 110 this.parser = new BXHtmlEditor.BXEditorParser(this); 111 112 this.On("OnEditorInitedBefore", [this]); 113 114 if (!this.BuildSceleton()) 115 return; 116 117 this.HTMLStyler = HTMLStyler; 118 119 // Textarea 120 this.dom.textarea = this.dom.textareaCont.appendChild(BX.create("TEXTAREA", {props: {className: "bxhtmled-textarea"}})); 121 this.dom.pValueInput = BX('bxed_' + this.id); 122 if (!this.dom.pValueInput) 123 { 124 this.dom.pValueInput = this.dom.cont.appendChild(BX.create("INPUT", {props: {type: "hidden", id: 'bxed_' + this.id, name: this.config.inputName}})); 125 } 126 this.dom.pValueInput.value = this.config.content; 127 128 this.dom.form = this.dom.textarea.form || false; 129 this.document = null; 130 131 // Protected iframe for wysiwyg 132 this.sandbox = this.CreateIframeSandBox(); 133 var iframe = this.sandbox.GetIframe(); 134 135 iframe.style.width = '100%'; 136 // TODO: height in pixels 137 iframe.style.height = '100%'; 138 139 // Views: 140 if (this.config.content) 141 { 142 this.dom.textareaCont.style.opacity = 0; 143 this.dom.iframeCont.style.opacity = 0; 144 } 145 // 1. TextareaView 146 this.textareaView = new BXEditorTextareaView(this, this.dom.textarea, this.dom.textareaCont); 147 // 2. IframeView 148 this.iframeView = new BXEditorIframeView(this, this.dom.textarea, this.dom.iframeCont); 149 // 3. Syncronizer 150 this.synchro = new BXEditorViewsSynchro(this, this.textareaView, this.iframeView); 151 152 if (this.bbCode) 153 { 154 this.bbParser = new BXHtmlEditor.BXEditorBbCodeParser(this); 155 } 156 157 // Php parser 158 this.phpParser = new BXHtmlEditor.BXEditorPhpParser(this); 159 this.components = new BXHtmlEditor.BXEditorComponents(this); 160 161 this.styles = new BXStyles(this); 162 163 // Toolbar 164 this.overlay = new BXHtmlEditor.Overlay(this); 165 this.BuildToolbar(); 166 167 // Taskbars 168 if (this.showTaskbars) 169 { 170 this.taskbarManager = new BXHtmlEditor.TaskbarManager(this, true); 171 172 // Components 173 if (this.showComponents) 174 { 175 this.componentsTaskbar = new BXHtmlEditor.ComponentsControl(this); 176 this.taskbarManager.AddTaskbar(this.componentsTaskbar); 177 } 178 // Snippets 179 if (this.showSnippets) 180 { 181 this.snippets = new BXHtmlEditor.BXEditorSnippets(this); 182 this.snippetsTaskbar = new BXHtmlEditor.SnippetsControl(this, this.taskbarManager); 183 this.taskbarManager.AddTaskbar(this.snippetsTaskbar); 184 } 185 this.taskbarManager.ShowTaskbar(this.showComponents ? this.componentsTaskbar.GetId() : this.snippetsTaskbar.GetId()); 186 } 187 else 188 { 189 this.dom.taskbarCont.style.display = 'none'; 190 } 191 // Context menu 192 this.contextMenu = new BXHtmlEditor.ContextMenu(this); 193 194 if (this.config.showNodeNavi) 195 { 196 this.nodeNavi = new BXHtmlEditor.NodeNavigator(this); 197 this.nodeNavi.Show(); 198 } 199 200 this.pasteControl = new BXHtmlEditor.PasteControl(this); 201 202 this.InitEventHandlers(); 203 this.ResizeSceleton(); 204 205 // Restore taskbar mode from user settings 206 if (this.showTaskbars && this.config.taskbarShown) 207 { 208 this.taskbarManager.Show(false); 209 } 210 this.inited = true; 211 this.On("OnEditorInitedAfter", [this]); 212 213 if (!this.CheckBrowserCompatibility()) 214 { 215 this.dom.cont.parentNode.insertBefore(BX.create("DIV", {props: {className: "bxhtmled-warning"}, text: BX.message('BXEdInvalidBrowser')}), this.dom.cont); 216 } 217 218 this.InitImageUploader(); 219 this.Show(); 220 BX.onCustomEvent(BXHtmlEditor, 'OnEditorCreated', [this]); 221 }, 222 223 InitConfig: function(config) 224 { 225 this.config = config; 226 this.id = this.config.id; 227 this.dialogs = {}; 228 this.bbCode = !!this.config.bbCode; 229 this.config.splitVertical = !!this.config.splitVertical; 230 this.config.splitRatio = parseFloat(this.config.splitRatio); 231 this.config.view = this.config.view || 'wysiwyg'; 232 this.config.taskbarShown = !!this.config.taskbarShown; 233 this.config.taskbarWidth = parseInt(this.config.taskbarWidth); 234 this.config.showNodeNavi = this.config.showNodeNavi !== false; 235 this.config.setFocusAfterShow = this.config.setFocusAfterShow !== false; 236 this.cssCounter = 0; 237 this.iframeCssText = this.config.iframeCss; 238 this.fileDialogsLoaded = this.bbCode || this.config.useFileDialogs === false; 239 240 if (this.config.bbCodeTags && this.bbCode) 241 { 242 this.BBCODE_TAGS = this.config.bbCodeTags; 243 } 244 245 if (this.config.minBodyWidth) 246 this.MIN_WIDTH = parseInt(this.config.minBodyWidth); 247 if (this.config.minBodyHeight) 248 this.MIN_HEIGHT = parseInt(this.config.minBodyHeight); 249 250 if (this.config.normalBodyWidth) 251 this.NORMAL_WIDTH = parseInt(this.config.normalBodyWidth); 252 this.normalWidth = this.NORMAL_WIDTH; 253 254 if (!this.config.height) 255 this.config.height = this.MIN_HEIGHT; 256 if (!this.config.width) 257 this.config.width = this.MIN_WIDTH; 258 259 if (this.config.smiles) 260 { 261 this.smilesIndex = {}; 262 this.sortedSmiles = []; 263 264 var i, smile, j, arCodes; 265 for(i = 0; i < this.config.smiles.length; i++) 266 { 267 smile = this.config.smiles[i]; 268 if (!smile['codes'] || smile['codes'] == smile['code']) 269 { 270 this.smilesIndex[this.config.smiles[i].code] = smile;
271 this.sortedSmiles.push(smile); 272 } 273 else if(smile['codes'].length > 0) 274 { 275 arCodes = smile['codes'].split(' '); 276 for(j = 0; j < arCodes.length; j++) 277 { 278 this.smilesIndex[arCodes[j]] = smile; 279 this.sortedSmiles.push({name: smile.name, path: smile.path, code: arCodes[j]}); 280 } 281 } 282 } 283 284 this.sortedSmiles = this.sortedSmiles.sort(function(a, b){return b.code.length - a.code.length;}); 285 } 286 287 this.allowPhp = !!this.config.allowPhp; 288 // Limited Php Access - when user can only move or delete php code or change component params 289 this.lpa = !this.config.allowPhp && this.config.limitPhpAccess; 290 this.templateId = this.config.templateId; 291 this.componentFilter = this.config.componentFilter; 292 this.showSnippets = this.config.showSnippets !== false; 293 this.showComponents = this.config.showComponents !== false && (this.allowPhp || this.lpa); 294 this.showTaskbars = this.config.showTaskbars !== false && (this.showSnippets || this.showComponents); 295 296 this.templates = {}; 297 this.templates[this.templateId] = this.config.templateParams; 298 }, 299 300 InitEventHandlers: function() 301 { 302 var _this = this; 303 BX.bind(this.dom.cont, 'click', BX.proxy(this.OnClick, this)); 304 BX.bind(this.dom.cont, 'mousedown', BX.proxy(this.OnMousedown, this)); 305 306 BX.bind(window, 'resize', function(){_this.ResizeSceleton();}); 307 if (BX.adminMenu) 308 { 309 BX.addCustomEvent(BX.adminMenu, 'onAdminMenuResize', function(){_this.ResizeSceleton();}); 310 } 311 312 BX.addCustomEvent(this, "OnIframeFocus", function() 313 { 314 _this.bookmark = null; 315 if (_this.statusInterval) 316 { 317 clearInterval(_this.statusInterval); 318 } 319 _this.statusInterval = setInterval(BX.proxy(_this.CheckCurrentStatus, _this), 500); 320 }); 321 BX.addCustomEvent(this, "OnIframeBlur", function() 322 { 323 _this.bookmark = null; 324 if (_this.statusInterval) 325 { 326 clearInterval(_this.statusInterval); 327 } 328 _this.iframeView.stopBugusScroll = false; 329 }); 330 BX.addCustomEvent(this, "OnTextareaFocus", function() 331 { 332 _this.bookmark = null; 333 if (_this.statusInterval) 334 { 335 clearInterval(_this.statusInterval); 336 } 337 }); 338 339 // Surrogates 340 BX.addCustomEvent(this, "OnSurrogateDblClick", function(bxTag, origTag, target, e) 341 { 342 if (origTag) 343 { 344 switch (origTag.tag) 345 { 346 case 'php': 347 case 'javascript': 348 case 'htmlcomment': 349 case 'iframe': 350 case 'style': 351 _this.GetDialog('Source').Show(origTag); 352 break; 353 } 354 } 355 }); 356 357 if (this.dom.form) 358 { 359 BX.bind(this.dom.form, 'submit', BX.proxy(this.OnSubmit, this)); 360 361 // Autosave 362 if (this.config.initAutosave !== false) 363 { 364 setTimeout(function() 365 { 366 if (_this.dom.form.BXAUTOSAVE) 367 { 368 _this.InitAutosaveHandlers(); 369 } 370 }, 100); 371 } 372 } 373 374 BX.addCustomEvent(this, "OnSpecialcharInserted", function(entity) 375 { 376 var 377 lastChars = _this.GetLastSpecialchars(), 378 exInd = BX.util.array_search(entity, lastChars); 379 380 if (exInd !== -1) 381 { 382 lastChars = BX.util.deleteFromArray(lastChars, exInd); 383 lastChars.unshift(entity); 384 } 385 else 386 { 387 lastChars.unshift(entity); 388 lastChars.pop(); 389 } 390 391 _this.config.lastSpecialchars = lastChars; 392 _this.SaveOption('specialchars', lastChars.join('|')); 393 }); 394 395 this.parentDialog = BX.WindowManager.Get(); 396 if (this.parentDialog && this.parentDialog.DIV && BX.isNodeInDom(this.parentDialog.DIV) 397 && 398 BX.findParent(this.dom.cont, function(n){return n == _this.parentDialog.DIV;})) 399 { 400 BX.addCustomEvent(this.parentDialog, 'onWindowResizeExt', function(){_this.ResizeSceleton();}); 401 } 402 403 if (this.config.autoResize) 404 { 405 BX.addCustomEvent(this, "OnIframeKeyup", BX.proxy(this.AutoResizeSceleton, this)); 406 BX.addCustomEvent(this, "OnInsertHtml", BX.proxy(this.AutoResizeSceleton, this)); 407 BX.addCustomEvent(this, "OnIframeSetValue", BX.proxy(this.AutoResizeSceleton, this)); 408 BX.addCustomEvent(this, "OnFocus", BX.proxy(this.AutoResizeSceleton, this)); 409 BX.addCustomEvent(this, "OnSetViewAfter", BX.proxy(this.AutoResizeSceleton, this)); 410 } 411 412 BX.addCustomEvent(this, "OnIframeKeyup", BX.proxy(this.CheckBodyHeight, this)); 413 }, 414 415 BuildSceleton: function() 416 { 417 var result = false;
418 // Main container contain all editor parts 419 this.dom.cont = BX('bx-html-editor-' + this.id); 420 if (this.dom.cont && BX.isNodeInDom(this.dom.cont)) 421 { 422 this.dom.toolbarCont = BX('bx-html-editor-tlbr-cnt-' + this.id); 423 this.dom.toolbar = BX('bx-html-editor-tlbr-' + this.id); 424 this.dom.areaCont = BX('bx-html-editor-area-cnt-' + this.id); 425 426 // Container for content editable iframe 427 this.dom.iframeCont = BX('bx-html-editor-iframe-cnt-' + this.id); 428 this.dom.textareaCont = BX('bx-html-editor-ta-cnt-' + this.id); 429 430 this.dom.resizerOverlay = BX('bx-html-editor-res-over-' + this.id); 431 this.dom.splitResizer = BX('bx-html-editor-split-resizer-' + this.id); 432 this.dom.splitResizer.style.display = 'none'; 433 this.dom.splitResizer.className = this.config.splitVertical ? "bxhtmled-split-resizer-ver" : "bxhtmled-split-resizer-hor"; 434 BX.bind(this.dom.splitResizer, 'mousedown', BX.proxy(this.StartSplitResize, this)); 435 436 // Taskbars 437 this.dom.taskbarCont = BX('bx-html-editor-tskbr-cnt-' + this.id); 438 439 // Node navigation at the bottom 440 this.dom.navCont = BX('bx-html-editor-nav-cnt-' + this.id); 441 442 this.dom.fileDialogsWrap = BX('bx-html-editor-file-dialogs-' + this.id) 443 444 result = true; 445 } 446 return result; 447 }, 448 449 UpdateHeight: function() 450 { 451 const minHeight = parseInt(this.config.autoResizeMinHeight || 50); 452 let maxHeight = parseInt(this.config.autoResizeMaxHeight || 0); 453 if (!maxHeight || maxHeight < 10) 454 { 455 maxHeight = Math.round(BX.GetWindowInnerSize().innerHeight * 0.9); 456 } 457 458 const newHeight = Math.max(Math.min(this.GetHeightByContent(), maxHeight), minHeight); 459 if (this.GetSceletonSize().height < newHeight) 460 { 461 this.config.height = newHeight; 462 this.ResizeSceleton(); 463 } 464 }, 465 466 ResizeSceleton: function(width, height, params) 467 { 468 if (this.config.autoResizeMaxHeight === 'Infinity' && this.sandbox.loaded && this.GetIframeDoc().body) 469 { 470 this.GetIframeDoc().body.style.minHeight = this.MIN_HEIGHT + 'px'; 471 this.dom.cont.style.minHeight = this.MIN_HEIGHT + 'px'; 472 473 const toolbarHeight = (this.toolbar.pCont.offsetHeight > 0) * this.toolbar.height; 474 height = this.config.height = toolbarHeight + this.GetIframeDoc().body.scrollHeight; 475 } 476 477 var _this = this; 478 if (this.expanded) 479 { 480 var innerSize = BX.GetWindowInnerSize(document); 481 width = this.config.width = innerSize.innerWidth; 482 height = this.config.height = innerSize.innerHeight; 483 } 484 485 if (!width) 486 { 487 width = this.config.width; 488 } 489 if (!height) 490 { 491 height = this.config.height; 492 } 493 494 this.dom.cont.style.minWidth = this.MIN_WIDTH + 'px'; 495 this.dom.cont.style.minHeight = this.MIN_HEIGHT + 'px'; 496 497 var styleW, styleH; 498 499 if (this.resizeTimeout) 500 { 501 clearTimeout(this.resizeTimeout); 502 this.resizeTimeout = null; 503 } 504 505 if (width.toString().indexOf('%') !== -1) 506 { 507 styleW = width; 508 width = this.dom.cont.offsetWidth; 509 510 if (!width) 511 { 512 this.resizeTimeout = setTimeout(function(){_this.ResizeSceleton(width, height, params);}, 500); 513 return; 514 } 515 } 516 else 517 { 518 if (width < this.MIN_WIDTH) 519 { 520 width = this.MIN_WIDTH; 521 } 522 styleW = width + 'px'; 523 } 524 525 this.dom.cont.style.width = styleW; 526 this.dom.toolbarCont.style.width = styleW; 527 528 if (height.toString().indexOf('%') !== -1) 529 { 530 styleH = height; 531 height = this.dom.cont.offsetHeight; 532 } 533 else 534 { 535 if (height < this.MIN_HEIGHT) 536 { 537 height = this.MIN_HEIGHT; 538 } 539 styleH = height + 'px'; 540 } 541 this.dom.cont.style.height = styleH; 542 543 var 544 w = Math.max(width, this.MIN_WIDTH), 545 h = Math.max(height, this.MIN_HEIGHT), 546 toolbarHeight = this.toolbar.GetHeight(), 547 taskbarWidth = this.showTaskbars ? (this.taskbarManager.GetWidth(true, w * 0.8)) : 0, 548 areaH = h - toolbarHeight - (this.config.showNodeNavi && this.nodeNavi ? this.nodeNavi.GetHeight() : 0), 549 areaW = w - taskbarWidth; 550 551 this.dom.areaCont.style.top = toolbarHeight ? toolbarHeight + 'px' : 0; 552 553 // Area 554 this.SetAreaContSize(areaW, areaH, params); 555 556 // Taskbars 557 this.dom.taskbarCont.style.height = areaH + 'px'; 558 this.dom.taskbarCont.style.width = taskbarWidth + 'px'; 559 if (this.showTaskbars) 560 {
561 this.taskbarManager.Resize(taskbarWidth, areaH); 562 } 563 this.toolbar.AdaptControls(width); 564 565 this.On('OnEditorResizedAfter', [{ 566 width: width, 567 height: height 568 }]); 569 }, 570 571 CheckBodyHeight: function() 572 { 573 if (this.iframeView.IsShown()) 574 { 575 var 576 padding = 15, 577 minHeight, 578 doc = this.GetIframeDoc(); 579 580 if (doc && doc.body) 581 { 582 minHeight = doc.body.parentNode.offsetHeight - padding * 2; 583 584 if (minHeight <= 20) 585 { 586 setTimeout(BX.proxy(this.CheckBodyHeight, this), 300); 587 } 588 else if (this.config.autoResize || minHeight > doc.body.offsetHeight) 589 { 590 setTimeout(function() 591 { 592 minHeight = doc.body.parentNode.offsetHeight - padding * 2; 593 doc.body.style.minHeight = minHeight + 'px'; 594 }, 300); 595 } 596 } 597 } 598 }, 599 600 GetSceletonSize: function() 601 { 602 return { 603 width: this.dom.cont.offsetWidth, 604 height: this.dom.cont.offsetHeight 605 }; 606 }, 607 608 AutoResizeSceleton: function() 609 { 610 if (this.expanded || !this.IsShown() || this.iframeView.IsEmpty()) 611 return; 612 613 if (this.config.autoResizeMaxHeight === 'Infinity') 614 { 615 this.ResizeSceleton(); 616 return; 617 } 618 619 var 620 maxHeight = parseInt(this.config.autoResizeMaxHeight || 0), 621 minHeight = parseInt(this.config.autoResizeMinHeight || 50), 622 areaHeight = this.dom.areaCont.offsetHeight, 623 newHeight, 624 _this = this; 625 626 if (this.autoResizeTimeout) 627 { 628 clearTimeout(this.autoResizeTimeout); 629 } 630 631 this.autoResizeTimeout = setTimeout(function() 632 { 633 newHeight = _this.GetHeightByContent(); 634 if (newHeight > areaHeight) 635 { 636 if (BX.browser.IsIOS()) 637 { 638 maxHeight = Infinity; 639 } 640 else if (!maxHeight || maxHeight < 10) 641 { 642 maxHeight = Math.round(BX.GetWindowInnerSize().innerHeight * 0.9); // 90% from screen height 643 } 644 645 newHeight = Math.min(newHeight, maxHeight); 646 newHeight = Math.max(newHeight, minHeight); 647 _this.SmoothResizeSceleton(newHeight); 648 } 649 }, 300); 650 }, 651 652 GetHeightByContent: function() 653 { 654 var 655 heightOffset = parseInt(this.config.autoResizeOffset || 80), 656 contentHeight; 657 658 if (this.GetViewMode() == 'wysiwyg') 659 { 660 var 661 body = this.GetIframeDoc().body, 662 node = body.lastChild, 663 offsetTop = false; 664 665 contentHeight = body.offsetHeight; 666 while (true) 667 { 668 if (!node) 669 { 670 break; 671 } 672 if (node.offsetTop) 673 { 674 offsetTop = node.offsetTop + (node.offsetHeight || 0); 675 contentHeight = offsetTop + heightOffset; 676 break; 677 } 678 else 679 { 680 node = node.previousSibling; 681 } 682 } 683 684 var oEdSize = BX.GetWindowSize(this.GetIframeDoc()); 685 if (oEdSize.scrollHeight - oEdSize.innerHeight > 5) 686 { 687 contentHeight = Math.max(oEdSize.scrollHeight + heightOffset, contentHeight); 688 } 689 } 690 else 691 { 692 contentHeight = (this.textareaView.element.value.split("\n").length /* rows count*/ + 5) * 17; 693 } 694 695 return contentHeight; 696 }, 697 698 SmoothResizeSceleton: function(height) 699 { 700 this.On('AutoResizeStarted'); 701 702 var 703 _this = this, 704 size = this.GetSceletonSize(), 705 curHeight = size.height, 706 count = 0, 707 bRise = height > curHeight, 708 timeInt = 50, 709 dy = 5; 710 711 if (!bRise) 712 return; 713 714 if (this.smoothResizeInt) 715 { 716 clearInterval(this.smoothResizeInt); 717 } 718 719 this.smoothResizeInt = setInterval(function() 720 { 721 curHeight += Math.round(dy * count); 722 var finished = curHeight >= height; 723 if (curHeight > height) 724 { 725 clearInterval(_this.smoothResizeInt); 726 if (curHeight > height) 727 { 728 curHeight = height; 729 } 730 } 731 _this.config.height = curHeight; 732 _this.ResizeSceleton(); 733 if (finished) 734 { 735 _this.On('AutoResizeFinished'); 736 } 737 count++; 738 }, 739 timeInt 740 ); 741 }, 742 743 SetAreaContSize: function(areaW, areaH, params) 744 { 745 areaW += 2; 746 this.dom.areaCont.style.width = areaW + 'px'; 747 this.dom.areaCont.style.height = areaH + 'px'; 748 749 if (params && params.areaContTop) 750 { 751 this.dom.areaCont.style.top = params.areaContTop + 'px'; 752 } 753 754 var WIDTH_DIF = 3; 755 756 if (this.currentViewName == 'split') 757 { 758 function getValue(value, min, max) 759 { 760 if (value < min) 761 { 762 value = min; 763 } 764 if (value > max) 765 { 766 value = max; 767 } 768 return value; 769 } 770 771 var MIN_SPLITTER_PAD = 10, delta, a, b; 772 773 if (this.config.splitVertical == true) 774 { 775 delta = params && params.deltaX ? params.deltaX : 0; 776 a = getValue((areaW * this.config.splitRatio / (1 + this.config.splitRatio)) - delta, MIN_SPLITTER_PAD, areaW - MIN_SPLITTER_PAD); 777 b = areaW - a; 778 779 this.dom.iframeCont.style.width = (a - WIDTH_DIF) + 'px'; 780 this.dom.iframeCont.style.height = areaH + 'px'; 781 this.dom.iframeCont.style.top = 0; 782 this.dom.iframeCont.style.left = 0; 783 784 this.dom.textareaCont.style.width = (b - WIDTH_DIF) + 'px'; 785 this.dom.textareaCont.style.height = areaH + 'px'; 786 this.dom.textareaCont.style.top = 0; 787 this.dom.textareaCont.style.left = a + 'px'; 788 789 this.dom.splitResizer.className = 'bxhtmled-split-resizer-ver'; 790 this.dom.splitResizer.style.top = 0; 791 this.dom.splitResizer.style.left = (a - 3) + 'px'; 792 793 this.dom.textareaCont.style.height = areaH + 'px'; 794 } 795 else 796 { 797 delta = params && params.deltaY ? params.deltaY : 0; 798 a = getValue((areaH * this.config.splitRatio / (1 + this.config.splitRatio)) - delta, MIN_SPLITTER_PAD, areaH - MIN_SPLITTER_PAD); 799 b = areaH - a; 800 801 this.dom.iframeCont.style.width = (areaW - WIDTH_DIF) + 'px'; 802 this.dom.iframeCont.style.height = a + 'px'; 803 this.dom.iframeCont.style.top = 0; 804 this.dom.iframeCont.style.left = 0; 805 806 this.dom.textareaCont.style.width = (areaW - WIDTH_DIF) + 'px'; 807 this.dom.textareaCont.style.height = b + 'px'; 808 this.dom.textareaCont.style.top = a + 'px'; 809 this.dom.textareaCont.style.left = 0; 810 811 this.dom.splitResizer.className = 'bxhtmled-split-resizer-hor'; 812 this.dom.splitResizer.style.top = (a - 3) + 'px'; 813 this.dom.splitResizer.style.left = 0; 814 } 815 816 if (params && params.updateSplitRatio) 817 { 818 this.config.splitRatio = a / b; 819 this.SaveOption('split_ratio', this.config.splitRatio); 820 } 821 } 822 else 823 {
824 // Set size and position of iframe container to the normal state 825 this.dom.iframeCont.style.width = (areaW - WIDTH_DIF) + 'px'; 826 this.dom.iframeCont.style.height = areaH + 'px'; 827 this.dom.iframeCont.style.top = 0; 828 this.dom.iframeCont.style.left = 0; 829 830 // Set size and position of textarea container to the normal state 831 this.dom.textareaCont.style.width = (areaW - WIDTH_DIF) + 'px'; 832 this.dom.textareaCont.style.height = areaH + 'px'; 833 this.dom.textareaCont.style.top = 0; 834 this.dom.textareaCont.style.left = 0; 835 } 836 }, 837 838 ShowCopilotAtTheBottom: function() 839 { 840 if (!this.iframeView.isCopilotInitialized()) 841 { 842 return false; 843 } 844 845 this.iframeView.copilot.showAtTheBottom(); 846 847 return true; 848 }, 849 850 BuildToolbar: function() 851 { 852 this.toolbar = new BXHtmlEditor.Toolbar(this, this.GetTopControls()); 853 }, 854 855 GetTopControls: function() 856 { 857 this.On("GetTopButtons", [window.BXHtmlEditor.Controls]); 858 return window.BXHtmlEditor.Controls; 859 }, 860 861 CreateIframeSandBox: function() 862 { 863 return new Sandbox( 864 // Callback 865 BX.proxy(this.OnCreateIframe, this), 866 // Config 867 { 868 editor: this, 869 cont: this.dom.iframeCont 870 } 871 ); 872 }, 873 874 OnCreateIframe: function() 875 { 876 if (!document.body.contains(this.dom.iframeCont)) 877 { 878 //do not create frame if DOM doesn't contain editor's html structure (autocomposite). 879 return; 880 } 881 882 this.On('OnCreateIframeBefore'); 883 this.iframeView.OnCreateIframe(); 884 this.selection = new BXEditorSelection(this); 885 this.action = new BXEditorActions(this); 886 this.config.content = this.dom.pValueInput.value; 887 this.SetContent(this.config.content, true); 888 this.undoManager = new BXEditorUndoManager(this); 889 this.action.Exec("styleWithCSS", false, true); 890 this.iframeView.InitAutoLinking(); 891 // Simulate html5 placeholder attribute on contentEditable element 892// var placeholderText = typeof(this.config.placeholder) === "string" 893// ? this.config.placeholder 894// : this.textarea.element.getAttribute("placeholder"); 895// if (placeholderText) { 896// dom.simulatePlaceholder(this.parent, this, placeholderText); 897// } 898 899 if (this.config.view != 'wysiwyg') 900 { 901 var i, changeViewBut = false, switchCodeButton = false, controls = this.toolbar.GetControlsMap(); 902 903 // Mantis: 72063 904 if (this.config.view == 'split') 905 { 906 for (i = 0; i < controls.length; i++) 907 { 908 if (controls[i] && controls[i].id == 'ChangeView') 909 { 910 changeViewBut = true; 911 break; 912 } 913 } 914 if (!changeViewBut) 915 this.config.view = 'wysiwyg'; 916 } 917 918 // Mantis: 80663 919 if (this.config.view != 'wysiwyg') 920 { 921 for (i = 0; i < controls.length; i++) 922 {
923 if (controls[i] && (controls[i].id == 'BbCode' || controls[i].id == 'ChangeView')) 924 { 925 switchCodeButton = true; 926 break; 927 } 928 } 929 if (!switchCodeButton) 930 this.config.view = 'wysiwyg'; 931 } 932 } 933 934 this.SetView(this.config.view, false); 935 if (this.config.setFocusAfterShow !== false) 936 { 937 this.Focus(false); 938 } 939 940 this.sandbox.inited = true; 941 this.On('OnCreateIframeAfter', [this]); 942 }, 943 944 GetDialog: function(dialogName, params) 945 { 946 if (!this.dialogs[dialogName] && window.BXHtmlEditor.dialogs[dialogName]) 947 this.dialogs[dialogName] = new window.BXHtmlEditor.dialogs[dialogName](this, params); 948 949 return this.dialogs[dialogName] || null; 950 }, 951 952 Show: function() 953 { 954 this.dom.cont.style.display = ''; 955 }, 956 957 Hide: function() 958 { 959 this.dom.cont.style.display = 'none'; 960 }, 961 962 IsShown: function() 963 { 964 return this.inited && this.dom.cont.style.display !== 'none' && this.dom.cont.offsetWidth > 0 && BX.isNodeInDom(this.dom.cont); 965 }, 966 967 SetView: function(view, saveValue) 968 { 969 this.On('OnSetViewBefore'); 970 if (view == 'split' && this.bbCode) 971 view = 'wysiwyg'; 972 973 if (this.currentViewName != view) 974 { 975 this.toolbar.HideControl('ai-image-generator'); 976 this.toolbar.HideControl('ai-text-generator'); 977 978 if (view == 'wysiwyg') 979 { 980 this.iframeView.Show(); 981 this.textareaView.Hide(); 982 this.dom.splitResizer.style.display = 'none'; 983 this.CheckBodyHeight(); 984 this.toolbar.ShowControl('ai-image-generator'); 985 this.toolbar.ShowControl('ai-text-generator'); 986 } 987 else if (view == 'code') 988 { 989 this.iframeView.Hide(); 990 this.textareaView.Show(); 991 this.CheckCurrentStatus(false); 992 this.dom.splitResizer.style.display = 'none'; 993 } 994 else if (view == 'split') 995 { 996 this.textareaView.Show(); 997 this.iframeView.Show(); 998 this.dom.splitResizer.style.display = ''; 999 this.CheckBodyHeight(); 1000 } 1001 1002 this.currentViewName = view; 1003 } 1004 1005 if (saveValue !== false) 1006 { 1007 this.SaveOption('view', view); 1008 } 1009 1010 this.ResizeSceleton(); 1011 this.On('OnSetViewAfter'); 1012 }, 1013 1014 GetViewMode: function() 1015 { 1016 return this.currentViewName; 1017 }, 1018 1019 SetContent: function(value, bParse) 1020 { 1021 this.On('OnSetContentBefore'); 1022 if (this.bbCode) 1023 { 1024 var htmlFromBbCode = this.bbParser.Parse(value); 1025 this.iframeView.SetValue(htmlFromBbCode, bParse); 1026 if (htmlFromBbCode !== value || htmlFromBbCode.indexOf('[') < 0) 1027 { 1028 this.dom.textareaCont.style.opacity = 1; 1029 this.dom.iframeCont.style.opacity = 1; 1030 } 1031 } 1032 else 1033 { 1034 this.iframeView.SetValue(value, bParse); 1035 this.dom.textareaCont.style.opacity = 1; 1036 this.dom.iframeCont.style.opacity = 1; 1037 } 1038 1039 this.textareaView.SetValue(value, false); 1040 1041 this.On('OnSetContentAfter'); 1042 }, 1043 1044 Focus: function(setToEnd) 1045 { 1046 if (this.currentViewName == 'wysiwyg') 1047 { 1048 this.iframeView.Focus(setToEnd); 1049 } 1050 else if (this.currentViewName == 'code') 1051 { 1052 this.textareaView.Focus(setToEnd); 1053 } 1054 else if (this.currentViewName == 'split') 1055 { 1056 if (this.synchro.GetSplitMode() == 'wysiwyg') 1057 { 1058 this.iframeView.Focus(setToEnd); 1059 } 1060 else 1061 { 1062 this.textareaView.Focus(setToEnd); 1063 } 1064 } 1065 this.On('OnFocus'); 1066 return this; 1067 }, 1068 1069 SaveContent: function() 1070 { 1071 if (this.currentViewName == 'wysiwyg' || 1072 (this.currentViewName == 'split' && this.synchro.GetSplitMode() == 'wysiwyg')) 1073 { 1074 this.synchro.lastIframeValue = ''; 1075 this.synchro.FromIframeToTextarea(true, true); 1076 } 1077 else 1078 { 1079 this.textareaView.SaveValue(); 1080 } 1081 }, 1082 1083 GetContent: function() 1084 { 1085 this.SaveContent(); 1086 return this.textareaView.GetValue(); 1087 }, 1088 1089 IsExpanded: function() 1090 { 1091 return this.expanded; 1092 }, 1093 1094 Expand: function(bExpand) 1095 { 1096 if (!bExpand) 1097 { 1098 bExpand = !this.expanded; 1099 } 1100 this.expanded = bExpand; 1101 1102 this.On('OnFullscreenExpand', [this]); 1103 1104 const innerSize = BX.GetWindowInnerSize(document); 1105 let startWidth, startHeight, startTop, startLeft, endWidth, endHeight, endTop, endLeft; 1106 1107 if (bExpand) 1108 { 1109 const scrollPos = BX.GetWindowScrollPos(document); 1110 const pos = this.dom.cont.getBoundingClientRect(); 1111 1112 startWidth = this.dom.cont.offsetWidth; 1113 startHeight = this.dom.cont.offsetHeight; 1114 startTop = pos.top; 1115 startLeft = pos.left;
1116 endWidth = innerSize.innerWidth; 1117 endHeight = innerSize.innerHeight; 1118 endTop = 0; 1119 endLeft = 0; 1120 1121 this.savedSize = { 1122 width: startWidth, 1123 height: startHeight, 1124 top: startTop, 1125 left: startLeft, 1126 scrollLeft: scrollPos.scrollLeft, 1127 scrollTop: scrollPos.scrollTop, 1128 configWidth: this.config.width, 1129 configHeight: this.config.height 1130 }; 1131 this.savedStyle = { 1132 position: this.dom.cont.style.position, 1133 zIndex: this.dom.cont.style.zIndex 1134 }; 1135 this.config.width = endWidth; 1136 this.config.height = endHeight; 1137 1138 //dummy element to keep place for the editor 1139 this.dummieDiv = BX.create('DIV'); 1140 this.dummieDiv.style.width = startWidth + 'px'; 1141 this.dummieDiv.style.height = startHeight + 'px'; 1142 this.dom.cont.parentNode.insertBefore(this.dummieDiv, this.dom.cont); 1143 1144 //fix parent styles for correct positioning 1145 this.savedParentOpacity = []; 1146 this.savedParentPosition = []; 1147 let parent = this.dom.cont.parentNode; 1148 while (parent && parent !== document) { 1149 if (parseInt(window.getComputedStyle(parent).getPropertyValue('opacity')) < 1) 1150 { 1151 let opacity = ''; 1152 if (parseInt(parent.style.opacity) < 1) 1153 { 1154 opacity = parent.style.opacity; 1155 } 1156 this.savedParentOpacity.push({ 1157 parent: parent, 1158 opacity: opacity 1159 }) 1160 parent.style.opacity = '1'; 1161 } 1162 //zIndex of fixed elements cannot be greater than the relative parent element has 1163 if (window.getComputedStyle(parent).getPropertyValue('position') === 'relative' 1164 && parseInt(window.getComputedStyle(parent).getPropertyValue('z-index')) > 0) 1165 { 1166 let position = ''; 1167 if (parent.style.position === 'relative') 1168 { 1169 position = parent.style.position; 1170 } 1171 this.savedParentPosition.push({ 1172 parent: parent, 1173 position: position 1174 }) 1175 parent.style.position = 'static'; 1176 } 1177 //will-change property (it is experimental) breaks positioning, so we also disable this 1178 if (window.getComputedStyle(parent).getPropertyValue('will-change').includes('height')) 1179 { 1180 parent.style.willChange = 'unset'; 1181 } 1182 parent = parent.parentNode; 1183 } 1184 1185 this.dom.cont.style.setProperty('background', 'white', 'important'); 1186 this.dom.cont.style.position = 'fixed'; 1187 this.dom.cont.style.zIndex = 999; 1188 1189 BX.addCustomEvent(this, 'OnIframeKeydown', BX.proxy(this.CheckEscCollapse, this)); 1190 BX.bind(document.body, "keydown", BX.proxy(this.CheckEscCollapse, this)); 1191 BX.bind(window, "scroll", BX.proxy(this.PreventScroll, this)); 1192 } 1193 else 1194 { 1195 startWidth = this.dom.cont.offsetWidth; 1196 startHeight = this.dom.cont.offsetHeight; 1197 startTop = 0; 1198 startLeft = 0; 1199 endWidth = this.savedSize.width; 1200 endHeight = this.savedSize.height; 1201 endTop = this.savedSize.top; 1202 endLeft = this.savedSize.left; 1203 1204 BX.removeCustomEvent(this, 'OnIframeKeydown', BX.proxy(this.CheckEscCollapse, this)); 1205 BX.unbind(document.body, "keydown", BX.proxy(this.CheckEscCollapse, this)); 1206 BX.unbind(window, "scroll", BX.proxy(this.PreventScroll, this)); 1207 } 1208 1209 this.dom.cont.style.width = startWidth + 'px'; 1210 this.dom.cont.style.height = startHeight + 'px'; 1211 this.dom.cont.style.top = startTop + 'px'; 1212 this.dom.cont.style.left = startLeft + 'px'; 1213 1214 var _this = this; 1215 1216 this.expandAnimation = new BX.easing({ 1217 duration: 300, 1218 start : { 1219 height: startHeight, 1220 width: startWidth, 1221 top: startTop, 1222 left: startLeft 1223 }, 1224 finish : { 1225 height: endHeight, 1226 width: endWidth, 1227 top: endTop, 1228 left: endLeft 1229 }, 1230 transition : BX.easing.makeEaseOut(BX.easing.transitions.quart), 1231 step : function(state) 1232 { 1233 _this.dom.cont.style.width = state.width + 'px'; 1234 _this.dom.cont.style.height = state.height + 'px'; 1235 _this.dom.cont.style.top = state.top + 'px'; 1236 _this.dom.cont.style.left = state.left + 'px'; 1237 _this.ResizeSceleton(state.width, state.height); 1238 }, 1239 complete : function() 1240 { 1241 _this.dom.cont.style.width = endWidth + 'px'; 1242 _this.dom.cont.style.height = endHeight + 'px'; 1243 _this.dom.cont.style.top = endTop + 'px'; 1244 _this.dom.cont.style.left = endLeft + 'px'; 1245 1246 if (!bExpand) 1247 { 1248 let parent = _this.dom.cont.parentNode; 1249 while (parent && parent !== document) { 1250 if (parent.style.willChange === 'unset') 1251 { 1252 parent.style.willChange = ''; 1253 } 1254 parent = parent.parentNode; 1255 } 1256 for (const parentOpacity of _this.savedParentOpacity) 1257 { 1258 parentOpacity.parent.style.opacity = parentOpacity.opacity; 1259 } 1260 for (const parentPosition of _this.savedParentPosition) 1261 { 1262 parentPosition.parent.style.position = parentPosition.position; 1263 } 1264 _this.dummieDiv.remove(); 1265 _this.dom.cont.style.position = _this.savedStyle.position; 1266 _this.dom.cont.style.zIndex = _this.savedStyle.zIndex; 1267 _this.dom.cont.style.width = ''; 1268 _this.dom.cont.style.height = ''; 1269 _this.dom.cont.style.top = ''; 1270 _this.dom.cont.style.left = ''; 1271 _this.config.width = _this.savedSize.configWidth; 1272 _this.config.height = _this.savedSize.configHeight; 1273 } 1274 _this.ResizeSceleton(); 1275 _this.CheckAndReInit(); 1276 } 1277 }); 1278 1279 this.expandAnimation.animate(); 1280 }, 1281 1282 CheckEscCollapse: function(e, keyCode, command, selectedNode) 1283 { 1284 if (!keyCode) 1285 { 1286 keyCode = e.keyCode; 1287 } 1288 1289 if ( 1290 this.IsExpanded() && 1291 keyCode == this.KEY_CODES['escape'] && 1292 !this.IsPopupsOpened() 1293 ) 1294 { 1295 this.Expand(false); 1296 return BX.PreventDefault(e); 1297 } 1298 }, 1299 1300 PreventScroll: function(e) 1301 { 1302 window.scrollTo(this.savedSize.scrollLeft, this.savedSize.scrollTop); 1303 return BX.PreventDefault(e); 1304 }, 1305 1306 IsPopupsOpened: function() 1307 { 1308 return !!(this.dialogShown ||
1309 this.popupShown || 1310 this.contextMenuShown || 1311 this.overlay.bShown); 1312 }, 1313 1314 ReInitIframe: function(callback) 1315 { 1316 this.sandbox.InitIframe(null, () => { 1317 this.iframeView.OnCreateIframe(); 1318 1319 this.synchro.StopSync(); 1320 this.synchro.lastTextareaValue = ''; 1321 this.synchro.FromTextareaToIframe(true); 1322 this.synchro.StartSync(); 1323 this.iframeView.ReInit(); 1324 this.selection.lastCheckedRange = null; 1325 if (this.config.setFocusAfterShow !== false) 1326 { 1327 this.Focus(); 1328 } 1329 1330 if (typeof callback === 'function') 1331 { 1332 callback(); 1333 } 1334 1335 this.On('OnAfterIframeInit', [this]); 1336 }); 1337 }, 1338 1339 CheckAndReInit: function(content) 1340 { 1341 if (this.sandbox.inited) 1342 { 1343 var win = this.sandbox.GetWindow(); 1344 if (win) 1345 { 1346 var doc = this.sandbox.GetDocument(); 1347 if (doc !== this.iframeView.document || !doc.head || doc.head.innerHTML == '') 1348 { 1349 this.iframeView.document = doc; 1350 this.iframeView.element = doc.body; 1351 this.ReInitIframe(() => { 1352 if (content !== undefined) 1353 { 1354 this.SetContent(content, true); 1355 this.CheckBodyHeight(); 1356 } 1357 }); 1358 } 1359 else if(doc.body) 1360 { 1361 doc.body.style.minHeight = ''; 1362 } 1363 } 1364 else 1365 { 1366 throw new Error("HtmlEditor: CheckAndReInit error iframe isn't in the DOM"); 1367 } 1368 } 1369 1370 if (content !== undefined) 1371 { 1372 this.SetContent(content, true); 1373 this.CheckBodyHeight(); 1374 } 1375 }, 1376 1377 Disable: function() 1378 { 1379 }, 1380 1381 Enable: function() 1382 { 1383 }, 1384 1385 CheckConfig: function(config) 1386 { 1387 if (config.content === undefined) 1388 { 1389 config.content = ''; 1390 } 1391 1392 return config; 1393 }, 1394 1395 GetInnerHtml: function(el) 1396 { 1397 var 1398 TILDA = "%7E", 1399 AMP = "&", 1400 innerHTML = el.innerHTML; 1401 1402 if (innerHTML.indexOf(AMP) !== -1 || innerHTML.indexOf(TILDA) !== -1) 1403 { 1404 innerHTML = innerHTML.replace(/(?:href|src)\s*=\s*("|')([\s\S]*?)(\1)/ig, function(s) 1405 { 1406 // See https://bugzilla.mozilla.org/show_bug.cgi?id=664398 1407 s = s.replace(/%7E/ig, '~'); 1408 s = s.replace(/&/ig, '&'); 1409 return s; 1410 }); 1411 } 1412 1413 innerHTML = innerHTML.replace(/(?:title|alt)\s*=\s*("|')([\s\S]*?)(\1)/ig, function(s) 1414 { 1415 s = s.replace(/</g, '<'); 1416 s = s.replace(/>/g, '>'); 1417 return s; 1418 }); 1419 1420 if (this.bbCode) 1421 { 1422 innerHTML = innerHTML.replace(/[\s\n\r]*?<!--[\s\S]*?-->[\s\n\r]*?/ig, ""); 1423 } 1424 1425 return innerHTML; 1426 }, 1427 1428 InitUtil: function() 1429 { 1430 var _this = this; 1431 1432 this.util = {}; 1433 if ("textContent" in document.documentElement) 1434 { 1435 this.util.SetTextContent = function(element, text){element.textContent = text;}; 1436 this.util.GetTextContent = function(element){return element.textContent;}; 1437 } 1438 else if ("innerText" in document.documentElement) 1439 { 1440 this.util.SetTextContent = function(element, text){element.innerText = text;}; 1441 this.util.GetTextContent = function(element){return element.innerText;}; 1442 } 1443 else 1444 { 1445 this.util.SetTextContent = function(element, text){element.nodeValue = text;}; 1446 this.util.GetTextContent = function(element){return element.nodeValue;}; 1447 } 1448 1449 this.util.AutoCloseTagSupported = function() 1450 { 1451 var 1452 element = document.createElement("div"), 1453 result, 1454 innerHTML; 1455 1456 element.innerHTML = "<p><div></div>"; 1457 innerHTML = element.innerHTML.toLowerCase(); 1458 result = innerHTML === "<p></p><div></div>" || innerHTML === "<p><div></div></p>"; 1459 1460 _this.util.AutoCloseTagSupported = function(){return result;}; 1461 return result; 1462 }; 1463 1464 this.util.FirstLetterSupported = function() 1465 { 1466 var result = !BX.browser.IsChrome() && !BX.browser.IsSafari(); 1467 _this.util.FirstLetterSupported = function(){return result;}; 1468 return result; 1469 }; 1470 1471 // IE sometimes gives wrong results for hasAttribute/getAttribute 1472 this.util.CheckGetAttributeTruth = function() 1473 { 1474 var 1475 td = document.createElement("td"), 1476 result = td.getAttribute("rowspan") != "1"; 1477 1478 _this.util.CheckGetAttributeTruth = function(){return result;}; 1479 return result; 1480 }; 1481 1482 // Check if browser supports HTML5 1483 this.util.CheckHTML5Support = function(doc) 1484 { 1485 if (!doc) 1486 { 1487 doc = document; 1488 } 1489 1490 var 1491 result = false, 1492 html = "<article>bitrix</article>", 1493 el = doc.createElement("div"); 1494 1495 el.innerHTML = html; 1496 result = el.innerHTML.toLowerCase() === html; 1497 1498 _this.util.CheckHTML5Support = function(){return result;}; 1499 return result; 1500 }; 1501 1502 // Check if browser supports HTML5 (checks all tags) 1503 this.util.CheckHTML5FullSupport = function(doc) 1504 { 1505 if (!doc) 1506 { 1507 doc = document; 1508 } 1509 1510 var 1511 html, 1512 tags = _this.GetHTML5Tags(), 1513 result = false, 1514 el = doc.createElement("div"); 1515 1516 for (var i = 0; i < tags.length; i++) 1517 { 1518 html = "<" + tags[i] + ">bitrix</" + tags[i] + ">"; 1519 el.innerHTML = html; 1520 result = el.innerHTML.toLowerCase() === html; 1521 if (!result) 1522 { 1523 break; 1524 } 1525 } 1526 1527 _this.util.CheckHTML5FullSupport = function(){return result;}; 1528 return result; 1529 }; 1530 1531 this.util.GetEmptyImage = function() 1532 { 1533 return _this.EMPTY_IMAGE_SRC; 1534 }; 1535 1536 this.util.CheckDataTransferSupport = function() 1537 { 1538 var result = false;
1539 try { 1540 result = !!(window.Clipboard || window.DataTransfer).prototype.getData; 1541 } catch(e) {} 1542 _this.util.CheckDataTransferSupport = function(){return result;}; 1543 return result; 1544 }; 1545 1546 this.util.CheckImageSelectSupport = function() 1547 { 1548 var result = !(BX.browser.IsChrome() || BX.browser.IsSafari()); 1549 _this.util.CheckImageSelectSupport = function(){return result;}; 1550 return result; 1551 }; 1552 1553 this.util.CheckPreCursorSupport = function() 1554 { 1555 var result = !(BX.browser.IsIE() || BX.browser.IsIE10() || BX.browser.IsIE11()); 1556 _this.util.CheckPreCursorSupport = function(){return result;}; 1557 return result; 1558 }; 1559 1560 // Following hack is needed for firefox to make sure that image resize handles are properly removed 1561 this.util.Refresh = function(element) 1562 { 1563 if (element && element.parentNode) 1564 { 1565 var cn = "bx-editor-refresh"; 1566 1567 BX.addClass(element, cn); 1568 BX.removeClass(element, cn); 1569 1570 // Hack for firefox 1571 if (BX.browser.IsFirefox()) 1572 { 1573 try { 1574 var 1575 i, 1576 doc = element.ownerDocument, 1577 italics = doc.getElementsByTagName('I'), 1578 italicLen = italics.length; 1579 1580 for (i = 0; i < italics.length; i++) 1581 { 1582 italics[i].setAttribute('data-bx-orgig-i', true); 1583 } 1584 1585 doc.execCommand("italic", false, null); 1586 doc.execCommand("italic", false, null); 1587 1588 var italicsNew = doc.getElementsByTagName('I'); 1589 if (italicsNew.length !== italicLen) 1590 { 1591 for (i = 0; i < italicsNew.length; i++) 1592 { 1593 if (italicsNew[i].getAttribute('data-bx-orgig-i')) 1594 { 1595 italicsNew[i].removeAttribute('data-bx-orgig-i'); 1596 } 1597 else 1598 { 1599 _this.util.ReplaceWithOwnChildren(italicsNew[i]); 1600 } 1601 } 1602 } 1603 } catch(e) {} 1604 } 1605 } 1606 }; 1607 1608 this.util.addslashes = function(str) 1609 { 1610 str = str.replace(/\\/g,'\\\\'); 1611 str = str.replace(/"/g,'\\"'); 1612 return str; 1613 }; 1614 1615 this.util.stripslashes = function(str) 1616 { 1617 str = str.replace(/\\"/g,'"'); 1618 str = str.replace(/\\\\/g,'\\'); 1619 return str; 1620 }; 1621 1622 this.util.ReplaceNode = function(node, newNode) 1623 { 1624 node.parentNode.insertBefore(newNode, node); 1625 node.parentNode.removeChild(node); 1626 return newNode; 1627 }; 1628 1629 this.util.spaceUrlEncode = function(str) 1630 { 1631 str = str.replace(/ /g,'%20'); 1632 return str; 1633 }; 1634 1635 this.util.spaceUrlDecode = function(str) 1636 { 1637 str = str.replace(/%20/g,' '); 1638 return str; 1639 }; 1640 1641 // Fast way to check whether an element with a specific tag name is in the given document 1642 this.util.DocumentHasTag = function(doc, tag) 1643 { 1644 var 1645 LIVE_CACHE = {}, 1646 key = _this.id + ":" + tag, 1647 cacheEntry = LIVE_CACHE[key]; 1648 1649 if (!cacheEntry) 1650 cacheEntry = LIVE_CACHE[key] = doc.getElementsByTagName(tag); 1651 1652 return cacheEntry.length > 0; 1653 }; 1654 1655 this.util.IsSplitPoint = function(node, offset) 1656 { 1657 var res = offset > 0 && offset < node.childNodes.length; 1658 if (rangy.dom.isCharacterDataNode(node)) 1659 { 1660 if (offset == 0) 1661 res = !!node.previousSibling; 1662 else if (offset == node.length) 1663 res = !!node.nextSibling; 1664 else 1665 res = true; 1666 } 1667 return res; 1668 }; 1669 1670 this.util.SplitNodeAt = function(node, descendantNode, descendantOffset) 1671 { 1672 var newNode; 1673 if (rangy.dom.isCharacterDataNode(descendantNode)) 1674 { 1675 if (descendantOffset == 0) 1676 { 1677 descendantOffset = rangy.dom.getNodeIndex(descendantNode); 1678 descendantNode = descendantNode.parentNode; 1679 } 1680 else if (descendantOffset == descendantNode.length) 1681 { 1682 descendantOffset = rangy.dom.getNodeIndex(descendantNode) + 1; 1683 descendantNode = descendantNode.parentNode; 1684 } 1685 else 1686 { 1687 newNode = rangy.dom.splitDataNode(descendantNode, descendantOffset); 1688 } 1689 } 1690 1691 if (!newNode) 1692 { 1693 newNode = descendantNode.cloneNode(false); 1694 if (newNode.id) 1695 { 1696 newNode.removeAttribute("id"); 1697 } 1698 1699 var child; 1700 while ((child = descendantNode.childNodes[descendantOffset])) 1701 { 1702 newNode.appendChild(child); 1703 } 1704 1705 rangy.dom.insertAfter(newNode, descendantNode); 1706 } 1707 1708 if (descendantNode && descendantNode.nodeName == "BODY") 1709 { 1710 return newNode; 1711 } 1712 1713 return (descendantNode == node) ? newNode : _this.util.SplitNodeAt(node, newNode.parentNode, rangy.dom.getNodeIndex(newNode)); 1714 }; 1715 1716 this.util.ReplaceWithOwnChildren = function (el) 1717 { 1718 var parent = el.parentNode; 1719 while (el.firstChild) 1720 { 1721 parent.insertBefore(el.firstChild, el); 1722 } 1723 parent.removeChild(el); 1724 }; 1725 1726 this.util.IsBlockElement = function (node) 1727 { 1728 var styleDisplay = BX.style(node, 'display'); 1729 return styleDisplay && BX.type.isString(styleDisplay) && styleDisplay.toLowerCase() === "block"; 1730 }; 1731 1732 this.util.IsBlockNode = function (node) 1733 { 1734 return node && node.nodeType == 1 && BX.util.in_array(node.nodeName, _this.GetBlockTags()); 1735 }; 1736 1737 this.util.CopyAttributes = function(attributes, from, to) 1738 { 1739 if (from && to) 1740 { 1741 var 1742 attribute, 1743 i, 1744 length = attributes.length; 1745 1746 for (i = 0; i < length; i++) 1747 { 1748 attribute = attributes[i]; 1749 if (from[attribute]) 1750 to[attribute] = from[attribute]; 1751 } 1752 } 1753 }; 1754 1755 this.util.RenameNode = function(node, newNodeName) 1756 { 1757 var 1758 newNode = node.ownerDocument.createElement(newNodeName), 1759 firstChild; 1760 1761 while (firstChild = node.firstChild) 1762 newNode.appendChild(firstChild); 1763 1764 _this.util.CopyAttributes(["align", "className"], node, newNode); 1765 1766 if (node.style.cssText != '') 1767 { 1768 newNode.style.cssText = node.style.cssText; 1769 } 1770 1771 node.parentNode.replaceChild(newNode, node); 1772 return newNode; 1773 }; 1774 1775 this.util.GetInvisibleTextNode = function() 1776 { 1777 return _this.iframeView.document.createTextNode(_this.INVISIBLE_SPACE); 1778 }; 1779 1780 this.util.IsEmptyNode = function(node, bCheckNewLine, bCheckSpaces) 1781 { 1782 var res; 1783 if (node.nodeType == 3) 1784 { 1785 res = node.data === "" || node.data === _this.INVISIBLE_SPACE || (node.data === '\n' && bCheckNewLine); 1786 if (!res && bCheckSpaces && node.data.toString().match(/^[\s\n\r\t]+$/ig)) 1787 { 1788 res = true; 1789 } 1790 } 1791 else if(node.nodeType == 1) 1792 { 1793 res = node.innerHTML === "" || node.innerHTML === _this.INVISIBLE_SPACE; 1794 if (!res && bCheckSpaces && node.innerHTML.toString().match(/^[\s\n\r\t]+$/ig)) 1795 { 1796 res = true; 1797 } 1798 } 1799 return res; 1800 }; 1801 1802 var documentElement = document.documentElement; 1803 if ("textContent" in documentElement) 1804 { 1805 this.util.SetTextContent = function(node, text) 1806 { 1807 node.textContent = text; 1808 }; 1809 1810 this.util.GetTextContent = function(node) 1811 { 1812 return node.textContent; 1813 }; 1814 } 1815 else if ("innerText" in documentElement) 1816 { 1817 this.util.SetTextContent = function(node, text) 1818 { 1819 node.innerText = text; 1820 }; 1821 1822 this.util.GetTextContent = function(node) 1823 { 1824 return node.innerText; 1825 }; 1826 } 1827 else 1828 {
1829 this.util.SetTextContent = function(node, text) 1830 { 1831 node.nodeValue = text; 1832 }; 1833 1834 this.util.GetTextContent = function(node) 1835 { 1836 return node.nodeValue; 1837 }; 1838 } 1839 1840 this.util.GetTextContentEx = function(node) 1841 { 1842 var 1843 i, html, linkMap = [], 1844 clone = node.cloneNode(true), 1845 scripts = clone.getElementsByTagName('SCRIPT'), 1846 links = clone.getElementsByTagName('A'); 1847 1848 for (i = scripts.length - 1; i >= 0 ; i--) 1849 { 1850 BX.remove(scripts[i]); 1851 } 1852 1853 // mantis:64329, mantis:70550 1854 for (i = links.length - 1; i >= 0 ; i--) 1855 { 1856 var href = links[i].href; 1857 if (href.toLowerCase().indexOf('javascript:') !== -1) 1858 { 1859 _this.util.ReplaceNode(links[i], links[i].ownerDocument.createTextNode(_this.util.GetTextContent(links[i]))); 1860 } 1861 else 1862 { 1863 linkMap.push('<a href="' + links[i].href + '">' + _this.util.GetTextContent(links[i]) + '</a>'); 1864 _this.util.ReplaceNode(links[i], links[i].ownerDocument.createTextNode('#BX~TMP~LINK' + (linkMap.length - 1) + '#')); 1865 } 1866 } 1867 1868 html = _this.util.GetTextContent(clone); 1869 1870 if (linkMap.length > 0) 1871 { 1872 html = html.replace(/#BX~TMP~LINK(\d+)#/ig, function(s, num) 1873 { 1874 return linkMap[num] || ''; 1875 }); 1876 } 1877 return html; 1878 }; 1879 1880 this.util.RgbToHex = function(str) 1881 { 1882 if (!str) 1883 str = ''; 1884 1885 if (str.search("rgb") !== -1) 1886 { 1887 function hex(x) 1888 { 1889 return ("0" + parseInt(x).toString(16)).slice(-2); 1890 } 1891 1892 str = str.replace(/rgba\(0,\s*0,\s*0,\s*0\)/ig, 'transparent'); 1893 str = str.replace(/rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*([\d\.]{1,3}))?\)/ig, 1894 function(s, s1, s2, s3, s4) 1895 { 1896 return "#" + hex(s1) + hex(s2) + hex(s3); 1897 } 1898 ); 1899 } 1900 1901 return str; 1902 }; 1903 1904 this.util.CheckCss = function(node, arCss, bMatch) 1905 { 1906 var res = true, i; 1907 for (i in arCss) 1908 { 1909 if (arCss.hasOwnProperty(i)) 1910 { 1911 if (node.style[i] != '') 1912 { 1913 res = res && (bMatch ? node.style[i] == arCss[i] : true); 1914 } 1915 else 1916 { 1917 res = false; 1918 } 1919 } 1920 } 1921 return res; 1922 }; 1923 1924 this.util.SetCss = function(n, arCss) 1925 { 1926 if (n && arCss && typeof arCss == 'object') 1927 { 1928 for (var i in arCss) 1929 { 1930 if (arCss.hasOwnProperty(i)) 1931 { 1932 n.style[i] = arCss[i]; 1933 } 1934 } 1935 } 1936 }; 1937 1938 this.util.InsertAfter = function(node, precedingNode) 1939 { 1940 return rangy.dom.insertAfter(node, precedingNode); 1941 }; 1942 1943 this.util.GetNodeDomOffset = function(node) 1944 { 1945 var i = 0; 1946 while (node.parentNode && node.parentNode.nodeName !== 'BODY') 1947 { 1948 node = node.parentNode; 1949 i++; 1950 } 1951 return i; 1952 }; 1953 1954 this.util.CheckSurrogateNode = function(node) 1955 { 1956 return _this.phpParser.CheckParentSurrogate(node); 1957 }; 1958 1959 this.util.CheckSurrogateDd = function(node) 1960 { 1961 return _this.phpParser.CheckSurrogateDd(node); 1962 }; 1963 1964 this.util.GetPreviousNotEmptySibling = function(node) 1965 { 1966 var prev = node.previousSibling; 1967 while (prev && prev.nodeType == 3 && _this.util.IsEmptyNode(prev, true, true)) 1968 { 1969 prev = prev.previousSibling; 1970 } 1971 return prev; 1972 }; 1973 1974 this.util.GetNextNotEmptySibling = function(node) 1975 { 1976 var next = node.nextSibling; 1977 while (next && next.nodeType == 3 && _this.util.IsEmptyNode(next, true, true)) 1978 { 1979 next = next.nextSibling; 1980 } 1981 return next; 1982 }; 1983 1984 this.util.IsEmptyLi = function(li) 1985 { 1986 if (li && li.nodeName == 'LI') 1987 { 1988 return _this.util.IsEmptyNode(li, true, true) || li.innerHTML.toLowerCase() == '<br>'; 1989 } 1990 return false; 1991 }; 1992 1993 this.util.FindParentEx = function(obj, params, maxParent) 1994 { 1995 if (BX.checkNode && BX.checkNode(obj, params)) 1996 return obj; 1997 return BX.findParent(obj, params, maxParent); 1998 }; 1999 2000 this.util.IsEmptyObject = function(obj) 2001 { 2002 for (var i in obj) 2003 { 2004 if (obj.hasOwnProperty(i)) 2005 { 2006 return false; 2007 } 2008 } 2009 return true; 2010 }; 2011 2012 this.util.GetNextSibling = function(node) 2013 { 2014 var res = node.nextSibling; 2015 while (res && res.nodeType == 3 && res.nodeValue == '\ufeff' && res.nextSibling) 2016 { 2017 res = res.nextSibling; 2018 } 2019 return res || null; 2020 }; 2021 }, 2022 2023 Parse: function(content, bParseBxNodes, bFormat) 2024 { 2025 bParseBxNodes = !!bParseBxNodes; 2026 this.content = content; 2027 this.On("OnParse", [bParseBxNodes]); 2028 2029 if (bParseBxNodes) 2030 { 2031 this.content = this.parser.Parse(this.content, this.GetParseRules(), this.GetIframeDoc(), true, bParseBxNodes); 2032 if ((bFormat === true || this.textareaView.IsShown()) && !this.bbCode) 2033 { 2034 this.content = this.FormatHtml(this.content); 2035 } 2036 2037 this.content = this.phpParser.ParseBxNodes(this.content); 2038 } 2039 else 2040 {
2041 this.content = this.phpParser.ParsePhp(this.content); 2042 this.content = this.parser.Parse(this.content, this.GetParseRules(), this.GetIframeDoc(), true, bParseBxNodes); 2043 } 2044 2045 this.On("OnAfterParse", [bParseBxNodes]); 2046 2047 return this.content; 2048 }, 2049 2050 On: function(eventName, arEventParams) 2051 { 2052 BX.onCustomEvent(this, eventName, arEventParams || []); 2053 }, 2054 2055 GetIframeDoc: function() 2056 { 2057 if (!this.document) 2058 { 2059 this.document = this.sandbox.GetDocument(); 2060 BX.addCustomEvent(this, 'OnIframeReInit', BX.proxy(function(){this.document = this.sandbox.GetDocument();}, this)); 2061 } 2062 return this.document; 2063 }, 2064 2065 GetParseRules: function() 2066 { 2067 this.rules = __BXHtmlEditorParserRules; 2068 // Here we can make changes to this.rules 2069 this.On("OnGetParseRules"); 2070 var _this = this; 2071 this.GetParseRules = function(){return _this.rules;}; 2072 return this.rules; 2073 }, 2074 2075 GetHTML5Tags: function() 2076 { 2077 return this.HTML5_TAGS; 2078 }, 2079 2080 GetBlockTags: function() 2081 { 2082 return this.BLOCK_TAGS; 2083 }, 2084 2085 SetBxTag: function(el, params) 2086 { 2087 var id; 2088 if (params.id || el && el.id) 2089 id = params.id || el.id; 2090 2091 if (!id) 2092 { 2093 id = 'bxid' + Math.round(Math.random() * 1000000000); 2094 } 2095 else 2096 { 2097 if (this.bxTags[id]) 2098 { 2099 if (!params.tag) 2100 params.tag = this.bxTags[id].tag; 2101 } 2102 } 2103 2104 params.id = id; 2105 if (el) 2106 el.id = params.id; 2107 2108 this.bxTags[params.id] = params; 2109 return params.id; 2110 }, 2111 2112 GetBxTag: function(element) 2113 { 2114 var id; 2115 if (typeof element == 'object' && element && element.id) 2116 id = element.id; 2117 else 2118 id = element; 2119 2120 if (id) 2121 { 2122 if (typeof id != "string" && id.id) 2123 id = id.id; 2124 2125 if (id && id.length > 0 && this.bxTags[id] && this.bxTags[id].tag) 2126 { 2127 this.bxTags[id].tag = this.bxTags[id].tag.toLowerCase(); 2128 return this.bxTags[id]; 2129 } 2130 } 2131 2132 return {tag: false}; 2133 }, 2134 2135 OnMousedown: function(e) 2136 { 2137 var 2138 node = e.target || e.srcElement; 2139 2140 if (node && (node.getAttribute || node.parentNode)) 2141 { 2142 var 2143 _this = this, 2144 bxType = node.getAttribute('data-bx-type'); 2145 2146 if (!bxType) 2147 { 2148 node = BX.findParent(node, function(n) 2149 { 2150 return n == _this.dom.cont || (n.getAttribute && n.getAttribute('data-bx-type')); 2151 }, this.dom.cont); 2152 2153 bxType = (node && node.getAttribute) ? node.getAttribute('data-bx-type') : null; 2154 } 2155 2156 if (bxType == 'action') // Any type of button or element which runs action 2157 { 2158 return BX.PreventDefault(e); 2159 } 2160 } 2161 return true; 2162 }, 2163 2164 OnClick: function(e) 2165 { 2166 var 2167 target = e.target || e.srcElement, 2168 bxType = (target && target.getAttribute) ? target.getAttribute('data-bx-type') : false; 2169 2170 this.On("OnClickBefore", [{e: e, target: target, bxType: bxType}]); 2171 this.CheckCommand(target); 2172 }, 2173 2174 CheckCommand: function(node) 2175 { 2176 if (node && (node.getAttribute || node.parentNode)) 2177 { 2178 var 2179 _this = this, 2180 bxType = node.getAttribute('data-bx-type'); 2181 2182 if (!bxType) 2183 { 2184 node = BX.findParent(node, function(n) 2185 { 2186 return n == _this.dom.cont || (n.getAttribute && n.getAttribute('data-bx-type')); 2187 }, this.dom.cont); 2188 2189 bxType = (node && node.getAttribute) ? node.getAttribute('data-bx-type') : null; 2190 } 2191 2192 if (bxType == 'action') // Any type of button or element which runs action 2193 { 2194 var 2195 action = node.getAttribute('data-bx-action'), 2196 value = node.getAttribute('data-bx-value'); 2197 2198 if (this.action.IsSupported(action)) 2199 { 2200 this.iframeView.copilot?.hideInvitationLine(); 2201 this.action.Exec(action, value); 2202 } 2203 } 2204 } 2205 }, 2206 2207 SetSplitMode: function(vertical, saveValue) 2208 { 2209 this.config.splitVertical = !!vertical; 2210 2211 if (saveValue !== false) 2212 { 2213 this.SaveOption('split_vertical', this.config.splitVertical ? 1 : 0); 2214 } 2215 2216 this.SetView('split', saveValue); 2217 }, 2218 2219 GetSplitMode: function() 2220 { 2221 return this.config.splitVertical; 2222 }, 2223 2224 StartSplitResize: function(e) 2225 { 2226 this.dom.resizerOverlay.style.display = 'block'; 2227 var 2228 dX = 0, dY = 0, 2229 windowScroll = BX.GetWindowScrollPos(), 2230 startX = e.clientX + windowScroll.scrollLeft, 2231 startY = e.clientY + windowScroll.scrollTop, 2232 _this = this; 2233 2234 function moveResizer(e, bFinish) 2235 { 2236 var 2237 x = e.clientX + windowScroll.scrollLeft, 2238 y = e.clientY + windowScroll.scrollTop; 2239 2240 if(startX == x && startY == y) 2241 { 2242 return; 2243 } 2244 2245 dX = startX - x; 2246 dY = startY - y; 2247 2248 _this.ResizeSceleton(0, 0, {deltaX: dX, deltaY: dY, updateSplitRatio: bFinish}); 2249 } 2250 2251 function finishResizing(e) 2252 { 2253 moveResizer(e, true); 2254 BX.unbind(document, 'mousemove', moveResizer); 2255 BX.unbind(document, 'mouseup', finishResizing); 2256 _this.dom.resizerOverlay.style.display = 'none'; 2257 } 2258 2259 BX.bind(document, 'mousemove', moveResizer); 2260 BX.bind(document, 'mouseup', finishResizing); 2261 }, 2262 2263 Request: function(P) 2264 { 2265 if (!P.url) 2266 P.url = this.config.actionUrl; 2267 if (P.bIter !== false) 2268 P.bIter = true; 2269 2270 if (!P.postData && !P.getData) 2271 P.getData = this.GetReqData(); 2272 2273// var errorText; 2274// if (!P.errorText) 2275// errorText = false;
2276 2277 var reqId = P.getData ? P.getData.reqId : P.postData.reqId; 2278 2279 var _this = this, iter = 0; 2280 var handler = function(result) 2281 { 2282 function handleRes() 2283 { 2284 //_this.CloseWaitWindow(); 2285// var erInd = result.toLowerCase().indexOf('bx_event_calendar_action_error'); 2286// if (!result || result.length <= 0 || erInd != -1) 2287// { 2288// var errorText = ''; 2289// if (erInd >= 0) 2290// { 2291// var 2292// ind1 = erInd + 'BX_EVENT_CALENDAR_ACTION_ERROR:'.length, 2293// ind2 = result.indexOf('-->', ind1); 2294// errorText = result.substr(ind1, ind2 - ind1); 2295// } 2296// if (P.onerror && typeof P.onerror == 'function') 2297// P.onerror(); 2298// 2299// return _this.DisplayError(errorText || P.errorText || ''); 2300// } 2301 2302 var res = P.handler(_this.GetRequestRes(reqId), result); 2303 if(res === false && ++iter < 20 && P.bIter) 2304 setTimeout(handleRes, 5); 2305 else 2306 _this.ClearRequestRes(reqId); 2307 } 2308 setTimeout(handleRes, 50); 2309 }; 2310 //this.ShowWaitWindow(); 2311 2312 if (P.postData) 2313 BX.ajax.post(P.url, P.postData, handler); 2314 else 2315 BX.ajax.get(P.url, P.getData, handler); 2316 }, 2317 2318 GetRequestRes: function(key) 2319 { 2320 if (top.BXHtmlEditorAjaxResponse[key] != undefined) 2321 return top.BXHtmlEditorAjaxResponse[key]; 2322 2323 return {}; 2324 }, 2325 2326 ClearRequestRes: function(key) 2327 { 2328 if (top.BXHtmlEditorAjaxResponse) 2329 { 2330 top.BXHtmlEditorAjaxResponse[key] = null; 2331 delete top.BXHtmlEditorAjaxResponse[key]; 2332 } 2333 }, 2334 2335 GetReqData: function(action, O) 2336 { 2337 if (!O) 2338 O = {}; 2339 if (action) 2340 O.action = action; 2341 O.sessid = BX.bitrix_sessid(); 2342 O.bx_html_editor_request = 'Y'; 2343 O.reqId = Math.round(Math.random() * 1000000); 2344 return O; 2345 }, 2346 2347 GetTemplateId: function() 2348 { 2349 return this.templateId; 2350 }, 2351 2352 GetSiteId: function() 2353 { 2354 return this.config.siteId; 2355 }, 2356 2357 GetComponentFilter: function() 2358 { 2359 return this.componentFilter; 2360 }, 2361 2362 GetTemplateParams: function() 2363 { 2364 return this.templates[this.templateId]; 2365 }, 2366 2367 GetTemplateStyles: function() 2368 { 2369 var params = this.templates[this.templateId] || {}; 2370 return params.STYLES || ''; 2371 }, 2372 2373 ApplyTemplate: function(templateId) 2374 { 2375 if (this.templateId !== templateId) 2376 { 2377 if (this.templates[templateId]) 2378 { 2379 this.templateId = templateId; 2380 var 2381 templ = this.templates[templateId], 2382 i, 2383 doc = this.sandbox.GetDocument(), 2384 head = doc.head || doc.getElementsByTagName('HEAD')[0], 2385 styles = head.getElementsByTagName('STYLE'), 2386 links = head.getElementsByTagName('LINK'); 2387 2388 // Clean old template styles 2389 for (i = 0; i < styles.length; i++) 2390 { 2391 if (styles[i].getAttribute('data-bx-template-style') == 'Y') 2392 BX.cleanNode(styles[i], true); 2393 } 2394 2395 // Clean links with template styles 2396 i = 0; 2397 while (i < links.length) 2398 { 2399 if (links[i].getAttribute('data-bx-template-style') == 'Y') 2400 { 2401 BX.remove(links[i], true); 2402 } 2403 else 2404 { 2405 i++; 2406 } 2407 } 2408 2409 // Add new node in the iframe head 2410 if (templ['STYLES']) 2411 { 2412 head.appendChild(BX.create('STYLE', {props: {type: 'text/css'}, text: templ['STYLES']}, doc)).setAttribute('data-bx-template-style', 'Y'); 2413 } 2414 2415 if (templ && templ['EDITOR_STYLES']) 2416 { 2417 for (i = 0; i < templ['EDITOR_STYLES'].length; i++) 2418 { 2419 head.appendChild(BX.create('link', {props: {rel: 'stylesheet', href: templ['EDITOR_STYLES'][i] + '_' + this.cssCounter++}}, doc)).setAttribute('data-bx-template-style', 'Y'); 2420 } 2421 } 2422 2423 this.On("OnApplySiteTemplate", [templateId]); 2424 } 2425 else 2426 { 2427 var _this = this; 2428 this.Request({ 2429 getData: this.GetReqData('load_site_template', 2430 { 2431 site_template: templateId 2432 } 2433 ), 2434 handler: function(res) 2435 { 2436 _this.templates[templateId] = res; 2437 _this.ApplyTemplate(templateId); 2438 } 2439 }); 2440 } 2441 } 2442 }, 2443 2444 FormatHtml: function(html, bForceFormating) 2445 { 2446 if (html.length < this.MAX_HANDLED_FORMAT_LENGTH || bForceFormating === true) 2447 { 2448 if (!this.formatter) 2449 this.formatter = new window.BXHtmlEditor.BXCodeFormatter(this); 2450 2451 var time1 = new Date().getTime(); 2452 html = this.formatter.Format(html); 2453 var time2 = new Date().getTime(); 2454 2455 if (time2 - time1 > this.MAX_HANDLED_FORMAT_TIME) 2456 this.MAX_HANDLED_FORMAT_LENGTH -= 5000; 2457 } 2458 2459 return html; 2460 }, 2461 2462 GetFontFamilyList: function() 2463 { 2464 if (!this.fontFamilyList) 2465 { 2466 this.fontFamilyList = [ 2467 {value: ['Times New Roman', 'Times'], name: 'Times New Roman'}, 2468 {value: ['Courier New'], name: 'Courier New'}, 2469 {value: ['Arial', 'Helvetica'], name: 'Arial / Helvetica'}, 2470 {value: ['Arial Black', 'Gadget'], name: 'Arial Black'}, 2471 {value: ['Tahoma','Geneva'], name: 'Tahoma / Geneva'}, 2472 {value: 'Verdana', name: 'Verdana'}, 2473 {value: ['Georgia', 'serif'], name: 'Georgia'}, 2474 {value: 'monospace', name: 'monospace'} 2475 ]; 2476 this.On("GetFontFamilyList", [this.fontFamilyList]); 2477 } 2478 return this.fontFamilyList; 2479 }, 2480 2481 CheckCurrentStatus: function(status) 2482 { 2483 var 2484 arAction, action, actionState, value, 2485 actionList = this.GetActiveActions(); 2486 2487 if (status === false) 2488 { 2489 for (action in actionList) 2490 { 2491 if (actionList.hasOwnProperty(action) && this.action.IsSupported(action)) 2492 { 2493 arAction = actionList[action]; 2494 arAction.control.SetValue(false, null, action); 2495 } 2496 } 2497 } 2498 2499 if (!this.iframeView.IsFocused()) 2500 return this.On("OnIframeBlur"); 2501 2502 var range = this.selection.GetRange(); 2503 2504 if (!range || !range.isValid()) 2505 return this.On("OnIframeBlur"); 2506 2507 for (action in actionList) 2508 { 2509 if (actionList.hasOwnProperty(action) && this.action.IsSupported(action)) 2510 { 2511 arAction = actionList[action]; 2512 actionState = this.action.CheckState(action, arAction.value); 2513 value = arAction.control.GetValue(); 2514 2515 if (actionState) 2516 { 2517 arAction.control.SetValue(true, actionState, action); 2518 } 2519 else 2520 { 2521 arAction.control.SetValue(false, null, action); 2522 } 2523 } 2524 } 2525 }, 2526 2527 RegisterCheckableAction: function(action, params) 2528 { 2529 if (!this.checkedActionList) 2530 this.checkedActionList = {}; 2531 2532 this.checkedActionList[action] = params; 2533 }, 2534 2535 GetActiveActions: function() 2536 { 2537 return this.checkedActionList; 2538 }, 2539 2540 SaveOption: function(name, value) 2541 { 2542 BX.userOptions.save('html_editor', this.config.settingsKey, name, value); 2543 }, 2544
2545 GetCurrentCssClasses: function(filterTag) 2546 { 2547 return this.styles.GetCSS(this.templateId, this.templates[this.templateId].STYLES, this.templates[this.templateId].PATH || '', filterTag || false); 2548 }, 2549 2550 GetStylesDescription: function(templateId) 2551 { 2552 if (!templateId) 2553 templateId = this.templateId; 2554 var res = {}; 2555 if (templateId && this.templates[templateId]) 2556 { 2557 res = this.templates[templateId].STYLES_TITLE || {}; 2558 } 2559 return res; 2560 }, 2561 2562 IsInited: function() 2563 { 2564 return !!this.inited; 2565 }, 2566 2567 IsContentChanged: function() 2568 { 2569 var 2570 cont1 = this.config.content.replace(/[\s\n\r\t]+/ig, ''), 2571 cont2 = this.GetContent().replace(/[\s\n\r\t]+/ig, ''); 2572 2573 return cont1 != cont2; 2574 }, 2575 2576 IsSubmited: function() 2577 { 2578 return this.isSubmited; 2579 }, 2580 2581 OnSubmit: function() 2582 { 2583 if (!this.isSubmited && this.dom.cont.style.display !== 'none') 2584 { 2585 this.RemoveCursorNode(); 2586 this.isSubmited = true; 2587 2588 if (this.iframeView.IsFocused()) 2589 this.On("OnIframeBlur"); 2590 2591 this.On('OnSubmit'); 2592 this.SaveContent(); 2593 } 2594 }, 2595 2596 AllowBeforeUnloadHandler: function() 2597 { 2598 this.beforeUnloadHandlerAllowed = true; 2599 }, 2600 2601 DenyBeforeUnloadHandler: function() 2602 { 2603 this.beforeUnloadHandlerAllowed = false; 2604 }, 2605 2606 Destroy: function() 2607 { 2608 if (this.sandbox) 2609 this.sandbox.Destroy(); 2610 2611 if (this.Check()) 2612 BX.remove(this.dom.cont); 2613 }, 2614 2615 Check: function() 2616 { 2617 return this.dom.cont && BX.isNodeInDom(this.dom.cont); 2618 }, 2619 2620 IsVisible: function() 2621 { 2622 return this.Check() && this.dom.cont.offsetWidth > 0; 2623 }, 2624 2625 GetLastSpecialchars: function() 2626 { 2627 var def = ['¢', '§', '€', '£','¥','©', '®', '«', '»', '°', '±', '¶', '…','′','″', '™', '≈', '≠', '<', '>']; 2628 2629 if (this.config.lastSpecialchars && typeof this.config.lastSpecialchars == 'object' && this.config.lastSpecialchars.length > 1) 2630 { 2631 return this.config.lastSpecialchars; 2632 } 2633 else 2634 { 2635 return def; 2636 } 2637 }, 2638 2639 GetIframeElement: function(id) 2640 { 2641 var doc = this.GetIframeDoc(); 2642 return doc ? doc.getElementById(id) : null; 2643 }, 2644 2645 RegisterDialog: function(id, dialog) 2646 { 2647 window.BXHtmlEditor.dialogs[id] = dialog; 2648 }, 2649 2650 SetConfigHeight: function(height) 2651 { 2652 this.config.height = height; 2653 if (this.IsExpanded()) 2654 { 2655 this.savedSize.configHeight = height; 2656 this.savedSize.height = height; 2657 } 2658 }, 2659 2660 CheckBrowserCompatibility: function() 2661 { 2662 return !(BX.browser.IsOpera() || BX.browser.IsIE8() || BX.browser.IsIE7() || BX.browser.IsIE6() || !document.querySelectorAll); 2663 }, 2664 2665 GetCursorHtml: function() 2666 { 2667 return '<span id="bx-cursor-node"> </span>'; 2668 }, 2669 2670 SetCursorNode: function(range) 2671 { 2672 if (!range) 2673 range = this.selection.GetRange(); 2674 this.RemoveCursorNode(); 2675 this.selection.InsertHTML(this.GetCursorHtml(), range); 2676 }, 2677 2678 RestoreCursor: function() 2679 { 2680 var cursor = this.GetIframeElement('bx-cursor-node'); 2681 if (cursor) 2682 { 2683 this.selection.SetAfter(cursor); 2684 BX.remove(cursor); 2685 } 2686 }, 2687 2688 RemoveCursorNode: function() 2689 { 2690 if (this.synchro.IsFocusedOnTextarea()) 2691 { 2692 2693 } 2694 else 2695 { 2696 var cursor = this.GetIframeElement('bx-cursor-node'); 2697 if (cursor) 2698 { 2699 this.selection.SetAfter(cursor); 2700 BX.remove(cursor); 2701 } 2702 } 2703 }, 2704 2705 AddButton: function(params) 2706 { 2707 if (params.compact == undefined) 2708 params.compact = false; 2709 if (params.toolbarSort == undefined) 2710 params.toolbarSort = 301; 2711 if (params.hidden == undefined) 2712 params.hidden = false; 2713 2714 // 1. Create Button 2715 var but = function(editor, wrap) 2716 { 2717 // Call parrent constructor 2718 but.superclass.constructor.apply(this, arguments); 2719 this.id = params.id; 2720 this.title = params.name; 2721 if (params.iconClassName) 2722 this.className += ' ' + params.iconClassName; 2723 if (params.action) 2724 this.action = params.action; 2725 2726 if (params.disabledForTextarea !== undefined) 2727 this.disabledForTextarea = params.disabledForTextarea; 2728 2729 this.Create(); 2730 2731 if (params.src) 2732 this.pCont.firstChild.style.background = 'url("' + params.src + '") no-repeat scroll 0 0'; 2733 2734 if (wrap) 2735 wrap.appendChild(this.GetCont()); 2736 }; 2737 2738 BX.extend(but, window.BXHtmlEditor.Button); 2739 if (params.handler) 2740 but.prototype.OnClick = params.handler; 2741 2742 window.BXHtmlEditor.Controls[params.id] = but; 2743 2744 // 2. Add button to controls map 2745 BX.addCustomEvent(this, "GetControlsMap", function(controlsMap) 2746 { 2747 controlsMap.push({ 2748 id: params.id, 2749 compact: params.compact, 2750 hidden: params.hidden, 2751 sort: params.toolbarSort, 2752 checkWidth: params.checkWidth == undefined ? true : !!params.checkWidth, 2753 offsetWidth: (params.offsetWidth || 32) 2754 }); 2755 }); 2756 }, 2757 2758 AddCustomParser: function(parser) 2759 { 2760 if (this.phpParser && this.phpParser.AddCustomParser) 2761 { 2762 this.phpParser.AddCustomParser(parser); 2763 } 2764 else 2765 {
2766 var _this = this; 2767 BX.addCustomEvent("OnEditorInitedAfter", function(){_this.phpParser.AddCustomParser(parser);}); 2768 } 2769 }, 2770 2771 AddParser: function(parser) 2772 { 2773 if (parser && parser.name && typeof parser.obj == 'object') 2774 { 2775 this.parser.specialParsers[parser.name] = parser.obj; 2776 } 2777 }, 2778 2779 InsertHtml: function(html, range) 2780 { 2781 if (!this.synchro.IsFocusedOnTextarea()) 2782 { 2783 this.Focus(); 2784 2785 if (this.selection.lastCheckedRange && 2786 this.selection.lastCheckedRange.range && 2787 !range) 2788 { 2789 try 2790 { 2791 this.selection.SetSelection(this.selection.lastCheckedRange.range); 2792 } 2793 catch(e){} 2794 } 2795 2796 if (!range) 2797 { 2798 range = this.selection.GetRange(); 2799 } 2800 2801 if (!range && this.selection.lastRange) 2802 range = this.selection.lastRange; 2803 2804 if (range) 2805 { 2806 if (!range.collapsed && range.startContainer == range.endContainer && range.startContainer.nodeName !== 'BODY') 2807 { 2808 var surNode = this.util.CheckSurrogateNode(range.startContainer); 2809 if (surNode) 2810 { 2811 this.selection.SetAfter(surNode); 2812 } 2813 } 2814 2815 this.selection.InsertHTML(html, range); 2816 this.selection.ScrollIntoView(); 2817 } 2818 2819 this.iframeView.copilot?.hideInvitationLine(); 2820 this.iframeView.copilot?.update(); 2821 } 2822 }, 2823 2824 ParseContentFromBbCode: function(content) 2825 { 2826 if (this.bbCode) 2827 { 2828 content = this.bbParser.Parse(content); 2829 content = this.Parse(content, true, true); 2830 } 2831 return content; 2832 }, 2833 2834 LoadFileDialogs: function(callback) 2835 { 2836 var _this = this; 2837 2838 this.Request({ 2839 getData: this.GetReqData('load_file_dialogs', 2840 { 2841 editor_id: this.id 2842 } 2843 ), 2844 handler: function(res, html) 2845 { 2846 html = BX.util.trim(html); 2847 _this.dom.fileDialogsWrap.innerHTML = html; 2848 _this.fileDialogsLoaded = true; 2849 setTimeout(callback, 100); 2850 } 2851 }); 2852 }, 2853 2854 InitAutosaveHandlers: function() 2855 { 2856 var 2857 editor = this, 2858 form = this.dom.form; 2859 2860 try{ 2861// BX.addCustomEvent(this, 'OnSubmit', function(){form.BXAUTOSAVE.Init();}); // to prevent save ticker after form submit, OnContentChanged is enough 2862 BX.addCustomEvent(this, 'OnContentChanged', function(){form.BXAUTOSAVE.Init();}); 2863 2864 BX.addCustomEvent(form, 'onAutoSave', function (ob, data) 2865 { 2866 if (editor.IsShown() && !editor.IsSubmited()) 2867 { 2868 // Get it from textarea and put to form_data to saving 2869 data[editor.config.inputName] = editor.GetContent(); 2870 } 2871 }); 2872 2873 BX.addCustomEvent(form, 'onAutoSaveRestore', function (ob, data) 2874 { 2875 if (editor.IsShown()) 2876 { 2877 editor.SetContent(data[editor.config.inputName], true); 2878 } 2879 }); 2880 }catch(e){} 2881 }, 2882 2883 InitImageUploader: function() 2884 { 2885 if (this.config.uploadImagesFromClipboard !== false) 2886 { 2887 var uploadUrl = this.config.actionUrl; 2888 uploadUrl += (uploadUrl.indexOf('?') !== -1 ? "&" : "?") + 'action=uploadfile'; 2889 this.imageUploader = BX.Uploader.getInstance({ 2890 id: this.CID, 2891 streams: 1, 2892 allowUpload: "A", 2893 uploadFileUrl: uploadUrl, 2894 uploadMethod: "immediate", 2895 showImage: false, 2896 sortItems: false, 2897 input: null, 2898 placeHolder: null, 2899 uploadFormData: "N" 2900 }); 2901 2902 var _this = this; 2903 2904 BX.addCustomEvent(this, "OnImageDataUriHandle", function (editor, imageBase64) 2905 { 2906 var blob = BX.UploaderUtils.dataURLToBlob(imageBase64.src); 2907 if (blob && blob.size > 0 && blob.type.indexOf("image/") == 0) 2908 { 2909 blob.name = (blob.name || imageBase64.title || (this.GetDefaultImageName() + "." + blob.type.substr(6))); 2910 blob.uniqId = imageBase64.uniqId; 2911 blob.editorBase64Src = imageBase64.src; 2912 _this.imageUploader.onChange([blob]); 2913 } 2914 }); 2915 2916 BX.addCustomEvent(this.imageUploader, "onFileIsCreated", function (id, item) 2917 { 2918 BX.addCustomEvent(item, "onUploadDone", function (item, result) 2919 { 2920 _this.HandleImageDataUriCaughtUploadedCallback({ 2921 src: item.file.editorBase64Src, uniqId: item.file.uniqId 2922 }, { 2923 src: result.file.uploadedPath 2924 }); 2925 }); 2926 2927 //BX.addCustomEvent(item, "onUploadError", function(item, result){}); 2928 }); 2929 } 2930 }, 2931 2932 GetDefaultImageName: function() 2933 { 2934 var imageName = {value: false}; 2935 this.On("OnGetDefaultUploadImageName", [imageName]); 2936 if (!imageName.value) 2937 { 2938 imageName.value = 'content-img'; 2939 } 2940 2941 return imageName.value; 2942 }, 2943 2944 InitClipboardHandler: function() 2945 { 2946 var 2947 _this = this; 2948 2949 this.base64Images = []; 2950 2951 function checkImages(images) 2952 { 2953 _this.pasteCheckItteration++; 2954 var i; 2955 for (i = 0; i < images.length; i++) 2956 { 2957 const isInlineVideo = BX.hasClass(images[i], 'bxhtmled-player-surrogate'); 2958 if (isInlineVideo) 2959 { 2960 continue; 2961 } 2962 if (!images[i].getAttribute('data-bx-paste-check')) 2963 { 2964 if (images[i].complete) 2965 _this.CheckImage(images[i], false); 2966 else 2967 BX.bind(images[i], 'load', BX.proxy(_this.CheckImage, _this)); 2968 images[i].setAttribute('data-bx-paste-check', 'Y'); 2969 } 2970 } 2971 2972 if (_this.pasteCheckItteration === 1) 2973 setTimeout(function(){checkImages(images);}, 500); 2974 else if (_this.pasteCheckItteration < 15) 2975 setTimeout(function(){checkImages(images);}, 1000); 2976 } 2977 2978 BX.bind(this.iframeView.element, 'paste', function (e) 2979 { 2980 var 2981 chromeVerion = 0, 2982 imageHandled = false, 2983 clipboard = e.clipboardData; 2984 2985 if (BX.browser.IsChrome() || BX.browser.IsSafari()) 2986 { 2987 var ua = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); 2988 chromeVerion = ua ? parseInt(ua[2], 10) : Infinity; 2989 } 2990 2991 // For firefox works wrong (see mantis:88928, mantis:122421) 2992 if (clipboard && clipboard.items 2993 && !BX.browser.IsFirefox()
2994 && (!chromeVerion || chromeVerion < 83) 2995 ) 2996 { 2997 var item = clipboard.items[0]; 2998 if (item && item.type.indexOf('image/') > -1) 2999 { 3000 var blob = item.getAsFile(); 3001 if (blob) 3002 { 3003 var reader = new FileReader(); 3004 reader.readAsDataURL(blob); 3005 reader.onload = function (event) 3006 { 3007 imageHandled = true; 3008 var img = new Image(); 3009 img.src = event.target.result; 3010 setTimeout(function() 3011 { 3012 _this.selection.InsertNode(img); 3013 _this.HandleImageDataUri(img); 3014 }, 100); 3015 } 3016 } 3017 } 3018 } 3019 3020 if (!imageHandled) 3021 { 3022 _this.pasteCheckItteration = 0; 3023 checkImages(_this.GetIframeDoc().body.getElementsByTagName('IMG')); 3024 } 3025 }); 3026 3027 BX.removeCustomEvent(this, 'OnImageDataUriCaughtUploaded', BX.proxy(this.HandleImageDataUriCaughtUploadedCallback, this)); 3028 BX.addCustomEvent(this, 'OnImageDataUriCaughtUploaded', BX.proxy(this.HandleImageDataUriCaughtUploadedCallback, this)); 3029 }, 3030 3031 GetBase64Image: function(base64source) 3032 { 3033 var i, result = false;
3034 3035 for (i = 0; i < this.base64Images.length; i++) 3036 { 3037 if (this.base64Images[i].source == base64source) 3038 { 3039 result = this.base64Images[i]; 3040 } 3041 } 3042 return result; 3043 }, 3044 3045 RegisterBase64Image: function(base64source, status) 3046 { 3047 this.base64Images.push({ 3048 source: base64source, 3049 status: status, 3050 index: this.base64Images.length 3051 }); 3052 }, 3053 3054 CheckImage: function(image, unbind) 3055 { 3056 if (image && image.complete && image.naturalHeight !== 0) 3057 { 3058 if (image.src.indexOf('blob:http') !== -1) 3059 { 3060 image.src = this.GetImageBase64(image); 3061 image.title = ''; 3062 } 3063 3064 if (image.src.indexOf('data:image/') !== -1) 3065 { 3066 this.HandleImageDataUri(image); 3067 } 3068 3069 if (unbind !== false) 3070 { 3071 BX.unbind(image, 'load', BX.proxy(this.CheckImage, this)); 3072 } 3073 } 3074 }, 3075 3076 GetImageBase64: function(image) { 3077 const canvas = document.createElement("canvas"); 3078 let width = image.width; 3079 let height = image.height; 3080 if (width > 1440) 3081 { 3082 height = height*1440/width; 3083 width = 1440; 3084 } 3085 canvas.width = width; 3086 canvas.height = height; 3087 canvas.getContext("2d").drawImage(image, 0, 0, width, height); 3088 return canvas.toDataURL("image/jpeg", 0.7); //compress image to 70% quality 3089 }, 3090 3091 HandleImageDataUri: function(image) 3092 { 3093 if (!image.getAttribute('data-bx-unique-id')) 3094 { 3095 this.skipPasteControl = true; 3096 if (this.pasteControl.isOpened) 3097 { 3098 this.pasteControl.Hide(); 3099 } 3100 3101 var base64Image = this.GetBase64Image(image.src); 3102 3103 if (base64Image === false) 3104 { 3105 this.RegisterBase64Image(image.src, 'requested'); 3106 3107 var uniqId = 'bx_base64_id_' + Math.round(Math.random() * 1000000000); 3108 image.setAttribute('data-bx-unique-id', uniqId); 3109 image.removeAttribute('data-bx-orig-src'); 3110 3111 this.On('OnImageDataUriHandle', [this, 3112 { 3113 src: image.src, 3114 title: image.title || '', 3115 uniqId: uniqId 3116 }]); 3117 } 3118 else 3119 { 3120 if (base64Image.status == 'uploaded') 3121 { 3122 this.HandleImageDataUriCaughtUploadedCallback( 3123 { 3124 src: image.src, 3125 title: image.title || '', 3126 uniqId: base64Image.uniqId 3127 }, 3128 { 3129 src: base64Image.fileSrc 3130 }, 3131 base64Image.htmlForInsert || null 3132 ); 3133 } 3134 } 3135 } 3136 }, 3137 3138 HandleImageDataUriCaughtUploadedCallback: function(imageReferer, file, htmlForInsert) 3139 { 3140 if (imageReferer && imageReferer.uniqId && file && file.src) 3141 { 3142 var base64Image = this.GetBase64Image(imageReferer.src); 3143 if (base64Image && !base64Image.fileSrc) 3144 { 3145 base64Image.status = 'uploaded'; 3146 base64Image.uniqId = imageReferer.uniqId; 3147 base64Image.fileSrc = file.src; 3148 base64Image.htmlForInsert = htmlForInsert; 3149 } 3150 3151 var 3152 i,image, 3153 images = this.GetIframeDoc().body.getElementsByTagName('IMG'); 3154 3155 for (i = 0; i < images.length; i++) 3156 { 3157 image = images[i]; 3158 if (image.getAttribute('data-bx-unique-id') == imageReferer.uniqId 3159 || 3160 image.getAttribute('src') == imageReferer.src 3161 ) 3162 { 3163 if (htmlForInsert && htmlForInsert.replacement) 3164 { 3165 this.selection.SetAfter(image); 3166 this.selection.InsertHTML(htmlForInsert.replacement); 3167 3168 BX.remove(image); 3169 if (htmlForInsert.callback) 3170 { 3171 setTimeout(htmlForInsert.callback, 300); 3172 } 3173 } 3174 else 3175 { 3176 image.src = file.src; 3177 image.setAttribute('src', file.src); 3178 image.setAttribute('data-bx-orig-src', file.src); 3179 image.removeAttribute('data-bx-paste-check'); 3180 image.removeAttribute('data-bx-unique-id'); 3181 } 3182 } 3183 } 3184 } 3185 this.skipPasteControl = false; 3186 } 3187 }; 3188 3189 window.BXEditor = BXEditor; 3190 3191 function Sandbox(callBack, config) 3192 { 3193 this.callback = callBack || BX.DoNothing; 3194 this.config = config || {}; 3195 this.editor = this.config.editor; 3196 this.iframe = this.CreateIframe(); 3197 this.bSandbox = false; 3198 3199 // Properties to unset/protect on the window object 3200 this.windowProperties = ["parent", "top", "opener", "frameElement", "frames", 3201 "localStorage", "globalStorage", "sessionStorage", "indexedDB"]; 3202 //Properties on the window object which are set to an empty function 3203 this.windowProperties2 = ["open", "close", "openDialog", "showModalDialog", "alert", "confirm", "prompt", "openDatabase", "postMessage", "XMLHttpRequest", "XDomainRequest"]; 3204 //Properties to unset/protect on the document object 3205 this.documentProperties = ["referrer", "write", "open", "close"]; 3206 } 3207 3208 Sandbox.prototype = 3209 { 3210 GetIframe: function() 3211 { 3212 return this.iframe; 3213 }, 3214 3215 GetWindow: function() 3216 { 3217 this._readyError(); 3218 }, 3219 3220 GetDocument: function() 3221 { 3222 this._readyError(); 3223 }, 3224 3225 Destroy: function() 3226 { 3227 var iframe = this.GetIframe(); 3228 iframe.parentNode.removeChild(iframe); 3229 }, 3230 3231 _readyError: function() 3232 { 3233 throw new Error("Sandbox: Sandbox iframe isn't loaded yet"); 3234 }, 3235 3236 CreateIframe: function() 3237 { 3238 var 3239 _this = this, 3240 iframe = BX.create("IFRAME", { 3241 props: { 3242 className: "bx-editor-iframe", 3243 frameborder: 0, 3244 allowtransparency: "true", 3245 width: 0, 3246 height: 0, 3247 marginwidth: 0, 3248 marginheight: 0 3249 //scrolling: 'no' 3250 } 3251 }); 3252 3253 iframe.onload = function() 3254 { 3255 iframe.onreadystatechange = iframe.onload = null; 3256 _this.OnLoadIframe(iframe); 3257 }; 3258 3259 iframe.onreadystatechange = function() 3260 { 3261 if (/loaded|complete/.test(iframe.readyState)) 3262 { 3263 iframe.onreadystatechange = iframe.onload = null; 3264 _this.OnLoadIframe(iframe); 3265 } 3266 }; 3267 3268 // Append iframe to ext container 3269 this.config.cont.appendChild(iframe); 3270 3271 return iframe; 3272 }, 3273 3274 OnLoadIframe: function(iframe) 3275 { 3276 if (BX.isNodeInDom(iframe)) 3277 { 3278 this.InitIframe(iframe, () => { 3279 var iframeWindow = iframe.contentWindow; 3280 var iframeDocument = iframeWindow.document; 3281 3282 iframeWindow.onerror = function(errorMessage, fileName, lineNumber) { 3283 throw new Error("Sandbox: " + errorMessage, fileName, lineNumber); 3284 }; 3285 3286 if (this.bSandbox) 3287 { 3288 var i, length; 3289 for (i = 0, length = this.windowProperties.length; i < length; i++) 3290 { 3291 this._unset(iframeWindow, this.windowProperties[i]); 3292 } 3293 3294 for (i=0, length = this.windowProperties2.length; i < length; i++) 3295 { 3296 this._unset(iframeWindow, this.windowProperties2[i], BX.DoNothing()); 3297 } 3298 3299 for (i = 0, length = this.documentProperties.length; i < length; i++) 3300 { 3301 this._unset(iframeDocument, this.documentProperties[i]); 3302 } 3303 3304 this._unset(iframeDocument, "cookie", "", true); 3305 } 3306 3307 this.loaded = true; 3308 3309 this.callback(this); 3310 3311 this.editor.On('OnAfterIframeInit', [this.editor]); 3312 }); 3313 } 3314 }, 3315 3316 InitIframe: function(iframe, callback) 3317 { 3318 iframe = this.iframe || iframe; 3319 var 3320 iframeDocument = iframe.contentWindow.document, 3321 iframeHtml = this.GetHtml(this.config.stylesheets, this.editor.GetTemplateStyles()); 3322 3323 // Create the basic dom tree including proper DOCTYPE and charset 3324 iframeDocument.open("text/html", "replace"); 3325 iframeDocument.write(iframeHtml); 3326 iframeDocument.close(); 3327 iframeDocument.documentElement.innerHTML = iframeHtml; 3328 3329 this.GetWindow = function() 3330 { 3331 return iframe.contentWindow; 3332 }; 3333 this.GetDocument = function() 3334 { 3335 return iframe.contentWindow.document; 3336 }; 3337 3338 const triggerCallback = () => { 3339 if (iframeDocument.body) 3340 { 3341 3342 if (this.editor.config.autoResizeMaxHeight === 'Infinity') 3343 { 3344 const link = document.createElement('link'); 3345 link.rel = 'stylesheet'; 3346 link.href = this.editor.config.cssIframePath + '_' + this.editor.cssCounter++; 3347 link.onload = () => this.editor.ResizeSceleton(); 3348 iframeDocument.head.append(link); 3349 } 3350 3351 this.editor.On('OnIframeInit'); 3352 3353 if (typeof callback === 'function') 3354 { 3355 callback(); 3356 } 3357 } 3358 else 3359 { 3360 setTimeout(triggerCallback, 0); 3361 } 3362 }; 3363 3364 setTimeout(triggerCallback, 0); 3365 }, 3366 3367 GetHtml: function(css, cssText) 3368 { 3369 let bodyParams = ''; 3370 if (this.editor.config.bodyClass || this.editor.IsExpanded()) 3371 { 3372 let bodyClass = this.editor.config.bodyClass || ''; 3373 if (this.editor.IsExpanded()) 3374 bodyClass += ' fullscreen'; 3375 bodyParams += ' class="' + BX.util.trim(bodyClass) + '"'; 3376 } 3377 if (this.editor.config.bodyId) 3378 { 3379 bodyParams += ' id="' + this.editor.config.bodyId + '"'; 3380 } 3381 3382 let headHtml = ''; 3383 if (BX.Type.isStringFilled(this.editor.config.designTokens)) 3384 { 3385 3386 headHtml += this.editor.config.designTokens; 3387 } 3388 3389 if (BX.Type.isStringFilled(this.editor.config.headHtml)) 3390 { 3391 headHtml += this.editor.config.headHtml; 3392 } 3393 3394 const templ = this.editor.GetTemplateParams(); 3395 if (templ && templ['EDITOR_STYLES']) 3396 { 3397 for (let i = 0; i < templ['EDITOR_STYLES'].length; i++) 3398 { 3399 headHtml += '<link data-bx-template-style="Y" rel="stylesheet" href="' + templ['EDITOR_STYLES'][i] + '_' + this.editor.cssCounter++ + '">'; 3400 } 3401 } 3402 3403 css = typeof css === "string" ? [css] : css; 3404 if (css) 3405 { 3406 for (let i = 0; i < css.length; i++) 3407 { 3408 headHtml += '<link rel="stylesheet" href="' + css[i] + '">'; 3409 } 3410 } 3411 3412 headHtml += '<link rel="stylesheet" href="' + this.editor.config.cssIframePath + '_' + this.editor.cssCounter++ + '">'; 3413 3414 if (typeof cssText === "string") 3415 { 3416 headHtml += '<style type="text/css" data-bx-template-style="Y">' + cssText + '</style>'; 3417 } 3418 3419 if (BX.Type.isStringFilled(this.editor.config.fontSize)) 3420 { 3421 headHtml += `<style>body { font-size: ${this.editor.config.fontSize}; }</style>`; 3422 } 3423 3424 if (this.editor.iframeCssText && this.editor.iframeCssText.length > 0) 3425 { 3426 headHtml += '<style type="text/css">' + this.editor.iframeCssText + '</style>'; 3427 } 3428 3429 return '<!DOCTYPE html><html><head>' + headHtml + '</head><body' + bodyParams + '></body></html>'; 3430 }, 3431 3432 /** 3433 * Method to unset/override existing variables 3434 * @example 3435 * // Make cookie unreadable and unwritable
3436 * this._unset(document, "cookie", "", true); 3437 */ 3438 _unset: function(object, property, value, setter) 3439 { 3440 try { object[property] = value; } catch(e) {} 3441 3442 try { object.__defineGetter__(property, function() { return value; }); } catch(e) {} 3443 if (setter) { 3444 try { object.__defineSetter__(property, function() {}); } catch(e) {} 3445 } 3446 3447 if (!crashesWhenDefineProperty(property)) 3448 { 3449 try { 3450 var config = { 3451 get: function() { return value; } 3452 }; 3453 if (setter) { 3454 config.set = function() {}; 3455 } 3456 Object.defineProperty(object, property, config); 3457 } catch(e) {} 3458 } 3459 } 3460 }; 3461 3462 function BXEditorSelection(editor) 3463 { 3464 this.editor = editor; 3465 this.document = editor.sandbox.GetDocument(); 3466 BX.addCustomEvent(this.editor, 'OnIframeReInit', BX.proxy(function(){this.document = this.editor.sandbox.GetDocument();}, this)); 3467 // Make sure that our external range library is initialized 3468 window.rangy.init(); 3469 } 3470 3471 BXEditorSelection.prototype = 3472 { 3473 // Get the current selection as a bookmark to be able to later restore it 3474 GetBookmark: function() 3475 { 3476 if (!this.editor.synchro.IsFocusedOnTextarea()) 3477 { 3478 var range = this.GetRange(); 3479 return range && range.cloneRange(); 3480 } 3481 return false; 3482 }, 3483 3484 // Restore a selection 3485 SetBookmark: function(bookmark) 3486 { 3487 if (bookmark && this.editor.currentViewName !== 'code') 3488 { 3489 this.SetSelection(bookmark); 3490 } 3491 }, 3492 3493 // Save current selection 3494 SaveBookmark: function() 3495 { 3496 this.lastRange = this.GetBookmark(); 3497 return this.lastRange; 3498 }, 3499 3500 GetLastRange: function() 3501 { 3502 if (this.lastRange) 3503 return this.lastRange; 3504 }, 3505 3506 // Restore selection 3507 RestoreBookmark: function() 3508 { 3509 if (this.lastRange) 3510 { 3511 this.SetBookmark(this.lastRange); 3512 this.lastRange = false; 3513 } 3514 }, 3515 3516 /** 3517 * Set the caret in front of the given node 3518 * @param {Object} node The element or text node where to position the caret in front of 3519 */ 3520 SetBefore: function(node) 3521 { 3522 var range = rangy.createRange(this.document); 3523 range.setStartBefore(node); 3524 range.setEndBefore(node); 3525 return this.SetSelection(range); 3526 }, 3527 3528 /** 3529 * Set the caret after the given node 3530 * 3531 * @param {Object} node The element or text node where to position the caret in front of 3532 */ 3533 SetAfter: function(node) 3534 { 3535 var range = rangy.createRange(this.document); 3536 range.setStartAfter(node); 3537 range.setEndAfter(node); 3538 return this.SetSelection(range); 3539 }, 3540 3541 /** 3542 * Ability to select/mark nodes 3543 * 3544 * @param {Element} node The node/element to select 3545 */ 3546 SelectNode: function(node) 3547 { 3548 if (!node) 3549 return; 3550 3551 var 3552 range = rangy.createRange(this.document), 3553 isElement = node.nodeType === 1, 3554 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : (node.nodeName !== "IMG"), 3555 content = isElement ? node.innerHTML : node.data, 3556 isEmpty = (content === "" || content === this.editor.INVISIBLE_SPACE), 3557 styleDisplay = BX.style(node, 'display'), 3558 bBlock = (styleDisplay === "block" || styleDisplay === "list-item"); 3559 3560 if ((BX.browser.IsIE() || BX.browser.IsIE10() || BX.browser.IsIE11()) && node && 3561 BX.util.in_array(node.nodeName.toUpperCase(), this.editor.TABLE_TAGS)) 3562 { 3563 //"TD", "TR", "TH", "TABLE", "TBODY", "CAPTION", "COL", "COLGROUP", "TFOOT", "THEAD"]; 3564 if (node.tagName == 'TABLE' || node.tagName == 'TBODY') 3565 { 3566 var 3567 firstRow = node.rows[0], 3568 lastRow = node.rows[node.rows.length - 1]; 3569 3570 range.setStartBefore(firstRow.cells[0]); 3571 range.setEndAfter(lastRow.cells[lastRow.cells.length - 1]); 3572 } 3573 else if (node.tagName == 'TR' || node.tagName == 'TH') 3574 { 3575 range.setStartBefore(node.cells[0]); 3576 range.setEndAfter(node.cells[node.cells.length - 1]); 3577 } 3578 else 3579 { 3580 range.setStartBefore(node); 3581 range.setEndAfter(node); 3582 } 3583 3584 this.SetSelection(range); 3585 return range; 3586 } 3587 3588 if (isEmpty && isElement && canHaveHTML) 3589 { 3590 // Make sure that caret is visible in node by inserting a zero width no breaking space 3591 try { 3592 node.innerHTML = this.editor.INVISIBLE_SPACE; 3593 } catch(e) {} 3594 } 3595 3596 if (canHaveHTML) 3597 range.selectNodeContents(node); 3598 else 3599 range.selectNode(node); 3600 3601 if (canHaveHTML && isEmpty && isElement) 3602 { 3603 range.collapse(bBlock); 3604 } 3605 else if (canHaveHTML && isEmpty) 3606 { 3607 range.setStartAfter(node); 3608 range.setEndAfter(node); 3609 } 3610 3611 try 3612 { 3613 this.SetSelection(range); 3614 } 3615 catch(e){} 3616 3617 return range; 3618 }, 3619 3620 /** 3621 * Get the node which contains the selection 3622 * 3623 * @param {Boolean} [controlRange] (only IE) Whether it should return the selected ControlRange element when the selection type is a "ControlRange" 3624 * @return {Object}
3624 The node that contains the caret 3625 */ 3626 GetSelectedNode: function(controlRange) 3627 { 3628 var 3629 res, 3630 selection, 3631 range; 3632 3633 if (controlRange && this.document.selection && this.document.selection.type === "Control") 3634 { 3635 range = this.document.selection.createRange(); 3636 if (range && range.length) 3637 { 3638 res = range.item(0); 3639 } 3640 } 3641 3642 if (!res) 3643 { 3644 selection = this.GetSelection(); 3645 if (selection.focusNode === selection.anchorNode) 3646 { 3647 res = selection.focusNode; 3648 } 3649 } 3650 3651 if (!res) 3652 { 3653 range = this.GetRange(); 3654 res = range ? range.commonparentContainer : this.document.body 3655 } 3656 3657 if (res && res.ownerDocument != this.editor.GetIframeDoc()) 3658 { 3659 res = this.document.body; 3660 } 3661 3662 return res; 3663 }, 3664 3665 ExecuteAndRestore: function(method, restoreScrollPosition) 3666 { 3667 var 3668 body = this.document.body, 3669 oldScrollTop = restoreScrollPosition && body.scrollTop, 3670 oldScrollLeft = restoreScrollPosition && body.scrollLeft, 3671 className = "_bx-editor-temp-placeholder", 3672 placeholderHTML = '<span class="' + className + '">' + this.editor.INVISIBLE_SPACE + '</span>', 3673 range = this.GetRange(), 3674 newRange; 3675 3676 // Nothing selected, execute and say goodbye 3677 if (!range) 3678 { 3679 method(body, body); 3680 return; 3681 } 3682 3683 var node = range.createContextualFragment(placeholderHTML); 3684 range.insertNode(node); 3685 3686 // Make sure that a potential error doesn't cause our placeholder element to be left as a placeholder 3687 try 3688 { 3689 method(range.startContainer, range.endContainer); 3690 } 3691 catch(e3) 3692 { 3693 setTimeout(function() { throw e3; }, 0); 3694 } 3695 3696 if (document.querySelector) 3697 { 3698 var caretPlaceholder = this.document.querySelector("." + className); 3699 if (caretPlaceholder) 3700 { 3701 newRange = rangy.createRange(this.document); 3702 newRange.selectNode(caretPlaceholder); 3703 newRange.deleteContents(); 3704 this.SetSelection(newRange); 3705 } 3706 else 3707 { 3708 // fallback for when all hell breaks loose 3709 body.focus(); 3710 } 3711 } 3712 3713 if (restoreScrollPosition) 3714 { 3715 body.scrollTop = oldScrollTop; 3716 body.scrollLeft = oldScrollLeft; 3717 } 3718 3719 // Remove it again, just to make sure that the placeholder is definitely out of the dom tree 3720 try { 3721 if (caretPlaceholder && caretPlaceholder.parentNode) 3722 caretPlaceholder.parentNode.removeChild(caretPlaceholder); 3723 } catch(e4) {} 3724 }, 3725 3726 /** 3727 * Different approach of preserving the selection (doesn't modify the dom) 3728 * Takes all text nodes in the selection and saves the selection position in the first and last one 3729 */ 3730 ExecuteAndRestoreSimple: function(method) 3731 { 3732 var 3733 range = this.GetRange(), 3734 body = this.document.body, 3735 newRange, 3736 firstNode, 3737 lastNode, 3738 textNodes, 3739 rangeBackup; 3740 3741 // Nothing selected, execute and say goodbye 3742 if (!range) 3743 { 3744 method(body, body); 3745 return; 3746 } 3747 3748 textNodes = range.getNodes([3]); 3749 firstNode = textNodes[0] || range.startContainer; 3750 lastNode = textNodes[textNodes.length - 1] || range.endContainer; 3751 3752 rangeBackup = { 3753 collapsed: range.collapsed, 3754 startContainer: firstNode, 3755 startOffset: firstNode === range.startContainer ? range.startOffset : 0, 3756 endContainer: lastNode, 3757 endOffset: lastNode === range.endContainer ? range.endOffset : lastNode.length 3758 }; 3759 3760 try 3761 { 3762 method(range.startContainer, range.endContainer); 3763 } 3764 catch(e) 3765 { 3766 setTimeout(function() { throw e; }, 0); 3767 } 3768 3769 newRange = rangy.createRange(this.document); 3770 try { newRange.setStart(rangeBackup.startContainer, rangeBackup.startOffset); } catch(e1) {} 3771 try { newRange.setEnd(rangeBackup.endContainer, rangeBackup.endOffset); } catch(e2) {} 3772 try { this.SetSelection(newRange); } catch(e3) {} 3773 }, 3774 3775 /** 3776 * Insert html at the caret position and move the cursor after the inserted html 3777 * 3778 * @param {String} html HTML string to insert 3779 */ 3780 InsertHTML: function(html, range) 3781 { 3782 var 3783 rng = rangy.createRangyRange(this.document), 3784 node = rng.createContextualFragment(html), 3785 lastChild = node.lastChild; 3786 3787 this.InsertNode(node, range); 3788 if (lastChild) 3789 { 3790 this.SetAfter(lastChild); 3791 } 3792 3793 this.editor.On('OnInsertHtml'); 3794 }, 3795 3796 /** 3797 * Insert a node at the caret position and move the cursor behind it 3798 * 3799 * @param {Object}
3799 node HTML string to insert 3800 */ 3801 InsertNode: function(node, range) 3802 { 3803 if (!range || !range.isValid || !range.isValid()) 3804 range = this.GetRange(); 3805 3806 if (range) 3807 { 3808 range.insertNode(node); 3809 } 3810 3811 this.editor.On('OnInsertHtml'); 3812 }, 3813 3814 RemoveNode: function(node) 3815 { 3816 this.editor.On('OnHtmlContentChangedByControl'); 3817 var 3818 parent = node.parentNode, 3819 cursorNode = node.nextSibling; 3820 BX.remove(node); 3821 this.editor.util.Refresh(parent); 3822 3823 if (cursorNode) 3824 { 3825 this.editor.selection.SetBefore(cursorNode); 3826 this.editor.Focus(); 3827 } 3828 this.editor.synchro.StartSync(100); 3829 }, 3830 3831 /** 3832 * Wraps current selection with the given node 3833 * 3834 * @param {Object} node The node to surround the selected elements with 3835 * @param {Object} range Current range 3836 */ 3837 Surround: function(node, range) 3838 { 3839 range = range || this.GetRange(); 3840 if (range) 3841 { 3842 try 3843 { 3844 // This only works when the range boundaries are not overlapping other elements 3845 range.surroundContents(node); 3846 this.SelectNode(node); 3847 } 3848 catch(e) 3849 { 3850 node.appendChild(range.extractContents()); 3851 range.insertNode(node); 3852 } 3853 } 3854 }, 3855 3856 ScrollIntoView: function() 3857 { 3858 var 3859 node, 3860 _this = this, 3861 doc = this.document, 3862 win = this.editor.sandbox.GetWindow(), 3863 bScrollBars = doc.documentElement.scrollHeight > doc.documentElement.offsetHeight; 3864 3865 if (bScrollBars && win) 3866 { 3867 var 3868 tempNode = doc.__scrollIntoViewElement = doc.__scrollIntoViewElement || (function() 3869 { 3870 return BX.create("SPAN", {html: _this.editor.INVISIBLE_SPACE}, doc); 3871 })(), 3872 top = 0; 3873 3874 this.InsertNode(tempNode); 3875 3876 if (tempNode.parentNode) 3877 { 3878 node = tempNode; 3879 do 3880 { 3881 top += node.offsetTop || 0; 3882 node = node.offsetParent; 3883 } while (node); 3884 tempNode.parentNode.removeChild(tempNode); 3885 } 3886 3887 var 3888 scrollPos = BX.GetWindowScrollPos(doc), 3889 innerSize = BX.GetWindowInnerSize(doc); 3890 3891 if (top > scrollPos.scrollTop + innerSize.innerHeight - 40) 3892 { 3893 win.scrollTo(scrollPos.scrollLeft, top); 3894 } 3895 } 3896 }, 3897 3898 /** 3899 * Select line where the caret is in 3900 */ 3901 SelectLine: function() 3902 { 3903 // See https://developer.mozilla.org/en/DOM/Selection/modify 3904 var bSelectionModify = "getSelection" in window && "modify" in window.getSelection(); 3905 if (bSelectionModify) 3906 { 3907 var 3908 win = this.document.defaultView, 3909 selection = win.getSelection(); 3910 selection.modify("move", "left", "lineboundary"); 3911 selection.modify("extend", "right", "lineboundary"); 3912 } 3913 else if (this.document.selection) // IE 3914 { 3915 var 3916 range = this.document.selection.createRange(), 3917 rangeTop = range.boundingTop, 3918 rangeHeight = range.boundingHeight, 3919 scrollWidth = this.document.body.scrollWidth, 3920 rangeBottom, 3921 rangeEnd, 3922 measureNode, 3923 i, 3924 j; 3925 3926 if (!range.moveToPoint) 3927 return; 3928 3929 if (rangeTop === 0) 3930 { 3931 // Don't know why, but when the selection ends at the end of a line 3932 // range.boundingTop is 0 3933 measureNode = this.document.createElement("span"); 3934 this.insertNode(measureNode); 3935 rangeTop = measureNode.offsetTop; 3936 measureNode.parentNode.removeChild(measureNode); 3937 } 3938 rangeTop += 1; 3939 3940 for (i =- 10; i < scrollWidth; i += 2) 3941 { 3942 try { 3943 range.moveToPoint(i, rangeTop); 3944 break; 3945 } catch(e1) {} 3946 } 3947 3948 // Investigate the following in order to handle multi line selections 3949 // rangeBottom = rangeTop + (rangeHeight ? (rangeHeight - 1) : 0); 3950 rangeBottom = rangeTop; 3951 rangeEnd = this.document.selection.createRange(); 3952 for (j = scrollWidth; j >= 0; j--) 3953 { 3954 try 3955 { 3956 rangeEnd.moveToPoint(j, rangeBottom); 3957 break; 3958 } catch(e2) {} 3959 } 3960 3961 range.setEndPoint("EndToEnd", rangeEnd); 3962 range.select(); 3963 } 3964 }, 3965 3966 GetText: function() 3967 { 3968 var selection = this.GetSelection(); 3969 return selection ? selection.toString() : ""; 3970 }, 3971 3972 GetNodes: function(nodeType, filter) 3973 { 3974 var range = this.GetRange(); 3975 if (range) 3976 return range.getNodes([nodeType], filter); 3977 else 3978 return []; 3979 }, 3980 3981 GetRange: function(selection, bSetFocus) 3982 { 3983 if (!selection) 3984 { 3985 if (!this.editor.iframeView.IsFocused() && bSetFocus !== false) 3986 { 3987 var 3988 doc = this.editor.GetIframeDoc(), 3989 originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop, 3990 originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft; 3991 3992 this.editor.iframeView.Focus(); 3993 3994 var 3995 newScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop, 3996 newScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft; 3997 3998 if (newScrollTop !== originalScrollTop || newScrollLeft !== originalScrollLeft) 3999 { 4000 var win = this.editor.sandbox.GetWindow(); 4001 if (win) 4002 win.scrollTo(originalScrollLeft, originalScrollTop); 4003 } 4004 } 4005 4006 selection = this.GetSelection(); 4007 } 4008 4009 var range = selection && selection.rangeCount && selection.getRangeAt(0); 4010 if (range.commonAncestorContainer && range.commonAncestorContainer.nodeName == '#document') 4011 { 4012 range.selectNodeContents(this.editor.GetIframeDoc().body); 4013 } 4014 4015 return range; 4016 }, 4017 4018 GetSelection: function(doc) 4019 { 4020 return rangy.getSelection(doc || this.document.defaultView || this.document.parentWindow); 4021 }, 4022 4023 SetSelection: function(range) 4024 { 4025 var 4026 win = this.document.defaultView || this.document.parentWindow, 4027 selection = rangy.getSelection(win); 4028 return selection.setSingleRange(range); 4029 }, 4030 4031 TrimRange: function() 4032 {
4033 const text = this.editor.selection.GetText(); 4034 4035 if (BX.util.trim(text).length !== text.length) 4036 { 4037 const trimStart = text.length - text.trimStart().length; 4038 const trimEnd = text.length - text.trimEnd().length; 4039 4040 this.MoveRange(this.GetRange().startOffset + trimStart, this.GetRange().endOffset - trimEnd); 4041 } 4042 4043 return this.GetRange(); 4044 }, 4045 4046 MoveRange: function(start, end) 4047 { 4048 if (end < 0) 4049 { 4050 end = 0; 4051 } 4052 4053 const startNode = this.GetRange().startContainer; 4054 const endNode = this.GetRange().endContainer; 4055 const range = this.GetRange(); 4056 range.setStart(startNode, start); 4057 range.setEnd(endNode, end); 4058 this.GetSelection().removeAllRanges(); 4059 this.SetSelection(range); 4060 return this.GetRange(); 4061 }, 4062 4063 GetStructuralTags: function() 4064 { 4065 if (!this.structuralTags) 4066 { 4067 var tblRe = /^TABLE/i; 4068 this.structuralTags = { 4069 'LI': /^UL|OL|MENU/i, 4070 'DT': /^DL/i, 4071 'DD': /^DL/i, 4072 // table 4073 'TD': tblRe, 4074 'TR': tblRe, 4075 'TH': tblRe, 4076 'TBODY': tblRe, 4077 'TFOOT': tblRe, 4078 'THEAD': tblRe, 4079 'CAPTION': tblRe, 4080 'COL': tblRe, 4081 'COLGROUP': tblRe 4082 }; 4083 this.structuralTagsMatchRe = /^LI|DT|DD|TD|TR|TH|TBODY|CAPTION|COL|COLGROUP|TFOOT|THEAD/i; 4084 } 4085 return this.structuralTags; 4086 }, 4087 4088 SetCursorBeforeNode: function(e) 4089 { 4090 4091 }, 4092 4093 _GetNonTextLastChild: function(n) 4094 { 4095 var res = n.lastChild; 4096 while (res.nodeType != 1 && res.previousSibling) 4097 res = res.previousSibling; 4098 4099 return res.nodeType == 1 ? res : false; 4100 }, 4101 4102 _GetNonTextFirstChild: function(n) 4103 { 4104 var res = n.firstChild; 4105 while (res.nodeType != 1 && res.nextSibling) 4106 res = res.nextSibling; 4107 4108 return res.nodeType == 1 ? res : false; 4109 }, 4110 4111 _MoveCursorBeforeNode: function(node) 4112 { 4113 var 4114 _this = this, 4115 possibleParentRe, parNode, isFirstNode; 4116 4117 this.GetStructuralTags(); 4118 4119 // Check if it's child node which have special parent 4120 // We can't add text node and put carret <td> and <tr> or between <li>... 4121 // So we trying handle the only case when carret before beginning of the first child of our structural tags (UL, OL, MENU, DIR, TABLE, DL) 4122 if (node.nodeType == 1 && node.nodeName.match(this.structuralTagsMatchRe)) 4123 { 4124 isFirstNode = this._GetNonTextFirstChild(node.parentNode) === no
4124de; 4125 if (!isFirstNode) 4126 { 4127 return; 4128 } 4129 4130 possibleParentRe = this.structuralTags[node.nodeName]; 4131 if (possibleParentRe) 4132 { 4133 parNode = BX.findParent(node, function(n) 4134 { 4135 if (n.nodeName.match(possibleParentRe)) 4136 { 4137 return true; 4138 } 4139 4140 isFirstNode = isFirstNode && _this._GetNonTextFirstChild(n.parentNode) === n; 4141 return false; 4142 }, node.ownerDocument.BODY); 4143 4144 if (parNode && isFirstNode) 4145 { 4146 node = parNode; // Put carret before parrent tag 4147 } 4148 else 4149 { 4150 return; 4151 } 4152 } 4153 } 4154 4155 this.SetInvisibleTextBeforeNode(node); 4156 }, 4157 4158 _MoveCursorAfterNode: function(node) 4159 { 4160 var 4161 _this = this, 4162 possibleParentRe, parNode, isLastNode; 4163 4164 this.GetStructuralTags(); 4165 // Check if it's child node which have special parent 4166 // We can't add text node and put carret <td> and <tr> or between <li>... 4167 // So we trying handle the only case when carret in the end of the last child of last child of our structural tags (UL, OL, MENU, DIR, TABLE, DL) 4168 if (node.nodeType == 1 && node.nodeName.match(this.structuralTagsMatchRe)) 4169 { 4170 isLastNode = this._GetNonTextLastChild(node.parentNode) === no
4170de; 4171 if (!isLastNode) 4172 { 4173 return; 4174 } 4175 4176 possibleParentRe = this.structuralTags[node.nodeName]; 4177 if (possibleParentRe) 4178 { 4179 parNode = BX.findParent(node, function(n) 4180 { 4181 if (n.nodeName.match(possibleParentRe)) 4182 { 4183 return true; 4184 } 4185 4186 isLastNode = isLastNode && _this._GetNonTextLastChild(n.parentNode) === n; 4187 return false; 4188 }, node.ownerDocument.BODY); 4189 4190 if (parNode && isLastNode) 4191 { 4192 node = parNode; // Put carret after parrent tag 4193 } 4194 else 4195 { 4196 return; 4197 } 4198 } 4199 } 4200 4201 this.SetInvisibleTextAfterNode(node); 4202 }, 4203 4204 SaveRange: function(bSetFocus) 4205 { 4206 var range = this.GetRange(false, bSetFocus); 4207 if (range) 4208 { 4209 this.lastCheckedRange = {endOffset: range.endOffset, endContainer: range.endContainer, range: range}; 4210 } 4211 else 4212 { 4213 // Mantis: #66025 4214 setTimeout(BX.proxy(this.SaveRange, this), 0); 4215 } 4216 }, 4217 4218 CheckLastRange: function(range) 4219 { 4220 return this.lastCheckedRange && this.lastCheckedRange.endOffset == range.endOffset && this.lastCheckedRange.endContainer == range.endContainer; 4221 }, 4222 4223 SetInvisibleTextAfterNode: function(node, setCursorBefore) 4224 { 4225 var invis_text = this.editor.util.GetInvisibleTextNode(); 4226 if (node.nextSibling && node.nextSibling.nodeType == 3 && this.editor.util.IsEmptyNode(node.nextSibling)) 4227 { 4228 this.editor.util.ReplaceNode(node.nextSibling, invis_text); 4229 } 4230 else 4231 { 4232 this.editor.util.InsertAfter(invis_text, node); 4233 } 4234 4235 if (setCursorBefore) 4236 { 4237 this.SetBefore(invis_text); 4238 } 4239 else 4240 { 4241 this.SetAfter(invis_text); 4242 } 4243 4244 this.editor.Focus(); 4245 }, 4246 4247 SetInvisibleTextBeforeNode: function(node) 4248 { 4249 var invis_text = this.editor.util.GetInvisibleTextNode(); 4250 if (node.previousSibling && node.previousSibling.nodeType == 3 && this.editor.util.IsEmptyNode(node.previousSibling)) 4251 { 4252 this.editor.util.ReplaceNode(node.previousSibling, invis_text); 4253 } 4254 else 4255 { 4256 node.parentNode.insertBefore(invis_text, node); 4257 } 4258 4259 this.SetBefore(invis_text); 4260 this.editor.Focus(); 4261 }, 4262 4263 GetCommonAncestorForRange: function(range) 4264 { 4265 return range.collapsed ? 4266 range.startContainer : 4267 rangy.dom.getCommonAncestor(range.startContainer, range.endContainer); 4268 } 4269 }; 4270 4271 function NodeMerge(firstNode) 4272 { 4273 this.isElementMerge = (firstNode.nodeType == 1); 4274 this.firstTextNode = this.isElementMerge ? firstNode.lastChild : firstNode; 4275 this.firstNode = firstNode; 4276 this.textNodes = [this.firstTextNode]; 4277 } 4278 4279 NodeMerge.prototype = { 4280 DoMerge: function() 4281 { 4282 var 4283 i, len = this.textNodes.length, 4284 textBits = [], textNode, parent, text; 4285 4286 for (i = 0; i < len; ++i) 4287 { 4288 textNode = this.textNodes[i]; 4289 if (this.textNodes[i].nodeType !== 3) 4290 { 4291 return false; 4292 } 4293 4294 parent = textNode.parentNode; 4295 textBits[i] = textNode.data; 4296 if (i) 4297 { 4298 parent.removeChild(textNode); 4299 if (!parent.hasChildNodes()) 4300 parent.parentNode.removeChild(parent); 4301 } 4302 } 4303 this.firstTextNode.data = text = textBits.join(""); 4304 4305 return text; 4306 }, 4307 4308 GetLength: function() 4309 { 4310 var i = this.textNodes.length, len = 0; 4311 while (i--) 4312 len += this.textNodes[i].length; 4313 return len; 4314 } 4315 4316// ToString: function() 4317// { 4318// var textBits = []; 4319// 4320// for (var i = 0, len = this.textNodes.length; i < len; ++i) 4321// textBits[i] = "'" + this.textNodes[i].data + "'"; 4322// 4323// return "[Merge(" + textBits.join(",") + ")]"; 4324// } 4325 }; 4326 4327 function HTMLStyler(editor, tagNames, arStyle, cssClass, normalize) 4328 { 4329 this.editor = editor; 4330 this.document = editor.iframeView.document; 4331 this.tagNames = tagNames || [defaultTagName]; 4332 this.arStyle = arStyle || {}; 4333 this.cssClass = cssClass || ""; 4334 this.similarClassRegExp = null; 4335 this.normalize = normalize; 4336 this.applyToAnyTagName = false; 4337 } 4338 4339 HTMLStyler.prototype = 4340 { 4341 GetStyledParent: function(node, bMatchCss) 4342 { 4343 bMatchCss = bMatchCss !== false; 4344 var cssClassMatch, cssStyleMatch; 4345 while (node) 4346 { 4347 if (node.nodeType == 1) 4348 { 4349 cssStyleMatch = this.CheckCssStyle(node, bMatchCss); 4350 cssClassMatch = this.CheckCssClass(node); 4351 4352 if (BX.util.in_array(node.tagName.toLowerCase(), this.tagNames) && cssClassMatch && cssStyleMatch) 4353 return node; 4354 } 4355 4356 node = node.parentNode; 4357 } 4358 return false;
4359 }, 4360 4361 CheckCssStyle: function(node, bMatch) 4362 { 4363 return this.editor.util.CheckCss(node, this.arStyle, bMatch); 4364 }, 4365 4366 SimplifyNodesWithCss: function(node) 4367 { 4368 var i, parent = node.parentNode; 4369 if (parent.childNodes.length == 1) // container over our node 4370 { 4371 if (node.nodeName == parent.nodeName) 4372 { 4373 for (i in this.arStyle) 4374 { 4375 if (this.arStyle.hasOwnProperty(i) && node.style[i]) 4376 { 4377 parent.style[i] = node.style[i]; 4378 } 4379 } 4380 this.editor.util.ReplaceWithOwnChildren(node); 4381 } 4382 else 4383 { 4384 for (i in this.arStyle) 4385 { 4386 if (this.arStyle.hasOwnProperty(i) && parent.style[i] && node.style[i]) 4387 { 4388 parent.style[i] = ''; 4389 } 4390 } 4391 } 4392 } 4393 }, 4394 4395 CheckCssClass: function(node) 4396 { 4397 return !this.cssClass || (this.cssClass && BX.hasClass(node, this.cssClass)); 4398 }, 4399 4400 // Normalizes nodes after applying a CSS class to a Range. 4401 PostApply: function(textNodes, range) 4402 { 4403 var 4404 i, 4405 firstNode = textNodes[0], 4406 lastNode = textNodes[textNodes.length - 1], 4407 merges = [], 4408 currentMerge, 4409 rangeStartNode = firstNode, 4410 rangeEndNode = lastNode, 4411 rangeStartOffset = 0, 4412 rangeEndOffset = lastNode.length, 4413 textNode, precedingTextNode; 4414 4415 for (i = 0; i < textNodes.length; ++i) 4416 { 4417 textNode = textNodes[i]; 4418 precedingTextNode = this.GetAdjacentMergeableTextNode(textNode.parentNode, false); 4419 if (precedingTextNode) 4420 { 4421 if (!currentMerge) 4422 { 4423 currentMerge = new NodeMerge(precedingTextNode); 4424 merges.push(currentMerge); 4425 } 4426 currentMerge.textNodes.push(textNode); 4427 4428 if (textNode === firstNode) 4429 { 4430 rangeStartNode = currentMerge.firstTextNode; 4431 rangeStartOffset = rangeStartNode.length; 4432 } 4433 4434 if (textNode === lastNode) 4435 { 4436 rangeEndNode = currentMerge.firstTextNode; 4437 rangeEndOffset = currentMerge.GetLength(); 4438 } 4439 } 4440 else 4441 { 4442 currentMerge = null; 4443 } 4444 } 4445 4446 // Test whether the first node after the range needs merging 4447 var nextTextNode = this.GetAdjacentMergeableTextNode(lastNode.parentNode, true); 4448 if (nextTextNode) 4449 { 4450 if (!currentMerge) 4451 { 4452 currentMerge = new NodeMerge(lastNode); 4453 merges.push(currentMerge); 4454 } 4455 currentMerge.textNodes.push(nextTextNode); 4456 } 4457 4458 // Do the merges 4459 if (merges.length) 4460 { 4461 for (i = 0; i < merges.length; ++i) 4462 merges[i].DoMerge(); 4463 4464 // Set the range boundaries 4465 range.setStart(rangeStartNode, rangeStartOffset); 4466 range.setEnd(rangeEndNode, rangeEndOffset); 4467 } 4468 4469 // Simplify elements 4470 textNodes = range.getNodes([3]); 4471 for (i = 0; i < textNodes.length; ++i) 4472 { 4473 textNode = textNodes[i]; 4474 this.SimplifyNodesWithCss(textNode.parentNode); 4475 } 4476 }, 4477 4478 NormalizeNewNode: function(node, range) 4479 { 4480 var 4481 parent = node.parentNode; 4482 4483 if (parent && parent.nodeName !== 'BODY') 4484 { 4485 var 4486 childs = this.GetNonEmptyChilds(parent), 4487 cssStyleMatch = this.CheckCssStyle(parent, false), 4488 cssClassMatch = this.CheckCssClass(parent); 4489 4490 if (childs.length == 1 && parent.nodeName == node.nodeName && cssStyleMatch && cssClassMatch) 4491 { 4492 parent.parentNode.insertBefore(node, parent); 4493 BX.remove(parent); 4494 } 4495 } 4496 4497 return range; 4498 }, 4499 4500 GetNonEmptyChilds: function(node) 4501 { 4502 var 4503 i, 4504 childs = node.childNodes, 4505 res = []; 4506 4507 for (i = 0; i < childs.length; i++) 4508 { 4509 if (childs[i].nodeType == 1 || 4510 (childs[i].nodeType == 3 4511 && childs[i].nodeValue != "" 4512 && childs[i].nodeValue != this.editor.INVISIBLE_SPACE 4513 && !childs[i].nodeValue.match(/^[\s\n\r\t]+$/ig))) 4514 { 4515 res.push(childs[i]); 4516 } 4517 } 4518 return res; 4519 }, 4520 4521 GetAdjacentMergeableTextNode: function(node, forward) 4522 { 4523 var 4524 isTextNode = (node.nodeType == 3), 4525 el = isTextNode ? node.parentNode : node, 4526 adjacentNode, 4527 propName = forward ? "nextSibling" : "previousSibling"; 4528 4529 if (isTextNode) 4530 { 4531 // Can merge if the node's previous/next sibling is a text node 4532 adjacentNode = node[propName]; 4533 if (adjacentNode && adjacentNode.nodeType == 3) 4534 { 4535 return adjacentNode; 4536 } 4537 } 4538 else 4539 {
4540 // TODO: fix it. Now code fails on this example, try to make "22" italic 4541 // <i>11 </i>22<i><br> 4542 // 33 </i><br> 4543 /* 4544 // Compare element with its sibling 4545 adjacentNode = el[propName]; 4546 if (adjacentNode && this.AreElementsMergeable(node, adjacentNode)) 4547 { 4548 4549 return adjacentNode[forward ? "firstChild" : "lastChild"]; 4550 } 4551 */ 4552 } 4553 return null; 4554 }, 4555 4556 AreElementsMergeable: function(el1, el2) 4557 { 4558 return rangy.dom.arrayContains(this.tagNames, (el1.tagName || "").toLowerCase()) 4559 && rangy.dom.arrayContains(this.tagNames, (el2.tagName || "").toLowerCase()) 4560 && el1.className.replace(/\s+/g, " ") == el2.className.replace(/\s+/g, " ") 4561 && this.CompareNodeAttributes(el1, el2); 4562 }, 4563 4564 CompareNodeAttributes: function(el1, el2) 4565 { 4566 if (el1.attributes.length != el2.attributes.length) 4567 return false; 4568 4569 var i, len = el1.attributes.length, attr1, attr2, name; 4570 for (i = 0; i < len; i++) 4571 { 4572 attr1 = el1.attributes[i]; 4573 name = attr1.name; 4574 if (name != "class") 4575 { 4576 attr2 = el2.attributes.getNamedItem(name); 4577 if (attr1.specified != attr2.specified) 4578 return false; 4579 4580 if (attr1.specified && attr1.nodeValue !== attr2.nodeValue) 4581 { 4582 return false; 4583 } 4584 } 4585 } 4586 return true; 4587 }, 4588 4589 CreateContainer: function() 4590 { 4591 var el = this.document.createElement(this.tagNames[0]); 4592 if (this.cssClass) 4593 { 4594 el.className = this.cssClass; 4595 } 4596 if (this.arStyle) 4597 { 4598 for (var i in this.arStyle) 4599 { 4600 if (this.arStyle.hasOwnProperty(i)) 4601 { 4602 el.style[i] = this.arStyle[i]; 4603 } 4604 } 4605 } 4606 return el; 4607 }, 4608 4609 ApplyToTextNode: function(textNode) 4610 { 4611 var parent = textNode.parentNode, i; 4612 4613 if (parent.childNodes.length == 1 && BX.util.in_array(parent.tagName.toLowerCase(), this.tagNames)) 4614 { 4615 if (this.cssClass) 4616 { 4617 BX.addClass(parent, this.cssClass); 4618 } 4619 4620 if (this.arStyle) 4621 { 4622 for (i in this.arStyle) 4623 { 4624 if (this.arStyle.hasOwnProperty(i)) 4625 { 4626 parent.style[i] = this.arStyle[i]; 4627 } 4628 } 4629 } 4630 } 4631 else 4632 { 4633 if (parent.childNodes.length == 1) // container over the text node 4634 { 4635 if (this.cssClass && BX.hasClass(parent, this.cssClass)) 4636 { 4637 BX.removeClass(parent, this.cssClass); 4638 } 4639 4640 if (this.arStyle) 4641 { 4642 for (i in this.arStyle) 4643 { 4644 if (this.arStyle.hasOwnProperty(i) && parent.style[i]) 4645 { 4646 parent.style[i] = ''; 4647 } 4648 } 4649 } 4650 } 4651 4652 var el = this.CreateContainer(); 4653 textNode.parentNode.insertBefore(el, textNode); 4654 el.appendChild(textNode); 4655 } 4656 }, 4657 4658 IsRemovable: function(el) 4659 { 4660 return rangy.dom.arrayContains(this.tagNames, el.tagName.toLowerCase()) && BX.util.trim(el.className) == this.cssClass; 4661 }, 4662 4663 IsAvailableTextNodeParent: function(node) 4664 { 4665 return node && node.nodeName && 4666 node.nodeName !== 'OL' && node.nodeName !== 'UL' && node.nodeName !== 'MENU' && // LLIST 4667 node.nodeName !== 'TBODY' && node.nodeName !== 'TFOOT' && node.nodeName !== 'THEAD' && node.nodeName !== 'TABLE' && // TABLE 4668 node.nodeName !== 'DL'; 4669 }, 4670 4671 UndoToTextNode: function(textNode, range, styledParent) 4672 { 4673 if (!range.containsNode(styledParent)) 4674 { 4675 // Split out the portion of the parent from which we can remove the CSS class 4676 var parentRange = range.cloneRange(); 4677 parentRange.selectNode(styledParent); 4678 4679 4680 BX.isParentForNode(styledParent, range.endContainer) 4681 if ( 4682 range.endContainer.nodeName !== 'BODY' && 4683 parentRange.isPointInRange(range.endContainer, range.endOffset) && 4684 this.editor.util.IsSplitPoint(range.endContainer, range.endOffset) && 4685 BX.isParentForNode(styledParent, range.endContainer) 4686 ) 4687 { 4688 this.editor.util.SplitNodeAt(styledParent, range.endContainer, range.endOffset); 4689 range.setEndAfter(styledParent); 4690 } 4691 4692 if ( 4693 range.startContainer.nodeName !== 'BODY' && 4694 parentRange.isPointInRange(range.startContainer, range.startOffset) && 4695 this.editor.util.IsSplitPoint(range.startContainer, range.startOffset) && 4696 BX.isParentForNode(styledParent, range.startContainer) 4697 ) 4698 { 4699 styledParent = this.editor.util.SplitNodeAt(styledParent, range.startContainer, range.startOffset); 4700 } 4701 } 4702 4703 if (styledParent && styledParent.nodeName != 'BODY' && this.IsRemovable(styledParent)) 4704 { 4705 if (this.arStyle) 4706 { 4707 for (var i in this.arStyle) 4708 { 4709 if (this.arStyle.hasOwnProperty(i) && styledParent.style[i]) 4710 { 4711 styledParent.style[i] = ''; 4712 } 4713 } 4714 } 4715 4716 if (!styledParent.style.cssText || BX.util.trim(styledParent.style.cssText) === '') 4717 { 4718 this.editor.util.ReplaceWithOwnChildren(styledParent); 4719 } 4720 else if (this.tagNames.length > 1 || this.tagNames[0] !== 'span') 4721 { 4722 this.editor.util.RenameNode(styledParent, "span"); 4723 } 4724 } 4725 }, 4726 4727 ApplyToRange: function(range) 4728 { 4729 var textNodes = range.getNodes([3]); 4730 4731 if (!textNodes.length) 4732 { 4733 try { 4734 var node = this.CreateContainer(); 4735 range.surroundContents(node); 4736 range = this.NormalizeNewNode(node, range); 4737 this.SelectNode(range, node); 4738 return range; 4739 } catch(e) {} 4740 } 4741 4742 range.splitBoundaries(); 4743 textNodes = range.getNodes([3]); 4744 4745 if (!textNodes.length && range.collapsed && range.startContainer == range.endContainer) 4746 { 4747 var inv = this.editor.util.GetInvisibleTextNode(); 4748 this.editor.selection.InsertNode(inv); 4749 textNodes = [inv]; 4750 } 4751 4752 if (textNodes.length) 4753 { 4754 var textNode; 4755 4756 for (var i = 0, len = textNodes.length; i < len; ++i) 4757 { 4758 textNode = textNodes[i]; 4759 if (!this.GetStyledParent(textNode) && this.IsAvailableTextNodeParent(textNode.parentNode)) 4760 {
4761 this.ApplyToTextNode(textNode); 4762 } 4763 } 4764 4765 range.setStart(textNodes[0], 0); 4766 textNode = textNodes[textNodes.length - 1]; 4767 range.setEnd(textNode, textNode.length); 4768 4769 if (this.normalize) 4770 { 4771 this.PostApply(textNodes, range); 4772 } 4773 } 4774 return range; 4775 }, 4776 4777 UndoToRange: function(range, bMatchCss) 4778 { 4779 var 4780 textNodes = range.getNodes([3]), 4781 textNode, 4782 styledParent; 4783 4784 bMatchCss = bMatchCss !== false; 4785 4786 if (textNodes.length) 4787 { 4788 range.splitBoundaries(); 4789 textNodes = range.getNodes([3]); 4790 } 4791 else 4792 { 4793 var node = this.editor.util.GetInvisibleTextNode(); 4794 range.insertNode(node); 4795 range.selectNode(node); 4796 textNodes = [node]; 4797 } 4798 4799 var i, len, sorted = []; 4800 for (i = 0, len = textNodes.length; i < len; i++) 4801 { 4802 sorted.push({node: textNodes[i], nesting: this.GetNodeNesting(textNodes[i])}); 4803 } 4804 4805 sorted = sorted.sort(function(a, b){return b.nesting - a.nesting}); 4806 4807 for (i = 0, len = sorted.length; i < len; i++) 4808 { 4809 textNode = sorted[i].node; 4810 styledParent = this.GetStyledParent(textNode, bMatchCss); 4811 if (styledParent) 4812 { 4813 this.UndoToTextNode(textNode, range, styledParent); 4814 range = this.editor.selection.GetRange(); 4815 } 4816 } 4817 4818 if (len == 1) 4819 { 4820 this.SelectNode(range, textNodes[0]); 4821 } 4822 else 4823 { 4824 range.setStart(textNodes[0], 0); 4825 range.setEnd(textNodes[textNodes.length - 1], textNodes[textNodes.length - 1].length); 4826 this.editor.selection.SetSelection(range); 4827 4828 if (this.normalize) 4829 { 4830 this.PostApply(textNodes, range); 4831 } 4832 } 4833 4834 return range; 4835 }, 4836 4837 // Node dom offset 4838 GetNodeNesting: function(node) 4839 { 4840 return this.editor.util.GetNodeDomOffset(node); 4841 }, 4842 4843 SelectNode: function(range, node) 4844 { 4845 var 4846 isElement = node.nodeType === 1, 4847 canHaveHTML = "canHaveHTML" in node ? node.canHaveHTML : true, 4848 content = isElement ? node.innerHTML : node.data, 4849 isEmpty = (content === "" || content === this.editor.INVISIBLE_SPACE); 4850 4851 if (isEmpty && isElement && canHaveHTML) 4852 { 4853 // Make sure that caret is visible in node by inserting a zero width no breaking space 4854 try {node.innerHTML = this.editor.INVISIBLE_SPACE;} catch(e) {} 4855 } 4856 4857 range.selectNodeContents(node); 4858 if (isEmpty && isElement) 4859 { 4860 range.collapse(false); 4861 } 4862 else if (isEmpty) 4863 { 4864 range.setStartAfter(node); 4865 range.setEndAfter(node); 4866 } 4867 }, 4868 4869 GetTextSelectedByRange: function(textNode, range) 4870 { 4871 var textRange = range.cloneRange(); 4872 textRange.selectNodeContents(textNode); 4873 4874 var intersectionRange = textRange.intersection(range); 4875 var text = intersectionRange ? intersectionRange.toString() : ""; 4876 textRange.detach(); 4877 4878 return text; 4879 }, 4880 4881 IsAppliedToRange: function(range, bMatchCss) 4882 { 4883 var 4884 parents = [], 4885 parent, 4886 textNodes = range.getNodes([3]); 4887 bMatchCss = bMatchCss !== false; 4888 4889 if (!textNodes.length) 4890 { 4891 parent = this.GetStyledParent(range.startContainer, bMatchCss); 4892 return parent ? [parent] : false; 4893 } 4894 4895 var i, selectedText; 4896 for (i = 0; i < textNodes.length; ++i) 4897 { 4898 selectedText = this.GetTextSelectedByRange(textNodes[i], range); 4899 parent = this.GetStyledParent(textNodes[i], bMatchCss); 4900 if (selectedText != "" && !parent) 4901 { 4902 return false; 4903 } 4904 else 4905 { 4906 parents.push(parent); 4907 } 4908 } 4909 return parents; 4910 }, 4911 4912 ToggleRange: function(range) 4913 { 4914 return this.IsAppliedToRange(range) ? this.UndoToRange(range) : this.ApplyToRange(range); 4915 } 4916 }; 4917 4918 function BXEditorUndoManager(editor) 4919 { 4920 this.editor = editor; 4921 this.history = [this.editor.iframeView.GetValue()]; 4922 this.position = 1; 4923 this.document = editor.sandbox.GetDocument(); 4924 this.historyLength = 30; 4925 BX.addCustomEvent(this.editor, 'OnIframeReInit', BX.proxy(function(){this.document = this.editor.sandbox.GetDocument();}, this)); 4926 this.Init(); 4927 } 4928 4929 BXEditorUndoManager.prototype = { 4930 Init: function() 4931 { 4932 var _this = this; 4933 BX.addCustomEvent(this.editor, "OnHtmlContentChangedByControl", BX.delegate(this.Transact, this)); 4934 BX.addCustomEvent(this.editor, "OnIframeNewWord", BX.delegate(this.Transact, this)); 4935 BX.addCustomEvent(this.editor, "OnIframeKeyup", BX.delegate(this.Transact, this)); 4936 BX.addCustomEvent(this.editor, "OnBeforeCommandExec", function(isContentAction) 4937 { 4938 if (isContentAction) 4939 { 4940 _this.Transact(); 4941 } 4942 }); 4943 4944 // CTRL+Z and CTRL+Y and handle DEL & BACKSPACE 4945 BX.addCustomEvent(this.editor, "OnIframeKeydown", BX.proxy(this.Keydown, this)); 4946 }, 4947 4948 Keydown: function(e, keyCode, command, selectedNode) 4949 { 4950 if ((e.ctrlKey || e.metaKey) && !e.altKey) 4951 { 4952 var 4953 isUndo = keyCode === this.editor.KEY_CODES['z'] && !e.shiftKey, 4954 isRedo = (keyCode === this.editor.KEY_CODES['z'] && e.shiftKey) || (keyCode === this.editor.KEY_CODES['y']); 4955 4956 if (isUndo) 4957 { 4958 this.Undo(); 4959 return BX.PreventDefault(e); 4960 } 4961 else if (isRedo) 4962 { 4963 this.Redo(); 4964 return BX.PreventDefault(e); 4965 } 4966 } 4967 4968 if (keyCode !== this.lastKey) 4969 { 4970 if (keyCode === this.editor.KEY_CODES['backspace'] || keyCode === this.editor.KEY_CODES['delete']) 4971 { 4972 this.Transact(); 4973 } 4974 this.lastKey = keyCode; 4975 } 4976 }, 4977 4978 Transact: function() 4979 { 4980 var 4981 previousHtml = this.history[this.position - 1], 4982 currentHtml = this.editor.iframeView.GetValue().replaceAll(decodeURIComponent('%EF%BB%BF'), ''); 4983 4984 if (currentHtml.match(/<div class="bxhtmled-copilot"(.*?)>(.*?)<\/div>/g) !== null) 4985 { 4986 return; 4987 } 4988 4989 if (currentHtml !== previousHtml) 4990 { 4991 var length = this.history.length = this.position; 4992 if (length > this.historyLength) 4993 { 4994 this.history.shift(); 4995 this.position--; 4996 } 4997 4998 this.position++; 4999 this.history.push(currentHtml); 5000 5001 this.CheckControls(); 5002 } 5003 }, 5004 5005 Undo: function() 5006 { 5007 if (this.position > 1) 5008 { 5009 this.Transact(); 5010 this.position--; 5011 this.Set(this.history[this.position - 1]); 5012 this.editor.On("OnUndo"); 5013 this.CheckControls(); 5014 } 5015 }, 5016 5017 Redo: function() 5018 { 5019 if (this.position < this.history.length) 5020 { 5021 this.position++; 5022 this.Set(this.history[this.position - 1]); 5023 this.editor.On("OnRedo"); 5024 this.CheckControls(); 5025 } 5026 }, 5027 5028 Set: function(html) 5029 { 5030 this.editor.iframeView.SetValue(html); 5031 this.editor.Focus(true); 5032 }, 5033 5034 CheckControls: function() 5035 { 5036 this.editor.On("OnEnableUndo", [this.position > 1]); 5037 this.editor.On("OnEnableRedo", [this.position < this.history.length]); 5038 } 5039 }; 5040 5041 function BXStyles(editor) 5042 { 5043 this.editor = editor; 5044 this.arStyles = {}; 5045 this.sStyles = ''; 5046 } 5047 5048 BXStyles.prototype = { 5049 GetIframe: function(styles) 5050 { 5051 if (!this.cssIframe) 5052 { 5053 this.cssIframe = this.CreateIframe(styles); 5054 } 5055 return this.cssIframe; 5056 }, 5057 5058 CreateIframe: function(styles) 5059 { 5060 this.cssIframe = document.body.appendChild(BX.create("IFRAME", {props: {className: "bx-editor-css-iframe"}})); 5061 this.iframeDocument = this.cssIframe.contentDocument || this.cssIframe.contentWindow.document; 5062 this.iframeDocument.open("text/html", "replace");
5063 this.iframeDocument.write('<!DOCTYPE html><html><head><style type="text/css" data-bx-template-style="Y">' + styles + '</style></head><body></body></html>'); 5064 this.iframeDocument.close(); 5065 return this.cssIframe; 5066 }, 5067 5068 GetCSS: function(templateId, styles, templatePath, filter) 5069 { 5070 if (!this.arStyles[templateId]) 5071 { 5072 if (!this.cssIframe) 5073 { 5074 this.cssIframe = this.CreateIframe(styles); 5075 } 5076 else 5077 { 5078 var 5079 i, 5080 doc = this.iframeDocument, 5081 head = doc.head || doc.getElementsByTagName('HEAD')[0], 5082 styleNodes = head.getElementsByTagName('STYLE'); 5083 5084 // Clean old template styles 5085 for (i = 0; i < styleNodes.length; i++) 5086 { 5087 if (styleNodes[i].getAttribute('data-bx-template-style') == 'Y') 5088 BX.cleanNode(styleNodes[i], true); 5089 } 5090 5091 // Add new node in the iframe head 5092 if (styles) 5093 { 5094 head.appendChild(BX.create('STYLE', {props: {type: 'text/css'}, text: styles}, doc)).setAttribute('data-bx-template-style', 'Y'); 5095 } 5096 } 5097 this.arStyles[templateId] = this.ParseCss(); 5098 } 5099 5100 var res = this.arStyles[templateId]; 5101 5102 if (filter) 5103 { 5104 var filteredRes = [], tag; 5105 if (typeof filter != 'object' ) 5106 { 5107 filter = [filter]; 5108 } 5109 filter.push('DEFAULT'); 5110 for (i = 0; i < filter.length; i++) 5111 { 5112 tag = filter[i]; 5113 if (res[tag] && typeof res[tag] == 'object') 5114 { 5115 filteredRes = filteredRes.concat(res[tag]); 5116 } 5117 } 5118 res = filteredRes; 5119 } 5120 5121 return res; 5122 }, 5123 5124 ParseCss: function() 5125 { 5126 var 5127 doc = this.iframeDocument, 5128 arAllSt = [], 5129 result = {}, 5130 rules, 5131 cssTag, arTags, i, j, k, 5132 t1, t2, l1, l2, l3; 5133 5134 if(!doc.styleSheets) 5135 { 5136 return result; 5137 } 5138 5139 var 5140 x1 = doc.styleSheets, 5141 stylesDescription = this.editor.GetStylesDescription(); 5142 5143 for(i = 0, l1 = x1.length; i < l1; i++) 5144 { 5145 rules = (x1[i].rules ? x1[i].rules : x1[i].cssRules); 5146 for(j = 0, l2 = rules.length; j < l2; j++) 5147 { 5148 if (rules[j].type != rules[j].STYLE_RULE) 5149 { 5150 continue; 5151 } 5152 5153 cssTag = rules[j].selectorText; 5154 arTags = cssTag.split(","); 5155 for(k = 0, l3 = arTags.length; k < l3; k++) 5156 { 5157 t1 = arTags[k].split(" "); 5158 t1 = t1[t1.length - 1].trim(); 5159 5160 if(t1.substr(0, 1) == '.') 5161 { 5162 t1 = t1.substr(1); 5163 t2 = 'DEFAULT'; 5164 } 5165 else 5166 { 5167 t2 = t1.split("."); 5168 t1 = (t2.length > 1) ? t2[1] : ''; 5169 t2 = t2[0].toUpperCase(); 5170 } 5171 5172 if(!arAllSt[t1]) 5173 { 5174 arAllSt[t1] = true; 5175 if(!result[t2]) 5176 { 5177 result[t2] = []; 5178 } 5179 5180 result[t2].push({ 5181 className: t1, 5182 classTitle: stylesDescription[t1] || null, 5183 original: arTags[k], 5184 cssText: rules[j].style.cssText 5185 }); 5186 } 5187 } 5188 } 5189 } 5190 return result; 5191 } 5192 }; 5193 5194 5195 // Parse rules 5196 /** 5197 * Full HTML5 compatibility rule set 5198 * These rules define which tags and css classes are supported and which tags should be specially treated. 5199 * 5200 * Examples based on this rule set: 5201 * 5202 * <a href="http://foobar.com">foo</a> 5203 * ... becomes ... 5204 * <a href="http://foobar.com" target="_blank" rel="nofollow">foo</a> 5205 * 5206 * <img align="left" src="http://foobar.com/image.png"> 5207 * ... becomes ... 5208 * <img class="wysiwyg-float-left" src="http://foobar.com/image.png" alt=""> 5209 * 5210 * <div>foo<script>alert(document.cookie)</script></div> 5211 * ... becomes ... 5212 * <div>foo</div> 5213 * 5214 * <marquee>foo</marquee> 5215 * ... becomes ... 5216 * <span>foo</marquee> 5217 * 5218 * foo <br clear="both"> bar 5219 * ... becomes ... 5220 * foo <br class="wysiwyg-clear-both"> bar 5221 * 5222 * <div>hello <iframe src="http://google.com"></iframe></div> 5223 * ... becomes ... 5224 * <div>hello </div> 5225 * 5226 * <center>hello</center> 5227 * ... becomes ... 5228 * <div class="wysiwyg-text-align-center">hello</div> 5229 */ 5230 __BXHtmlEditorParserRules = { 5231 /** 5232 * CSS Class white-list 5233 * Following css classes won't be removed when parsed by the parser 5234 */ 5235 classes: {}, 5236 5237 "tags": { 5238 "b": {clean_empty: true}, 5239 "strong": {clean_empty: true}, 5240 "i": {clean_empty: true}, 5241 "em": {clean_empty: true}, 5242 "u": {clean_empty: true}, 5243 "del": {clean_empty: true}, 5244 "s": {rename_tag: "del"}, 5245 "strike": {rename_tag: "del"}, 5246 5247 // headers 5248 "h1": {}, 5249 "h2": {}, 5250 "h3": {}, 5251 "h4": {}, 5252 "h5": {}, 5253 "h6": {}, 5254 5255 // popular tags 5256 "span": {clean_empty: true}, 5257 "p": {}, 5258 "br": {}, 5259 "div": {}, 5260 "hr": {}, 5261 "nobr": {}, 5262 "code": {}, 5263 "section": {}, 5264 "figure": {}, 5265 "figcaption": {}, 5266 "fieldset": {}, 5267 "legend": {}, 5268 5269 // Lists 5270 "menu": {rename_tag: "ul"}, // ?? 5271 "ol": {}, 5272 "ul": {}, 5273 "li": {}, 5274 "pre": {}, 5275 5276 // Table 5277 "table": {}, 5278 "tr": {}, 5279 "td": { 5280 "check_attributes": { 5281 "rowspan": "numbers", 5282 "colspan": "numbers" 5283 } 5284 }, 5285 "tbody": {}, 5286 "tfoot": {}, 5287 "thead": {}, 5288 "th": { 5289 "check_attributes": { 5290 "rowspan": "numbers", 5291 "colspan": "numbers" 5292 } 5293 }, 5294 "caption": {}, 5295 "col": {}, 5296 "colgroup": {}, 5297 5298 // Definitions // <dl>, <dt>, <dd> 5299 "dl": {rename_tag: ""}, 5300 "dd": {rename_tag: ""}, 5301 "dt": {rename_tag: ""}, 5302 5303 "iframe": {}, 5304 "noindex": {}, 5305 5306 "font": { 5307 rename_tag: "span", 5308 convert_attributes: { 5309 face: 'fontFamily', 5310 size: 'fontSize', 5311 color: 'color' 5312 }, 5313 replace_with_children: 1 5314 }, 5315 5316 "embed": {}, 5317 "noembed": {}, 5318 "object": {}, 5319 "param": {}, 5320 5321 "sup": {}, 5322 "sub": {}, 5323 5324 "address": {}, 5325 "nav": {}, 5326 "aside": {}, 5327 "article": {}, 5328 "main": {}, 5329 "acronym": {}, 5330 "abbr": {}, 5331 "label": {}, 5332 "time": {}, 5333 5334 "small": {}, 5335 "big": {}, 5336 5337 "details": {}, 5338 "summary": {}, 5339 "footer": {}, 5340 5341 "video": {}, 5342 "source": {}, 5343 "audio": {}, 5344 "nofollow": {}, 5345 5346 // tags to remove 5347 "title": {remove: 1}, 5348 "area": {remove: 1}, 5349 "command": {remove: 1}, 5350 "noframes": {remove: 1}, 5351 "bgsound": {remove: 1}, 5352 "basefont": {remove: 1}, 5353 "head": {remove: 1}, 5354 "track": {remove: 1}, 5355 "wbr": {remove: 1}, 5356 "noscript": {remove: 1}, 5357 "svg": {remove: 1}, 5358 "keygen": {remove: 1}, 5359 "meta": {remove: 1}, 5360 "isindex": {remove: 1}, 5361 "base": {remove: 1}, 5362 "canvas": {remove: 1}, 5363 "applet": {remove: 1}, 5364 "spacer": {remove: 1}, 5365 "frame": {remove: 1}, 5366 "style": {remove: 1}, 5367 "device": {remove: 1}, 5368 "xml": {remove: 1}, 5369 "nextid": {remove: 1}, 5370 "link": {remove: 1}, 5371 "script": {remove: 1}, 5372 "comment": {remove: 1}, 5373 "frameset": {remove: 1}, 5374 5375 // Tags to rename 5376 // to DIV 5377 "multicol": {rename_tag: "div"}, 5378 "map": {rename_tag: "div"}, 5379 "body": {rename_tag: "div"}, 5380 "html": {rename_tag: "div"}, 5381 "hgroup": {rename_tag: "div"}, 5382 "listing": {rename_tag: "div"}, 5383 "header": {rename_tag: "div"}, 5384 // to SPAN
5385 "rt": {rename_tag: "span"}, 5386 "xmp": {rename_tag: "span"}, 5387 "bdi": {rename_tag: "span"}, 5388 "progress": {rename_tag: "span"}, 5389 "dfn": {rename_tag: "span"}, 5390 "rb": {rename_tag: "span"}, 5391 "mark": {rename_tag: "span"}, 5392 "output": {rename_tag: "span"}, 5393 "marquee": {rename_tag: "span"}, 5394 "rp": {rename_tag: "span"}, 5395 5396 "var": {rename_tag: "span"}, 5397 "tt": {rename_tag: "span"}, 5398 "blink": {rename_tag: "span"}, 5399 "plaintext": {rename_tag: "span"}, 5400 "kbd": {rename_tag: "span"}, 5401 "meter": {rename_tag: "span"}, 5402 "datalist": {rename_tag: "span"}, 5403 "samp": {rename_tag: "span"}, 5404 "bdo": {rename_tag: "span"}, 5405 "ruby": {rename_tag: "span"}, 5406 "ins": {rename_tag: "span"}, 5407 "optgroup": {rename_tag: "span"}, 5408 5409 // Form elements 5410 "form": {}, 5411 "option": {}, 5412 "select": {}, 5413 "textarea": {}, 5414 "button": {}, 5415 "input": {}, 5416 5417 "dir": {rename_tag: "ul"}, 5418 "a": {}, 5419 "img": { 5420 "check_attributes": { 5421 "width": "numbers", 5422 "alt": "alt", 5423 "src": "url", 5424 "height": "numbers" 5425 }, 5426 "add_class": { 5427 "align": "align_img" 5428 } 5429 }, 5430 "q": { 5431 "check_attributes": { 5432 "cite": "url" 5433 } 5434 }, 5435 "blockquote": { 5436 "check_attributes": { 5437 "cite": "url" 5438 } 5439 }, 5440 "center": { 5441 rename_tag: "div", 5442 add_css: 5443 { 5444 textAlign : 'center' 5445 } 5446 }, 5447 "cite": {} 5448 } 5449 }; 5450 5451})(window);
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.