PageSourceSearch

https://www.skm-mebel.ru/bitrix/js/fileman/html_editor/html-views.js

js skm-mebel.ru collected 2026-10-06 04:41:50 UTC 86,574 bytes, 3,273 lines download raw bytes

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

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.