PageSourceSearch

https://ralk.info/bitrix/js/fileman/html_editor/html-editor.js?1512998988125670

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