Your IP : 13.58.32.115


Current Path : /var/www/www-root/data/www/monolith-realty.ru/bitrix/js/fileman/html_editor/
Upload File :
Current File : /var/www/www-root/data/www/monolith-realty.ru/bitrix/js/fileman/html_editor/html-views.js

/**
 * Bitrix HTML Editor 3.0
 * Date: 24.04.13
 * Time: 4:23
 *
 * Views class
 */
(function()
{

function BXEditorView(editor, element, container)
{
	this.editor = editor;
	this.element = element;
	this.container = container;
	this.config = editor.config || {};
	this.isShown = null;
	this.bbCode = editor.bbCode;
	BX.addCustomEvent(this.editor, "OnClickBefore", BX.proxy(this.OnClick, this));
}

BXEditorView.prototype = {
	Focus: function()
	{
		if (!document.querySelector || this.element.ownerDocument.querySelector(":focus") === this.element)
			return;

		try{this.element.focus();}catch(e){}
	},

	Hide: function()
	{
		this.isShown = false;
		this.container.style.display = "none";
	},

	Show: function()
	{
		this.isShown = true;
		this.container.style.display = "";
	},

	Disable: function()
	{
		this.element.setAttribute("disabled", "disabled");
	},

	Enable: function()
	{
		this.element.removeAttribute("disabled");
	},

	OnClick: function(params)
	{

	},

	IsShown: function()
	{
		return !!this.isShown;
	}
};

function BXEditorTextareaView(parent, textareaElement, container)
{
	// Call parrent constructor
	BXEditorIframeView.superclass.constructor.apply(this, arguments);
	this.name = "textarea";
	this.InitEventHandlers();

	if (!this.element.value && this.editor.config.content)
		this.SetValue(this.editor.config.content, false);
}

BX.extend(BXEditorTextareaView, BXEditorView);

BXEditorTextareaView.prototype.Clear = function()
{
	this.element.value = "";
};

BXEditorTextareaView.prototype.GetValue = function(bParse)
{
	var value = this.IsEmpty() ? "" : this.element.value;

	if (bParse)
	{
		value = this.parent.parse(value);
	}

	return value;
};

BXEditorTextareaView.prototype.SetValue = function(html, bParse, bFormat)
{
	if (bParse)
	{
		html = this.editor.Parse(html, true, bFormat);
	}
	this.editor.dom.pValueInput.value = this.element.value = html;
};


BXEditorTextareaView.prototype.SaveValue = function()
{
	if (this.editor.inited)
	{
		this.editor.dom.pValueInput.value = this.element.value;
	}
};

BXEditorTextareaView.prototype.HasPlaceholderSet = function()
{
	var
		placeholderText = this.element.getAttribute("placeholder") || null,
		value = this.element.value;
	return !value || (value === placeholderText);
};

BXEditorTextareaView.prototype.IsEmpty = function()
{
	var value = BX.util.trim(this.element.value);
	return value === '' || this.HasPlaceholderSet();
};

BXEditorTextareaView.prototype.InitEventHandlers = function()
{
	var _this = this;
	BX.bind(this.element, "focus", function()
	{
		_this.editor.On("OnTextareaFocus");
		_this.isFocused = true;
	});

	BX.bind(this.element, "blur", function()
	{
		_this.editor.On("OnTextareaBlur");
		_this.isFocused = false;
	});

	BX.bind(this.element, "keydown", function(e)
	{
		_this.editor.textareaKeyDownPreventDefault = false;

		// Handle Ctrl+Enter
		if ((e.ctrlKey || e.metaKey) && !e.altKey && e.keyCode === _this.editor.KEY_CODES["enter"])
		{
			_this.editor.On('OnCtrlEnter', [e, _this.editor.GetViewMode()]);
			return BX.PreventDefault(e);
		}
		_this.editor.On('OnTextareaKeydown', [e]);

		if (_this.editor.textareaKeyDownPreventDefault)
			return BX.PreventDefault(e);
	});

	BX.bind(this.element, "keyup", function(e)
	{
		_this.editor.On('OnTextareaKeyup', [e]);
	});
};

BXEditorTextareaView.prototype.IsFocused = function()
{
	return this.isFocused;
};

BXEditorTextareaView.prototype.ScrollToSelectedText = function(searchText)
{
// http://blog.blupixelit.eu/scroll-textarea-to-selected-word-using-javascript-jquery/
//	var parola_cercata = "parola"; // the searched word
//	var posi = jQuery('#my_textarea').val().indexOf(parola_cercata); // take the position of the word in the text
//	if (posi != -1) {
//		var target = document.getElementById("my_textarea");
//		// select the textarea and the word
//		target.focus();
//		if (target.setSelectionRange)
//			target.setSelectionRange(posi, posi+parola_cercata.length);
//		else {
//			var r = target.createTextRange();
//			r.collapse(true);
//			r.moveEnd('character',  posi+parola_cercata);
//			r.moveStart('character', posi);
//			r.select();
//		}
//		var objDiv = document.getElementById("my_textarea");
//		var sh = objDiv.scrollHeight; //height in pixel of the textarea (n_rows*line_height)
//		var line_ht = jQuery('#my_textarea').css('line-height').replace('px',''); //height in pixel of each row
//		var n_lines = sh/line_ht; // the total amount of lines
//		var char_in_line = jQuery('#insert_textarea').val().length / n_lines; // amount of chars for each line
//		var height = Math.floor(posi/char_in_line); // amount of lines in the textarea
//		jQuery('#my_textarea').scrollTop(height*line_ht); // scroll to the selected line
//	} else {
//		alert('parola '+parola_cercata+' non trovata'); // alert word not found
//	}
};

BXEditorTextareaView.prototype.SelectText = function(searchText)
{
	var
		value = this.element.value,
	 	ind = value.indexOf(searchText);

	if(ind != -1)
	{
		this.element.focus();
		this.element.setSelectionRange(ind, ind + searchText.length);
	}
};

BXEditorTextareaView.prototype.GetTextSelection = function()
{
	var res = false;
	if (this.element.selectionStart != undefined)
	{
		res = this.element.value.substr(this.element.selectionStart, this.element.selectionEnd - this.element.selectionStart);
	}
	else if (document.selection && document.selection.createRange)
	{
		res = document.selection.createRange().text;
	}
	else if (window.getSelection)
	{
		res = window.getSelection();
		res = res.toString();
	}

	return res;
};

BXEditorTextareaView.prototype.WrapWith = function (tagBegin, tagEnd, postText)
{
	if (!tagBegin)
		tagBegin = "";
	if (!tagEnd)
		tagEnd = "";
	if (!postText)
		postText = "";

	if (tagBegin.length <= 0 && tagEnd.length <= 0 && postText.length <= 0)
		return true;

	var
		bReplaceText = !!postText,
		selectedText = this.GetTextSelection(),
		mode = (selectedText ? 'select' : (bReplaceText ? 'after' : 'in'));

	//if (!this.bTextareaFocus)
	//	this.pTextarea.focus(); // BUG IN IE

	if (bReplaceText)
	{
		postText = tagBegin + postText + tagEnd;
	}
	else if (selectedText)
	{
		postText = tagBegin + selectedText + tagEnd;
	}
	else
	{
		postText = tagBegin + tagEnd;
	}

	if (this.element.selectionStart != undefined)
	{
		var
			currentScroll = this.element.scrollTop,
			start = this.element.selectionStart,
			end = this.element.selectionEnd;

		this.element.value = this.element.value.substr(0, start) + postText + this.element.value.substr(end);

		if (mode == 'select')
		{
			this.element.selectionStart = start;
			this.element.selectionEnd = start + postText.length;
		}
		else if (mode == 'in')
		{
			this.element.selectionStart = this.element.selectionEnd = start + tagBegin.length;
		}
		else
		{
			this.element.selectionStart = this.element.selectionEnd = start + postText.length;
		}
		this.element.scrollTop = currentScroll;
	}
	else if (document.selection && document.selection.createRange)
	{
		var sel = document.selection.createRange();
		var selection_copy = sel.duplicate();
		postText = postText.replace(/\r?\n/g, '\n');
		sel.text = postText;
		sel.setEndPoint('StartToStart', selection_copy);
		sel.setEndPoint('EndToEnd', selection_copy);

		if (mode == 'select')
		{
			sel.collapse(true);
			postText = postText.replace(/\r\n/g, '1');
			sel.moveEnd('character', postText.length);
		}
		else if (mode == 'in')
		{
			sel.collapse(false);
			sel.moveEnd('character', tagBegin.length);
			sel.collapse(false);
		}
		else
		{
			sel.collapse(false);
			sel.moveEnd('character', postText.length);
			sel.collapse(false);
		}
		sel.select();
	}
	else
	{
		// failed - just stuff it at the end of the message
		this.element.value += postText;
	}
	return true;
};


BXEditorTextareaView.prototype.GetCursorPosition = function()
{
	return this.element.selectionStart;
};

class BXEditorIframeCopilot
{
	copilotParams = {};
	copilotLoaded = false;
	selectionTypeCaret = 'Caret';
	resultNodeAttr = 'bxhtmled-copilot-result-node';
	resultColor = '#8d52ec';

	invitationLineModes = {
		NONE: 'none',
		LAST_LINE: 'lastLine',
		EACH_LINE: 'eachLine',
	};

	invitationLineMode = this.invitationLineModes.LAST_LINE;

	/**
	 * @param iframeView BXEditorIframeView
	 * @param copilotParams {{moduleId, contextId, category, contextParameters, invitationLineMode, isMentionUnavailable}}
	 */
	constructor(iframeView, copilotParams = {})
	{
		this.iframeView = iframeView;
		this.iframeContainer = iframeView.container;
		this.contentEditable = iframeView.element;

		this.copilotParams = copilotParams;

		this.invitationLineMode = copilotParams.invitationLineMode ?? this.invitationLineModes.LAST_LINE;
		this.invitationLine = this.renderInvitationLine();
		this.invitationLineAbsolute = this.renderInvitationLine();
		this.insertResultNode = this.renderInsertResultNode();

		this.copilot = new BX.AI.Copilot({
			moduleId: copilotParams.moduleId,
			contextId: copilotParams.contextId,
			category: copilotParams.category,
			contextParameters: copilotParams.contextParameters,
			autoHide: true,
			preventAutoHide: (event) => this.iframeContainer.contains(event.target),
		});

		this.bindHandlers();

		this.copilot.init();
	}

	renderInvitationLine()
	{
		let placeHolder = BX.message('BXEdCopilotPlaceholder_MSGVER_2');
		if (this.copilotParams.isMentionUnavailable)
		{
			placeHolder = BX.message('BXEdCopilotPlaceholderWithoutMention_MSGVER_1');
		}

		return BX.Tag.render`
			<div class="bxhtmled-copilot" placeholder="${placeHolder}"></div>
		`;
	}

	renderInsertResultNode()
	{
		return BX.Tag.render`
			<span class="bxhtmled-insert-result"></span>
		`;
	}

	bindHandlers()
	{
		this.copilot.subscribe('finish-init', this.finishInitHandler.bind(this));
		this.copilot.subscribe('aiResult', this.aiResultHandler.bind(this));
		this.copilot.subscribe('save', this.saveHandler.bind(this));
		this.copilot.subscribe('add_below', this.addBelowHandler.bind(this));
		this.copilot.subscribe('cancel', this.cancelHandler.bind(this));
		this.copilot.subscribe('hide', this.saveResultNodes.bind(this));
		document.addEventListener('click', this.documentClickHandler.bind(this));
		document.addEventListener('keydown', this.onWindowKeyDownHandler.bind(this));
		this.contentEditable.addEventListener('input', this.onContentEditableKeyDown.bind(this));
		window.addEventListener('scroll', this.onScrollHandler.bind(this), true);
		new ResizeObserver(this.onScrollHandler.bind(this)).observe(this.contentEditable);
		window.addEventListener('resize', this.handleResizeWindow.bind(this));
		BX.addCustomEvent(window, "onPullEvent-unicomments", this.startAdjustAnimation.bind(this));
		this.hideObserver = new MutationObserver(() => {
			if (this.iframeContainer.offsetHeight <= 0) {
				this.hideInvitationLine();
				this.hideObserver.disconnect();
			}
		});
		this.hideObserver.observe(document.body, {childList: true});
	}

	documentClickHandler(event)
	{
		if (!this.iframeContainer.contains(event.target))
		{
			this.hideInvitationLine();
		}
	}

	startAdjustAnimation()
	{
		this.animation?.stop();
		this.animation = new BX.easing({
			duration: 1000,
			start: {},
			finish: {},
			transition : BX.easing.makeEaseOut(BX.easing.transitions.linear),
			step: () => {
				if (this.copilot.isShown())
				{
					this.copilot.adjust(this.getAdjustOptionsForRect(this.adjustmentRect));
				}
				if (this.copilotBtnPopup?.isShown())
				{
					this.adjustCopilotButton(this.getAdjustOptionsForRectSpaced(this.adjustmentRect, true));
				}
			},
			complete: () => this.animation = null,
		});
		this.animation.animate();
	}

	finishInitHandler()
	{
		this.copilotLoaded = true;
		this.updateInvitationLine();
	}

	aiResultHandler(event)
	{
		const resultNodes = this.getResultNodes();

		let lastResultSpan;
		if (resultNodes.length !== 0)
		{
			lastResultSpan = [...resultNodes].pop();
			lastResultSpan.innerText = event.data.result;
		}
		else
		{
			lastResultSpan = this.getSpanWithText(event.data.result);
			BX.Dom.style(lastResultSpan, 'color', this.resultColor);
			BX.Dom.attr(lastResultSpan, this.resultNodeAttr, 'true');

			if (this.invitationLineMode === this.invitationLineModes.LAST_LINE)
			{
				this.insertResultNode.after(BX.Tag.render`<br>`);
			}

			this.insertResultNode.after(lastResultSpan);
			this.insertResultNode.remove();
		}

		this.iframeView.ScrollToInsertedText();
		this.getSelection().removeAllRanges();

		this.copilot.adjust(this.getAdjustOptions(lastResultSpan));
	}

	saveHandler(event)
	{
		if (this.getResultNodes().length === 0)
		{
			const selection = this.getSelection().getRangeAt(0);
			const span = this.getSpanWithText(event.data.result);
			selection.deleteContents();
			selection.insertNode(span);
			this.iframeView.UpdateHeight();
		}

		this.saveResultNodes();

		this.iframeView.editor.synchro.FromIframeToTextarea(true, true);
		this.iframeView.editor.synchro.FromTextareaToIframe(true);

		this.copilot.hide();
	}

	addBelowHandler(event)
	{
		const span = this.getSpanWithText(event.data.result);

		let lastSelectedElement = null;
		if (this.getSelectionText() !== '')
		{
			lastSelectedElement = this.getSelection().getRangeAt(0).endContainer;
		}

		const isSelectionEndOutOfBounds = lastSelectedElement === this.iframeView.element;
		if (lastSelectedElement && !isSelectionEndOutOfBounds)
		{
			lastSelectedElement.after(span);
			lastSelectedElement.after(BX.Tag.render`<br>`);
		}
		else
		{
			BX.Dom.append(BX.Tag.render`<br>`, this.contentEditable);
			BX.Dom.append(span, this.contentEditable);
			BX.Dom.append(BX.Tag.render`<br>`, this.contentEditable);
		}

		this.iframeView.ScrollToInsertedText();
		this.getSelection().removeAllRanges();

		this.iframeView.editor.synchro.FromIframeToTextarea(true, true);
		this.iframeView.editor.synchro.FromTextareaToIframe(true);

		this.copilot.hide();
	}

	cancelHandler()
	{
		[...this.getResultNodes()][0]?.before(this.insertResultNode);
		this.removeResultNodes();
		if (this.showRect)
		{
			this.copilot.adjust(this.getAdjustOptionsForRect(this.showRect));
		}
	}

	getSpanWithText(text)
	{
		const span = BX.Dom.create('span');
		span.innerText = text;

		return span;
	}

	onWindowKeyDownHandler(event)
	{
		if (event.key === "Escape")
		{
			this.copilot.hide();
		}
	}

	handleResizeWindow()
	{
		this.copilot.adjustWidth(this.getCopilotWidth());
	}

	removeResultNodes()
	{
		this.getResultNodes().forEach(resultNode => resultNode.remove());
	}

	saveResultNodes()
	{
		for (const resultNode of this.getResultNodes())
		{
			for (const childNode of resultNode.childNodes)
			{
				resultNode.before(childNode.cloneNode(true));
			}

			resultNode.remove();
		}
	}

	onContentEditableMouseDown()
	{
		this.insertResultNode.remove();
		this.hideInvitationLine();
		this.hideCopilotButton();
	}

	onIframeWindowClick()
	{
		this.update();
	}

	onContentEditableKeyDown()
	{
		this.insertResultNode.remove();
		this.updatePopup();
		this.hideInvitationLine();
		this.hideCopilotButton();
	}

	onContentEditableKeyUp(e)
	{
		if (e.key === ' ')
		{
			return;
		}

		this.update();
	}

	onScrollHandler()
	{
		if (this.contentEditable.offsetHeight <= 0)
		{
			return;
		}

		if (this.invitationLineAbsolute.parentNode)
		{
			this.showInvitationLine();
		}

		if (this.copilotBtnPopup?.isShown() && this.getSelectionText() !== '')
		{
			const range = this.getSelection().getRangeAt(0);
			this.adjustCopilotButton(this.getAdjustOptions(range, true));
		}

		if (this.copilot.isShown() && this.getSelectionText() !== '')
		{
			const range = this.getSelection().getRangeAt(0);
			this.copilot.adjust(this.getAdjustOptions(range));

			return;
		}

		if (this.getResultNodes().pop())
		{
			this.updatePopup();
		}
		else if (this.adjustmentRect)
		{
			this.copilot.adjust(this.getAdjustOptionsForRect(this.adjustmentRect));
		}
	}

	onContentEditableBlur()
	{
		setTimeout(() => {
			this.hideInvitationLine();
		}, 0);
	}

	onIframeFocus()
	{
		setTimeout(() => {
			this.updateInvitationLine();
		}, 0);
	}

	shouldBeShown()
	{
		return this.invitationLineAbsolute.offsetWidth !== 0 && !this.copilot.isShown();
	}

	show(showFromSpace = false)
	{
		this.copilot.setContext(this.contentEditable.innerText);

		this.insertResultNode.remove();
		this.getSelection().getRangeAt(0).insertNode(this.insertResultNode);

		const bindElement = {
			top: parseInt(this.invitationLineAbsolute.style.top),
			left: parseInt(this.invitationLineAbsolute.style.left),
		};

		if (showFromSpace)
		{
			bindElement.top += 28;
		}

		const containerRect = this.iframeContainer.getBoundingClientRect();
		this.adjustmentRect = {
			bottom: bindElement.top - containerRect.y - window.scrollY,
			x: bindElement.left - containerRect.x - window.scrollX,
			width: 0,
		};
		this.showRect = this.adjustmentRect;

		this.copilot.show({
			bindElement,
			showFromSpace,
			width: this.getCopilotWidth(),
		});

		this.hideInvitationLine();
	}

	showAtTheBottom()
	{
		this.copilot.setContext(this.contentEditable.innerText);

		this.insertResultNode.remove();

		let emptyBr;
		let emptyLine;
		if (this.needToAppendEmptyElement())
		{
			emptyBr = BX.Tag.render`<br>`;
			emptyLine = BX.Tag.render`<div></div>`;
			const lastNode = this.getFilteredChildNodes(this.contentEditable).pop();
			if (this.contentEditable.innerText !== '' && lastNode?.tagName !== 'BR')
			{
				this.contentEditable.append(BX.Tag.render`<br>`);
			}
			this.contentEditable.append(emptyBr);
			this.contentEditable.append(emptyLine);
		}

		const lastNode = this.getFilteredChildNodes(this.contentEditable).pop();
		const bindElement = this.getAdjustOptions(lastNode).position;

		emptyLine?.remove();

		this.showRect = this.adjustmentRect;

		if (emptyBr)
		{
			emptyBr.before(this.insertResultNode);
		}
		else
		{
			this.contentEditable.append(this.insertResultNode);
		}

		this.copilot.show({
			bindElement,
			width: this.getCopilotWidth(),
		});

		this.hideInvitationLine();
	}

	needToAppendEmptyElement()
	{
		const innerText = this.contentEditable.innerText;
		return !innerText
			|| (innerText.at(-1) !== '\n')
			|| (innerText.at(-2) && innerText.at(-2) !== '\n');
	}

	update()
	{
		this.updateCopilotButton();
		this.updatePopup();
		this.updateInvitationLine();
	}

	updateCopilotButton()
	{
		const shouldShowCopilotButton = !this.copilot.isShown() && this.getSelectionText() !== '';

		if (shouldShowCopilotButton && !this.copilotBtnPopup?.isShown() && this.copilotLoaded)
		{
			this.showCopilotButton();
		}

		if (!shouldShowCopilotButton)
		{
			this.hideCopilotButton();
		}
	}

	showCopilotButton()
	{
		if (!this.copilotBtnPopup)
		{
			this.copilotBtnPopup = new BX.Main.Popup({
				bindElement: this.contentEditable,
				padding: 6,
				borderRadius: '6px',
				content: this.renderCopilotButton(),
				autoHide: true,
			});
		}

		this.copilotBtnPopup.setMaxWidth(null);
		this.copilotBtnPopup.setMinWidth(null);
		this.copilotBtnPopup.setPadding(6);
		this.adjustCopilotButton(this.getAdjustOptions(this.getSelection().getRangeAt(0), true));
		this.copilotBtnPopup.adjustPosition();

		this.copilotBtnPopup.show();
		setTimeout(() => this.updateCopilotButton(), 0);
	}

	renderCopilotButton()
	{
		BX.Runtime.loadExtension('ui.icon-set.main');

		this.copilotButton = BX.Tag.render`
			<button class="bxhtmled-copilot-btn" onclick="${this.copilotButtonClickHandler.bind(this)}">
				<div class="bxhtmled-copilot-btn-icon ui-icon-set --copilot-ai"></div>
				${ BX.message('BXEdCopilotButtonText') }
			</button>
		`;

		return this.copilotButton;
	}

	copilotButtonClickHandler()
	{
		const adjustOptions = this.getAdjustOptions(this.getSelection().getRangeAt(0));

		this.copilot.setSelectedText(this.getSelection().toString());
		this.copilot.show({
			bindElement: adjustOptions.position,
			showFromPopup: true,
			width: this.getCopilotWidth(),
		});
		this.hideCopilotButton();
	}

	adjustCopilotButton(options)
	{
		if (options.hide)
		{
			this.copilotBtnPopup.setMaxWidth(0);
			this.copilotBtnPopup.setMinWidth(0);
			this.copilotBtnPopup.setPadding(0);
		}
		else
		{
			this.copilotBtnPopup.setMaxWidth(null);
			this.copilotBtnPopup.setMinWidth(null);
			this.copilotBtnPopup.setPadding(6);
			this.copilotBtnPopup.setBindElement(options.position);
			this.copilotBtnPopup.adjustPosition();
		}
	}

	getCopilotWidth()
	{
		return this.contentEditable.offsetWidth - 30;
	}

	hideCopilotButton()
	{
		this.copilotBtnPopup?.close();
	}

	updatePopup()
	{
		if (!this.copilot.isShown())
		{
			return;
		}

		const lastResultSpan = this.getResultNodes().pop();
		if (!this.isNotFocusedOrCursorAtResultNode(this.getSelection()) || !lastResultSpan)
		{
			this.copilot.hide();

			return;
		}

		this.copilot.adjust(this.getAdjustOptions(lastResultSpan));
	}

	getAdjustOptions(pivot, isCentered = false)
	{
		const pivotRect = pivot.getBoundingClientRect();

		return this.getAdjustOptionsForRectSpaced(pivotRect, isCentered);
	}

	getAdjustOptionsForRectSpaced(pivotRect, isCentered = false)
	{
		const adjustment = this.getAdjustOptionsForRect(pivotRect, isCentered);
		adjustment.position.top += 10;

		return adjustment;
	}

	getAdjustOptionsForRect(pivotRect, isCentered = false)
	{
		this.adjustmentRect = pivotRect;
		const containerRect = this.iframeContainer.getBoundingClientRect();

		return {
			hide: pivotRect.bottom > this.iframeView.document.documentElement.offsetHeight + 13 || pivotRect.bottom + 13 < 0,
			position: {
				top: pivotRect.bottom + containerRect.y + window.scrollY,
				left: pivotRect.x + containerRect.x + isCentered * (pivotRect.width / 2 - 55) + window.scrollX,
			},
		};
	}

	isNotFocusedOrCursorAtResultNode(selection)
	{
		if (!selection.focusNode)
		{
			return true;
		}

		return this.getResultNodes().filter(node => selection.focusNode === node || selection.focusNode.parentElement === node).length;
	}

	getResultNodes()
	{
		return [...this.contentEditable.querySelectorAll(`[${this.resultNodeAttr}=true]`)];
	}

	updateInvitationLine()
	{
		this.insertResultNode.remove();

		if (this.shouldShowInvitationLine() && this.copilotLoaded)
		{
			this.showInvitationLine();
		}
		else
		{
			this.hideInvitationLine();
		}
	}

	showInvitationLine()
	{
		const selection = this.getSelection();
		if (selection.rangeCount === 0)
		{
			return false;
		}

		const lastNode = this.getFilteredChildNodes(this.contentEditable).pop();
		if (this.isZwnbspNode(lastNode))
		{
			lastNode.replaceWith(BX.Tag.render`<br>`);
		}

		const range = selection.getRangeAt(0);
		range.insertNode(this.invitationLine);

		const container = this.iframeContainer.getBoundingClientRect();
		const invitationLineRect = {
			top: this.invitationLine.offsetTop + container.y - this.contentEditable.parentElement.scrollTop + window.scrollY,
			left: this.invitationLine.offsetLeft + container.x + window.scrollX,
			width: this.invitationLine.offsetWidth,
		};
		this.invitationLineAbsolute.style.top = `${invitationLineRect.top}px`;
		this.invitationLineAbsolute.style.left = `${invitationLineRect.left}px`;
		this.invitationLineAbsolute.style.width = `${invitationLineRect.width}px`;
		this.invitationLineAbsolute.style.display = '';

		this.hideInvitationLine();
		document.body.append(this.invitationLineAbsolute);

		if (!this.invitationLineAbsolute.registered)
		{
			BX.ZIndexManager.register(this.invitationLineAbsolute);
			BX.ZIndexManager.bringToFront(this.invitationLineAbsolute);
			this.invitationLineAbsolute.registered = true;
		}

		if (!this.shouldDisplayInvitationLine())
		{
			this.invitationLineAbsolute.style.display = 'none';
		}
	}

	shouldDisplayInvitationLine()
	{
		const contentEditableRect = this.iframeContainer.getBoundingClientRect();
		const invitationLineRect = this.invitationLineAbsolute.getBoundingClientRect();
		return invitationLineRect.bottom + 20 < contentEditableRect.bottom && invitationLineRect.top > contentEditableRect.top;
	}

	hideInvitationLine()
	{
		this.invitationLine.remove();
		this.invitationLineAbsolute.remove();
	}

	getSelectionText()
	{
		return this.getSelection().toString().replaceAll('\n', '').trim();
	}

	getSelection()
	{
		return this.iframeView.GetSelection();
	}

	shouldShowInvitationLine()
	{
		if (!this.iframeContainer.contains(document.activeElement))
		{
			return false;
		}

		if (this.invitationLineMode === this.invitationLineModes.NONE)
		{
			return false;
		}

		if (this.invitationLineMode === this.invitationLineModes.EACH_LINE)
		{
			return this.isCursorAtStartOfLine(this.getSelection());
		}

		return this.isCursorAtNewLine(this.getSelection());
	}

	isCursorAtStartOfLine(selection)
	{
		if (selection.type !== this.selectionTypeCaret)
		{
			return false;
		}

		this.removeZwnbspSequence();

		const range = selection.getRangeAt(0);
		const contentEditableOffset = parseInt(getComputedStyle(this.contentEditable).paddingLeft);
		if (range.getBoundingClientRect().x > contentEditableOffset)
		{
			return false;
		}

		const tmpNode = BX.Tag.render`<span style="width: 10px; height: 10px;"></span>`;
		range.insertNode(tmpNode);

		const previousNode = tmpNode.previousSibling;
		const nextNode = tmpNode.nextSibling;
		const offset = tmpNode.getBoundingClientRect().x - contentEditableOffset;

		if (
			selection.focusNode === this.contentEditable
			&& nextNode === null
			&& (
				previousNode?.tagName === 'DIV' || this.isZwnbspNode(previousNode)
			)
		)
		{
			if (this.isZwnbspNode(previousNode))
			{
				if (!previousNode.nextSibling)
				{
					previousNode.replaceWith(BX.Tag.render`<br>`);
				}
				else
				{
					previousNode.remove();
				}
			}

			tmpNode.remove();
			return false;
		}

		const afterBr = !previousNode || this.doesNodeMakeLine(previousNode) || tmpNode.nodeName !== '#text' && tmpNode.textContent === '';
		const beforeBr = !nextNode || this.doesNodeMakeLine(nextNode) && nextNode.tagName !== 'TABLE';
		const atStart = offset <= 0;
		tmpNode.remove();

		return ((afterBr && beforeBr) || (this.isZwnbspNode(previousNode) && nextNode?.nodeName === '#text')) && atStart;
	}

	isCursorAtNewLine(selection)
	{
		if (selection.toString() !== '')
		{
			return false;
		}

		this.removeZwnbspSequence();

		if (selection.focusNode.outerHTML === '<span><br></span>' || selection.focusNode.outerHTML === '<div><br></div>')
		{
			selection.focusNode.replaceWith(BX.Tag.render`<br>`);
		}

		const nodes = this.getFilteredChildNodes(this.contentEditable);
		const isEmptyOrOnlyLine = (nodes.length === 0) || (nodes.length === 1 && nodes[0].tagName === 'BR');
		const lastNode = nodes.pop();

		let doesLastNodeMakeLine = false;
		if (lastNode?.tagName === 'SPAN')
		{
			const spanNodes = this.getFilteredChildNodes(lastNode);
			const lastSpanNode = spanNodes.pop();
			const oneSpanNodeBeforeLast = spanNodes.pop();
			doesLastNodeMakeLine = this.doesNodeMakeLine(oneSpanNodeBeforeLast) && this.doesNodeMakeLine(lastSpanNode);
		}

		if (!this.isZwnbspNode(selection.focusNode) && selection.focusNode !== this.contentEditable && lastNode?.tagName !== 'SPAN')
		{
			return false;
		}

		const oneNodeBeforeLast = nodes.pop();
		const doLastTwoNodesMakeLine = this.doesNodeMakeLine(oneNodeBeforeLast) && this.doesNodeMakeLine(lastNode) && lastNode?.tagName !== 'BLOCKQUOTE';

		return (isEmptyOrOnlyLine || doLastTwoNodesMakeLine || doesLastNodeMakeLine) && this.isCursorAtTheEndOfDocument(selection);
	}

	removeZwnbspSequence()
	{
		const nodesToRemove = [];
		this.contentEditable.childNodes.forEach((node) => {
			if (this.isZwnbspNode(node) && this.isZwnbspNode(node.nextSibling))
			{
				nodesToRemove.push(node);
			}
		});

		nodesToRemove.forEach((node) => node.remove());
		if (this.contentEditable.innerHTML === this.createZwnbspNode().innerHTML)
		{
			this.contentEditable.innerHTML = '';
		}
	}

	getFilteredChildNodes(element)
	{
		return [...element.childNodes].filter((node) => {
			return node.nodeName !== '#text' || this.isZwnbspNode(node) || node.textContent !== ''
		});
	}

	doesNodeMakeLine(node)
	{
		if (!node)
		{
			return false;
		}

		if (node.outerHTML === '<span><br></span>')
		{
			node.replaceWith(BX.Tag.render`<br>`);
			return true;
		}

		if (this.isZwnbspNode(node))
		{
			if (!node.nextSibling)
			{
				node.replaceWith(BX.Tag.render`<br>`);
			}
			else
			{
				node.remove();
			}

			return true;
		}

		const makingLineNodes = ['BR', 'UL', 'OL', 'PRE', 'TABLE', 'DIV', 'P', 'BLOCKQUOTE'];

		if (node.tagName === 'SPAN')
		{
			return this.doesNodeMakeLine(this.getFilteredChildNodes(node).pop());
		}

		return makingLineNodes.includes(node.tagName) || makingLineNodes.includes([...node.childNodes].pop()?.tagName);
	}

	isZwnbspNode(node)
	{
		if (!node)
		{
			return false;
		}

		return BX.Tag.render`<div>${node.cloneNode()}</div>`.innerHTML === this.createZwnbspNode().innerHTML;
	}

	createZwnbspNode()
	{
		return BX.Tag.render`<div>&#XFEFF;</div>`;
	}

	isCursorAtTheEndOfDocument(selection)
	{
		const offset = selection.focusOffset;
		const node = selection.focusNode;

		selection.modify("move", "forward", "character");
		if (offset === selection.focusOffset && node === selection.focusNode)
		{
			return true;
		}
		else
		{
			selection.modify("move", "backward", "character");

			return false;
		}
	}
}

function BXEditorIframeView(editor, textarea, container)
{
	// Call parrent constructor
	BXEditorIframeView.superclass.constructor.apply(this, arguments);
	this.name = "wysiwyg";
	this.caretNode = "<br>";
}

BX.extend(BXEditorIframeView, BXEditorView);

BXEditorIframeView.prototype.OnCreateIframe = function()
{
	this.document = this.editor.sandbox.GetDocument();
	this.element = this.document.body;
	this.editor.document = this.document;
	this.textarea = this.editor.dom.textarea;
	this.isFocused = false;
	this.InitEventHandlers();

	// Check and init external range library
	window.rangy.init();

	if (this.config.isCopilotEnabled && BX.AI?.Copilot)
	{
		this.copilot = new BXEditorIframeCopilot(this, {
			...this.config.copilotParams,
			isMentionUnavailable: this.config.isMentionUnavailable,
		});
	}

	this.Enable();
};

BXEditorIframeView.prototype.setCopilotContextParameters = function(formData)
{
	this.config.copilotParams.contextParameters = {
		xmlId: formData[0],
		entityId: formData[1],
	};
};

BXEditorIframeView.prototype.ScrollToInsertedText = function()
{
	this.document.documentElement.scrollTop = this.document.documentElement.scrollHeight;
	this.UpdateHeight();
};

BXEditorIframeView.prototype.UpdateHeight = function()
{
	this.editor.UpdateHeight();
};

BXEditorIframeView.prototype.GetSelection = function()
{
	return this.document.getSelection();
};

BXEditorIframeView.prototype.isCopilotInitialized = function()
{
	return this.copilot && this.copilot.copilotLoaded;
};

BXEditorIframeView.prototype.Clear = function()
{
	//this.element.innerHTML = BX.browser.IsFirefox() ? this.caretNode : "";
	this.element.innerHTML = this.caretNode;
};

BXEditorIframeView.prototype.GetValue = function(bParse, bFormat)
{
	this.iframeValue = this.IsEmpty() ? "" : this.editor.GetInnerHtml(this.element);
	this.iframeValue = this.iframeValue.replaceAll(/<span style="font-family: var\(--ui-font-family-primary, var\(--ui-font-family-helvetica\)\);">(.*?)<\/span>/g, '$1');
	this.iframeValue = this.iframeValue.replace(/<span class="bxhtmled-insert-result"><\/span>/g, '');
	this.editor.On('OnIframeBeforeGetValue', [this.iframeValue]);
	if (bParse)
	{
		this.iframeValue = this.editor.Parse(this.iframeValue, false, bFormat);
	}
	return this.iframeValue;
};

BXEditorIframeView.prototype.SetValue = function(html, bParse)
{
	if (bParse)
	{
		html = this.editor.Parse(html);
	}
	this.element.innerHTML = html;
	// Check last child - if it's block node in the end - add <br> tag there
	this.CheckContentLastChild(this.element);
	this.editor.On('OnIframeSetValue', [html]);
};

BXEditorIframeView.prototype.Show = function()
{
	this.isShown = true;
	this.container.style.display = "";
	this.ReInit();
};

BXEditorIframeView.prototype.ReInit = function()
{
	// Firefox needs this, otherwise contentEditable becomes uneditable
	this.Disable();
	this.Enable();

	this.editor.On('OnIframeReInit');
};

BXEditorIframeView.prototype.Hide = function()
{
	this.isShown = false;
	this.container.style.display = "none";
};

BXEditorIframeView.prototype.Disable = function()
{
	this.element.removeAttribute("contentEditable");
};

BXEditorIframeView.prototype.Enable = function()
{
	this.element.setAttribute("contentEditable", "true");
};

BXEditorIframeView.prototype.Focus = function(setToEnd)
{
	if (BX.browser.IsIE() && this.HasPlaceholderSet())
	{
		this.Clear();
	}

	if (!document.querySelector
		|| this.element.ownerDocument.querySelector(":focus") !== this.element
		|| !this.IsFocused())
	{
		if (BX.browser.IsIOS())
		{
			var _this = this;
			if (this.focusTimeout)
				clearTimeout(this.focusTimeout);

			this.focusTimeout = setTimeout(function()
			{
				var
					orScrollTop = document.documentElement.scrollTop || document.body.scrollTop,
					orScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
					BX.focus(_this.element);
					window.scrollTo(orScrollLeft, orScrollTop);
			}, 200);
		}
		else
		{
			BX.focus(this.element);
		}
	}

	if (setToEnd && this.element.lastChild)
	{
		if (this.element.lastChild.nodeName === "BR")
		{
			this.editor.selection.SetBefore(this.element.lastChild);
		}
		else
		{
			this.editor.selection.SetAfter(this.element.lastChild);
		}
	}
};

BXEditorIframeView.prototype.SetFocusedFlag = function(isFocused)
{
	this.isFocused = isFocused;
};

BXEditorIframeView.prototype.IsFocused = function()
{
	return this.isFocused;
};

BXEditorIframeView.prototype.GetTextContent = function(clearInvisibleSpace)
{
	var txt = this.editor.util.GetTextContent(this.element);
	return clearInvisibleSpace === true ? txt.replace(/\uFEFF/ig, '') : txt;
};

BXEditorIframeView.prototype.HasPlaceholderSet = function()
{
	return this.textarea && this.GetTextContent() == this.textarea.getAttribute("placeholder");
};

BXEditorIframeView.prototype.IsEmpty = function(clearInvisibleSpace)
{
	if (!document.querySelector)
		return false;

	var
		innerHTML = this.element.innerHTML,
		elementsWithVisualValue = "blockquote, ul, ol, img, embed, object, table, iframe, svg, video, audio, button, input, select, textarea";

	return innerHTML === "" ||
		innerHTML === this.caretNode ||
		this.HasPlaceholderSet() ||
		(this.GetTextContent(clearInvisibleSpace) === "" && !this.element.querySelector(elementsWithVisualValue));
};

BXEditorIframeView.prototype._initObjectResizing = function()
{
	var properties = ["width", "height"],
		propertiesLength = properties.length,
		element = this.element;

	this.commands.exec("enableObjectResizing", this.config.allowObjectResizing);

	if (this.config.allowObjectResizing) {
		// IE sets inline styles after resizing objects
		// The following lines make sure _this the width/height css properties
		// are copied over to the width/height attributes
		if (browser.supportsEvent("resizeend")) {
			dom.observe(element, "resizeend", function(event) {
				var target = event.target || event.srcElement,
					style = target.style,
					i = 0,
					property;
				for(; i<propertiesLength; i++) {
					property = properties[i];
					if (style[property]) {
						target.setAttribute(property, parseInt(style[property], 10));
						style[property] = "";
					}
				}
				// After resizing IE sometimes forgets to remove the old resize handles
				redraw(element);
			});
		}
	} else {
		if (browser.supportsEvent("resizestart")) {
			dom.observe(element, "resizestart", function(event) { event.preventDefault(); });
		}
	}
};

/**
 * With "setActive" IE offers a smart way of focusing elements without scrolling them into view:
 * http://msdn.microsoft.com/en-us/library/ms536738(v=vs.85).aspx
 *
 * Other browsers need a more hacky way: (pssst don't tell my mama)
 * In order to prevent the element being scrolled into view when focusing it, we simply
 * move it out of the scrollable area, focus it, and reset it's position
 */

var focusWithoutScrolling = function(element)
{
	if (element.setActive) {
		// Following line could cause a js error when the textarea is invisible
		// See https://github.com/xing/wysihtml5/issues/9
		try { element.setActive(); } catch(e) {}
	} else {
		var elementStyle = element.style,
			originalScrollTop = doc.documentElement.scrollTop || doc.body.scrollTop,
			originalScrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft,
			originalStyles = {
				position: elementStyle.position,
				top: elementStyle.top,
				left: elementStyle.left,
				WebkitUserSelect: elementStyle.WebkitUserSelect
			};

		dom.setStyles({
			position: "absolute",
			top: "-99999px",
			left: "-99999px",
			// Don't ask why but temporarily setting -webkit-user-select to none makes the whole thing performing smoother
			WebkitUserSelect: "none"
		}).on(element);

		element.focus();

		dom.setStyles(originalStyles).on(element);

		if (win.scrollTo) {
			// Some browser extensions unset this method to prevent annoyances
			// "Better PopUp Blocker" for Chrome http://code.google.com/p/betterpopupblocker/source/browse/trunk/blockStart.js#100
			// Issue: http://code.google.com/p/betterpopupblocker/issues/detail?id=1
			win.scrollTo(originalScrollLeft, originalScrollTop);
		}
	}
};


/**
 * Taking care of events
 * - Simulating 'change' event on contentEditable element
 * - Handling drag & drop logic
 * - Catch paste events
 * - Dispatch proprietary newword:composer event
 * - Keyboard shortcuts
 */
	BXEditorIframeView.prototype.InitEventHandlers = function()
	{
		var
			_this = this,
			editor = this.editor,
			value = this.GetValue(),
			element = this.element,
			iframeWindow = this.editor.sandbox.GetWindow(),
			_element = !BX.browser.IsOpera() ? element : this.editor.sandbox.GetWindow();

		if (this._eventsInitedObject && this._eventsInitedObject === _element)
			return;

		this._eventsInitedObject = _element;

		BX.bind(_element, "focus", function()
		{
			editor.On("OnIframeFocus");
			_this.isFocused = true;
			if (value !== _this.GetValue())
			{
				BX.onCustomEvent(editor, "OnIframeChange");
			}
			_this.copilot?.onIframeFocus();
		});

		BX.bind(_element, "blur", function()
		{
			editor.On("OnIframeBlur");
			_this.isFocused = false;
			setTimeout(function(){value = _this.GetValue();}, 0);
			_this.copilot?.onContentEditableBlur();
		});

		BX.bind(_element, "contextmenu", function(e)
		{
			if(e && !e.ctrlKey && !e.shiftKey && (BX.getEventButton(e) & BX.MSRIGHT))
			{
				editor.On("OnIframeContextMenu", [e, e.target || e.srcElement, _this.contMenuRangeCollapsed]);
			}
		});

		BX.bind(_element, "mousedown", function(e)
		{
			var
				range = editor.selection.GetRange(),
				target = e.target || e.srcElement,
				bxTag = editor.GetBxTag(target);

			//mantis: 71174
			_this.contMenuRangeCollapsed = range && range.collapsed;
			if (editor.synchro.IsSyncOn())
			{
				editor.synchro.StopSync();
			}

			if (BX.browser.IsIE10() || BX.browser.IsIE11())
			{
				editor.phpParser.RedrawSurrogates();
			}

			if (target.nodeName == 'BODY' || !editor.phpParser.CheckParentSurrogate(target))
			{
				setTimeout(function()
				{
					range = editor.selection.GetRange();
					if (range && range.collapsed && range.startContainer && range.startContainer == range.endContainer)
					{
						var surr = editor.phpParser.CheckParentSurrogate(range.startContainer);
						if (surr)
						{
							editor.selection.SetInvisibleTextAfterNode(surr);
							editor.selection.SetInvisibleTextBeforeNode(surr);
						}
					}
				}, 10);
			}

			editor.action.actions.quote.checkSpaceAfterQuotes(target);

			editor.selection.SaveRange(false);
			setTimeout(function(){editor.selection.SaveRange(false);}, 10);
			editor.On("OnIframeMouseDown", [e, target, bxTag]);
		});

		BX.bind(_element, "touchend", function(){_this.Focus();});
		BX.bind(_element, "touchstart", function(){_this.Focus();});

		BX.bind(_element, "click", function(e)
		{
			var
				target = e.target || e.srcElement;
			editor.On("OnIframeClick", [e, target]);
		});

		BX.bind(_element, "dblclick", function(e)
		{
			var
				target = e.target || e.srcElement;
			editor.On("OnIframeDblClick", [e, target]);
		});

		BX.bind(_element, "mouseup", function(e)
		{
			var target = e.target || e.srcElement;
			if (!editor.synchro.IsSyncOn())
			{
				editor.synchro.StartSync();
			}

			editor.On("OnIframeMouseUp", [e, target]);
		});

		BX.bind(iframeWindow, 'click', () => this.copilot?.onIframeWindowClick());

		// Mantis: 90137
		//if (BX.browser.IsIOS())
		//{
		//	// When on iPad/iPhone/IPod after clicking outside of editor, the editor loses focus
		//	// but the UI still acts as if the editor has focus (blinking caret and onscreen keyboard visible)
		//	// We prevent _this by focusing a temporary input element which immediately loses focus
		//	BX.bind(iframeWindow, "blur", function()
		//	{
		//		var
		//			orScrollTop = document.documentElement.scrollTop || document.body.scrollTop,
		//			orScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft,
		//			input = BX.create('INPUT', {
		//				props:{type: 'text', value: ''}
		//			}, iframeWindow.ownerDocument);
		//
		//		try
		//		{
		//			editor.selection.InsertNode(input);
		//		}
		//		catch(e)
		//		{
		//			iframeWindow.appendChild(input);
		//		}
		//
		//		BX.focus(input);
		//		BX.remove(input);
		//		window.scrollTo(orScrollLeft, orScrollTop);
		//	});
		//}

		// --------- Drag & Drop events  ---------
		BX.bind(element, "dragover", function(){editor.On("OnIframeDragOver", arguments);});
		BX.bind(element, "dragenter", function(){editor.On("OnIframeDragEnter", arguments);});
		BX.bind(element, "dragleave", function(){editor.On("OnIframeDragLeave", arguments);});
		BX.bind(element, "dragexit", function(){editor.On("OnIframeDragExit", arguments);});
		BX.bind(element, "drop", function(){editor.On("OnIframeDrop", arguments);});

		// Chrome & Safari & Firefox only fire the ondrop/ondragend/... events when the ondragover event is cancelled
		//if (BX.browser.IsChrome() || BX.browser.IsFirefox())
		// TODO: Truobles with firefox during selections http://jabber.bx/view.php?id=49370
		if (BX.browser.IsFirefox())
		{
			BX.bind(element, "dragover", function(e)
			{
				e.preventDefault();
			});
			BX.bind(element, "dragenter", function(e)
			{
				e.preventDefault();
			});
		}

		BX.bind(element, 'drop', BX.delegate(this.OnPasteHandler, this));

		BX.bind(element, 'paste', (clipboardEvent) => {
			const event = new BX.Event.BaseEvent({ data: { clipboardEvent: clipboardEvent } });
			BX.Event.EventEmitter.emitAsync(this.editor, 'BXEditor:onBeforePasteAsync', event).then(() => {
				if (!event.isDefaultPrevented())
				{
					this.OnPasteHandler(clipboardEvent);
				}
			});
		});

		BX.bind(element, "keyup", function(e)
		{
			var
				keyCode = e.keyCode,
				target = editor.selection.GetSelectedNode(true);

			_this.SetFocusedFlag(true);
			if (keyCode === editor.KEY_CODES['space'] || keyCode === editor.KEY_CODES['enter'])
			{
				if (keyCode === editor.KEY_CODES['enter'])
				{
					_this.OnEnterHandlerKeyUp(e, keyCode, target);
				}
				editor.On("OnIframeNewWord");
			}
			else
			{
				_this.OnKeyUpArrowsHandler(e, keyCode);
			}

			editor.selection.SaveRange();
			editor.On('OnIframeKeyup', [e, keyCode, target]);

			// Mantis:#67998
			if (keyCode === editor.KEY_CODES['backspace'] && BX.browser.IsChrome()
				&& target && target.nodeType == '3' && target.nextSibling && target.nextSibling.nodeType == '3')
			{
				_this.editor.selection.ExecuteAndRestoreSimple(function()
				{
					_this.editor.util.SetTextContent(target, _this.editor.util.GetTextContent(target) + _this.editor.util.GetTextContent(target.nextSibling));
					target.nextSibling.parentNode.removeChild(target.nextSibling);
				});
			}

			if (!editor.util.FirstLetterSupported() && _this.editor.parser.firstNodeCheck)
			{
				_this.editor.parser.FirstLetterCheckNodes('', '', true);
			}

			//mantis:91555, mantis:93629
			if (BX.browser.IsChrome())
			{
				if (_this.stopBugusScrollTimeout)
					clearTimeout(_this.stopBugusScrollTimeout);
				_this.stopBugusScrollTimeout = setTimeout(function(){_this.stopBugusScroll = false;}, 200);
			}

			_this.copilot?.onContentEditableKeyUp(e);
		});

		BX.bind(this.document, 'scroll', () => this.copilot?.onScrollHandler());

		BX.bind(element, "mousedown", function(e)
		{
			var target = e.target || e.srcElement;
			if (!editor.util.CheckImageSelectSupport() && target.nodeName === 'IMG')
			{
				editor.selection.SelectNode(target);
			}

			// Handle mousedown for "code" element in IE
			if (!editor.util.CheckPreCursorSupport() && target.nodeName === 'PRE')
			{
				var selectedNode = editor.selection.GetSelectedNode(true);
				if (selectedNode && selectedNode != target)
				{
					_this.FocusPreElement(target, true);
				}
			}

			_this.copilot?.onContentEditableMouseDown();
		});

		BX.bind(element, "keydown", BX.proxy(this.KeyDown, this));

		// Workaround for chrome bug with bugus scrolling to the top of the page (mantis:91555, mantis:93629)
		if (BX.browser.IsChrome())
		{
			BX.bind(window, "scroll", BX.proxy(function(e)
			{
				if (_this.stopBugusScroll)
				{
					if ((!_this.savedScroll || !_this.savedScroll.scrollTop) && _this.lastSavedScroll)
						_this.savedScroll = _this.lastSavedScroll;

					if (_this.savedScroll && !_this.lastSavedScroll)
						_this.lastSavedScroll = _this.savedScroll;
					_this._RestoreScrollTop();
				}
			}, this));
		}

		// Show urls and srcs in tooltip when hovering links or images
		var nodeTitles = {
			IMG: BX.message.SrcTitle + ": ",
			A: BX.message.UrlTitle + ": "
		};

		BX.bind(element, "mouseover", function(e)
		{
			var
				target = e.target || e.srcElement,
				value = (target.getAttribute("href") || target.getAttribute("src")),
				nodeName = target.nodeName;

			if (nodeTitles[nodeName]
					&& !target.hasAttribute("title")
					&& value
					&& value.indexOf('data:image/') === -1
			)
			{
				target.setAttribute("title", nodeTitles[nodeName] + value);
				target.setAttribute("data-bx-clean-attribute", "title");
			}
		});

		this.editor.InitClipboardHandler();
	};

	BXEditorIframeView.prototype.KeyDown = function(e)
	{
		var
			_this = this,
			keyCode = e.keyCode,
			KEY_CODES = this.editor.KEY_CODES;

		this.SetFocusedFlag(true);
		this.editor.iframeKeyDownPreventDefault = false;

		// Workaround for chrome bug with bugus scrollint to the top of the page (mantis:91555, mantis:93629)
		if (BX.browser.IsChrome())
		{
			this.stopBugusScroll = true;
			this.savedScroll = BX.GetWindowScrollPos(document);
		}

		var
			command = this.editor.SHORTCUTS[keyCode],
			selectedNode = this.editor.selection.GetSelectedNode(true),
			range = this.editor.selection.GetRange(),
			body = this.document.body,
			parent;

		if (e.key === ' ' && this.copilot?.shouldBeShown())
		{
			this.copilot.show(true);
			e.preventDefault();

			return;
		}

		this.copilot?.onContentEditableKeyDown();

		if ((BX.browser.IsIE() || BX.browser.IsIE10() || BX.browser.IsIE11()) &&
			!BX.util.in_array(keyCode, [16, 17, 18, 20, 65, 144, 37, 38, 39, 40]))
		{
			if (selectedNode && selectedNode.nodeName == "BODY"
				||
				range.startContainer && range.startContainer.nodeName == "BODY"
				||
				(range.startContainer == body.firstChild &&
				range.endContainer == body.lastChild &&
				range.startOffset == 0 &&
				range.endOffset == body.lastChild.length))
			{
				BX.addCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._IEBodyClearHandler, this));
			}
		}

		// Last symbol in iframe and new paragraph in IE
		if ((BX.browser.IsIE() || BX.browser.IsIE10() || BX.browser.IsIE11()) &&
			keyCode == KEY_CODES['backspace'])
		{
			BX.addCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._IEBodyClearHandlerEx, this));
		}

		this.isUserTyping = true;
		if (this.typingTimeout)
		{
			this.typingTimeout = clearTimeout(this.typingTimeout);
		}
		this.typingTimeout = setTimeout(function()
		{
			_this.isUserTyping = false;
		}, 1000);

		this.editor.synchro.StartSync(200);

		this.editor.On('OnIframeKeydown', [e, keyCode, command, selectedNode]);

		if (this.editor.iframeKeyDownPreventDefault)
			return BX.PreventDefault(e);

		// Handle  Shortcuts
		if ((e.ctrlKey || e.metaKey) && !e.altKey && command)
		{
			this.editor.action.Exec(command);
			return BX.PreventDefault(e);
		}

		// Bug mantis: #59759 workaround *Chrome only
		// Begin
		if (
			keyCode === KEY_CODES['backspace'] &&
			range.startOffset == 0 &&
			range.startContainer.nodeType == 3 &&
			range.startContainer.parentNode.firstChild == range.startContainer &&
			range.startContainer.parentNode &&
			range.startContainer.parentNode.nodeName == 'BLOCKQUOTE' &&
			range.startContainer.parentNode.className
		)
		{
			range.startContainer.parentNode.className = '';
		}

		if (
			!BX.browser.IsSafari()
			&& (keyCode === KEY_CODES['backspace'] || keyCode === KEY_CODES['delete'])
			&& range.startContainer.nodeName === '#text'
			&& range.startContainer.previousSibling?.nodeName === 'BLOCKQUOTE'
			&& range.startContainer.nextSibling?.nodeName !== 'BR'
			&& range.startContainer.textContent.length === 1
		)
		{
			range.startContainer.before(BX.Tag.render`<span>&#XFEFF;</span>`.innerHTML);
		}

		if (
			keyCode === KEY_CODES['delete'] &&
				range.collapsed &&
				range.endContainer.nodeType == 3 &&
				range.endOffset == range.endContainer.length
			)
		{
			var next = this.editor.util.GetNextNotEmptySibling(range.endContainer);
			if (next)
			{
				if(next.nodeName == 'BR')
				{
					next = this.editor.util.GetNextNotEmptySibling(next);
				}

				if (next && next.nodeName == 'BLOCKQUOTE' && next.className)
				{
					next.className = '';
				}
			}
		}
		// END: Bug mantis: #59759

		// Clear link with image
		if (selectedNode && selectedNode.nodeName === "IMG" &&
			(keyCode === KEY_CODES['backspace'] || keyCode === KEY_CODES['delete']))
		{
			parent = selectedNode.parentNode;
			parent.removeChild(selectedNode); // delete image

			// Parent - is LINK, and it's hasn't got any other childs
			if (parent.nodeName === "A" && !parent.firstChild)
			{
				parent.parentNode.removeChild(parent);
			}

			setTimeout(function(){_this.editor.util.Refresh(_this.element);}, 0);
			BX.PreventDefault(e);
		}

		if (range.collapsed && this.OnKeyDownArrowsHandler(e, keyCode, range) === false)
		{
			return false;
		}

		// Handle Ctrl+Enter
		if ((e.ctrlKey || e.metaKey) && !e.altKey && keyCode === KEY_CODES["enter"])
		{
			if (this.IsFocused())
				this.editor.On("OnIframeBlur");

			this.editor.On('OnCtrlEnter', [e, this.editor.GetViewMode()]);
			return BX.PreventDefault(e);
		}

		// Firefox's bug it remove first node for customized lists
		if (BX.browser.IsFirefox() && selectedNode && (keyCode === KEY_CODES["delete"] || keyCode === KEY_CODES["backspace"]))
		{
			var li = selectedNode.nodeName == 'LI' ? selectedNode : BX.findParent(selectedNode, {tag: 'LI'}, body);
			if (li && li.firstChild && li.firstChild.nodeName == 'I')
			{
				var ul = BX.findParent(li, {tag: 'UL'}, body);
				if (ul)
				{
					var customBullitClass = this.editor.action.actions.insertUnorderedList.getCustomBullitClass(ul);
					if (customBullitClass)
					{
						setTimeout(function()
						{
							if (ul && li && li.innerHTML !== '')
							{
								_this.editor.action.actions.insertUnorderedList.checkCustomBullitList(ul, customBullitClass);
							}
						}, 0);
					}
				}
			}
		}

		if (!this.editor.util.FirstLetterSupported()  &&
			_this.editor.parser.firstNodeCheck &&
			keyCode === this.editor.KEY_CODES['backspace'])
		{
			_this.editor.parser.FirstLetterBackspaceHandler(range);
		}

		// Handle "Enter"
		if (!e.shiftKey && (keyCode === KEY_CODES["enter"] || keyCode === KEY_CODES["backspace"]))
		{
			return this.OnEnterHandler(e, keyCode, selectedNode, range);
		}

		if (keyCode === KEY_CODES["pageUp"] || keyCode === KEY_CODES["pageDown"])
		{
			this.savedScroll = BX.GetWindowScrollPos(document);
			BX.addCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._RestoreScrollTop, this));
			setTimeout(BX.proxy(this._RestoreScrollTop, this), 0);
		}
	};

	BXEditorIframeView.prototype._RestoreScrollTop = function(e)
	{
		if (this.savedScroll)
		{
			window.scrollTo(this.savedScroll.scrollLeft, this.savedScroll.scrollTop);
			this.savedScroll = null;
		}
		BX.removeCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._RestoreScrollTop, this));
	};

	BXEditorIframeView.prototype._IEBodyClearHandler = function(e)
	{
		var
			p = this.document.body.firstChild;

		if (e.keyCode == this.editor.KEY_CODES['enter'] && p.nodeName == "P" && p != this.document.body.lastChild)
		{
			if (p.innerHTML && p.innerHTML.toLowerCase() == '<br>')
			{
				var newPar = p.nextSibling;
				this.editor.util.ReplaceWithOwnChildren(p);
				p = newPar;
			}
		}

		if (p && p.nodeName == "P" && p == this.document.body.lastChild)
		{
			this.editor.util.ReplaceWithOwnChildren(p);
		}
		BX.removeCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._IEBodyClearHandler, this));
	};

	BXEditorIframeView.prototype._IEBodyClearHandlerEx = function(e)
	{
		var p = this.document.body.firstChild;

		if (e.keyCode == this.editor.KEY_CODES['backspace'] &&
			p && p.nodeName == "P" && p == this.document.body.lastChild &&
			(this.editor.util.IsEmptyNode(p, true, true) || p.innerHTML && p.innerHTML.toLowerCase() == '<br>'))
		{
			this.editor.util.ReplaceWithOwnChildren(p);
		}

		BX.removeCustomEvent(this.editor, "OnIframeKeyup", BX.proxy(this._IEBodyClearHandlerEx, this));
	};

	BXEditorIframeView.prototype.OnEnterHandler = function(e, keyCode, selectedNode, range)
	{
		// TODO: check it again later maybe chrome will fix it
		// mantis: 55872. Chrome 38 rendering bug workaround
		if (BX.browser.IsChrome())
		{
			this.document.body.style.minHeight = (parseInt(this.document.body.style.minHeight) + 1) + 'px';
			// mantis: 60033
			this.document.body.style.minHeight = (parseInt(this.document.body.style.minHeight) - 1) + 'px';
		}

		// Check selectedNode
		if (!selectedNode)
		{
			return;
		}

		var _this = this;
		function unwrap(node)
		{
			if (node)
			{
				if (node.nodeName !== "P" && node.nodeName !== "DIV")
				{
					node = BX.findParent(node, function(n)
					{
						return n.nodeName === "P" || n.nodeName === "DIV";
					}, _this.document.body);
				}

				var emptyNode = _this.editor.util.GetInvisibleTextNode();
				if (node)
				{
					node.parentNode.insertBefore(emptyNode, node);
					_this.editor.util.ReplaceWithOwnChildren(node);
					_this.editor.selection.SelectNode(emptyNode);
				}
			}
		}

		var
			list, br, blockElement,
			blockTags  = ["LI", "P", "H1", "H2", "H3", "H4", "H5", "H6"],
			listTags  = ["UL", "OL", "MENU"];

		if (BX.util.in_array(selectedNode.nodeName, blockTags))
		{
			blockElement = selectedNode;
		}
		else
		{
			blockElement = BX.findParent(selectedNode, function(n)
			{
				return BX.util.in_array(n.nodeName, blockTags);
			}, this.document.body);
		}

		if (blockElement)
		{
			if (blockElement.nodeName === "LI")
			{
				if (keyCode === _this.editor.KEY_CODES["enter"] && blockElement && blockElement.parentNode)
				{
					var bullitClass = _this.editor.action.actions.insertUnorderedList.getCustomBullitClass(blockElement.parentNode);
				}

				// Some browsers create <p> elements after leaving a list
				// check after keydown of backspace and return whether a <p> got inserted and unwrap it
				setTimeout(function()
				{
					var node = _this.editor.selection.GetSelectedNode(true);
					if (node)
					{
						list = BX.findParent(node, function(n)
						{
							return BX.util.in_array(n.nodeName, listTags);
						}, _this.document.body);

						// Check if it's list with custom styled bullits - we have to check it all items have same style
						if (keyCode === _this.editor.KEY_CODES["enter"] && blockElement && blockElement.parentNode)
						{
							_this.editor.action.actions.insertUnorderedList.checkCustomBullitList(blockElement.parentNode, bullitClass, true);
						}

						// mantis: 82028, 83178
						if (list && BX.browser.IsChrome() && keyCode === _this.editor.KEY_CODES["enter"])
						{
							var i, li = list.getElementsByTagName('LI');

							for (i = 0; i < li.length; i++)
							{
								//&65279; === \uFEFF - invisible space
								li[i].innerHTML = li[i].innerHTML.replace(/\uFEFF/ig, '');
							}
						}

						if (!list)
						{
							unwrap(node);
						}
					}
				}, 0);
			}
			else if (blockElement.nodeName.match(/H[1-6]/) && keyCode === this.editor.KEY_CODES["enter"])
			{
				setTimeout(function()
				{
					unwrap(_this.editor.selection.GetSelectedNode());
				}, 0);
			}

			return true;
		}

		if (keyCode === this.editor.KEY_CODES["enter"] && !BX.browser.IsFirefox() && this.editor.action.IsSupported('insertLineBreak'))
		{
			if (BX.browser.IsIE10() || BX.browser.IsIE11())
			{
				this.editor.action.Exec('insertHTML', '<br>' + this.editor.INVISIBLE_SPACE);
			}
			else if(BX.browser.IsChrome())
			{
				this.editor.action.Exec('insertLineBreak');

				// Bug in Chrome - when you press enter but it put carret on the prev string
				// Chrome 43.0.2357 in Mac puts visible space instead of invisible
				if (BX.browser.IsMac())
				{
					var tmpId = "bx-editor-temp-" + Math.round(Math.random() * 1000000);
					this.editor.action.Exec('insertHTML', '<span id="' + tmpId + '">' + this.editor.INVISIBLE_SPACE + '</span>');
					var tmpElement = this.editor.GetIframeElement(tmpId);
					if (tmpElement)
						BX.remove(tmpElement);
				}
				else
				{
					this.editor.action.Exec('insertHTML', this.editor.INVISIBLE_SPACE);
				}
			}
			else
			{
				this.editor.action.Exec('insertLineBreak');
			}
			return BX.PreventDefault(e);
		}

		if ((BX.browser.IsChrome() || BX.browser.IsIE10() || BX.browser.IsIE11()) && keyCode == this.editor.KEY_CODES['backspace'] && range.collapsed)
		{
			var checkNode = BX.create('SPAN', false, this.document);
			this.editor.selection.InsertNode(checkNode);
			var prev = checkNode.previousSibling;
			if (prev && prev.nodeType == 3 && this.editor.util.IsEmptyNode(prev, false, false))
			{
				BX.remove(prev);
			}
			this.editor.selection.SetBefore(checkNode);
			BX.remove(checkNode);
		}
	};

	BXEditorIframeView.prototype.OnEnterHandlerKeyUp = function(e, keyCode, node)
	{
		// Clean class of all block nodes when they created after Enter pressing
		// All new Ps and DIVs should be without classNames
		if (node)
		{
			var _this = this;
			if (!BX.util.in_array(node.nodeName, this.editor.GetBlockTags()))
			{
				node = BX.findParent(node, function(n)
				{
					return BX.util.in_array(n.nodeName, _this.editor.GetBlockTags());
				}, this.document.body);
			}

			if (node && BX.util.in_array(node.nodeName, this.editor.GetBlockTags()))
			{
				var html = BX.util.trim(node.innerHTML).toLowerCase();
				if (this.editor.util.IsEmptyNode(node, true, true) || html == '' || html == '<br>')
				{
					node.removeAttribute("class");
				}
			}
		}
	};

	BXEditorIframeView.prototype.OnKeyDownArrowsHandler = function(e, keyCode, range)
	{
		var
			node, parentNode, nextNode, prevNode,
			KC = this.editor.KEY_CODES;

		this.keyDownRange = range;

		if (keyCode === KC['right'] || keyCode === KC['down'])
		{
			node = range.endContainer;
			nextNode = node ? node.nextSibling : false;
			parentNode = node ? node.parentNode : false;

			if (
				node.nodeType == 3 && node.length == range.endOffset
				&& parentNode && parentNode.nodeName !== 'BODY'
				&& (!nextNode || (nextNode && nextNode.nodeName == 'BR' && !nextNode.nextSibling))
				&& (this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode))
				)
			{
				this.editor.selection.SetInvisibleTextAfterNode(parentNode, true);
				return BX.PreventDefault(e);
			}
			else if(
					node.nodeType == 3 && this.editor.util.IsEmptyNode(node)
					&& nextNode
					&& (this.editor.util.IsBlockElement(nextNode) || this.editor.util.IsBlockNode(nextNode))
				)
			{
				BX.remove(node);
				if (nextNode.firstChild)
				{
					this.editor.selection.SetBefore(nextNode.firstChild);
				}
				else
				{
					this.editor.selection.SetAfter(nextNode);
				}
				return BX.PreventDefault(e);
			}
			else if (
					node.nodeType == 3 && node.length == range.endOffset
					&& parentNode && parentNode.nodeName !== 'BODY'
					&& nextNode && nextNode.nodeName == 'BR'
					&& !nextNode.nextSibling
					&& (this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode))
			)
			{
				this.editor.selection.SetInvisibleTextAfterNode(parentNode, true);
				return BX.PreventDefault(e);
			}

		}
		else if (keyCode === KC['left'] || keyCode === KC['up'])
		{
			node = range.startContainer;
			parentNode = node ? node.parentNode : false;
			prevNode = node ? node.previousSibling : false;

			if (
				node.nodeType == 3 && range.endOffset === 0
				&& parentNode && parentNode.nodeName !== 'BODY'
				&& !prevNode
				&& (this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode))
				)
			{
				this.editor.selection.SetInvisibleTextBeforeNode(parentNode);
				return BX.PreventDefault(e);
			}
			else if(
				node.nodeType == 3 && this.editor.util.IsEmptyNode(node)
					&& prevNode
					&& (this.editor.util.IsBlockElement(prevNode) || this.editor.util.IsBlockNode(prevNode))
				)
			{
				BX.remove(node);
				if (prevNode.lastChild)
				{
					this.editor.selection.SetAfter(prevNode.lastChild);
				}
				else
				{
					this.editor.selection.SetBefore(prevNode);
				}
				return BX.PreventDefault(e);
			}
		}

		return true;
	};

	BXEditorIframeView.prototype.OnKeyUpArrowsHandler = function(e, keyCode)
	{
		var
			_this = this,
			pre, prevToSur, nextToSur,
			keyDownNode, keyDownPre,
			range = this.editor.selection.GetRange(),
			node, parentNode, nextNode, prevNode, isEmpty, isSur, sameLastRange,
			startCont, endCont, startIsSur, endIsSur,
			KC = this.editor.KEY_CODES;

		// Arrows right or down
		if (keyCode === KC['right'] || keyCode === KC['down'])
		{
			this.editor.selection.GetStructuralTags();
			// Moving cursor by arrows (right & down)
			if (range.collapsed)
			{
				node = range.endContainer;

				isEmpty = this.editor.util.IsEmptyNode(node);
				// We check if last range was the same - it means that cursor doesn't
				// moved when user tried to move it
				sameLastRange = this.editor.selection.CheckLastRange(range);
				nextNode = node.nextSibling;

				if (!this.editor.util.CheckPreCursorSupport())
				{
					if (node.nodeName === 'PRE')
					{
						pre = node;
					}
					else if (node.nodeType == 3)
					{
						pre = BX.findParent(node, {tag: 'PRE'}, this.element);
					}

					if(pre)
					{
						if (this.keyDownRange)
						{
							keyDownNode = this.keyDownRange.endContainer;
							keyDownPre = keyDownNode == pre ? pre : BX.findParent(keyDownNode, function(n){return n == pre;}, this.element);
						}

						_this.FocusPreElement(pre, false, keyDownPre ? null : 'start');
					}
				}

				// If cursor in the invisible node - we take next node
				if (node.nodeType == 3 && isEmpty && nextNode)
				{
					node = nextNode;
					isEmpty = this.editor.util.IsEmptyNode(node);
				}

				isSur = this.editor.util.CheckSurrogateNode(node);

				// It's surrogate
				if (isSur)
				{
					nextToSur = node.nextSibling;
					if (nextToSur && nextToSur.nodeType == 3 && this.editor.util.IsEmptyNode(nextToSur))
						this.editor.selection._MoveCursorAfterNode(nextToSur);
					else
						this.editor.selection._MoveCursorAfterNode(node);

					BX.PreventDefault(e);
				}
				// If it's element
				else if (node.nodeType == 1 && node.nodeName != "BODY" && !isEmpty)
				{
					if (sameLastRange)
					{
						this.editor.selection._MoveCursorAfterNode(node);
						BX.PreventDefault(e);
					}
				}
				else if (sameLastRange && node.nodeType == 3 && /*node.length == range.endOffset &&*/ !isEmpty)
				{
					parentNode = node.parentNode;
					if (parentNode && node === parentNode.lastChild && parentNode.nodeName != "BODY")
					{
						this.editor.selection._MoveCursorAfterNode(parentNode);
					}
				}
				else if (node.nodeType == 3 && node.parentNode)
				{
					parentNode = node.parentNode;
					prevNode = parentNode.previousSibling;

					// It's empty invisible node before block element which was put there by us.
					// So we should remove it.
					if (
						(this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode))
						&& prevNode && prevNode.nodeType == 3 && this.editor.util.IsEmptyNode(prevNode)
						)
					{
						BX.remove(prevNode);
					}
				}
			}
			else // Selection Shift + Right & Shift + down
			{
				startCont = range.startContainer;
				endCont = range.endContainer;
				startIsSur = this.editor.util.CheckSurrogateNode(startCont);
				endIsSur = this.editor.util.CheckSurrogateNode(endCont);

				if (startIsSur)
				{
					prevToSur = startCont.previousSibling;
					if (prevToSur && prevToSur.nodeType == 3 && this.editor.util.IsEmptyNode(prevToSur))
						range.setStartBefore(prevToSur);
					else
						range.setStartBefore(startCont);

					this.editor.selection.SetSelection(range);
				}

				if (endIsSur)
				{
					nextToSur = endCont.nextSibling;
					if (nextToSur && nextToSur.nodeType == 3 && this.editor.util.IsEmptyNode(nextToSur))
						range.setEndAfter(nextToSur);
					else
						range.setEndAfter(endCont);

					this.editor.selection.SetSelection(range);
				}
			}
		}
		// Arrows left or up
		else if (keyCode === KC['left'] || keyCode === KC['up'])
		{
			this.editor.selection.GetStructuralTags();

			// Moving cursor by arrows (left & up)
			if (range.collapsed)
			{
				node = range.startContainer;
				isEmpty = this.editor.util.IsEmptyNode(node);
				// We check if last range was the same - it means that cursor doesn't
				// moved when user tried to move it
				sameLastRange = this.editor.selection.CheckLastRange(range);

				// If cursor in the invisible node - we take next node
				if (node.nodeType == 3 && isEmpty && node.previousSibling)
				{
					node = node.previousSibling;
					isEmpty = this.editor.util.IsEmptyNode(node);
				}

				if (!this.editor.util.CheckPreCursorSupport())
				{
					if (node.nodeName === 'PRE')
					{
						pre = node;
					}
					else if (node.nodeType == 3)
					{
						pre = BX.findParent(node, {tag: 'PRE'}, this.element);
					}

					if(pre)
					{
						if (this.keyDownRange)
						{
							keyDownNode = this.keyDownRange.startContainer;
							keyDownPre = keyDownNode == pre ? pre : BX.findParent(keyDownNode, function(n){return n == pre;}, this.element);
						}
						_this.FocusPreElement(pre, false, keyDownPre ? null : 'end');
					}
				}

				isSur = this.editor.util.CheckSurrogateNode(node);
				// It's surrogate
				if (isSur)
				{
					prevToSur = node.previousSibling;
					if (prevToSur && prevToSur.nodeType == 3 && this.editor.util.IsEmptyNode(prevToSur))
						this.editor.selection._MoveCursorBeforeNode(prevToSur);
					else
						this.editor.selection._MoveCursorBeforeNode(node);

					BX.PreventDefault(e);
				}
				// If it's element
				else if (node.nodeType == 1 && node.nodeName != "BODY" && !isEmpty)
				{
					if (sameLastRange)
					{
						this.editor.selection._MoveCursorBeforeNode(node);
						BX.PreventDefault(e);
					}
				}
				//else if (sameLastRange && node.nodeType == 3 && range.startOffset == 0 && !isEmpty)
				else if (sameLastRange && node.nodeType == 3 && !isEmpty)
				{
					parentNode = node.parentNode;
					if (parentNode && node === parentNode.firstChild && parentNode.nodeName != "BODY")
					{
						this.editor.selection._MoveCursorBeforeNode(parentNode);
					}
				}
				else if (node.nodeType == 3 && node.parentNode)
				{
					parentNode = node.parentNode;
					prevNode = parentNode.nextSibling;

					// It's empty invisible node after block element which was put there by us.
					// So we should remove it.
					if (
						(this.editor.util.IsBlockElement(parentNode) || this.editor.util.IsBlockNode(parentNode))
							&& prevNode && prevNode.nodeType == 3 && this.editor.util.IsEmptyNode(prevNode)
						)
					{
						BX.remove(prevNode);
					}
				}

			}
			else // Selection Shift + left & Shift + up
			{
				startCont = range.startContainer;
				endCont = range.endContainer;
				startIsSur = this.editor.util.CheckSurrogateNode(startCont);
				endIsSur = this.editor.util.CheckSurrogateNode(endCont);

				if (startIsSur)
				{
					prevToSur = startCont.previousSibling;
					if (prevToSur && prevToSur.nodeType == 3 && this.editor.util.IsEmptyNode(prevToSur))
						range.setStartBefore(prevToSur);
					else
						range.setStartBefore(startCont);
					this.editor.selection.SetSelection(range);
				}

				if (endIsSur)
				{
					nextToSur = endCont.nextSibling;
					if (nextToSur && nextToSur.nodeType == 3 && this.editor.util.IsEmptyNode(nextToSur))
						range.setEndAfter(nextToSur);
					else
						range.setEndAfter(endCont);
					this.SetSelection(range);
				}
			}
		}

		this.keyDownRange = null;
	};

	BXEditorIframeView.prototype.FocusPreElement = function(preNode, timeout, mode)
	{
		var _this = this;

		if (this._focusPreElementTimeout)
			this._focusPreElementTimeout = clearTimeout(this._focusPreElementTimeout);

		if (timeout)
		{
			this._focusPreElementTimeout = setTimeout(function(){
				_this.FocusPreElement(preNode, false, mode);
			}, 100);
			return;
		}
		BX.focus(preNode);
		if (mode == 'end' && preNode.lastChild)
		{
			this.editor.selection.SetAfter(preNode.lastChild);
		}
		else if (mode == 'start' && preNode.firstChild)
		{
			this.editor.selection.SetBefore(preNode.firstChild);
		}
	};

	BXEditorIframeView.prototype.OnPasteHandler = function(e)
	{
		if (!this.editor.skipPasteHandler)
		{
			this.editor.skipPasteHandler = true;
			this.editor.pasteNodeIndex = {};
			var
				originalScrollTop = document.documentElement.scrollTop || document.body.scrollTop,
				originalScrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft,
				_this = this,
				arNodes = [],
				curNode, i, node, qnodes;

			function markGoodNode(n)
			{
				if (n && n.setAttribute)
				{
					var randValue = Math.round(Math.random() * 1000000);
					_this.editor.pasteNodeIndex[randValue] = true;
					n.setAttribute('data-bx-paste-flag', randValue);
				}
			}

			curNode = this.document.body;
			if (curNode)
			{
				qnodes = curNode.querySelectorAll("*");
				for (i = 0; i < qnodes.length; i++)
				{
					if (qnodes[i].nodeType == 1 && qnodes[i].nodeName != 'BODY' && qnodes[i].nodeName != 'HEAD')
					{
						arNodes.push(qnodes[i]);
					}
				}

				for (i = 0; i < curNode.parentNode.childNodes.length; i++)
				{
					node = curNode.parentNode.childNodes[i];
					if (node.nodeType == 1 && node.nodeName != 'BODY' && node.nodeName != 'HEAD')
					{
						arNodes.push(node);
					}
				}
			}

			for (i = 0; i < arNodes.length; i++)
			{
				markGoodNode(arNodes[i]);
			}

			var sync = this.editor.synchro.IsSyncOn();
			if (sync)
			{
				this.editor.synchro.StopSync();
			}

			if (this.editor.iframeView.pasteHandlerTimeout)
			{
				clearTimeout(this.editor.iframeView.pasteHandlerTimeout);
			}

			this.pasteHandlerTimeout = setTimeout(function()
			{
				_this.editor.SetCursorNode();

				_this.editor.pasteHandleMode = true;
				_this.editor.bbParseContentMode = true;

				_this.editor.synchro.lastIframeValue = false;

				// Paste control: show menu after pasting content
				// to let user select weather insert rich content or plain text
				if (!_this.editor.skipPasteControl)
				{
					_this.editor.pasteControl.SaveIframeContent(_this.GetValue());
					_this.editor.pasteControl.CheckAndShow();
				}

				_this.editor.synchro.FromIframeToTextarea(true, true);

				_this.editor.pasteHandleMode = false;
				_this.editor.bbParseContentMode = false;

				_this.editor.synchro.lastTextareaValue = false;
				_this.editor.synchro.FromTextareaToIframe(true);

				_this.editor.RestoreCursor();

				_this.editor.On("OnIframePaste");
				_this.editor.On("OnIframeNewWord");
				_this.editor.skipPasteHandler = false;

				if (sync)
				{
					_this.editor.synchro.StartSync();
				}

				if (window.scrollTo)
				{
					window.scrollTo(originalScrollLeft, originalScrollTop);
				}
			}, 0);
		}
	};

	BXEditorIframeView.prototype.InitAutoLinking = function()
	{
		var
			_this = this,
			editor = this.editor,
			nativeAutolinkCanBeDisabled = editor.action.IsSupportedByBrowser("autoUrlDetect"),
			nativeAutoLink = BX.browser.IsIE() || BX.browser.IsIE9() || BX.browser.IsIE10();

		if (nativeAutolinkCanBeDisabled)
			editor.action.Exec("autoUrlDetect", false);

		if (editor.config.autoLink === false)
			return;

		// Init Autolink system
		var
			ignorableParents = {"CODE" : 1, "PRE" : 1, "A" : 1, "SCRIPT" : 1, "HEAD" : 1, "TITLE" : 1, "STYLE" : 1},
			urlRegExp = /(((?:https?|ftp):\/\/|www\.)[^\s'"<]{3,500})/gi,
			emailRegExp = /[\.a-z0-9_\-]+@[\.a-z0-9_\-]+\.[\.a-z0-9_\-]+/gi,
			MAX_LENGTH = 100,
			BRACKETS = {
				")": "(",
				"]": "[",
				"}": "{"
			};
		this.editor.autolinkUrlRegExp = urlRegExp;
		this.editor.autolinkEmailRegExp = emailRegExp;

		function autoLinkHandler()
		{
			try
			{
				if (checkAutoLink())
				{
					editor.selection.ExecuteAndRestore(function(startContainer, endContainer)
					{
						if (endContainer && endContainer.parentNode)
							autoLink(endContainer.parentNode);
					});
				}
			}
			catch(e){}
		}

		function checkAutoLink()
		{
			var
				node, nodeValue,
				result = false,
				doc = editor.GetIframeDoc(),
				walker = doc.createTreeWalker(
				doc.body,
				NodeFilter.SHOW_TEXT,
				null,
				false
			);

			while(node = walker.nextNode())
			{
				nodeValue = node.nodeValue || '';
				if ((nodeValue.match(emailRegExp) || nodeValue.match(urlRegExp)) &&
					node.parentNode && node.parentNode.nodeName != 'A')
				{
					result = true;
					break;
				}
			}

			return result;
		}

		function autoLink(element)
		{
			if (element && !ignorableParents[element.nodeName])
			{
				var ignorableParent = BX.findParent(element, function(node)
				{
					return !!ignorableParents[node.nodeName];
				}, element.ownerDocument.body);

				if (ignorableParent)
					return element;

				if (element === element.ownerDocument.documentElement)
					element = element.ownerDocument.body;

				return parseNode(element);
			}
		}

		function convertUrlToLink(str)
		{
			str = BX.util.htmlspecialchars(str);
			return str.replace(urlRegExp, function(match, url)
			{
				var
					punctuation = (url.match(/([^\w\u0430-\u0456\u0451\/\-#](,?))$/i) || [])[1] || "",
					opening = BRACKETS[punctuation];

				url = url.replace(/([^\w\u0430-\u0456\u0451\/\-#](,?))$/i, "");

				if (url.split(opening).length > url.split(punctuation).length)
				{
					url = url + punctuation;
					punctuation = "";
				}
				var
					realUrl = url,
					displayUrl = BX.util.htmlspecialchars(url);

				if (url.length > MAX_LENGTH)
					displayUrl = displayUrl.substr(0, MAX_LENGTH) + "...";

				if (realUrl.substr(0, 4) === "www.")
					realUrl = "http://" + realUrl;

				BX.onCustomEvent(_this.editor, 'OnAfterUrlConvert', [realUrl]);
				return '<a href="' + realUrl + '">' + displayUrl + '</a>' + punctuation;
			});
		}

		function convertEmailToLink(str)
		{
			str = BX.util.htmlspecialchars(str);
			return str.replace(emailRegExp, function(email)
			{
				var
					punctuation = (email.match(/([^\w\/\-](,?))$/i) || [])[1] || "",
					opening = BRACKETS[punctuation];
				email = email.replace(/([^\w\/\-](,?))$/i, "");
				if (email.split(opening).length > email.split(punctuation).length)
				{
					email = email + punctuation;
					punctuation = "";
				}

				var realUrl = "mailto:" + email;

				return '<a href="' + realUrl + '">' + email + '</a>' + punctuation;
			});
		}

		function getTmpDiv(doc)
		{
			var tmp = doc._bx_autolink_temp_div;
			if (!tmp)
				tmp = doc._bx_autolink_temp_div = doc.createElement("div");
			return tmp;
		}

		function parseNode(element)
		{
			var res, parentNode, tmpDiv;
			if (element && !ignorableParents[element.nodeName])
			{
				// Replaces the content of the text node by link
				if (element.nodeType === 3 &&
					element.data.match(urlRegExp) && element.parentNode)
				{
					parentNode = element.parentNode;
					tmpDiv = getTmpDiv(parentNode.ownerDocument);
					tmpDiv.innerHTML = "<span></span>" + convertUrlToLink(element.data);
					tmpDiv.removeChild(tmpDiv.firstChild);

					while (tmpDiv.firstChild)
						parentNode.insertBefore(tmpDiv.firstChild, element);

					parentNode.removeChild(element);
				}
				else if (element.nodeType === 3 &&
					element.data.match(emailRegExp) && element.parentNode)
				{
					parentNode = element.parentNode;
					tmpDiv = getTmpDiv(parentNode.ownerDocument);
					tmpDiv.innerHTML = "<span></span>" + convertEmailToLink(element.data);
					tmpDiv.removeChild(tmpDiv.firstChild);

					while (tmpDiv.firstChild)
						parentNode.insertBefore(tmpDiv.firstChild, element);

					parentNode.removeChild(element);
				}
				else if (element.nodeType === 1)
				{
					var
						childNodes = element.childNodes,
						i;

					for (i = 0; i < childNodes.length; i++)
						parseNode(childNodes[i]);

					res = element;
				}
			}
			return res;
		}

		if (!nativeAutoLink || (nativeAutoLink && nativeAutolinkCanBeDisabled))
		{
			BX.addCustomEvent(editor, "OnIframeNewWord", function()
			{
				if (editor.autolinkTimeout)
					editor.autolinkTimeout = clearTimeout(editor.autolinkTimeout);

				editor.autolinkTimeout = setTimeout(autoLinkHandler, 500);
			});

			BX.addCustomEvent(editor, "OnSubmit", function()
			{
				try
				{
					autoLink(editor.GetIframeDoc().body);
				}
				catch(e){}
			});
		}

		var
			links = editor.sandbox.GetDocument().getElementsByTagName("a"),
			getTextContent  = function(element)
			{
				var textContent = BX.util.trim(editor.util.GetTextContent(element));
				if (textContent.substr(0, 4) === "www.")
					textContent = "http://" + textContent;
				return textContent;
			};

		BX.addCustomEvent(editor, "OnIframeKeydown", function(e, keyCode, command, selectedNode)
		{
			if (links.length > 0 && selectedNode)
			{
				var link = BX.findParent(selectedNode, {tag: 'A'}, selectedNode.ownerDocument.body);
				if (link)
				{
					var textContent = getTextContent(link);
					setTimeout(function()
					{
						var newTextContent = getTextContent(link);
						if (newTextContent === textContent)
							return;

						// Only set href when new href looks like a valid url
						if (newTextContent.match(urlRegExp))
							link.setAttribute("href", newTextContent);
					}, 0);
				}
			}
		});
	};

	BXEditorIframeView.prototype.IsUserTypingNow = function(e)
	{
		return this.isFocused && this.isShown && this.isUserTyping;
	};

	BXEditorIframeView.prototype.CheckContentLastChild = function(element)
	{
		if (!element)
		{
			element = this.element;
		}

		var lastChild = element.lastChild;
		if (lastChild && (this.editor.util.IsEmptyNode(lastChild, true) && this.editor.util.IsBlockNode(lastChild.previousSibling) || this.editor.phpParser.IsSurrogate(lastChild)))
		{
			element.appendChild(BX.create('BR', {}, element.ownerDocument));
			element.appendChild(this.editor.util.GetInvisibleTextNode());
		}
	};

/**
 * Class _this takes care that the value of the composer and the textarea is always in sync
 */
	function BXEditorViewsSynchro(editor, textareaView, iframeView)
	{
		this.INTERVAL = 500;

		this.editor = editor;
		this.textareaView = textareaView;
		this.iframeView = iframeView;
		this.lastFocused = 'wysiwyg';

		this.InitEventHandlers();
	}

	/**
	 * Sync html from composer to textarea
	 * Takes care of placeholders
	 * @param {Boolean} bParseHtml Whether the html should be sanitized before inserting it into the textarea
	 */
	BXEditorViewsSynchro.prototype =
	{
		FromIframeToTextarea: function(bParseHtml, bFormat)
		{
			var value;
			if (this.editor.bbCode)
			{
				value = this.iframeView.GetValue(this.editor.bbParseContentMode, false);

				value = BX.util.trim(value);
				if (value !== this.lastIframeValue)
				{
					var bbCodes = this.editor.bbParser.Unparse(value);
					this.textareaView.SetValue(bbCodes, false, bFormat || this.editor.bbParseContentMode);

					if (typeof this.lastSavedIframeValue !== 'undefined' && this.lastSavedIframeValue != value)
					{
						this.editor.On("OnContentChanged", [bbCodes, value]);
					}
					this.lastSavedIframeValue = value;
					this.lastIframeValue = value;
				}
			}
			else
			{
				value = this.iframeView.GetValue();
				value = BX.util.trim(value);
				if (value !== this.lastIframeValue)
				{
					this.textareaView.SetValue(value, true, bFormat);
					if (typeof this.lastSavedIframeValue !== 'undefined' && this.lastSavedIframeValue != value)
					{
						this.editor.On("OnContentChanged", [this.textareaView.GetValue() || '', value || '']);
					}
					this.lastSavedIframeValue = value;
					this.lastIframeValue = value;
				}
			}
		},

		/**
		* Sync value of textarea to composer
		* Takes care of placeholders
		* @param {Boolean} bParseHtml Whether the html should be sanitized before inserting it into the composer
		*/
		FromTextareaToIframe: function(bParseHtml)
		{
			var value = this.textareaView.GetValue();

			if (value !== this.lastTextareaValue)
			{
				if (value)
				{
					if (this.editor.bbCode)
					{
						var htmlFromBbCode = this.editor.bbParser.Parse(value);
						// INVISIBLE_CURSOR
						htmlFromBbCode = htmlFromBbCode.replace(/\u2060/ig, '<span id="bx-cursor-node"> </span>');
						this.iframeView.SetValue(htmlFromBbCode, bParseHtml);
					}
					else
					{
						// INVISIBLE_CURSOR
						value = value.replace(/\u2060/ig, '<span id="bx-cursor-node"> </span>');
						this.iframeView.SetValue(value, bParseHtml);
					}
				}
				else
				{
					this.iframeView.Clear();
				}
				this.lastTextareaValue = value;
				this.editor.On("OnContentChanged", [value || '', this.iframeView.GetValue() || '']);
			}
		},

		FullSyncFromIframe: function()
		{
			this.lastIframeValue = false;
			this.FromIframeToTextarea(true, true);
			this.lastTextareaValue = false;
			this.FromTextareaToIframe(true);
		},

		Sync: function()
		{
			var bParseHtml = true;
			var view = this.editor.currentViewName;

			if (view === "split")
			{
				if (this.GetSplitMode() === "code")
				{
					this.FromTextareaToIframe(bParseHtml);
				}
				else // wysiwyg
				{
					this.FromIframeToTextarea(bParseHtml);
				}
			}
			else if (view === "code")
			{
				this.FromTextareaToIframe(bParseHtml);
			}
			else // wysiwyg
			{
				this.FromIframeToTextarea(bParseHtml);
			}
		},

		GetSplitMode: function()
		{
			var mode = false;
			if (this.editor.currentViewName == "split")
			{
				if (this.editor.iframeView.IsFocused())
				{
					mode = "wysiwyg";
				}
				else if(this.editor.textareaView.IsFocused())
				{
					mode = "code";
				}
				else
				{
					mode = this.lastFocused;
				}
			}
			return mode;
		},

		InitEventHandlers: function()
		{
			var _this = this;
			BX.addCustomEvent(this.editor, "OnTextareaFocus", function()
			{
				_this.lastFocused = 'code';
				_this.StartSync();
			});
			BX.addCustomEvent(this.editor, "OnIframeFocus", function()
			{
				_this.lastFocused = 'wysiwyg';
				_this.StartSync();
			});

			BX.addCustomEvent(this.editor, "OnTextareaBlur", BX.delegate(this.StopSync, this));
			BX.addCustomEvent(this.editor, "OnIframeBlur", BX.delegate(this.StopSync, this));
		},

		StartSync: function(delay)
		{
			var _this = this;

			if (this.interval)
			{
				this.interval = clearTimeout(this.interval);
			}

			this.delay = delay || this.INTERVAL; // it can reduce or increase initial timeout
			function sync()
			{
				// set delay to normal value
				_this.delay = _this.INTERVAL;
				_this.Sync();
				_this.interval = setTimeout(sync, _this.delay);
			}
			this.interval = setTimeout(sync, _this.delay);
		},

		StopSync: function()
		{
			if (this.interval)
			{
				this.interval = clearTimeout(this.interval);
			}
		},

		IsSyncOn: function()
		{
			return !!this.interval;
		},

		OnIframeMousedown: function(e, target, bxTag)
		{
		},

		IsFocusedOnTextarea: function()
		{
			return this.editor.currentViewName === "code" || this.editor.currentViewName === "split" && this.GetSplitMode() === "code";
		}
	}

	// global interface
	window.BXEditorTextareaView = BXEditorTextareaView;
	window.BXEditorIframeView = BXEditorIframeView;
	window.BXEditorViewsSynchro = BXEditorViewsSynchro;
})();