import { __decorate } from "tslib";
import { observable, attr, DOM } from '@ni/fast-element';
import { applyMixins, ARIAGlobalStatesAndProperties, DesignSystem } from '@ni/fast-foundation';
import { keyEnter, keySpace } from '@ni/fast-web-utilities';
import { findParentNode, isList, Editor } from '@tiptap/core';
import HardBreak from '@tiptap/extension-hard-break';
import { template } from './template';
import { styles } from './styles';
import { TipTapNodeName, mentionPluginPrefix } from './types';
import { mixinErrorPattern } from '../../patterns/error/types';
import { RichTextMarkdownParser } from '../models/markdown-parser';
import { RichTextMarkdownSerializer } from '../models/markdown-serializer';
import { RichText } from '../base';
import { createTiptapEditor } from './models/create-tiptap-editor';
import { EditorConfiguration } from '../models/editor-configuration';
/**
 * A nimble styled rich text editor
 */
export class RichTextEditor extends mixinErrorPattern(RichText) {
    constructor() {
        super(...arguments);
        /**
         * @internal
         */
        this.editorDiv = this.createEditor();
        /**
         * @internal
         */
        this.tiptapEditor = createTiptapEditor(() => { }, () => { }, this.editorDiv, [], this.mentionListbox, this.placeholder);
        /**
         * @internal
         */
        this.xmlSerializer = new XMLSerializer();
        /**
         * @internal
         */
        this.richTextMarkdownSerializer = new RichTextMarkdownSerializer([]);
        /**
         * Whether to disable user from editing and interacting with toolbar buttons
         *
         * @public
         * HTML Attribute: disabled
         */
        this.disabled = false;
        /**
         * Whether to hide the footer of the rich text editor
         *
         * @public
         * HTML Attribute: footer-hidden
         */
        this.footerHidden = false;
        /**
         * The width of the vertical scrollbar, if displayed.
         * @internal
         */
        this.scrollbarWidth = -1;
        /**
         * @internal
         */
        this.activeMentionCharacter = '';
        this.updateScrollbarWidthQueued = false;
    }
    /**
     * True if the editor is empty or contains only whitespace, false otherwise.
     *
     * @public
     */
    get empty() {
        // Tiptap [isEmpty](https://tiptap.dev/api/editor#is-empty) returns false even if the editor has only whitespace.
        // Get the prose mirror textContent of all the nodes with whitespace trimmed to see if it is empty
        // Mention nodes are formatted as empty text content, so if empty make sure there are no mention nodes remaining
        if (this.tiptapEditor.state.doc.textContent.trim() === '') {
            let hasMention = false;
            this.tiptapEditor.state.doc.descendants(node => {
                if (node.type.name.startsWith(mentionPluginPrefix)) {
                    hasMention = true;
                }
                const continueDescent = !hasMention;
                return continueDescent;
            });
            return !hasMention;
        }
        return false;
    }
    /**
     * @internal
     */
    connectedCallback() {
        super.connectedCallback();
        if (!this.editorDiv.isConnected) {
            this.editorContainer.append(this.editorDiv);
        }
        this.bindEditorTransactionEvent();
        this.bindEditorUpdateEvent();
        this.stopNativeInputEventPropagation();
        this.resizeObserver = new ResizeObserver(() => this.onResize());
        this.resizeObserver.observe(this);
    }
    /**
     * @internal
     */
    disconnectedCallback() {
        super.disconnectedCallback();
        this.unbindEditorTransactionEvent();
        this.unbindEditorUpdateEvent();
        this.unbindNativeInputEvent();
        this.resizeObserver?.disconnect();
    }
    /**
     * @internal
     */
    disabledChanged() {
        this.disableEditor();
        this.disableMentionViewElement();
    }
    /**
     * Update the placeholder text and view of the editor.
     * @internal
     */
    placeholderChanged(_prev, _next) {
        // In TipTap v3, extension options cannot be modified after initialization.
        // We need to reinitialize the editor with the new placeholder value.
        // This workaround will be updated once the upstream issue is resolved.
        // See: https://github.com/ni/nimble/issues/2752
        const currentContent = this.getMarkdown();
        const { from, to } = this.tiptapEditor.view.state.selection;
        this.initializeEditor(); // Destroys and recreates entire editor
        this.setMarkdown(currentContent);
        this.tiptapEditor.commands.setTextSelection({ from, to });
    }
    /**
     * @internal
     */
    ariaLabelChanged(_prev, _next) {
        if (this.ariaLabel !== null && this.ariaLabel !== undefined) {
            this.editorDiv.setAttribute('aria-label', this.ariaLabel);
        }
        else {
            this.editorDiv.removeAttribute('aria-label');
        }
    }
    /**
     * @internal
     */
    configurationChanged(prev, next) {
        const formatButtonsState = this.getButtonsState(this.tiptapEditor);
        const { from, to } = this.tiptapEditor.view.state.selection;
        if (this.isMentionExtensionConfigUnchanged(prev, next)) {
            this.setMarkdown(this.getMarkdown());
        }
        else {
            const mentionExtensionConfig = this.getMentionExtensionConfig();
            const currentStateMarkdown = this.getMarkdown();
            this.richTextMarkdownSerializer = new RichTextMarkdownSerializer(mentionExtensionConfig.map(config => config.name));
            this.initializeEditor();
            this.setMarkdown(currentStateMarkdown);
        }
        this.tiptapEditor.commands.setTextSelection({ from, to });
        this.resetEditorButtonsState(formatButtonsState);
        this.setActiveMappingConfigs();
    }
    /**
     * @internal
     */
    activeMentionCharacterChanged() {
        this.setActiveMappingConfigs();
    }
    /**
     * Toggle the bold mark and focus back to the editor
     * @internal
     */
    boldButtonClick() {
        this.tiptapEditor.chain().focus().toggleBold().run();
    }
    /**
     * Toggle the bold mark and focus back to the editor
     * @internal
     */
    boldButtonKeyDown(event) {
        if (this.keyActivatesButton(event)) {
            this.tiptapEditor.chain().focus().toggleBold().run();
            return false;
        }
        return true;
    }
    /**
     * Toggle the italics mark and focus back to the editor
     * @internal
     */
    italicsButtonClick() {
        this.tiptapEditor.chain().focus().toggleItalic().run();
    }
    /**
     * Toggle the italics mark and focus back to the editor
     * @internal
     */
    italicsButtonKeyDown(event) {
        if (this.keyActivatesButton(event)) {
            this.tiptapEditor.chain().focus().toggleItalic().run();
            return false;
        }
        return true;
    }
    /**
     * Toggle the unordered list node and focus back to the editor
     * @internal
     */
    bulletListButtonClick() {
        this.tiptapEditor.chain().focus().toggleBulletList().run();
    }
    /**
     * Toggle the unordered list node and focus back to the editor
     * @internal
     */
    bulletListButtonKeyDown(event) {
        if (this.keyActivatesButton(event)) {
            this.tiptapEditor.chain().focus().toggleBulletList().run();
            return false;
        }
        return true;
    }
    /**
     * Toggle the ordered list node and focus back to the editor
     * @internal
     */
    numberedListButtonClick() {
        this.tiptapEditor.chain().focus().toggleOrderedList().run();
    }
    /**
     * Toggle the ordered list node and focus back to the editor
     * @internal
     */
    numberedListButtonKeyDown(event) {
        if (this.keyActivatesButton(event)) {
            this.tiptapEditor.chain().focus().toggleOrderedList().run();
            return false;
        }
        return true;
    }
    /**
     * Inserts the mention character into the editor and focus back to the editor
     * @internal
     */
    mentionButtonClick(character) {
        this.tiptapEditor
            .chain()
            .insertContent(this.shouldInsertSpace() ? ` ${character}` : character)
            .focus()
            .run();
    }
    /**
     * This function load tip tap editor with provided markdown content by parsing into html
     * @public
     */
    setMarkdown(markdown) {
        const html = this.getHtmlContent(markdown);
        this.tiptapEditor.commands.setContent(html, { emitUpdate: false });
        this.disableMentionViewElement();
    }
    /**
     * This function returns markdown string by serializing tiptap editor document using prosemirror MarkdownSerializer
     * @public
     */
    getMarkdown() {
        return this.richTextMarkdownSerializer.serializeDOMToMarkdown(this.tiptapEditor.state.doc);
    }
    /**
     * @internal
     */
    stopEventPropagation(event) {
        // Don't bubble the 'change' event from the toggle button because
        // all the formatting button has its own 'toggle' event through 'click' and 'keydown'.
        event.stopPropagation();
        return false;
    }
    getMentionedHrefs() {
        const mentionedHrefs = new Set();
        this.tiptapEditor.state.doc.descendants(node => {
            if (node.type.name.startsWith(mentionPluginPrefix)) {
                mentionedHrefs.add(node.attrs.href);
            }
        });
        return Array.from(mentionedHrefs);
    }
    /**
     * @internal
     */
    getMentionExtensionConfig() {
        return this.configuration instanceof EditorConfiguration
            ? this.configuration.mentionExtensionConfig
            : [];
    }
    /**
     * @internal
     */
    onMentionSelect(event) {
        if (this.activeMentionCommand) {
            this.activeMentionCommand({
                href: event.detail.href,
                label: event.detail.displayName
            });
        }
    }
    /**
     * @internal
     */
    focusoutHandler() {
        if (!this.mentionListbox?.open) {
            return;
        }
        this.mentionListbox?.close();
    }
    createConfig() {
        this.validate();
        if (this.richTextValidator.isValid()) {
            if (this.richTextUpdateTracker.updateButtonLabel
                || this.richTextUpdateTracker.updateMappingConfigs
                || this.richTextUpdateTracker.updatePattern) {
                this.configuration = new EditorConfiguration(this.mentionElements);
            }
        }
        else {
            this.configuration = undefined;
        }
    }
    isMentionExtensionConfigUnchanged(prev, next) {
        const prevConfigCharacters = prev?.mentionExtensionConfig
            .map(config => config.character)
            .sort((a, b) => a.localeCompare(b))
            .toString();
        const nextConfigCharacters = next?.mentionExtensionConfig
            .map(config => config.character)
            .sort((a, b) => a.localeCompare(b))
            .toString();
        return prevConfigCharacters === nextConfigCharacters;
    }
    createEditor() {
        const editor = document.createElement('div');
        editor.className = 'editor';
        editor.setAttribute('aria-multiline', 'true');
        editor.setAttribute('role', 'textbox');
        editor.setAttribute('aria-disabled', 'false');
        return editor;
    }
    initializeEditor() {
        this.unbindEditorTransactionEvent();
        this.unbindEditorUpdateEvent();
        this.unbindNativeInputEvent();
        this.tiptapEditor?.destroy();
        this.tiptapEditor = createTiptapEditor(character => {
            this.activeMentionCharacter = character;
        }, command => {
            this.activeMentionCommand = command;
        }, this.editorDiv, this.configuration instanceof EditorConfiguration
            ? this.configuration.mentionExtensionConfig
            : [], this.mentionListbox, this.placeholder);
        this.disableEditor();
        this.disableMentionViewElement();
        this.bindEditorTransactionEvent();
        this.bindEditorUpdateEvent();
        this.stopNativeInputEventPropagation();
    }
    /**
     * This function takes the Fragment from parseMarkdownToDOM function and return the serialized string using XMLSerializer
     */
    getHtmlContent(markdown) {
        const parseResult = RichTextMarkdownParser.parseMarkdownToDOM(markdown, this.configuration?.parserMentionConfig);
        return this.xmlSerializer.serializeToString(parseResult.fragment);
    }
    /**
     * Binding the "transaction" event to the editor allows continuous monitoring the events and updating the button state in response to
     * various actions such as mouse events, keyboard events, changes in the editor content etc,.
     * https://tiptap.dev/api/events#transaction
     */
    bindEditorTransactionEvent() {
        this.tiptapEditor.on('transaction', () => {
            this.updateEditorButtonsState();
        });
    }
    unbindEditorTransactionEvent() {
        this.tiptapEditor.off('transaction');
    }
    updateEditorButtonsState() {
        if (!this.$fastController.isConnected) {
            return;
        }
        const { extensionManager, state } = this.tiptapEditor;
        const { extensions } = extensionManager;
        const { selection } = state;
        const parentList = findParentNode((node) => isList(node.type.name, extensions))(selection);
        this.boldButton.checked = this.tiptapEditor.isActive('bold');
        this.italicsButton.checked = this.tiptapEditor.isActive('italic');
        this.bulletListButton.checked = parentList?.node.type.name === TipTapNodeName.bulletList;
        this.numberedListButton.checked = parentList?.node.type.name === TipTapNodeName.numberedList;
    }
    keyActivatesButton(event) {
        switch (event.key) {
            case keySpace:
            case keyEnter:
                return true;
            default:
                return false;
        }
    }
    unbindEditorUpdateEvent() {
        this.tiptapEditor.off('update');
    }
    /**
     * input event is fired when there is a change in the content of the editor.
     *
     * https://tiptap.dev/api/events#update
     */
    bindEditorUpdateEvent() {
        this.tiptapEditor.on('update', () => {
            this.$emit('input');
            this.queueUpdateScrollbarWidth();
        });
    }
    disableEditor() {
        this.tiptapEditor.setEditable(!this.disabled);
        this.setEditorTabIndex();
        this.editorDiv.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
        this.mentionListbox?.close();
    }
    disableMentionViewElement() {
        this.tiptapEditor.state.doc.descendants((node, pos) => {
            if (node.type.name.startsWith(mentionPluginPrefix)) {
                const updatedAttrs = {
                    ...node.attrs,
                    disabled: this.disabled ? '' : null
                };
                const updatedNode = this.tiptapEditor.schema.node(node.type.name, updatedAttrs, node.content);
                const updatedTransaction = this.tiptapEditor.state.tr.replaceWith(pos, pos + node.nodeSize, updatedNode);
                this.tiptapEditor.view.dispatch(updatedTransaction);
            }
            return true;
        });
    }
    /**
     * Stopping the native input event propagation emitted by the contenteditable element in the Tiptap
     * since there is an issue (linked below) in ProseMirror where selecting the text and removing it
     * does not trigger the native HTMLElement input event. So using the "update" event emitted by the
     * Tiptap to capture it as an "input" customEvent in the rich text editor.
     *
     * Prose Mirror issue: https://discuss.prosemirror.net/t/how-to-handle-select-backspace-delete-cut-type-kind-of-events-handletextinput-or-handledomevents-input-doesnt-help/4844
     */
    stopNativeInputEventPropagation() {
        this.tiptapEditor.view.dom.addEventListener('input', event => {
            event.stopPropagation();
        });
    }
    unbindNativeInputEvent() {
        this.tiptapEditor.view.dom.removeEventListener('input', () => { });
    }
    queueUpdateScrollbarWidth() {
        if (!this.$fastController.isConnected) {
            return;
        }
        if (!this.updateScrollbarWidthQueued) {
            this.updateScrollbarWidthQueued = true;
            DOM.queueUpdate(() => this.updateScrollbarWidth());
        }
    }
    updateScrollbarWidth() {
        this.updateScrollbarWidthQueued = false;
        this.scrollbarWidth = this.tiptapEditor.view.dom.offsetWidth
            - this.tiptapEditor.view.dom.clientWidth;
    }
    onResize() {
        this.scrollbarWidth = this.tiptapEditor.view.dom.offsetWidth
            - this.tiptapEditor.view.dom.clientWidth;
    }
    getTipTapExtension(extensionName) {
        return this.tiptapEditor.extensionManager.extensions.find((extension) => extension.name === extensionName);
    }
    setEditorTabIndex() {
        this.tiptapEditor.setOptions({
            editorProps: {
                attributes: {
                    tabindex: this.disabled ? '-1' : '0'
                }
            }
        });
    }
    setActiveMappingConfigs() {
        this.activeMappingConfigs = this.activeMentionCharacter
            ? this.getMentionExtensionConfigFromCharacter(this.activeMentionCharacter)?.mappingConfigs
            : undefined;
    }
    shouldInsertSpace() {
        const { $anchor, $head } = this.tiptapEditor.view.state.selection;
        const isAtStartOfLine = $head.parentOffset === 0 || $anchor.parentOffset === 0;
        const nodeBeforeSelection = $anchor.pos < $head.pos ? $anchor.nodeBefore : $head.nodeBefore;
        const isHardBreakNode = nodeBeforeSelection?.type.name === HardBreak.name;
        const hasWhitespaceBeforeCurrentPosition = nodeBeforeSelection?.textContent.endsWith(' ');
        return (!isAtStartOfLine
            && !isHardBreakNode
            && !hasWhitespaceBeforeCurrentPosition);
    }
    getMentionExtensionConfigFromCharacter(character) {
        return this.getMentionExtensionConfig().find(config => config.character === character);
    }
    resetEditorButtonsState(buttonsState) {
        if (buttonsState?.bold && !this.tiptapEditor.isActive('bold')) {
            this.tiptapEditor.chain().focus().toggleBold().run();
        }
        if (buttonsState?.italics && !this.tiptapEditor.isActive('italic')) {
            this.tiptapEditor.chain().focus().toggleItalic().run();
        }
    }
    getButtonsState(tiptapEditor) {
        if (!this.$fastController.isConnected) {
            return undefined;
        }
        return {
            bold: tiptapEditor.isActive('bold'),
            italics: tiptapEditor.isActive('italic')
        };
    }
}
__decorate([
    attr({ mode: 'boolean' })
], RichTextEditor.prototype, "disabled", void 0);
__decorate([
    attr({ attribute: 'footer-hidden', mode: 'boolean' })
], RichTextEditor.prototype, "footerHidden", void 0);
__decorate([
    attr
], RichTextEditor.prototype, "placeholder", void 0);
__decorate([
    observable
], RichTextEditor.prototype, "boldButton", void 0);
__decorate([
    observable
], RichTextEditor.prototype, "italicsButton", void 0);
__decorate([
    observable
], RichTextEditor.prototype, "bulletListButton", void 0);
__decorate([
    observable
], RichTextEditor.prototype, "numberedListButton", void 0);
__decorate([
    observable
], RichTextEditor.prototype, "scrollbarWidth", void 0);
__decorate([
    observable
], RichTextEditor.prototype, "activeMentionCharacter", void 0);
__decorate([
    observable
], RichTextEditor.prototype, "activeMappingConfigs", void 0);
applyMixins(RichTextEditor, ARIAGlobalStatesAndProperties);
const nimbleRichTextEditor = RichTextEditor.compose({
    baseName: 'rich-text-editor',
    template,
    styles,
    shadowOptions: {
        delegatesFocus: true
    }
});
DesignSystem.getOrCreate()
    .withPrefix('nimble')
    .register(nimbleRichTextEditor());
export const richTextEditorTag = 'nimble-rich-text-editor';
//# sourceMappingURL=index.js.map