PageSourceSearch

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

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