import type { VNode } from 'vue';
import type { DropdownButtonItemArgs } from '../dropdown-button/dropdown-button-item';
import type { FormItemWrapperArgs, MarginType } from '../form/form-item-wrapper';
import type {
	WysiwygButtonDef,
	WysiwygCommandContext,
	WysiwygDropdownItem,
	WysiwygModalDescriptor,
	WysiwygModalValues,
	WysiwygOptions,
	WysiwygTranslations,
	WysiwygUploadArgs,
} from './plugins/wysiwyg/types';
import { createApp, h } from 'vue';
import { Prop, toNative, Watch } from 'vue-facing-decorator';
import PowerduckState from '../../app/powerduck-state';
import TsxComponent, { Component } from '../../app/vuetsx';
import EsModuleImportHelper from '../../common/utils/esmodule-import-helper';
import FormItemWrapper from '../form/form-item-wrapper';
import { FileManagerDialog, FileManagerModalFileType } from '../modal/ts/file-manager-dialog';
import svgIcons from './img/wysiwyg-icons.svg';
import {
	WYSIWYG_DEFAULT_COLORS,
	WYSIWYG_DEFAULT_FONT_SIZES,
	WYSIWYG_DEFAULT_LINE_HEIGHTS,
} from './plugins/wysiwyg/colors';
import { getTranslations } from './plugins/wysiwyg/i18n';
import WysiwygPromptModal from './plugins/wysiwyg/wysiwyg-prompt-modal';
import './css/wysiwig.css';

export interface WysiwygEditorArgs extends FormItemWrapperArgs {
	value: string;
	placeholder?: string;
	useFileManager?: boolean;
	itemId?: number;
	useCommonUpload?: boolean;
	uploadPluginArgs?: () => WysiwygUploadArgs | null;
	resizable?: boolean;
	initialHeight?: number;
	onResized?: (height: number) => void;
	changed: (newValue: string) => void;
	htmlViewControlled?: boolean;
	onHtmlViewChange?: (htmlView: boolean) => void;
}

export class WysiwygConfig {
	static uploadArgsFactory: () => WysiwygUploadArgs | null = null;
}

// Toolbar icons live in a single SVG sprite file. Referencing symbols via
// `<use href="https://cdn.example.com/sprite.svg#icon">` is blocked by WebKit
// (Safari) and produces a console warning in Chromium whenever the sprite is
// served cross-origin (e.g. from a CDN). To make `<use href="#icon">` work
// reliably regardless of where the sprite is hosted, we fetch the sprite once
// at runtime and inline it into the current document. After that, all `<use>`
// references resolve against the inlined symbols (same document = no
// cross-origin concerns).
//
// Requires CORS (`Access-Control-Allow-Origin`) on the sprite asset when
// served from a different origin, otherwise the fetch body cannot be read.
// In that failure case `renderIcon()` simply renders an empty `<svg>` (no
// `<use>` child) — we deliberately do not fall back to the absolute URL,
// because that would re-introduce the cross-origin `<use>` warning the whole
// approach is meant to avoid.
let wysiwygSpritePromise: Promise<boolean> | null = null;
let wysiwygSpriteInjected = false;

const ensureWysiwygSpriteInjected = (): Promise<boolean> => {
	// One shared fetch for all wysiwyg instances on the page.
	if (wysiwygSpritePromise != null) {
		return wysiwygSpritePromise;
	}

	// SSR / non-browser guard.
	if (typeof document === 'undefined' || typeof fetch === 'undefined') {
		wysiwygSpritePromise = Promise.resolve(false);
		return wysiwygSpritePromise;
	}

	const svgPath = EsModuleImportHelper.getObj(svgIcons) || '';
	if (!svgPath) {
		wysiwygSpritePromise = Promise.resolve(false);
		return wysiwygSpritePromise;
	}

	wysiwygSpritePromise = fetch(svgPath)
		.then(r => (r.ok ? r.text() : null))
		.then((text) => {
			if (text == null) {
				return false;
			}

			// DOMParser avoids the XSS surface of innerHTML; the parsed SVG is
			// hidden from layout but kept reachable for `<use>` lookups.
			const parsed = new DOMParser().parseFromString(text, 'image/svg+xml');
			const svgEl = parsed.documentElement;
			if (svgEl == null || svgEl.nodeName.toLowerCase() !== 'svg') {
				return false;
			}

			svgEl.setAttribute('aria-hidden', 'true');
			svgEl.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;';
			document.body.appendChild(document.importNode(svgEl, true));
			wysiwygSpriteInjected = true;
			return true;
		})
		.catch((err) => {
			// Most likely cause: CDN is missing CORS headers. Icons will not
			// render, but the editor itself stays functional.
			console.warn('[wysiwyg] sprite inject failed', err);
			return false;
		});

	return wysiwygSpritePromise;
};

// Start fetching immediately at module evaluation so the sprite is usually
// already inlined before the first wysiwyg instance mounts. Without this, the
// initial render of any toolbar icon would emit a `<use>` referencing the
// cross-origin CDN URL — which is exactly what we are trying to avoid.
if (typeof document !== 'undefined') {
	ensureWysiwygSpriteInjected();
}

@Component
class WysiwygEditorComponent extends TsxComponent<WysiwygEditorArgs> implements WysiwygEditorArgs {
	@Prop() label!: string | VNode;
	@Prop() labelButtons!: DropdownButtonItemArgs[];
	@Prop() subtitle!: string;
	@Prop() value!: string;
	@Prop() placeholder!: string;
	@Prop() mandatory!: boolean;
	@Prop() marginType?: MarginType;
	@Prop() maxWidth?: number;
	@Prop() wrap!: boolean;
	@Prop() showClearValueButton!: boolean;
	@Prop() hint: string;
	@Prop() useFileManager?: boolean;
	@Prop() itemId?: number;
	@Prop() useCommonUpload?: boolean;
	@Prop() uploadPluginArgs?: () => WysiwygUploadArgs | null;
	@Prop() appendIcon: string;
	@Prop() prependIcon: string;
	@Prop() appendClicked: () => void;
	@Prop() prependClicked: () => void;
	@Prop() changed: (newValue: string) => void;
	@Prop() initialHeight: number = null;
	@Prop() onResized?: (height: number) => void;
	@Prop({ default: true }) resizable: boolean;
	@Prop() htmlViewControlled?: boolean;
	@Prop() onHtmlViewChange?: (htmlView: boolean) => void;

	openDropdownId: string | null = null;
	htmlView: boolean = false;
	fullscreen: boolean = false;
	htmlViewBuffer: string = '';
	activeImageEl: HTMLImageElement | null = null;
	skipNextValueWatch: boolean = false;
	private savedRange: Range | null = null;
	private resizeObserver: ResizeObserver = null;
	private lastNotifiedHeight: number | null = null;
	private syncTimer: any = null;
	private lastEmittedHtml: string = '';
	private documentClickHandler: ((e: MouseEvent) => void) | null = null;
	private modalContainer: HTMLElement = null;
	private modalApp: ReturnType<typeof createApp> = null;
	private imageResizeHandle: HTMLElement | null = null;
	private imageResizeHandlers: {
		mousemove?: (e: MouseEvent) => void;
		mouseup?: (e: MouseEvent) => void;
	} = {};

	private btnDefs: Record<string, WysiwygButtonDef> = {};
	private toolbarGroups: WysiwygButtonDef[][] = [];
	private translations: WysiwygTranslations = null;
	// Reactive flag: flips to true once the SVG sprite has been inlined into the
	// document. renderIcon() switches from an absolute URL ref to a fragment-only
	// ref (`#wysiwyg-X`) on the next render after this flips.
	spriteReady: boolean = wysiwygSpriteInjected;

	get options(): WysiwygOptions {
		return {
			colors: WYSIWYG_DEFAULT_COLORS,
			fontSizes: WYSIWYG_DEFAULT_FONT_SIZES,
			lineHeights: WYSIWYG_DEFAULT_LINE_HEIGHTS,
			formattingOptions: [
				{ tag: 'p', labelKey: 'p' },
				{ tag: 'h1', labelKey: 'header' },
				{ tag: 'h2', labelKey: 'header' },
				{ tag: 'h3', labelKey: 'header' },
				{ tag: 'h4', labelKey: 'header' },
				{ tag: 'blockquote', labelKey: 'blockquote' },
				{ tag: 'pre', labelKey: 'code' },
			],
			imageWidthModalEdit: true,
			imageResize: { minSize: 16, step: 4 },
		};
	}

	get editorEl(): HTMLElement | null {
		const refs = this.$refs as Record<string, unknown> | undefined;
		return (refs?.editor as HTMLElement) || null;
	}

	get boxEl(): HTMLElement | null {
		const refs = this.$refs as Record<string, unknown> | undefined;
		return (refs?.box as HTMLElement) || null;
	}

	get htmlAreaEl(): HTMLTextAreaElement | null {
		const refs = this.$refs as Record<string, unknown> | undefined;
		return (refs?.htmlArea as HTMLTextAreaElement) || null;
	}

	created() {
		// Adopt the controlled Source-view flag before the first render so the
		// initial DOM already matches (otherwise we'd flicker from rich → source).
		if (this.htmlViewControlled != null) {
			this.htmlView = this.htmlViewControlled;
		}
	}

	mounted() {
		this.translations = getTranslations(PowerduckState.getCurrentLanguage());
		this.buildToolbar();

		if (this.value != null && this.editorEl != null) {
			this.editorEl.innerHTML = this.value;
			this.lastEmittedHtml = this.value;
		}

		// When we mount directly into Source view, seed the textarea buffer
		// from the current value — the textarea is bound to `htmlViewBuffer`
		// rather than to the editor element.
		if (this.htmlView) {
			this.htmlViewBuffer = this.value || '';
		}

		this.attachEditorListeners();
		this.attachDocumentListeners();
		this.setupResizeObserver();
		this.applyInitialHeight();

		// Kick off the one-time sprite injection. The promise is shared across
		// every wysiwyg instance, so the fetch happens at most once per page load.
		if (!this.spriteReady) {
			ensureWysiwygSpriteInjected().then((ok) => {
				if (ok) {
					this.spriteReady = true;
				}
			});
		}
	}

	beforeUnmount() {
		this.detachDocumentListeners();
		this.detachImageResizeListeners();
		if (this.syncTimer != null) {
			clearTimeout(this.syncTimer);
			this.syncTimer = null;
		}

		if (this.resizeObserver != null) {
			this.resizeObserver.disconnect();
			this.resizeObserver = null;
		}

		this.destroyModalContainer();
	}

	@Watch('value')
	onValueChanged(val: string, oldVal: string) {
		if (val === oldVal) {
			return;
		}

		if (this.skipNextValueWatch) {
			this.skipNextValueWatch = false;
			return;
		}

		if (this.editorEl == null) {
			return;
		}

		const currentHtml = this.editorEl.innerHTML;
		if (val !== currentHtml) {
			this.editorEl.innerHTML = val || '';
			this.lastEmittedHtml = this.editorEl.innerHTML;
		}
	}

	private applyInitialHeight() {
		if (this.initialHeight != null && this.boxEl != null) {
			this.boxEl.style.height = `${this.initialHeight}px`;
			this.lastNotifiedHeight = this.initialHeight;
		}
	}

	private setupResizeObserver() {
		if (this.onResized == null || this.resizable === false || this.boxEl == null) {
			return;
		}

		this.resizeObserver = new ResizeObserver((entries) => {
			for (const entry of entries) {
				const height = entry?.contentRect?.height;
				if (height > 0 && (this.lastNotifiedHeight == null || Math.abs(height - this.lastNotifiedHeight) > 2)) {
					this.lastNotifiedHeight = height;
					this.onResized?.(height);
				}
			}
		});
		this.resizeObserver.observe(this.boxEl);
	}

	private handleEditorInput() {
		this.scheduleSync();
	}

	private handleEditorBlur() {
		this.saveCurrentRange();
	}

	private handleEditorKeydown(e: KeyboardEvent) {
		if (e.key === 'Delete' || e.key === 'Backspace') {
			if (this.activeImageEl != null) {
				e.preventDefault();
				this.activeImageEl.remove();
				this.setActiveImage(null);
				this.scheduleSync();
				return;
			}
		}

		if (this.activeImageEl != null && e.key.length === 1) {
			this.setActiveImage(null);
		}

		// Ctrl/Cmd shortcuts
		const mod = e.ctrlKey || e.metaKey;
		if (!mod) {
			return;
		}

		const key = e.key.toLowerCase();
		if (key === 'b') {
			e.preventDefault();
			this.execNative('bold');
		} else if (key === 'i') {
			e.preventDefault();
			this.execNative('italic');
		} else if (key === 'u') {
			e.preventDefault();
			this.execNative('underline');
		} else if (key === 'z' && !e.shiftKey) {
			e.preventDefault();
			this.execNative('undo');
		} else if ((key === 'z' && e.shiftKey) || key === 'y') {
			e.preventDefault();
			this.execNative('redo');
		}
	}

	private handleEditorClick(e: MouseEvent) {
		const target = e.target as HTMLElement;
		if (target?.tagName === 'IMG') {
			e.preventDefault();
			this.setActiveImage(target as HTMLImageElement);
		} else {
			this.setActiveImage(null);
		}
	}

	private handleEditorPaste(e: ClipboardEvent) {
		const html = e.clipboardData?.getData('text/html');
		if (html == null || html.length === 0) {
			return;
		}

		e.preventDefault();
		e.stopPropagation();
		this.handlePaste(html);
	}

	private handleEditorMouseUp() {
		this.saveCurrentRange();
	}

	private handleEditorKeyUp() {
		this.saveCurrentRange();
		this.scheduleSync();
	}

	private handleHtmlAreaInput() {
		this.htmlViewBuffer = this.htmlAreaEl?.value || '';
		// Without this, edits made in Source view are lost the moment the
		// editor unmounts (e.g. the user switches to another language tab),
		// because the parent never receives the updated HTML.
		this.scheduleSync();
	}

	private attachEditorListeners() {
		const ed = this.editorEl;
		if (ed == null) {
			return;
		}

		ed.addEventListener('input', () => this.handleEditorInput());
		ed.addEventListener('blur', () => this.handleEditorBlur());
		ed.addEventListener('keydown', e => this.handleEditorKeydown(e));
		ed.addEventListener('click', e => this.handleEditorClick(e));
		ed.addEventListener('paste', e => this.handleEditorPaste(e));
		ed.addEventListener('mouseup', () => this.handleEditorMouseUp());
		ed.addEventListener('keyup', () => this.handleEditorKeyUp());
	}

	private attachDocumentListeners() {
		this.documentClickHandler = (e: MouseEvent) => {
			if (this.openDropdownId == null) {
				return;
			}

			const target = e.target as HTMLElement;
			if (target.closest('.wysiwyg-dropdown') || target.closest('.wysiwyg-btn[data-dropdown-id]')) {
				return;
			}

			this.openDropdownId = null;
		};
		document.addEventListener(
			'mousedown',
			this.documentClickHandler,
			true,
		);
	}

	private detachDocumentListeners() {
		if (this.documentClickHandler != null) {
			document.removeEventListener(
				'mousedown',
				this.documentClickHandler,
				true,
			);
			this.documentClickHandler = null;
		}
	}

	private scheduleSync() {
		if (this.syncTimer != null) {
			clearTimeout(this.syncTimer);
		}

		this.syncTimer = setTimeout(() => {
			this.syncTimer = null;
			this.notifyChange();
		}, 50);
	}

	private notifyChange() {
		this.populateValidationDeclaration();
		const html = this.htmlView ? this.htmlViewBuffer : this.editorEl?.innerHTML || '';
		if (html === this.lastEmittedHtml) {
			return;
		}

		this.lastEmittedHtml = html;
		if (this.changed != null && this.value !== html) {
			this.skipNextValueWatch = true;
			this.changed(html);
		}
	}

	private saveCurrentRange() {
		const sel = window.getSelection();
		if (sel == null || sel.rangeCount === 0) {
			return;
		}

		const r = sel.getRangeAt(0);
		const ed = this.editorEl;
		if (ed != null && ed.contains(r.commonAncestorContainer)) {
			this.savedRange = r.cloneRange();
		}
	}

	private restoreRange() {
		if (this.savedRange == null || this.editorEl == null) {
			return;
		}

		const sel = window.getSelection();
		if (sel == null) {
			return;
		}

		sel.removeAllRanges();
		sel.addRange(this.savedRange);
	}

	private execNative(command: string, value?: string): boolean {
		try {
			const ok = document.execCommand(
				command,
				false,
				value,
			);
			this.scheduleSync();
			return ok;
		} catch {
			return false;
		}
	}

	private buildCommandContext(): WysiwygCommandContext {
		return {
			editor: this.editorEl,
			getHtml: () => this.editorEl?.innerHTML || '',
			setHtml: (h2: string) => {
				if (this.editorEl != null) {
					this.editorEl.innerHTML = h2;
					this.scheduleSync();
				}
			},
			saveRange: () => this.saveCurrentRange(),
			restoreRange: () => this.restoreRange(),
			getRange: () => this.savedRange,
			emit: () => { /* placeholder for future events */ },
			focus: () => {
				this.editorEl?.focus();
				this.restoreRange();
			},
			exec: (command: string, value?: string) => this.execNative(command, value),
			openModal: (descriptor: WysiwygModalDescriptor) => this.openModal(descriptor),
			insertImage: (
				url: string,
				alt?: string,
				width?: string,
			) => this.insertImage(
				url,
				alt,
				width,
			),
			insertHtmlAtCaret: (html2: string | Node) => this.insertHtmlAtCaret(html2),
			wrapSelectionInStyle: (style: Record<string, string>) => this.wrapSelectionInStyle(style),
			getActiveImage: () => this.activeImageEl,
			setActiveImage: (img: HTMLImageElement | null) => this.setActiveImage(img),
			getTranslations: () => this.translations,
			getUploadArgs: () => this.resolveUploadArgs(),
			getFileManagerEnabled: () => this.useFileManager === true,
			openFileManager: kind => this.openFileManagerInternal(kind),
			toggleHtmlView: () => this.toggleHtmlView(),
			toggleFullscreen: () => this.toggleFullscreen(),
			isHtmlView: () => this.htmlView,
			isFullscreen: () => this.fullscreen,
			getItemId: () => this.itemId ?? null,
			getOptions: () => this.options,
			notifyChange: () => this.notifyChange(),
		};
	}

	private setActiveImage(img: HTMLImageElement | null) {
		if (this.activeImageEl != null && this.activeImageEl !== img) {
			this.activeImageEl.classList.remove('wysiwyg-img-selected');
		}

		this.activeImageEl = img;
		if (img != null) {
			img.classList.add('wysiwyg-img-selected');
			this.showImageResizeHandle(img);
		} else {
			this.hideImageResizeHandle();
		}
	}

	private showImageResizeHandle(img: HTMLImageElement) {
		this.hideImageResizeHandle();
		const box = this.boxEl;
		if (box == null) {
			return;
		}

		const handle = document.createElement('div');
		handle.className = 'wysiwyg-img-resize-handle';
		this.imageResizeHandle = handle;
		box.appendChild(handle);
		this.positionImageResizeHandle();
		handle.addEventListener('mousedown', (e: MouseEvent) => this.beginImageResize(e, img));
	}

	private hideImageResizeHandle() {
		if (this.imageResizeHandle != null) {
			this.imageResizeHandle.remove();
			this.imageResizeHandle = null;
		}

		this.detachImageResizeListeners();
	}

	private positionImageResizeHandle() {
		const handle = this.imageResizeHandle;
		const img = this.activeImageEl;
		const box = this.boxEl;
		if (handle == null || img == null || box == null) {
			return;
		}

		const boxRect = box.getBoundingClientRect();
		const imgRect = img.getBoundingClientRect();
		handle.style.left = `${imgRect.right - boxRect.left - 8}px`;
		handle.style.top = `${imgRect.bottom - boxRect.top - 8 + box.scrollTop}px`;
	}

	private beginImageResize(e: MouseEvent, img: HTMLImageElement) {
		e.preventDefault();
		e.stopPropagation();
		const startX = e.clientX;
		const startY = e.clientY;
		const startW = img.offsetWidth;
		const startH = img.offsetHeight;
		const ratio = startW > 0 ? startH / startW : 1;
		const opts = this.options.imageResize;

		const onMove = (ev: MouseEvent) => {
			let newW = startW + (ev.clientX - startX);
			let newH = startH + (ev.clientY - startY);
			if (newH < opts.minSize) {
				newH = opts.minSize;
			}

			if (newW < opts.minSize) {
				newW = opts.minSize;
			}

			newH -= newH % opts.step;
			newW = Math.round(newH / ratio);
			img.style.height = `${newH}px`;
			img.style.width = `${newW}px`;
			img.setAttribute('height', String(newH));
			img.setAttribute('width', String(newW));
			this.positionImageResizeHandle();
		};

		const onUp = () => {
			document.removeEventListener('mousemove', onMove);
			document.removeEventListener('mouseup', onUp);
			this.imageResizeHandlers = {};
			this.scheduleSync();
		};

		this.imageResizeHandlers = { mousemove: onMove, mouseup: onUp };
		document.addEventListener('mousemove', onMove);
		document.addEventListener('mouseup', onUp);
	}

	private detachImageResizeListeners() {
		if (this.imageResizeHandlers.mousemove) {
			document.removeEventListener('mousemove', this.imageResizeHandlers.mousemove);
		}

		if (this.imageResizeHandlers.mouseup) {
			document.removeEventListener('mouseup', this.imageResizeHandlers.mouseup);
		}

		this.imageResizeHandlers = {};
	}

	private insertImage(
		url: string,
		alt?: string,
		width?: string,
	) {
		this.editorEl?.focus();
		this.restoreRange();
		const img = document.createElement('img');
		img.src = url;
		if (alt) {
			img.alt = alt;
		}

		if (width && /^\d+$/.test(width)) {
			img.setAttribute('width', width);
		}

		this.insertHtmlAtCaret(img);
		this.scheduleSync();
	}

	private insertHtmlAtCaret(html: string | Node) {
		this.editorEl?.focus();
		this.restoreRange();
		const sel = window.getSelection();
		if (sel == null) {
			return;
		}

		let range: Range;
		if (sel.rangeCount > 0) {
			range = sel.getRangeAt(0);
		} else {
			range = document.createRange();
			range.selectNodeContents(this.editorEl);
			range.collapse(false);
		}

		range.deleteContents();
		let node: Node;
		if (typeof html === 'string') {
			const tpl = document.createElement('template');
			tpl.innerHTML = html;
			node = tpl.content.cloneNode(true);
		} else {
			node = html;
		}

		range.insertNode(node);
		range.collapse(false);
		sel.removeAllRanges();
		sel.addRange(range);
		this.saveCurrentRange();
		this.scheduleSync();
	}

	private wrapSelectionInStyle(style: Record<string, string>) {
		this.editorEl?.focus();
		this.restoreRange();
		const sel = window.getSelection();
		if (sel == null || sel.rangeCount === 0) {
			return;
		}

		const range = sel.getRangeAt(0);
		if (range.collapsed) {
			// Nothing selected — create an empty span to type into
			const span = document.createElement('span');
			Object.assign(span.style, style);
			span.appendChild(document.createTextNode('​'));
			range.insertNode(span);
			const newRange = document.createRange();
			newRange.setStart(span.firstChild!, 1);
			newRange.collapse(true);
			sel.removeAllRanges();
			sel.addRange(newRange);
			this.saveCurrentRange();
			this.scheduleSync();
			return;
		}

		const contents = range.extractContents();
		const wrapper = document.createElement('span');
		Object.assign(wrapper.style, style);
		wrapper.appendChild(contents);
		range.insertNode(wrapper);
		const newRange = document.createRange();
		newRange.selectNodeContents(wrapper);
		sel.removeAllRanges();
		sel.addRange(newRange);
		this.saveCurrentRange();
		this.scheduleSync();
	}

	private resolveUploadArgs(): WysiwygUploadArgs | null {
		if (this.uploadPluginArgs != null) {
			const v = this.uploadPluginArgs();
			if (v != null) {
				return v;
			}
		}

		if (WysiwygConfig.uploadArgsFactory != null) {
			return WysiwygConfig.uploadArgsFactory();
		}

		return null;
	}

	private usesCommonUploadPlugin(): boolean {
		if (this.uploadPluginArgs != null) {
			return true;
		}

		if (this.useCommonUpload === true) {
			return true;
		}

		if (this.useFileManager !== true && WysiwygConfig.uploadArgsFactory != null) {
			return true;
		}

		return false;
	}

	private openFileManagerInternal(kind: 'image' | 'file'): Promise<{ url: string } | null> {
		return new Promise((resolve) => {
			FileManagerDialog.show({
				fileType: kind === 'image' ? FileManagerModalFileType.Image : FileManagerModalFileType.All,
				itemId: this.itemId ?? undefined,
				callback: (data) => {
					resolve(data ? { url: data.url } : null);
				},
			});
		});
	}

	private ensureModalContainer(): HTMLElement {
		if (this.modalContainer == null) {
			this.modalContainer = document.createElement('div');
			this.modalContainer.className = 'wysiwyg-modal-host';
			document.body.appendChild(this.modalContainer);
		}

		return this.modalContainer;
	}

	private safeUnmountModalApp() {
		if (this.modalApp != null) {
			try {
				this.modalApp.unmount();
			} catch {
				/* noop */
			}

			this.modalApp = null;
		}
	}

	private destroyModalContainer() {
		this.safeUnmountModalApp();

		if (this.modalContainer != null) {
			this.modalContainer.remove();
			this.modalContainer = null;
		}
	}

	private openModal(descriptor: WysiwygModalDescriptor): Promise<WysiwygModalValues | null> {
		return new Promise((resolve) => {
			const host = this.ensureModalContainer();
			host.innerHTML = '';
			this.safeUnmountModalApp();

			let settled = false;
			this.modalApp = createApp({
				render: () => h(WysiwygPromptModal, {
					descriptor,
					onSubmit: (values: WysiwygModalValues) => {
						if (settled) {
							return;
						}

						settled = true;
						setTimeout(() => {
							this.safeUnmountModalApp();
							resolve(values);
						}, 100);
					},
					onCancel: () => {
						if (settled) {
							return;
						}

						settled = true;
						setTimeout(() => {
							this.safeUnmountModalApp();
							resolve(null);
						}, 0);
					},
				}),
			});
			this.modalApp.mount(host);
		});
	}

	private toggleHtmlView() {
		if (!this.htmlView) {
			this.htmlViewBuffer = this.editorEl?.innerHTML || '';
			this.htmlView = true;
		} else {
			if (this.editorEl != null) {
				this.editorEl.innerHTML = this.htmlViewBuffer;
			}

			this.htmlView = false;
			this.scheduleSync();
		}

		// Notify the parent so it can mirror this state into sibling editors
		// (e.g. other language tabs that mount after this toggle).
		this.onHtmlViewChange?.(this.htmlView);
	}

	private toggleFullscreen() {
		this.fullscreen = !this.fullscreen;
		document.body.classList.toggle('wysiwyg-fullscreen-on', this.fullscreen);
	}

	private handlePaste(rawHtml: string) {
		const cleaned = this.cleanPastedHtml(rawHtml);
		const tpl = document.createElement('template');
		tpl.innerHTML = cleaned;
		const frag = tpl.content.cloneNode(true);
		this.insertHtmlAtCaret(frag);
	}

	private cleanPastedHtml(html: string): string {
		let inner = html;
		const bodyIdx = inner.indexOf('<body');
		if (bodyIdx > -1) {
			const endIdx = inner.indexOf('</body>');
			if (endIdx > bodyIdx) {
				const startTagEnd = inner.indexOf('>', bodyIdx);
				inner = inner.substring(startTagEnd + 1, endIdx).trim();
			}
		}

		const wrap = document.createElement('div');
		wrap.innerHTML = inner;
		const strip = (el: HTMLElement) => {
			Array.from(el.children).forEach(c => strip(c as HTMLElement));
			el.removeAttribute('style');
			el.removeAttribute('class');
		};
		strip(wrap);
		const out = wrap.innerHTML;
		if (wrap.children.length > 1) {
			return `<div>${out}</div>`;
		}

		return out;
	}

	private buildToolbar() {
		this.btnDefs = {};
		this.registerCoreButtons();
		const groups: WysiwygButtonDef[][] = [];

		const def = (id: string) => {
			const b = this.btnDefs[id];
			if (b == null) {
				console.warn('[wysiwyg] missing button def:', id);
				return null;
			}

			return b;
		};
		const pushGroup = (ids: string[]) => {
			const arr = ids.map(def).filter(Boolean) as WysiwygButtonDef[];
			if (arr.length > 0) {
				groups.push(arr);
			}
		};

		pushGroup(['viewHTML']);
		pushGroup([
			'undo',
			'redo',
		]);
		pushGroup(['formatting']);
		pushGroup([
			'strong',
			'em',
		]);
		pushGroup([
			'fontsize',
			'foreColor',
			'backColor',
		]);
		pushGroup(['lineheight']);
		pushGroup(['link']);
		pushGroup(['image']);
		pushGroup(['noembed']);
		pushGroup([
			'justifyLeft',
			'justifyCenter',
			'justifyRight',
			'justifyFull',
		]);
		pushGroup([
			'unorderedList',
			'orderedList',
		]);
		pushGroup(['horizontalRule']);
		pushGroup(['removeformat']);
		pushGroup(['fullscreen']);

		const uploadGroup: string[] = [];
		if (this.useFileManager === true) {
			uploadGroup.push('uploadFileManager');
		}

		if (this.usesCommonUploadPlugin()) {
			uploadGroup.push('uploadCustom');
		}

		if (uploadGroup.length > 0) {
			pushGroup(uploadGroup);
		}

		this.toolbarGroups = groups;
	}

	private registerCoreButtons() {
		const t = () => this.translations;

		const addBtn = (b: WysiwygButtonDef) => {
			this.btnDefs[b.id] = b;
		};

		const focusExec = (cmd: string) => (ctx: WysiwygCommandContext) => {
			ctx.focus();
			ctx.exec(cmd);
		};

		const cmdCtx = () => this.buildCommandContext();

		addBtn({
			id: 'viewHTML',
			icon: 'view-html',
			titleKey: 'viewHTML',
			exec: ctx => ctx.toggleHtmlView(),
			isActive: ctx => ctx.isHtmlView(),
			disableWhileHtml: false,
		});
		addBtn({
			id: 'undo',
			icon: 'undo',
			titleKey: 'historyUndo',
			exec: focusExec('undo'),
		});
		addBtn({
			id: 'redo',
			icon: 'redo',
			titleKey: 'historyRedo',
			exec: focusExec('redo'),
		});
		addBtn({
			id: 'formatting',
			icon: 'p',
			titleKey: 'formatting',
			isDropdown: true,
			dropdownItems: this.options.formattingOptions.map((opt, idx): WysiwygDropdownItem => ({
				id: `formatting-${opt.tag}-${idx}`,
				label: `${t()[opt.labelKey]} (${opt.tag.toUpperCase()})`,
				exec: (ctx) => {
					ctx.focus();
					ctx.exec('formatBlock', `<${opt.tag.toUpperCase()}>`);
				},
			})),
		});
		addBtn({
			id: 'strong',
			icon: 'strong',
			titleKey: 'strong',
			exec: focusExec('bold'),
		});
		addBtn({
			id: 'em',
			icon: 'em',
			titleKey: 'em',
			exec: focusExec('italic'),
		});
		addBtn({
			id: 'fontsize',
			icon: 'fontsize',
			titleKey: 'fontsize',
			isDropdown: true,
			dropdownItems: this.buildFontSizeDropdown(),
		});
		addBtn({
			id: 'foreColor',
			icon: 'fore-color',
			titleKey: 'foreColor',
			isDropdown: true,
			dropdownItems: this.buildColorDropdown('foreColor'),
			cssClass: 'wysiwyg-color-btn',
		});
		addBtn({
			id: 'backColor',
			icon: 'back-color',
			titleKey: 'backColor',
			isDropdown: true,
			dropdownItems: this.buildColorDropdown('backColor'),
			cssClass: 'wysiwyg-color-btn',
		});
		addBtn({
			id: 'lineheight',
			icon: 'lineheight',
			titleKey: 'lineheight',
			isDropdown: true,
			dropdownItems: this.buildLineHeightDropdown(),
		});
		addBtn({
			id: 'link',
			icon: 'link',
			titleKey: 'createLink',
			exec: ctx => this.handleLinkButton(ctx),
		});
		addBtn({
			id: 'image',
			icon: 'insert-image',
			titleKey: 'image',
			isDropdown: true,
			dropdownItems: this.buildImageDropdownItems(),
		});
		addBtn({
			id: 'noembed',
			icon: 'noembed',
			titleKey: 'noembed',
			exec: ctx => this.handleEmbedButton(ctx),
		});
		addBtn({
			id: 'justifyLeft',
			icon: 'justify-left',
			titleKey: 'justifyLeft',
			exec: focusExec('justifyLeft'),
		});
		addBtn({
			id: 'justifyCenter',
			icon: 'justify-center',
			titleKey: 'justifyCenter',
			exec: focusExec('justifyCenter'),
		});
		addBtn({
			id: 'justifyRight',
			icon: 'justify-right',
			titleKey: 'justifyRight',
			exec: focusExec('justifyRight'),
		});
		addBtn({
			id: 'justifyFull',
			icon: 'justify-full',
			titleKey: 'justifyFull',
			exec: focusExec('justifyFull'),
		});
		addBtn({
			id: 'unorderedList',
			icon: 'unordered-list',
			titleKey: 'unorderedList',
			exec: focusExec('insertUnorderedList'),
		});
		addBtn({
			id: 'orderedList',
			icon: 'ordered-list',
			titleKey: 'orderedList',
			exec: focusExec('insertOrderedList'),
		});
		addBtn({
			id: 'horizontalRule',
			icon: 'horizontal-rule',
			titleKey: 'horizontalRule',
			exec: focusExec('insertHorizontalRule'),
		});
		addBtn({
			id: 'removeformat',
			icon: 'removeformat',
			titleKey: 'removeformat',
			exec: focusExec('removeFormat'),
		});
		addBtn({
			id: 'fullscreen',
			icon: 'fullscreen',
			titleKey: 'fullscreen',
			exec: ctx => ctx.toggleFullscreen(),
			isActive: ctx => ctx.isFullscreen(),
		});
		addBtn({
			id: 'uploadFileManager',
			icon: 'upload',
			titleKey: 'uploadImage',
			exec: ctx => this.handleFileManagerImageUpload(ctx),
		});
		addBtn({
			id: 'uploadCustom',
			icon: 'upload',
			titleKey: 'upload',
			exec: ctx => this.handleCommonUpload(ctx),
		});
		// silence linter-style unused warnings
		void cmdCtx;
		void t;
	}

	private buildFontSizeDropdown(): WysiwygDropdownItem[] {
		const arr: WysiwygDropdownItem[] = [];
		this.options.fontSizes.forEach((size) => {
			arr.push({
				id: `fontsize-${size}`,
				label: size,
				exec: (ctx) => {
					ctx.focus();
					ctx.wrapSelectionInStyle({ fontSize: size });
				},
			});
		});
		arr.push({
			id: 'fontsize-custom',
			label: this.translations.customSize,
			exec: async (ctx) => {
				ctx.saveRange();
				const vals = await ctx.openModal({
					title: this.translations.customSize,
					fields: {
						size: {
							label: this.translations.fontsize,
							value: '',
							type: 'text',
							placeholder: 'e.g. 24px',
							required: true,
						},
					},
					submitLabel: this.translations.submit,
					cancelLabel: this.translations.reset,
				});
				if (vals?.size) {
					ctx.focus();
					ctx.wrapSelectionInStyle({ fontSize: String(vals.size) });
				}
			},
		});
		return arr;
	}

	private buildLineHeightDropdown(): WysiwygDropdownItem[] {
		const arr: WysiwygDropdownItem[] = [];
		this.options.lineHeights.forEach((lh) => {
			arr.push({
				id: `lineheight-${lh}`,
				label: lh,
				exec: (ctx) => {
					ctx.focus();
					ctx.wrapSelectionInStyle({ lineHeight: lh });
				},
			});
		});
		arr.push({
			id: 'lineheight-custom',
			label: this.translations.customSize,
			exec: async (ctx) => {
				ctx.saveRange();
				const vals = await ctx.openModal({
					title: this.translations.customSize,
					fields: {
						lineheight: { label: this.translations.lineheight, value: '', type: 'text', required: true },
					},
					submitLabel: this.translations.submit,
					cancelLabel: this.translations.reset,
				});
				if (vals?.lineheight) {
					ctx.focus();
					ctx.wrapSelectionInStyle({ lineHeight: String(vals.lineheight) });
				}
			},
		});
		return arr;
	}

	private buildColorDropdown(kind: 'foreColor' | 'backColor'): WysiwygDropdownItem[] {
		const arr: WysiwygDropdownItem[] = [];
		// Remove color
		arr.push({
			id: `${kind}-remove`,
			label: this.translations[`${kind}Remove`] || 'Remove',
			style: 'background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAG0lEQVQIW2NkQAAfEJMRmwBYhoGBYQtMBYoAADziAp0jtJTgAAAAAElFTkSuQmCC);',
			exec: (ctx) => {
				ctx.focus();
				ctx.exec('removeFormat', kind);
			},
		});
		// Custom color (free)
		arr.push({
			id: `${kind}-free`,
			label: `# (${this.translations.customColor})`,
			exec: async (ctx) => {
				ctx.saveRange();
				const vals = await ctx.openModal({
					title: this.translations[kind],
					fields: {
						color: { label: this.translations[kind], value: '#FFFFFF', type: 'color', forceCss: true },
						colorHex: { label: `${this.translations[kind]} (HEX)`, value: '#FFFFFF', type: 'text', forceCss: true },
					},
					submitLabel: this.translations.submit,
					cancelLabel: this.translations.reset,
				});
				if (vals?.color) {
					ctx.focus();
					this.applyColor(kind, String(vals.color));
				}
			},
		});
		this.options.colors.forEach((c) => {
			arr.push({
				id: `${kind}-${c}`,
				label: `#${c}`,
				style: `background-color: #${c};`,
				exec: (ctx) => {
					ctx.focus();
					this.applyColor(kind, `#${c}`);
				},
			});
		});
		return arr;
	}

	private applyColor(kind: 'foreColor' | 'backColor', color: string) {
		// Use span wrapping for cross-browser determinism and reliability under synthetic events.
		if (kind === 'foreColor') {
			this.wrapSelectionInStyle({ color });
		} else {
			this.wrapSelectionInStyle({ backgroundColor: color });
		}
	}

	private buildImageDropdownItems(): WysiwygDropdownItem[] {
		const arr: WysiwygDropdownItem[] = [];
		arr.push({
			id: 'image-insert-url',
			label: this.translations.insertImage,
			exec: async (ctx) => {
				ctx.saveRange();
				const opts = this.options;
				const fields: WysiwygModalDescriptor['fields'] = {
					url: { label: this.translations.url, value: '', type: 'url', required: true, placeholder: 'https://...' },
					alt: { label: this.translations.description, value: '', type: 'text' },
				};
				if (opts.imageWidthModalEdit) {
					fields.width = { label: this.translations.width, value: '', type: 'text' };
				}

				const vals = await ctx.openModal({
					title: this.translations.insertImage,
					fields,
					submitLabel: this.translations.submit,
					cancelLabel: this.translations.reset,
				});
				if (vals?.url) {
					ctx.focus();
					ctx.insertImage(
						String(vals.url),
						String(vals.alt || ''),
						vals.width ? String(vals.width) : undefined,
					);
				}
			},
		});
		if (this.useFileManager === true) {
			arr.push({
				id: 'image-upload-filemanager',
				label: this.translations.uploadImage,
				exec: ctx => this.handleFileManagerImageUpload(ctx),
			});
		}

		if (this.usesCommonUploadPlugin()) {
			arr.push({
				id: 'image-upload-custom',
				label: this.translations.upload,
				exec: ctx => this.handleCommonUpload(ctx),
			});
		}

		return arr;
	}

	private async handleLinkButton(ctx: WysiwygCommandContext) {
		ctx.saveRange();
		const range = ctx.getRange();
		const selText = range != null ? range.toString() : '';

		// Find ancestor link if any
		let existingHref = '';
		let existingTarget = '';
		if (range != null && this.editorEl != null) {
			let node: Node | null = range.startContainer;
			while (node && node !== this.editorEl) {
				if ((node as HTMLElement).tagName === 'A') {
					const a = node as HTMLAnchorElement;
					existingHref = a.getAttribute('href') || '';
					existingTarget = a.getAttribute('target') || '';
					break;
				}

				node = node.parentNode;
			}
		}

		const vals = await ctx.openModal({
			title: this.translations.createLink,
			fields: {
				url: { label: this.translations.url, value: existingHref, type: 'url', required: true, placeholder: 'https://...' },
				text: { label: this.translations.text, value: selText, type: 'text' },
				title: { label: this.translations.title, value: '', type: 'text' },
				target: {
					label: 'target',
					value: existingTarget,
					type: 'select',
					options: [
						{ value: '', label: 'self' },
						{ value: '_blank', label: '_blank' },
					],
				},
			},
			submitLabel: this.translations.submit,
			cancelLabel: this.translations.reset,
		});
		if (vals?.url) {
			ctx.focus();
			const url = String(vals.url);
			const text = String(vals.text || '') || url;
			const title = String(vals.title || '');
			const target = String(vals.target || '');
			const a = document.createElement('a');
			a.href = url;
			a.textContent = text;
			if (title) {
				a.title = title;
			}

			if (target) {
				a.target = target;
			}

			ctx.insertHtmlAtCaret(a);
		}
	}

	private async handleEmbedButton(ctx: WysiwygCommandContext) {
		ctx.saveRange();
		const vals = await ctx.openModal({
			title: this.translations.noembed,
			fields: {
				url: { label: this.translations.url, value: '', type: 'url', required: true, placeholder: 'https://...' },
			},
			submitLabel: this.translations.submit,
			cancelLabel: this.translations.reset,
		});
		if (!vals?.url) {
			return;
		}

		try {
			const url = String(vals.url);
			const resp = await fetch(`https://noembed.com/embed?url=${encodeURIComponent(url)}`);
			const json = await resp.json();
			if (json?.html) {
				ctx.focus();
				ctx.insertHtmlAtCaret(String(json.html));
			} else if (json?.error) {
				console.warn('[wysiwyg] noembed error', json.error);
			}
		} catch (err) {
			console.warn('[wysiwyg] embed failed', err);
		}
	}

	private async handleFileManagerImageUpload(ctx: WysiwygCommandContext) {
		ctx.saveRange();
		const result = await ctx.openFileManager('image');
		if (result?.url) {
			ctx.focus();
			ctx.insertImage(result.url);
		}
	}

	private async handleCommonUpload(ctx: WysiwygCommandContext) {
		ctx.saveRange();
		const args = ctx.getUploadArgs();
		if (args == null) {
			return;
		}

		const desc: WysiwygModalDescriptor = {
			title: this.translations.upload,
			progressEnabled: true,
			fields: {
				file: { label: this.translations.file, value: '', type: 'file', accept: 'image/*', required: true },
				alt: { label: this.translations.description, value: '', type: 'text' },
				...(this.options.imageWidthModalEdit ? { width: { label: this.translations.width, value: '', type: 'text' } } : {}),
			},
			submitLabel: this.translations.upload,
			cancelLabel: this.translations.reset,
			beforeSubmit: async (vals) => {
				const file = vals.file as File | null;
				if (file == null) {
					return false;
				}

				try {
					await this.performUpload(
						args,
						file,
						vals,
						ctx,
					);
				} catch (err) {
					console.warn('[wysiwyg] upload failed', err);
					if (args.error != null) {
						args.error(err);
					}

					return false;
				}
				return true;
			},
		};
		await ctx.openModal(desc);
	}

	private async performUpload(
		args: WysiwygUploadArgs,
		file: File,
		values: WysiwygModalValues,
		ctx: WysiwygCommandContext,
	) {
		const form = new FormData();
		(args.data || []).forEach(d => form.append(d.name, d.value));
		form.append(args.fileFieldName || 'fileToUpload', file);

		const url = args.serverPath;
		const xhr = new XMLHttpRequest();
		const promise = new Promise<any>((resolve, reject) => {
			xhr.open(
				'POST',
				url,
				true,
			);
			if (args.headers) {
				Object.keys(args.headers).forEach(h2 => xhr.setRequestHeader(h2, args.headers[h2]));
			}

			if (args.xhrFields?.withCredentials) {
				xhr.withCredentials = true;
			}

			xhr.onload = () => {
				try {
					const data = JSON.parse(xhr.responseText);
					resolve(data);
				} catch {
					resolve(xhr.responseText);
				}
			};
			xhr.onerror = () => reject(new Error('upload error'));
			xhr.send(form);
		});

		const data = await promise;
		const statusKey = args.statusPropertyName || 'success';
		const urlKey = args.urlPropertyName || 'file';

		if (args.success != null) {
			args.success(data, {
				closeModal: () => { /* handled by descriptor return */ },
				insertImage: (u: string) => ctx.insertImage(
					u,
					String(values.alt || ''),
					values.width ? String(values.width) : undefined,
				),
			});
			return;
		}

		const ok = this.getDeep(data, statusKey.split('.'));
		if (ok) {
			const uploadedUrl = this.getDeep(data, urlKey.split('.'));
			ctx.focus();
			ctx.insertImage(
				String(uploadedUrl),
				String(values.alt || ''),
				values.width ? String(values.width) : undefined,
			);
		}
	}

	private getDeep(obj: any, parts: string[]): any {
		let cur = obj;
		for (const p of parts) {
			if (cur == null) {
				return null;
			}

			cur = cur[p];
		}
		return cur;
	}

	private renderIcon(iconId: string): VNode {
		// Always reference the inlined sprite via fragment-only href to avoid the
		// cross-origin `<use>` warning entirely. While the sprite is still being
		// fetched the `<use>` is omitted (briefly empty icon), then it appears
		// once `spriteReady` flips. We deliberately do NOT fall back to the
		// absolute CDN URL — that path would re-introduce the warning on the
		// initial render.
		return (
			<svg viewBox="0 0 24 24">
				{this.spriteReady && <use xlinkHref={`#wysiwyg-${iconId}`}></use>}
			</svg>
		);
	}

	private renderButton(btn: WysiwygButtonDef): VNode {
		const ctx = this.buildCommandContext();
		const isActive = btn.isActive?.(ctx) ?? false;
		const title = btn.titleKey ? this.translations[btn.titleKey] : btn.titleText || btn.id;
		return (
			<button
				type="button"
				class={`wysiwyg-btn ${btn.cssClass || ''} ${isActive ? 'wysiwyg-btn-active' : ''} ${btn.isDropdown ? 'wysiwyg-has-dropdown' : ''}`}
				title={title}
				data-btn-id={btn.id}
				data-dropdown-id={btn.isDropdown ? btn.id : undefined}
				onMousedown={(e: MouseEvent) => {
					e.preventDefault();
					this.handleButtonClick(btn);
				}}
			>
				{btn.icon && this.renderIcon(btn.icon)}
				{btn.text && <span>{btn.text}</span>}
			</button>
		);
	}

	private handleButtonClick(btn: WysiwygButtonDef) {
		if (btn.isDropdown) {
			this.openDropdownId = this.openDropdownId === btn.id ? null : btn.id;
			return;
		}

		this.openDropdownId = null;
		this.saveCurrentRange();
		const ctx = this.buildCommandContext();
		try {
			Promise.resolve(btn.exec?.(ctx)).catch((e) => {
				console.error(
					'[wysiwyg] command failed',
					btn.id,
					e,
				);
			});
		} catch (e) {
			console.error(
				'[wysiwyg] command failed',
				btn.id,
				e,
			);
		}
	}

	private renderDropdown(btn: WysiwygButtonDef): VNode {
		const items = btn.dropdownItems || [];
		return (
			<div class={`wysiwyg-dropdown wysiwyg-dropdown-${btn.id}`} data-dropdown-for={btn.id}>
				{items.map(item => (
					<button
						type="button"
						class="wysiwyg-dropdown-item"
						style={item.style}
						onMousedown={(e: MouseEvent) => {
							e.preventDefault();
							this.handleDropdownItem(item);
						}}
					>
						{item.label}
					</button>
				))}
			</div>
		);
	}

	private handleDropdownItem(item: WysiwygDropdownItem) {
		this.openDropdownId = null;
		this.saveCurrentRange();
		const ctx = this.buildCommandContext();
		try {
			Promise.resolve(item.exec(ctx)).catch((e) => {
				console.error(
					'[wysiwyg] dropdown command failed',
					item.id,
					e,
				);
			});
		} catch (e) {
			console.error(
				'[wysiwyg] dropdown command failed',
				item.id,
				e,
			);
		}
	}

	private renderToolbar(): VNode {
		const groups = this.toolbarGroups;
		const openId = this.openDropdownId;
		return (
			<div class="wysiwyg-toolbar">
				{groups.map(group => (
					<div class="wysiwyg-btn-group">
						{group.map(btn => (
							<div class="wysiwyg-btn-wrap">
								{this.renderButton(btn)}
								{btn.isDropdown && openId === btn.id && this.renderDropdown(btn)}
							</div>
						))}
					</div>
				))}
			</div>
		);
	}

	render() {
		const sizeMode = this.htmlView ? 'wysiwyg-html-view' : '';
		const resizeCls = this.resizable !== false ? 'vertical-resize' : '';
		const fullscreenCls = this.fullscreen ? 'wysiwyg-fullscreen' : '';
		return (
			<FormItemWrapper
				label={this.label}
				mandatory={this.mandatory}
				wrap={this.wrap}
				appendIcon={this.appendIcon}
				prependIcon={this.prependIcon}
				hint={this.hint}
				marginType={this.marginType}
				maxWidth={this.maxWidth}
				appendClicked={this.appendClicked}
				prependClicked={this.prependClicked}
				validationState={this.validationState}
				labelButtons={this.labelButtons}
				subtitle={this.subtitle}
				showClearValueButton={this.showClearValueButton}
			>
				<div class={`wysiwyg-root ${fullscreenCls}`} data-rc={this.value != null ? 'hasval' : 'empty'}>
					<div class={`wysiwyg-box ${sizeMode} ${resizeCls}`} ref="box">
						{this.renderToolbar()}
						<div
							class="wysiwyg-editor"
							ref="editor"
							contenteditable={!this.htmlView}
							spellcheck="true"
							data-placeholder={this.placeholder}
						>
						</div>
						{this.htmlView && (
							<textarea
								ref="htmlArea"
								class="wysiwyg-html-area"
								value={this.htmlViewBuffer}
								onInput={() => this.handleHtmlAreaInput()}
							>
							</textarea>
						)}
					</div>
				</div>
			</FormItemWrapper>
		);
	}
}

const WysiwygEditor = toNative(WysiwygEditorComponent);
export default WysiwygEditor;
