PageSourceSearch

https://voenchast.ru/bitrix/js/fileman/html_editor/html-editor.js

js voenchast.ru collected 2026-10-09 22:40:54 UTC 135,416 bytes, 5,451 lines download raw bytes

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 = "&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(/&amp;/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, '&lt;');
1416				s = s.replace(/>/g, '&gt;');
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 = ['&cent;', '&sect;', '&euro;', '&pound;','&yen;','&copy;', '&reg;', '&laquo;', '&raquo;', '&deg;', '&plusmn;', '&para;', '&hellip;','&prime;','&Prime;', '&trade;', '&asymp;', '&ne;', '&lt;', '&gt;'];
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.