import { GOVBRSuggest } from '../../suggest/1.2.0/GOVBRSuggest.mjs';
import { ComponenteEditorTiposPropriedade, ListaComponentesEditor, ResolucoesPagina } from './ListaComponentesEditor.mjs';

import '../../custom-inputs/lista/1.0.2/GOVBRInputSelect.mjs';
import '../../custom-inputs/suggest/1.0.2/GOVBRInputSuggest.mjs';
import { GOVBRInputSelect } from '../../custom-inputs/lista/1.0.2/GOVBRInputSelect.mjs';

/**
 * @typedef {'posicao'|'posicaoSd'|'posicaoMd'|'posicaoLd'} StrPosTamanho
 */
/**
 * @typedef {'tamanho'|'tamanhoSd'|'tamanhoMd'|'tamanhoLd'} StrTamTamanho
 */
/**
 * @typedef {'numeroColunas'|'numeroLinhas'} StrPropTamanho
 */
/**
 * @typedef {'posicaoColuna'|'posicaoLinha'} StrPropPosicao
 */
/**
 * @typedef {StrPropPosicao|StrPropTamanho|
 *           'numeroColunasSd'|'numeroLinhasSd'|'posicaoColunaSd'|'posicaoLinhaSd'|
 *           'numeroColunasMd'|'numeroLinhasMd'|'posicaoColunaMd'|'posicaoLinhaMd'|
 *           'numeroColunasLd'|'numeroLinhasLd'|'posicaoColunaLd'|'posicaoLinhaLd'} StrPropPosicaoTamanho
 */

/**
 * Classe para o UC para editar páginas do institucional.
 */
class GOVBREditorPaginasUC {
    /**
     * @type {import('./GOVBREditorPaginasUserControl.mjs').GOVBREditorPaginasUserControl}
     */
    userControl;
    /**
     * @type {HTMLElement}
     */
    host;
    /**
     * @type {boolean}
     */
    iniciado;
    /**
     * @type {boolean}
     */
    disparandoEventos;

    /**
     * @type {import('./ListaComponentesEditor.mjs').ComponenteEditor[]}
     */
    componentes;
    /**
     * @type {import('./ListaComponentesEditor.mjs').ComponenteEditor[]}
     */
    _componentesAplicados = [];
    /**
     * @type {string}
     */
    _componenteSelecionado = '';
    /**
     * @type {string}
     */
    ultCompPropriedades = '';

    /**
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor[]} val
     */
    set componentesAplicados(val) {
        this._componentesAplicados = val;
        this.render();
    }
    get componentesAplicados() {
        return this._componentesAplicados;
    }
    /**
     * @param {string} val
     */
    set componenteSelecionado(val) {
        this._componenteSelecionado = val;
        this.render();
    }
    get componenteSelecionado() {
        return this._componenteSelecionado;
    }

    /**
     * @type {Record<string, {evento: string, fn: (event: Event) => void, dispatchOnInit?: boolean}>}
     */
    postRenderEvents = {};

    /**
     * @type {HTMLElement}
     */
    baseEditor;
    /**
     * @type {HTMLElement}
     */
    camadaLinhas;
    /**
     * @type {HTMLElement}
     */
    camadaComponentes;
    /**
     * @type {HTMLElement}
     */
    componentesEditor;
    /**
     * @type {HTMLElement}
     */
    componenteArrastandoPadrao;
    /**
     * @type {HTMLElement}
     */
    alvoDropComponenteVisual;
    /**
     * @type {HTMLElement}
     */
    areaPropriedades;
    /**
     * @type {{ x: number; y: number }}
     */
    offsetMovimento;
    /**
     * @type {HTMLElement}
     */
    alvoResize;
    /**
     * @type {{ x: number; y: number }}
     */
    posicaoInicialResize;
    /**
     * @type {number}
     */
    idxAbaPropriedades;

    /**
     * @type {HTMLElement}
     */
    componenteArrastando;
    /**
     * @type {HTMLElement}
     */
    alvoDropComponente;

    /**
     * @type {GOVBRSuggest}
     */
    suggestHint;

    /**
     * @type {ResolucoesPagina}
     */
    _resolucaoAlvo = ResolucoesPagina.DESKTOP;
    /**
     * @param {ResolucoesPagina} val
     */
    set resolucaoAlvo(val) {
        this._resolucaoAlvo = val;
        this.render();
    }
    get resolucaoAlvo() {
        return this._resolucaoAlvo;
    }

    /**
     * @type {{timeout: number, reRender: boolean, reRenderForce: boolean}}
     */
    renderQuery = {
        timeout: null,
        reRender: false,
        reRenderForce: false,
    };

    /**
     * @param {import('./GOVBREditorPaginasUserControl.mjs').GOVBREditorPaginasUserControl} userControl
     * @param {HTMLElement} host
     */
    constructor(userControl, host) {
        this.userControl = userControl;
        this.host = host;
        this.host.classList.add('govbr-pg-editor');
        this.host.setAttribute('tabindex', '0');
        this.suggestHint = new GOVBRSuggest(this.host);

        this.host.style.setProperty('width', this.userControl.LARGURA_COMPONENTE || '');
        this.host.style.setProperty('height', this.userControl.ALTURA_COMPONENTE || '');

        this.iniciado = false;
        GOVBRPlataformaJS.importarStyleSheet(this.userControl.URL_CDN, 'editor-paginas', this.userControl.VERSAO_JS_COMPONENTE, 'GOVBREditorPaginasUC', this.host).then(
            async () => {
                GOVBR.logs.debug('Editor de Páginas - Carregado');
                GOVBR.logs.debug(this);
                this.componentes = await ListaComponentesEditor(this.userControl.CATEGORIAS, this.userControl.PUBLICO_ALVO, this.userControl.TAGS, this.userControl.AGENDAS);
                this.carregaJSON(this.userControl.COMPONENTES || []);
                this.montaComponenteInicial();
                this.iniciado = true;
                this.render();
            }
        );
    }

    // ***** MONTAGEM INICIAL *****

    montaComponenteInicial() {
        this.host.innerHTML = `
        <div class="base-editor editor-resolucao-${(this.resolucaoAlvo || 'Ld').toLowerCase()}">
            <div class="pagina-editor-base">
                <div class="pagina-editor camada-linhas">
                    <div class="editor-pagina-regua">
                        <span class="editor-pagina-regua-ini">0px</span>
                        <span class="editor-pagina-regua-meio">0px</span>
                        <span class="editor-pagina-regua-fim">0px</span>
                    </div>
                </div>
                <div class="pagina-editor camada-componentes"></div>
                <div class="pagina-editor camada-overlays">
                    <div class="alvo-drop-componente oculto"></div>
                </div>
            </div>
            <div class="componentes-editor">${this.montaListaComponentesDraggable()}</div>
            <div class="acoes-editor">
                <button class="botao botao-download" type="button">
                    <i aria-hidden class="govbr-icone-fa fa-solid fa-download"></i>
                    Fazer Download
                </button>
                <a aria-hidden tabindex="-1" class="download-editor" download="pagina.json"></a>
                <button class="botao botao-carregar" type="button">
                <i aria-hidden class="govbr-icone-fa fa-solid fa-upload"></i>
                Carregar de um Arquivo
                </button>
                <input class="upload-editor" type="file" aria-hidden tabindex="-1"></input>
                <button class="botao botao-preview" type="button">
                    <i aria-hidden class="govbr-icone-fa fa-solid fa-eye"></i>
                    Pré-visualizar
                </button>
                <div class="toggle-resolucoes">
                    <button class="toggle" type="button" title="Desktops, Notebooks, Tablets na Horizontal">
                        <i aria-hidden class="govbr-icone-fa fa-solid fa-desktop"></i>
                    </button>
                    <button class="toggle" type="button" title="Laptops Pequenos, Tablets Pequenos na Horizontal">
                        <i aria-hidden class="govbr-icone-fa fa-solid fa-laptop"></i>
                    </button>
                    <button class="toggle" type="button" title="Tablets na Vertical, Celulares na Horizontal">
                        <i aria-hidden class="govbr-icone-fa fa-solid fa-tablet-screen-button"></i>
                    </button>
                    <button class="toggle" type="button" title="Celulares na Vertical">
                        <i aria-hidden class="govbr-icone-fa fa-solid fa-mobile-screen"></i>
                    </button>
                </div>
            </div>
            <fieldset class="fieldset-propriedades-editor"></fieldset>
            <div class="componente-arrastando oculto">
                <i aria-hidden class="govbr-icone-fa"></i>
            </div>
        </div>
        `;

        this.baseEditor = this.host.querySelector('.base-editor');
        this.camadaLinhas = this.host.querySelector('.pagina-editor.camada-linhas');
        this.camadaComponentes = this.host.querySelector('.pagina-editor.camada-componentes');
        this.componentesEditor = this.host.querySelector('.componentes-editor');
        this.componenteArrastandoPadrao = this.host.querySelector('.componente-arrastando');
        this.alvoDropComponenteVisual = this.host.querySelector('.alvo-drop-componente');
        this.areaPropriedades = this.host.querySelector('.fieldset-propriedades-editor');

        this.adicionaEventosBotoes();
        this.adicionaEventosComponente();
    }

    /**
     * Monta a lista de componentes que podem ser arrastados para o editor.
     */
    montaListaComponentesDraggable() {
        const componentes = this.componentes
            .filter((e) => e.exibeLateral)
            .map((componente) => {
                return `
                    <li class="componente-editor" data-id="${componente.id}">
                        <i aria-hidden class="govbr-icone-fa ${componente.icone}"></i> ${componente.titulo}
                    </li>
                `;
            });
        return `<ul class="lista-componentes-editor">${componentes.join('')}</ul>`;
    }

    adicionaEventosBotoes() {
        this.host.querySelector('.botao-download').addEventListener('click', () => {
            try {
                const a = /** @type {HTMLAnchorElement} */ (this.host.querySelector('.download-editor'));
                a.href = 'data:text/json,' + encodeURIComponent(JSON.stringify(this.salvaJSON()));
                a.click();
                GOVBR.mensagem('Download iniciado com sucesso.', 'sucesso');
            } catch (error) {
                GOVBR.logs.error(error.message);
                GOVBR.mensagem('Não foi possível realizar o download. Verifique as permissões de seu navegador.');
            }
        });

        this.host.querySelector('.botao-carregar').addEventListener('click', () => {
            const input = /** @type {HTMLInputElement} */ (this.host.querySelector('.upload-editor'));
            input.showPicker();
        });

        this.host.querySelector('.botao-preview').addEventListener('click', () => {
            const conteudo = this.salvaJSON();
            const dialog = this.abreDialog('Pré-visualização');
            const frame = document.createElement('iframe');
            frame.className = 'editor-iframe-preview';
            let largura;
            switch (this.resolucaoAlvo) {
                case ResolucoesPagina.CELULAR:
                    largura = '640px';
                    break;
                case ResolucoesPagina.TABLET:
                    largura = '860px';
                    break;
                case ResolucoesPagina.LAPTOPS:
                    largura = '1024px';
                    break;
                case ResolucoesPagina.DESKTOP:
                default:
                    largura = '1366px';
                    break;
            }
            frame.style.width = largura;
            frame.src = `/portal-institucional/preview/`;
            frame.onload = () => {
                setTimeout(() => {
                    frame.contentWindow?.postMessage?.(conteudo, location.origin);
                }, 100);
                frame.onload = null;
            };
            dialog.append(frame);
        });

        this.host.querySelector('.upload-editor').addEventListener('change', async (event) => {
            const tg = /** @type {HTMLInputElement} */ (event.target);
            if (!tg.files.length) {
                return;
            }
            try {
                const text = await tg.files[0].text();
                this.carregaJSON(JSON.parse(text));
            } catch (error) {
                GOVBR.logs.error(error.message);
                GOVBR.mensagem('Falha ao carregar o arquivo.');
            }
            tg.value = '';
        });

        Array.from(this.host.querySelector('.toggle-resolucoes').children).forEach((e, idx) => {
            e.addEventListener('click', () => {
                this.resolucaoAlvo = [ResolucoesPagina.DESKTOP, ResolucoesPagina.LAPTOPS, ResolucoesPagina.TABLET, ResolucoesPagina.CELULAR][idx];
            });
        });
    }

    adicionaEventosComponente() {
        this.host.addEventListener('keydown', (event) => {
            if (document.activeElement === this.host) {
                if (event.key === 'Delete' && this.componenteSelecionado) {
                    const idxComponenteAlvoDrop = this.componentesAplicados.findIndex((e) => e.id == this.componenteSelecionado);
                    this.componentesAplicados.splice(idxComponenteAlvoDrop, 1);
                    this.componenteSelecionado = null;
                    return true;
                }
            }
        });
        this.baseEditor.addEventListener('pointerdown', (event) => {
            if (this.onDragStartComponenteEditor(event)) {
                event.preventDefault();
            }
        });
        this.baseEditor.addEventListener('pointermove', (event) => {
            if (this.onDragMoveComponenteEditor(event)) {
                event.preventDefault();
            }
        });
        this.baseEditor.addEventListener('pointerup', (event) => {
            if (this.onDropComponenteEditor(true)) {
                event.preventDefault();
            }
        });
    }

    // ***** RENDER/RE-RENDER *****

    /**
     * @param {boolean} force
     */
    render(force = false) {
        if (!this.iniciado || this.disparandoEventos) {
            return;
        }
        if (this.renderQuery.timeout) {
            this.renderQuery.reRender = true;
            this.renderQuery.reRenderForce = this.renderQuery.reRenderForce || force;
            return;
        }
        const { componentes, idx } = this.montaBaseComponentesEditor();
        const linhas = this.montaBaseLinhasEditor(idx);

        this.renderComponentes(componentes);
        this.renderLinhas(linhas);
        this.renderPropriedades(force);

        this.atualizaBaseEditor();
        this.atualizaRegua();
        this.atualizaBotoesAtivos();
        this.atualizaComponenteSelecionado();

        this.userControl.COMPONENTES = this.salvaJSON();
        GOVBR.logs.debug(this.userControl.COMPONENTES);

        this.disparandoEventos = true;
        Object.entries(this.postRenderEvents).forEach(([id, ev]) => {
            const el = this.host.querySelector('[data-evento="' + id + '"]');
            if (el) {
                el.addEventListener(ev.evento, ev.fn);
                el.removeAttribute('data-evento');
                if (ev.dispatchOnInit) {
                    el.dispatchEvent(new Event(ev.evento));
                }
            }
        });
        this.postRenderEvents = {};
        this.disparandoEventos = false;
        this.renderQuery.timeout = setTimeout(() => {
            this.renderQuery.timeout = null;
            if (this.renderQuery.reRender) {
                this.renderQuery.reRender = false;
                force = this.renderQuery.reRenderForce;
                this.renderQuery.reRenderForce = false;
                this.render(force);
            }
        }, 100);
    }

    /**
     * Monta os componentes da base/parte central do editor.
     */
    montaBaseComponentesEditor() {
        let idx = 1;
        const componentes = this.componentesAplicados
            .filter((e) => e.exibeLateral)
            .map((componente) => {
                const attrPos = this.getAtributoResolucaoAlvo();
                const coluna = componente[attrPos].posicaoColuna;
                const linha = componente[attrPos].posicaoLinha;
                const tamColuna = componente[attrPos].numeroColunas;
                const tamLinha = componente[attrPos].numeroLinhas;
                if (linha + (tamLinha - 1) >= idx) {
                    idx = linha + (tamLinha - 1);
                }
                return [this.resolucaoAlvo + '-' + componente.id, this.montaComponenteAplicado(componente, coluna, linha, tamColuna, tamLinha)];
            });
        return { componentes: Object.fromEntries(componentes), idx: componentes.length ? idx : 0 };
    }

    /**
     * Monta um componente a ser aplicado.
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor} componente
     * @param {number} coluna
     * @param {number} linha
     * @param {number} tamColuna
     * @param {number} tamLinha
     * @returns {string}
     */
    montaComponenteAplicado(componente, coluna, linha, tamColuna, tamLinha) {
        return `
            <div
                class="componente-editor componente-editor-aplicado"
                data-id="${componente.id}"
                data-key="${this.resolucaoAlvo + '-' + componente.id}"
                style="--posicao-x: ${coluna}; --posicao-y: ${linha}; --tamanho-x: ${tamColuna}; --tamanho-y: ${tamLinha};"
            >
                <i aria-hidden class="govbr-icone-fa ${componente.icone}"></i> ${componente.titulo}
                ${['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']
                    .map((direcao) => {
                        return `<span class="drag-resize-componente drag-resize-componente-${direcao}" data-direcao=${direcao}></span>`;
                    })
                    .join('')}
            </div>
        `;
    }

    /**
     * Monta as linhas da base/parte central do editor.
     * @param {number} idx
     * @returns {string[]}
     */
    montaBaseLinhasEditor(idx) {
        const linhas = [];
        for (let i = 1; i <= idx + 1; i++) {
            linhas.push(this.montaLinhaEditor(12, i));
        }
        return linhas;
    }

    /**
     * Monta uma linha do editor.
     * @param {number} tamanho
     * @param {number} idx
     * @returns {string}
     */
    montaLinhaEditor(tamanho, idx) {
        return `
            <div class="linha-editor" data-idx="${idx}">
                ${Array.from(new Array(tamanho), (_, i) => i)
                    .map((i) => {
                        return `
                        <span class="coluna-editor" data-idx="${i + 1}" style="grid-column: ${i + 1}; grid-row: ${idx}"></span>
                    `;
                    })
                    .join('')}
            </div>
        `;
    }

    /**
     * @param {Record<string, string>} componentes
     */
    renderComponentes(componentes) {
        /** @type {HTMLElement[]} */ (Array.from(this.camadaComponentes.children)).forEach((filho) => {
            const dataKey = filho.getAttribute('data-key');
            if (componentes[dataKey]) {
                this.refreshComponente(filho, componentes[dataKey]);
                delete componentes[dataKey];
            } else {
                filho.remove();
            }
        });
        Object.values(componentes).forEach((filho) => {
            const div = document.createElement('div');
            this.camadaComponentes.append(div);
            div.outerHTML = filho;
        });
    }

    /**
     * @param {HTMLElement} el
     * @param {string} componente
     */
    refreshComponente(el, componente) {
        const div = document.createElement('div');
        div.innerHTML = componente;
        const novoComponente = div.children[0];
        Array.from(el.attributes).forEach((attr) => {
            if (!novoComponente.hasAttribute(attr.name)) {
                el.removeAttribute(attr.name);
            }
        });
        Array.from(novoComponente.attributes).forEach((attr) => {
            el.setAttribute(attr.name, attr.value);
        });
    }

    /**
     * @param {string[]} linhas
     */
    renderLinhas(linhas) {
        this.camadaLinhas.querySelectorAll('.linha-editor').forEach(() => {
            linhas.shift();
        });
        linhas.forEach((linhaNova) => {
            const div = document.createElement('div');
            this.camadaLinhas.append(div);
            div.outerHTML = linhaNova;
        });
    }

    renderPropriedades(force = false) {
        if (force || this.ultCompPropriedades !== this.componenteSelecionado) {
            this.ultCompPropriedades = this.componenteSelecionado;
            if (this.componenteSelecionado) {
                const componente = this.componentesAplicados.find((e) => e.id == this.componenteSelecionado);
                const tamanhos = [
                    ['numeroColunas', 'Tamanho em Colunas'],
                    ['numeroLinhas', 'Tamanho em Linhas'],
                    ['posicaoColuna', 'Posição nas Colunas'],
                    ['posicaoLinha', 'Posição nas Linhas'],
                ].reduce(
                    /**
                     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditorPropriedade[]} accum
                     * @param {[StrPropTamanho|StrPropPosicao, string]} _
                     */
                    (accum, [nomePropriedade, tituloPropriedade]) => {
                        let hint = '';
                        switch (nomePropriedade) {
                            case 'numeroColunas':
                                hint = 'Exibe o número de colunas que o componente irá ocupar.';
                                break;
                            case 'numeroLinhas':
                                hint = 'Exibe o número de linhas que o componente irá ocupar.';
                                break;
                            case 'posicaoColuna':
                                hint = 'Exibe a partir de qual coluna o componente será posicionado.';
                                break;
                            case 'posicaoLinha':
                                hint = 'Exibe a partir de qual linha o componente será posicionado.';
                                break;
                        }
                        ['', 'Ld', 'Md', 'Sd'].forEach((tamanho) => {
                            let hintAdd = ' Esta configuração se aplica a ';
                            switch (tamanho) {
                                case 'Ld':
                                    hintAdd += 'Tablets (na orientação Horizontal) e Laptops de pequeno porte.';
                                    break;
                                case 'Md':
                                    hintAdd += 'Tablets (na orientação Vertical) e celulares de grande porte (na orientação Horizontal).';
                                    break;
                                case 'Sd':
                                    hintAdd += 'Celulares e outros dispositivos de pequeno porte.';
                                    break;
                                default:
                                    hintAdd += 'Desktops, Notebooks e outros dispositivos de grande porte.';
                            }
                            const posicao = /** @type {StrPosTamanho} */ ('posicao' + tamanho);
                            accum.push({
                                atributo: nomePropriedade + tamanho,
                                tipo: ComponenteEditorTiposPropriedade.NUMERO,
                                valor: componente[posicao]?.[nomePropriedade],
                                titulo: tituloPropriedade + (tamanho ? ' (' + tamanho.toUpperCase() + ')' : ''),
                                leitura: true,
                                hint: hint + hintAdd,
                                quebraLinha: tamanho === 'Sd',
                                change: (_, __, ___) => {
                                    return;
                                },
                            });
                        });
                        return accum;
                    },
                    []
                );
                this.areaPropriedades.innerHTML = `
                    <legend>Configurar: ${componente.titulo}</legend>
                    ${this.montaPropriedadesComponente(componente, tamanhos, componente.propriedades)}
                `;
            } else {
                let tema = this.componentesAplicados.find((e) => e.id == 'tema-1');
                if (!tema) {
                    const baseTema = this.componentes.find((e) => e.id == 'tema');
                    if (!baseTema) {
                        this.areaPropriedades.innerHTML = `
                            <legend>Propriedades</legend>
                            <p class="nenhum-selecionado">Selecione um componente para alterar suas propriedades.</p>
                        `;
                        return;
                    }
                    const novoTema = JSON.parse(JSON.stringify(baseTema));
                    novoTema.id = 'tema-1';
                    this.atualizaPropriedadesNovoComponente(baseTema, novoTema, this.componentesAplicados);
                    this.componentesAplicados.push(novoTema);
                    tema = novoTema;
                }
                this.areaPropriedades.innerHTML = `
                    <legend>Propriedades Globais</legend>
                    ${this.montaPropriedadesComponente(tema, null, tema.propriedades)}
                `;
            }
            // Adiciona os comportamentos das tabs e hints
            this.areaPropriedades.querySelectorAll('.hint-propriedade').forEach((hint) => {
                hint.addEventListener('click', (event) => {
                    event.preventDefault();
                    const suggest = this.suggestHint.abreSuggest(hint);
                    const textoHint = document.createElement('p');
                    textoHint.classList.add('govbr-editor-paginas-hint');
                    textoHint.innerHTML = hint.getAttribute('data-hint');
                    suggest.append(textoHint);
                });
            });

            this.areaPropriedades.querySelectorAll('.govbr-container-etapas').forEach(
                /**
                 * @param {HTMLElement} container
                 */
                (container) => {
                    let idx = parseInt(container.getAttribute('data-idx')) || 0;
                    if (container.parentElement === this.areaPropriedades) {
                        idx = this.idxAbaPropriedades || 0;
                    }
                    const abas = Array.from(container.querySelectorAll(':scope > .govbr-aba-etapas'));
                    if (idx >= abas.length) {
                        idx = 0;
                    }
                    container.style.setProperty('--numero-abas', '' + abas.length);

                    const atualizaAba = () => {
                        abas.forEach((aba, idxAba) => {
                            if (idxAba === idx) {
                                if (container.parentElement === this.areaPropriedades) {
                                    this.idxAbaPropriedades = idx;
                                }
                                aba.classList.add('govbr-aba-etapas-ativa');
                            } else {
                                aba.classList.remove('govbr-aba-etapas-ativa');
                            }
                        });
                    };
                    abas.forEach((aba, idxAba) => {
                        aba.querySelector(':scope > .titulo').addEventListener('click', () => {
                            idx = idxAba;
                            atualizaAba();
                        });
                    });
                    atualizaAba();
                }
            );
        }
    }

    /**
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor} componente
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditorPropriedade[]} tamanhos
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditorPropriedade[]} propriedades
     * @returns {string}
     */
    montaPropriedadesComponente(componente, tamanhos, propriedades) {
        /**
         * @param {import('./ListaComponentesEditor.mjs').ComponenteEditorPropriedade} propriedade
         * @param {number} idx
         * @returns {string}
         */
        const montaElementoPropriedade = (propriedade, idx) => {
            let classe = 'componente-editor-propriedade';
            if (
                (!idx && propriedades?.length === 1) ||
                propriedade.tipo === ComponenteEditorTiposPropriedade.TEXTO_LONGO ||
                propriedade.tipo === ComponenteEditorTiposPropriedade.HTML ||
                propriedade.tipo === ComponenteEditorTiposPropriedade.ARRAY
            ) {
                classe += ' componente-editor-propriedade-full';
            }
            let quebraLinha = '';
            if (propriedade.quebraLinha) {
                quebraLinha = '<div class="componente-editor-propriedade-quebra-linha"></div>';
            }
            return `
                <div class="${classe}">
                    ${this.montaInputPropriedade(componente, propriedade)}
                </div>
                ${quebraLinha}
            `;
        };
        let abaPropriedades = '';
        propriedades = propriedades.filter((propriedades) => !propriedades.oculta);
        if (!tamanhos || propriedades?.length > 1) {
            abaPropriedades = `
                <div class="govbr-aba-etapas etapas">
                    <span class="titulo">${tamanhos ? 'Específicas' : 'Tema'}</span>
                    <article class="conteudo propriedades-editor">
                        ${propriedades.map((propriedade, idx) => montaElementoPropriedade(propriedade, idx)).join('')}
                    </article>
                </div>
            `;
        } else if (propriedades?.length) {
            tamanhos.unshift(propriedades[0]);
        }
        if (!tamanhos) {
            return `
                <div class="govbr-container-etapas etapas-propriedade">
                    ${abaPropriedades}
                </div>
            `;
        }
        return `
            <div class="govbr-container-etapas etapas-propriedade">
                <div class="govbr-aba-etapas etapas">
                    <span class="titulo">Geral</span>
                    <article class="conteudo propriedades-editor">
                        ${tamanhos.map((propriedade, idx) => montaElementoPropriedade(propriedade, idx)).join('')}
                    </article>
                </div>
                ${abaPropriedades}
            </div>
        `;
    }

    /**
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor} componente
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditorPropriedade} propriedade
     * @returns string
     */
    montaInputPropriedade(componente, propriedade) {
        let hint = '';
        if (propriedade.hint) {
            const textoHint = GOVBRPlataformaJS.ComponentUtils.escapaValorAtributoHTML(propriedade.hint);
            hint = `<button type="button" class="hint-propriedade fa-solid fa-info-circle" title="Mais informações" data-hint="${textoHint}"></button>`;
        }
        switch (propriedade.tipo) {
            case ComponenteEditorTiposPropriedade.ARRAY:
                return `
                    <fieldset class="componente-editor-propriedade-fs">
                        <legend class="componente-editor-propriedade-fs-titulo">
                            ${propriedade.titulo}
                            ${hint}
                        </legend>
                        ${this.montaGridPropriedade(componente, propriedade)}
                    </fieldset>
                `;
            case ComponenteEditorTiposPropriedade.HTML:
            case ComponenteEditorTiposPropriedade.TEXTO_LONGO: {
                const idEvento = this.addPostRenderEvent(
                    'change',
                    (event) => {
                        if (propriedade.leitura) {
                            return;
                        }
                        const tg = /** @type {HTMLInputElement} */ (event.target);
                        if (propriedade.change) {
                            propriedade.change(componente, propriedade, tg.value);
                            this.render(true);
                        } else {
                            propriedade.valor = tg.value;
                            this.render();
                        }
                    },
                    true
                );
                return `
                    <label class="componente-editor-propriedade-label" for="${propriedade.atributo}">
                        ${propriedade.titulo}:
                    </label>
                    <div class="componente-editor-container-campo">
                        <textarea
                            class="componente-editor-propriedade-valor"
                            id="${propriedade.atributo}"
                            "${propriedade.leitura ? 'readonly' : ''}"
                            data-evento="${idEvento}"
                        >${propriedade.valor || ''}</textarea>
                        ${hint}
                    </div>
                `;
            }
            case ComponenteEditorTiposPropriedade.LISTA:
            case ComponenteEditorTiposPropriedade.LISTA_MULTIPLA: {
                let multiplo = false;
                let valor = propriedade.valor;
                if (propriedade.tipo === ComponenteEditorTiposPropriedade.LISTA_MULTIPLA) {
                    multiplo = true;
                    valor = JSON.stringify(valor);
                } else if (!valor) {
                    propriedade.opcoes.forEach((opcao) => {
                        if (opcao.padrao) {
                            valor = opcao.valor;
                            propriedade.valor = opcao.valor;
                        }
                    });
                }
                const idEvento = this.addPostRenderEvent(
                    'change',
                    (event) => {
                        if (propriedade.leitura) {
                            return;
                        }
                        const tg = /** @type {GOVBRInputSelect} */ (event.target);
                        let valor = tg.dataValor;
                        if (propriedade.tipo === ComponenteEditorTiposPropriedade.LISTA_MULTIPLA) {
                            valor = JSON.parse(valor || '[]');
                        }
                        if (propriedade.change) {
                            propriedade.change(componente, propriedade, valor);
                            this.render(true);
                        } else {
                            propriedade.valor = valor;
                            this.render();
                        }
                    },
                    true
                );
                return `
                    <label class="componente-editor-propriedade-label" for="${propriedade.atributo}">
                        ${propriedade.titulo}:
                    </label>
                    <div class="componente-editor-container-campo">
                        <govbr-plataforma-input-select
                            class="componente-editor-propriedade-valor"
                            data-id="${propriedade.atributo}"
                            ${multiplo ? 'data-multiselecao' : ''}
                            data-valor="${multiplo ? (valor || '').replace(/"/g, '&quot;').replace(/>/g, '&gt;').replace(/<">/g, '&lt;') : valor || ''}"
                            ${propriedade.leitura ? 'data-leitura' : ''}
                            data-evento="${idEvento}"
                        >
                            ${propriedade.opcoes
                                .map(
                                    (opcao) => `
                                <govbr-plataforma-input-select-item data-valor=${opcao.valor}>
                                    ${opcao.icone ? `<i aria-hidden class="govbr-icone-fa ${opcao.icone}"></i>` : ''} ${opcao.titulo}
                                </govbr-plataforma-input-select-item>
                            `
                                )
                                .join('')}
                        </govbr-plataforma-input-select>
                        ${hint}
                    </div>
                `;
            }
            case ComponenteEditorTiposPropriedade.SUGGEST:
                const idEvento = this.addPostRenderEvent(
                    'select-item',
                    (event) => {
                        if (propriedade.leitura) {
                            return;
                        }
                        const tg = /** @type {HTMLElement} */ (event.target);
                        const input = /** @type {HTMLInputElement} */ (tg.querySelector('.componente-editor-propriedade-valor'));
                        if (propriedade.change) {
                            propriedade.change(componente, propriedade, input.value);
                            this.render(true);
                        } else {
                            propriedade.valor = input.value;
                            this.render();
                        }
                    },
                    true
                );
                const idEventoChange = this.addPostRenderEvent('change', (event) => {
                    if (propriedade.leitura) {
                        return;
                    }
                    const tg = /** @type {HTMLInputElement} */ (event.target);
                    if (tg.value === '') {
                        if (propriedade.change) {
                            propriedade.change(componente, propriedade, tg.value);
                            this.render(true);
                        } else {
                            propriedade.valor = tg.value;
                            this.render();
                        }
                    }
                });
                return `
                    <label class="componente-editor-propriedade-label" for="${propriedade.atributo}">
                        ${propriedade.titulo}:
                    </label>
                    <div class="componente-editor-container-campo">
                        <govbr-plataforma-input-suggest
                            data-force-valor
                            data-fonte=""
                            data-min-length="1"
                            data-dados="${JSON.stringify(
                                propriedade.opcoes.map((opcao) => {
                                    return {
                                        valor: opcao.valor,
                                        camposAdicionais: opcao.titulo ? { titulo: opcao.titulo } : null,
                                        icone: opcao.icone,
                                        termos: opcao.termos,
                                    };
                                })
                            )
                                .replace(/"/g, '&quot;')
                                .replace(/>/g, '&gt;')
                                .replace(/<">/g, '&lt;')}"
                            data-evento="${idEvento}"
                        >
                            <input
                                class="componente-editor-propriedade-valor"
                                id="${propriedade.atributo}"
                                type="text"
                                ${propriedade.leitura ? 'readonly' : ''}
                                data-evento="${idEventoChange}"
                                value="${propriedade.valor || ''}"
                            ></input>
                        </govbr-input-suggest>
                        ${hint}
                    </div>
                `;
            case ComponenteEditorTiposPropriedade.COR:
            case ComponenteEditorTiposPropriedade.BOOLEANO:
            case ComponenteEditorTiposPropriedade.TEXTO:
            default: {
                let limpar = '';
                if (propriedade.tipo == ComponenteEditorTiposPropriedade.COR) {
                    const idEventoClick = this.addPostRenderEvent('click', (event) => {
                        event.preventDefault();
                        const tg = /** @type {HTMLElement} */ (event.target);
                        /** @type {HTMLInputElement} */ (tg.previousElementSibling).value = '#000000';
                    });
                    limpar = `
                        <button
                            type="button"
                            class="toggle limpar-input"
                            data-evento="${idEventoClick}"
                        >
                            <i aria-hidden class="govbr-icone-fa fa-solid fa-times"></i>
                        </button>
                    `;
                }
                const idEventoChange = this.addPostRenderEvent(
                    'change',
                    (event) => {
                        if (propriedade.leitura) {
                            return;
                        }
                        const tg = /** @type {HTMLInputElement} */ (event.target);
                        const valor = propriedade.tipo === ComponenteEditorTiposPropriedade.BOOLEANO ? tg.checked : tg.value;
                        if (propriedade.change) {
                            propriedade.change(componente, propriedade, valor);
                            this.render(true);
                        } else {
                            propriedade.valor = valor;
                            this.render();
                        }
                    },
                    true
                );
                return `
                    <label class="componente-editor-propriedade-label" for="${propriedade.atributo}">
                        ${propriedade.titulo}:
                    </label>
                    <div class="componente-editor-container-campo">
                        <input
                            class="componente-editor-propriedade-valor"
                            id="${propriedade.atributo}"
                            type="${
                                Object.fromEntries([
                                    [ComponenteEditorTiposPropriedade.COR, 'color'],
                                    [ComponenteEditorTiposPropriedade.TEXTO, 'text'],
                                    [ComponenteEditorTiposPropriedade.NUMERO, 'number'],
                                    [ComponenteEditorTiposPropriedade.BOOLEANO, 'checkbox'],
                                ])[propriedade.tipo]
                            }"
                            ${propriedade.leitura ? 'readonly' : ''}
                            data-evento="${idEventoChange}"
                            ${propriedade.tipo !== ComponenteEditorTiposPropriedade.BOOLEANO && propriedade.valor ? `value="${propriedade.valor}"` : ''}
                            ${propriedade.tipo === ComponenteEditorTiposPropriedade.BOOLEANO && propriedade.valor ? 'checked' : ''}
                        ></input>
                        ${limpar}
                        ${hint}
                    </div>
                `;
            }
        }
    }

    /**
     * Monta o HTML do grid de uma propriedade.
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor} componente
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditorPropriedade} propriedadeOriginal
     * @returns
     */
    montaGridPropriedade(componente, propriedadeOriginal) {
        const propriedades = propriedadeOriginal.subtipos;
        const change = propriedadeOriginal.change;
        let id = 0;
        const valor = (propriedadeOriginal.valor || []).filter(
            /**
             * @param {Record<string, any>} valores
             */
            (valores) => {
                let possuiValor = false;
                propriedades.forEach((propriedade) => {
                    if (valores[propriedade.atributo]) {
                        possuiValor = true;
                    }
                });
                if (possuiValor && !valores['__id']) {
                    valores['__id'] = ++id;
                }
                return possuiValor;
            }
        );
        propriedadeOriginal.valor = valor;
        return valor
            .concat([{ __id: ++id }])
            .map(
                /**
                 * @param {Record<string, any>} valores
                 * @param {number} idx
                 */
                (valores, idx) => {
                    return `
<div class="componente-editor-propriedades-fs-linha">
    ${propriedades
        .map((propriedade) => {
            const novaPropriedade = JSON.parse(JSON.stringify(propriedade));
            let classe = 'componente-editor-propriedade';
            if (
                (!idx && propriedades?.length === 1) ||
                propriedade.tipo === ComponenteEditorTiposPropriedade.TEXTO_LONGO ||
                propriedade.tipo === ComponenteEditorTiposPropriedade.HTML ||
                propriedade.tipo === ComponenteEditorTiposPropriedade.ARRAY
            ) {
                classe += ' componente-editor-propriedade-full';
            }
            novaPropriedade.change =
                /**
                 * @param {never} _
                 * @param {never} __
                 * @param {any} novoValor
                 */
                (_, __, novoValor) => {
                    if (propriedade.change || change) {
                        propriedade?.change?.(componente, propriedadeOriginal, novoValor);
                        change?.(componente, propriedadeOriginal, novoValor);
                    } else {
                        valores[propriedade.atributo] = novoValor;
                    }
                    if (idx >= propriedadeOriginal.valor.length) {
                        propriedadeOriginal.valor = valor.concat(valores);
                    }
                };
            novaPropriedade.atributo = novaPropriedade.atributo + '_' + idx + 1;
            novaPropriedade.valor = valores[propriedade.atributo];
            return `
            <div class="${classe}">
                ${this.montaInputPropriedade(componente, novaPropriedade)}
            </div>
        `;
        })
        .join('')}
</div>`;
                }
            )
            .join('');
    }

    /**
     * Cria um identificador único de evento, após terminar a renderização, irá buscar o elemento contendo este identificador em um atributo data-evento e apendar o evento.
     * @param {string} evento
     * @param {(event: Event) => void} fn
     * @param {boolean} dispatchOnInit
     */
    addPostRenderEvent(evento, fn, dispatchOnInit = false) {
        const idEvento = ('' + Math.floor(Math.random() * 10000000)).padStart(7, '0') + new Date().getTime();
        this.postRenderEvents[idEvento] = { evento, fn, dispatchOnInit };
        return idEvento;
    }

    atualizaBaseEditor() {
        this.baseEditor.className = `base-editor editor-resolucao-${(this.resolucaoAlvo || 'Ld').toLowerCase()}`;
        this.baseEditor.style.setProperty('--numero-linhas', '' + this.camadaLinhas.querySelectorAll('.linha-editor').length);
    }

    atualizaRegua() {
        let resolucaoAlvo = 0;
        switch (this.resolucaoAlvo) {
            case ResolucoesPagina.CELULAR:
                resolucaoAlvo = 640;
                break;
            case ResolucoesPagina.TABLET:
                resolucaoAlvo = 860;
                break;
            case ResolucoesPagina.LAPTOPS:
                resolucaoAlvo = 1024;
                break;
            case ResolucoesPagina.DESKTOP:
            default:
                resolucaoAlvo = 1120;
                break;
        }
        this.host.querySelector('.editor-pagina-regua-meio').innerHTML = `${resolucaoAlvo / 2}px`;
        this.host.querySelector('.editor-pagina-regua-fim').innerHTML = `${resolucaoAlvo}px`;
    }

    atualizaBotoesAtivos() {
        Array.from(this.host.querySelector('.toggle-resolucoes').children).forEach((e, idx) => {
            if (this.resolucaoAlvo === [ResolucoesPagina.DESKTOP, ResolucoesPagina.LAPTOPS, ResolucoesPagina.TABLET, ResolucoesPagina.CELULAR][idx]) {
                e.classList.add('ativo');
            } else {
                e.classList.remove('ativo');
            }
        });
    }

    atualizaComponenteSelecionado() {
        this.camadaComponentes.querySelector('.elemento-selecionado')?.classList?.remove('elemento-selecionado');
        if (this.componenteSelecionado) {
            this.camadaComponentes.querySelector('[data-id=' + this.componenteSelecionado + ']')?.classList?.add('elemento-selecionado');
        }
    }

    // ***** DRAG/DROP *****

    /**
     * Tratamentos para iniciar arrastar um componente.
     * @param {PointerEvent} event
     */
    onDragStartComponenteEditor(event) {
        if (this.componenteArrastando) {
            this.onDropComponenteEditor(false);
        }
        if (this.alvoResize) {
            this.alvoResize.classList.remove('drag-resize-movendo');
            this.alvoResize = null;
            this.posicaoInicialResize = null;
        }
        const tg = /** @type {HTMLElement} */ (event.target);
        if (tg.classList.contains('componente-editor')) {
            this.iniciaDragComponente(event, tg);
            return true;
        }
        if (tg.classList.contains('drag-resize-componente')) {
            this.alvoResize = tg;
            this.alvoResize.classList.add('drag-resize-movendo');
            this.posicaoInicialResize = { x: event.clientX, y: event.clientY };
            return true;
        }
        if (tg.classList.contains('coluna-editor') || tg.classList.contains('pagina-editor')) {
            this.componenteSelecionado = null;
        }
        return false;
    }

    /**
     * Inicia o "Arrastar" de um componente.
     * @param {PointerEvent} event
     * @param {HTMLElement} tg
     */
    iniciaDragComponente(event, tg) {
        this.baseEditor.classList.add('arrastando-componente');
        this.componenteArrastando = this.componenteArrastandoPadrao;
        this.componenteArrastando.setAttribute('data-id', tg.getAttribute('data-id'));
        this.componenteArrastando.querySelector('.govbr-icone-fa').className = tg.querySelector('.govbr-icone-fa').className;
        if (tg.classList.contains('componente-editor-aplicado')) {
            this.componenteArrastando.classList.add('componente-editor-aplicado');
        } else {
            this.componenteArrastando.classList.remove('componente-editor-aplicado');
        }
        this.componenteArrastando.classList.remove('oculto');
        if (this.componenteArrastando.classList.contains('componente-editor-aplicado')) {
            const alvoInicialMovimento = document.elementFromPoint(event.clientX, event.clientY);
            if (alvoInicialMovimento.classList.contains('coluna-editor')) {
                const attrPos = this.getAtributoResolucaoAlvo();
                const alvoMovimento = { x: parseInt(alvoInicialMovimento.getAttribute('data-idx')), y: parseInt(alvoInicialMovimento.parentElement.getAttribute('data-idx')) };
                const posicaoElemento = this.componentesAplicados.find((e) => e.id == this.componenteArrastando.getAttribute('data-id'));
                this.offsetMovimento = { x: alvoMovimento.x - posicaoElemento[attrPos].posicaoColuna, y: alvoMovimento.y - posicaoElemento[attrPos].posicaoLinha };
            } else {
                this.offsetMovimento = { x: 0, y: 0 };
            }
        } else {
            this.offsetMovimento = { x: 0, y: 0 };
        }
        this.onDragMoveComponenteEditor(event);
    }

    /**
     * Tratamento para mover o componento.
     * @param {PointerEvent} event
     */
    onDragMoveComponenteEditor(event) {
        if (this.componenteArrastando) {
            if (event.pressure === 0) {
                this.onDropComponenteEditor(false);
                return true;
            }
            const rectBase = this.baseEditor.getBoundingClientRect();
            const posCursor = {
                x: event.clientX - rectBase.x,
                y: event.clientY - rectBase.y,
            };
            this.componenteArrastando.style.setProperty('--posicao-x', posCursor.x + 'px');
            this.componenteArrastando.style.setProperty('--posicao-y', posCursor.y + 'px');
            this.alvoDropComponente = /** @type {HTMLElement} */ (document.elementFromPoint(event.clientX, event.clientY));
            this.atualizaAlvoDropComponente();
            return true;
        }
        if (this.alvoResize) {
            if (event.pressure === 0) {
                this.alvoResize.classList.remove('drag-resize-movendo');
                this.alvoResize = null;
                this.posicaoInicialResize = null;
                this.render();
                return true;
            }
            const componenteAlvoDrop = this.componentesAplicados.find((e) => e.id == this.alvoResize.parentElement.getAttribute('data-id'));
            if (componenteAlvoDrop) {
                const deltaTamanho = {
                    x: event.clientX - this.posicaoInicialResize.x,
                    y: event.clientY - this.posicaoInicialResize.y,
                };
                const baseTamanho = this.host.querySelector('.coluna-editor').getBoundingClientRect();
                this.trataResizeComponente(this.alvoResize.getAttribute('data-direcao'), componenteAlvoDrop, baseTamanho, deltaTamanho);
            }
        }
        return false;
    }

    /**
     * Trata o redimensionamento do componente.
     * @param {string} direcao
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor} componenteAlvoDrop
     * @param {DOMRect} baseTamanho
     * @param {{x: number, y: number}} deltaTamanho
     */
    trataResizeComponente(direcao, componenteAlvoDrop, baseTamanho, deltaTamanho) {
        const dimensionaVerticalAcima = /n/.test(direcao);
        const dimensionaHorizontalEsquerda = /w/.test(direcao);
        const podeRedimensionarVertical = /[ns]/.test(direcao);
        const podeRedimensionarHorizontal = /[we]/.test(direcao);
        let redimensionou = false;
        if (podeRedimensionarHorizontal) {
            while (
                deltaTamanho.x > baseTamanho.width / 2 + 5 &&
                this.aplicaEtapaResizeComponente(componenteAlvoDrop, 'numeroColunas', 'posicaoColuna', dimensionaHorizontalEsquerda, true, 13)
            ) {
                deltaTamanho.x -= baseTamanho.width;
                this.posicaoInicialResize.x += baseTamanho.width;
                redimensionou = true;
            }
            while (
                deltaTamanho.x < baseTamanho.width / -2 - 5 &&
                this.aplicaEtapaResizeComponente(componenteAlvoDrop, 'numeroColunas', 'posicaoColuna', dimensionaHorizontalEsquerda, false, 13)
            ) {
                deltaTamanho.x += baseTamanho.width;
                this.posicaoInicialResize.x -= baseTamanho.width;
                redimensionou = true;
            }
        }
        if (podeRedimensionarVertical) {
            while (
                deltaTamanho.y > baseTamanho.height / 2 + 5 &&
                this.aplicaEtapaResizeComponente(componenteAlvoDrop, 'numeroLinhas', 'posicaoLinha', dimensionaVerticalAcima, true, -1)
            ) {
                deltaTamanho.y -= baseTamanho.height;
                this.posicaoInicialResize.y += baseTamanho.height;
                redimensionou = true;
            }
            while (
                deltaTamanho.y < baseTamanho.height / -2 - 5 &&
                this.aplicaEtapaResizeComponente(componenteAlvoDrop, 'numeroLinhas', 'posicaoLinha', dimensionaVerticalAcima, false, -1)
            ) {
                deltaTamanho.y += baseTamanho.height;
                this.posicaoInicialResize.y -= baseTamanho.height;
                redimensionou = true;
            }
        }
        if (redimensionou) {
            this.render(true);
        }
    }

    /**
     * Aplica uma etapa de redimensionamento sobre o componente.
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor} componente
     * @param {StrPropTamanho} propTamanho
     * @param {StrPropPosicao} propPosicao
     * @param {boolean} inverteDirecao
     * @param {boolean} aumenta
     * @param {number} tamanhoMaximo
     * @returns
     */
    aplicaEtapaResizeComponente(componente, propTamanho, propPosicao, inverteDirecao, aumenta, tamanhoMaximo) {
        const attrPos = this.getAtributoResolucaoAlvo();
        if (aumenta) {
            if (inverteDirecao && componente[attrPos][propTamanho] > 1) {
                // Movendo a borda esquerda para direita
                componente[attrPos][propTamanho]--;
                componente[attrPos][propPosicao]++;
                return true;
            } else if (!inverteDirecao && (tamanhoMaximo < 0 || componente[attrPos][propTamanho] + componente[attrPos][propPosicao] < tamanhoMaximo)) {
                // Movendo a borda direita para a direita
                componente[attrPos][propTamanho]++;
                return true;
            }
        } else if (inverteDirecao && componente[attrPos][propPosicao] > 1) {
            // Movendo a borda esquerda para a esquerda
            componente[attrPos][propTamanho]++;
            componente[attrPos][propPosicao]--;
            return true;
        } else if (!inverteDirecao && componente[attrPos][propTamanho] > 1) {
            // Movendo a borda direita para a esquerda
            componente[attrPos][propTamanho]--;
            return true;
        }
        return false;
    }

    /**
     * Atualiza o elemento alvo que mostra onde o componente será solto
     */
    atualizaAlvoDropComponente() {
        if (this.componenteArrastando && this.alvoDropComponente?.classList?.contains('coluna-editor')) {
            const attrPos = this.getAtributoResolucaoAlvo();
            this.alvoDropComponenteVisual.classList.remove('oculto');
            const idComponente = this.componenteArrastando.getAttribute('data-id');
            let componenteAlvoDrop;
            if (this.componenteArrastando.classList.contains('componente-editor-aplicado')) {
                componenteAlvoDrop = this.componentesAplicados.find((e) => e.id == idComponente);
            } else {
                componenteAlvoDrop = this.componentes.find((e) => e.id == idComponente);
            }
            const tamanho = componenteAlvoDrop[attrPos].numeroColunas;
            let coluna = Math.max(1, parseInt(this.alvoDropComponente.getAttribute('data-idx')) - this.offsetMovimento.x);
            const linha = Math.max(1, parseInt(this.alvoDropComponente.parentElement.getAttribute('data-idx')) - this.offsetMovimento.y);
            if (coluna + tamanho > 13) {
                coluna = 13 - tamanho;
            }
            this.alvoDropComponenteVisual.style.setProperty('--posicao-x', '' + coluna);
            this.alvoDropComponenteVisual.style.setProperty('--posicao-y', '' + linha);
            this.alvoDropComponenteVisual.style.setProperty('--tamanho-x', '' + tamanho);
            this.alvoDropComponenteVisual.style.setProperty('--tamanho-y', '' + componenteAlvoDrop[attrPos].numeroLinhas);
        } else {
            this.alvoDropComponenteVisual.classList.add('oculto');
        }
    }

    /**
     * Evento ao soltar o componente no editor.
     * @param {boolean} criaComponente
     */
    onDropComponenteEditor(criaComponente) {
        if (this.alvoResize) {
            this.alvoResize.classList.remove('drag-resize-movendo');
            this.alvoResize = null;
            this.posicaoInicialResize = null;
            this.render();
            return true;
        }
        let adicionouMoveuComponente = false;
        if (this.componenteArrastando && criaComponente) {
            adicionouMoveuComponente = this.montaComponenteDrop();
        }
        this.componenteArrastandoPadrao.classList.add('oculto');
        this.componenteArrastando = null;
        this.baseEditor.classList.remove('arrastando-componente');
        this.atualizaAlvoDropComponente();
        if (adicionouMoveuComponente) {
            this.render();
            return true;
        }
        return false;
    }

    /**
     * Monta o componente ao soltar.
     */
    montaComponenteDrop() {
        const alvo = this.alvoDropComponente;
        const idComponente = this.componenteArrastando.getAttribute('data-id');
        const isAplicado = this.componenteArrastando.classList.contains('componente-editor-aplicado');
        if (alvo.classList.contains('coluna-editor')) {
            /**
             * @type {import('./ListaComponentesEditor.mjs').ComponenteEditor}
             */
            let componenteAlvoDrop;
            /**
             * @type {import('./ListaComponentesEditor.mjs').ComponenteEditor}
             */
            let novoComponente;
            if (isAplicado) {
                componenteAlvoDrop = this.componentesAplicados.find((e) => e.id == idComponente);
                novoComponente = componenteAlvoDrop;
            } else {
                componenteAlvoDrop = this.componentes.find((e) => e.id == idComponente);
                novoComponente = JSON.parse(JSON.stringify(componenteAlvoDrop));
                this.atualizaPropriedadesNovoComponente(componenteAlvoDrop, novoComponente, this.componentesAplicados);
                const maxId = this.componentesAplicados
                    .filter((e) => e.id.startsWith(novoComponente.id))
                    .reduce((max, e) => {
                        const id = e.id.split('-');
                        if (parseInt(id[id.length - 1]) > max) {
                            max = parseInt(id[id.length - 1]);
                        }
                        return max;
                    }, 0);
                novoComponente.id += '-' + (maxId + 1);
                this.componentesAplicados.push(novoComponente);
            }
            const attrPos = this.getAtributoResolucaoAlvo();
            const tamanho = componenteAlvoDrop[attrPos].numeroColunas;
            let coluna = Math.max(1, parseInt(this.alvoDropComponente.getAttribute('data-idx')) - this.offsetMovimento.x);
            const linha = Math.max(1, parseInt(this.alvoDropComponente.parentElement.getAttribute('data-idx')) - this.offsetMovimento.y);
            if (coluna + tamanho > 13) {
                coluna = 13 - tamanho;
            }
            this.componenteSelecionado = novoComponente.id;
            this.host.focus();
            if (isAplicado && novoComponente[attrPos].posicaoColuna === coluna && novoComponente[attrPos].posicaoLinha === linha) {
                return false;
            }
            novoComponente[attrPos].posicaoColuna = coluna;
            novoComponente[attrPos].posicaoLinha = linha;
            return true;
        }
        if (isAplicado && !alvo.classList.contains('pagina-editor') && !alvo.classList.contains('pagina-editor-base')) {
            const idxComponenteAlvoDrop = this.componentesAplicados.findIndex((e) => e.id == idComponente);
            this.componentesAplicados.splice(idxComponenteAlvoDrop, 1);
            this.componenteSelecionado = null;
            return true;
        }
        return false;
    }

    /**
     * @returns {StrPosTamanho}
     */
    getAtributoResolucaoAlvo() {
        return /** @type {StrPosTamanho} */ ('posicao' + this.resolucaoAlvo);
    }

    // ***** MÉTODOS AUXILIARES *****

    /**
     * Salva o conteúdo do editor em um JSON.
     * @returns {import('./GOVBREditorPaginasUserControl.mjs').GOVBREditorPaginasUserControlComponente[]}
     */
    salvaJSON() {
        return this.componentesAplicados.map((componente) => {
            const elemento = componente.id.split('-');
            elemento.pop();
            /**
             * @type {import('./GOVBREditorPaginasUserControl.mjs').GOVBREditorPaginasUserControlComponente}
             */
            const retorno = {
                id: componente.id,
                elemento: /** @type {import('./GOVBREditorPaginasUserControl.mjs').GOVBREditorPaginasListaElementos} */ (elemento.join('-')),
            };
            [
                'numeroColunas',
                'numeroLinhas',
                'posicaoColuna',
                'posicaoLinha',
            ].forEach(
                /**
                 * @param {StrPropPosicao|StrPropTamanho} propriedade
                 */
                (propriedade) => {
                    ['', 'Ld', 'Md', 'Sd'].forEach((tamanho) => {
                        const posicao = /** @type {StrPosTamanho} */ ('posicao' + tamanho);
                        if (componente[posicao]?.[propriedade]) {
                            const propTamanho = /** @type {StrPropPosicaoTamanho} */ (propriedade + tamanho);
                            retorno[propTamanho] = componente[posicao][propriedade];
                        }
                    });
                },
                []
            );
            componente.propriedades.forEach((propriedade) => {
                if (!propriedade.valor) {
                    return;
                }
                const caminho = propriedade.atributo.split('.');
                const atributo = caminho.pop();
                let atual = /** @type {any} */ (retorno);
                while (caminho.length) {
                    const novo = caminho.shift();
                    if (!atual[novo]) {
                        atual[novo] = {};
                    }
                    atual = atual[novo];
                }
                atual[atributo] = propriedade.valor;
            });
            return retorno;
        });
    }

    /**
     * Salva o conteúdo do editor em um JSON.
     * @param {any} conteudo
     */
    carregaJSON(conteudo) {
        if (typeof conteudo === 'object' && conteudo['conteudo']) {
            conteudo = conteudo['conteudo'];
        }
        if (!Array.isArray(conteudo)) {
            throw new Error('Conteúdo inválido. Esperado um Array...');
        }
        /**
         * @type {import('./ListaComponentesEditor.mjs').ComponenteEditor[]}
         */
        const novosComponentesAplicados = [];
        conteudo.forEach((componente) => {
            const componenteOriginal = this.componentes.find((componenteOrig) => componente.elemento == componenteOrig.id);
            if (!componenteOriginal || !componente.id) {
                throw new Error('Conteúdo inválido. Não encontrou o componente ' + componente.elemento + ' com id ' + componente.id + '...');
            }
            /**
             * @type {import('./ListaComponentesEditor.mjs').ComponenteEditor}
             */
            const novoComponente = {
                id: componente.id,
                icone: componenteOriginal.icone,
                titulo: componenteOriginal.titulo,
                posicao: { numeroColunas: 1, numeroLinhas: 1 },
                posicaoLd: { numeroColunas: 12, posicaoColuna: 1, numeroLinhas: 1 },
                posicaoMd: { numeroColunas: 12, posicaoColuna: 1, numeroLinhas: 1 },
                posicaoSd: { numeroColunas: 12, posicaoColuna: 1, numeroLinhas: 1 },
                exibeLateral: componenteOriginal.exibeLateral,
                propriedades: componenteOriginal.propriedades.map((propriedade) => {
                    const novaPropriedade = JSON.parse(JSON.stringify(propriedade));
                    const caminho = propriedade.atributo.split('.');
                    const atributo = caminho.pop();
                    let atual = componente;
                    while (caminho.length) {
                        const novo = caminho.shift();
                        if (!atual[novo]) {
                            return novaPropriedade;
                        }
                        atual = atual[novo];
                    }
                    if (atual[atributo]) {
                        novaPropriedade.valor = atual[atributo];
                    }
                    return novaPropriedade;
                }),
            };
            this.atualizaPropriedadesNovoComponente(componenteOriginal, novoComponente, novosComponentesAplicados);
            [
                'numeroColunas',
                'numeroLinhas',
                'posicaoColuna',
                'posicaoLinha',
            ].forEach(
                /**
                 * @param {StrPropPosicao|StrPropTamanho} propriedade
                 */
                (propriedade) => {
                    ['', 'Ld', 'Md', 'Sd'].forEach((tamanho) => {
                        const posicao = /** @type {StrPosTamanho} */ ('posicao' + tamanho);
                        if (componente[propriedade + tamanho]) {
                            novoComponente[posicao][propriedade] = componente[propriedade + tamanho];
                        } else if (!tamanho) {
                            novoComponente[posicao][propriedade] = 1;
                        }
                    });
                },
                []
            );
            novosComponentesAplicados.push(novoComponente);
        });
        this.componentesAplicados = novosComponentesAplicados;
        this.componenteSelecionado = null;
        this.render(true);
    }

    /**
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor} componenteOriginal
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor} novoComponente
     * @param {import('./ListaComponentesEditor.mjs').ComponenteEditor[]} listaComponentes
     */
    atualizaPropriedadesNovoComponente(componenteOriginal, novoComponente, listaComponentes) {
        novoComponente.propriedades.forEach((propriedade, idx) => {
            // Quando realizado o clone para a montagem, as funções JS não são copiadas, neste caso será criada uma cópia manuamente.
            // Há uma cópia desse método no "drop".
            propriedade.change = componenteOriginal.propriedades[idx].change;
            propriedade.opcoes = componenteOriginal.propriedades[idx].opcoes;
            // Dispara o change na primeira criação, certificando assim que as validações destes estão corretas.
            propriedade.change?.(novoComponente, propriedade, propriedade.valor);
        });
        ['', 'Ld', 'Md', 'Sd'].forEach((tamanho) => {
            const posicao = /** @type {StrPosTamanho} */ ('posicao' + tamanho);
            if (!novoComponente[posicao]['posicaoLinha']) {
                let linha = 1;
                listaComponentes
                    .filter((e) => e.exibeLateral)
                    .forEach((componente) => {
                        if ((parseInt('' + componente[posicao]['posicaoLinha']) || 0) >= linha) {
                            linha = (parseInt('' + componente[posicao]['posicaoLinha']) || 0) + 1;
                        }
                    });
                novoComponente[posicao]['posicaoLinha'] = linha;
            }
            if (!novoComponente[posicao]['posicaoColuna']) {
                novoComponente[posicao]['posicaoColuna'] = 1;
            }
        });
    }

    /**
     * Abre um dialog para o Usuário.
     * @param {string} titulo
     */
    abreDialog(titulo) {
        const dialog = document.createElement('dialog');
        dialog.classList.add('govbr-editor-paginas-dialog');
        dialog.addEventListener('close', () => {
            dialog.remove();
        });
        const dialogCt = document.createElement('div');
        dialogCt.classList.add('govbr-editor-paginas-dialog-base');
        dialog.append(dialogCt);
        const conteudo = document.createElement('article');
        conteudo.classList.add('govbr-editor-paginas-dialog-conteudo');
        const cabecalho = document.createElement('h2');
        cabecalho.classList.add('govbr-editor-paginas-dialog-cabecalho');
        cabecalho.innerText = titulo;
        conteudo.append(cabecalho);
        dialogCt.append(conteudo);

        const rodape = document.createElement('nav');
        rodape.classList.add('govbr-editor-paginas-dialog-rodape');
        const botaoCancela = document.createElement('button');
        botaoCancela.type = 'button';
        botaoCancela.classList.add('govbr-editor-paginas-dialog-botao');
        botaoCancela.classList.add('govbr-editor-paginas-dialog-botao-fechar');
        botaoCancela.innerText = 'Fechar';
        botaoCancela.addEventListener('click', () => {
            dialog.close();
        });
        rodape.append(botaoCancela);
        dialogCt.append(rodape);

        document.body.append(dialog);
        dialog.show();
        return conteudo;
    }
}

// Caso esteja no browser, disponibiliza o widget no objeto de window.
if (typeof window !== 'undefined') {
    // @ts-ignore
    window['GOVBREditorPaginasUC'] = GOVBREditorPaginasUC;
}
export { GOVBREditorPaginasUC };
