silegis-mg/editor-articulacao

View on GitHub
src/ClipboardController.js

Summary

Maintainability
D
2 days
Test Coverage
/* Copyright 2017 Assembleia Legislativa de Minas Gerais
 * 
 * This file is part of Editor-Articulacao.
 *
 * Editor-Articulacao is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 3.
 *
 * Editor-Articulacao is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Editor-Articulacao.  If not, see <http://www.gnu.org/licenses/>.
 */

import { interceptarApos } from './hacks/interceptador';
import { interpretarArticulacao, transformarQuebrasDeLinhaEmP } from './interpretadorArticulacao';

/**
 * Controlador da área de transferência do editor de articulação.
 */
class ClipboardController {
    constructor(editorCtrl, validacaoCtrl) {
        this.editorCtrl = editorCtrl;
        this.validacaoCtrl = validacaoCtrl;

        editorCtrl.registrarEventListener('paste', (event) => aoColar(event, this));
    }

    /**
     * Cola um texto de articulação no editor.
     * 
     * @param {String} texto 
     */
    colarTexto(texto) {
        let selecao = this.editorCtrl.getSelection();
        let range = selecao.getRangeAt(0);

        if (!range.collapsed) {
            range.deleteContents();
            this.editorCtrl.atualizarContexto();
        }

        for (let brs = (range.endContainer.nodeType === Node.TEXT_NODE ? range.endContainer.parentNode : range.endContainer).querySelectorAll('br'), i = 0; i < brs.length; i++) {
            brs[i].remove();
        }

        let fragmento = transformar(texto, this.editorCtrl.contexto.cursor.tipo, this.editorCtrl.contexto.cursor.continuacao);

        colarFragmento(fragmento, this.editorCtrl, this.validacaoCtrl);
    }
}

/**
 * Transforma um texto em um fragmento a ser colado.
 * 
 * @param {*} texto Texto a ser transformado.
 * @param {*} tipo Tipo do contexto atual do cursor.
 * @param {*} continuacao Se o tipo é uma continuação.
 */
function transformar(texto, tipo, continuacao) {
    var fragmento;

    if (continuacao) {
        fragmento = transformarTextoPuro(texto, 'continuacao');
    } else {
        let dados = interpretarArticulacao(texto, 'json');

        if (dados.textoAnterior) {
            fragmento = transformarTextoPuro(dados.textoAnterior, tipo || 'artigo');
        }

        if (dados.articulacao.length > 0) {
            let fragmentoArticulacao = transformarArticulacao(dados.articulacao);

            if (fragmento) {
                fragmento.appendChild(fragmentoArticulacao);
            } else {
                fragmento = fragmentoArticulacao;
            }
        }
    }

    return fragmento;
}

/**
 * Trata a colagem da área de transferência.
 * 
 * @param {ClipboardEvent} event 
 * @param {ClipboardController} clipboardCtrl 
 */
function aoColar(event, clipboardCtrl) {
    var clipboardData = event.clipboardData || window.clipboardData;
    var itens = clipboardData.items;

    if (itens) {
        for (let i = 0; i < itens.length; i++) {
            if (itens[i].type === 'text/plain') {
                itens[i].getAsString(clipboardCtrl.colarTexto.bind(clipboardCtrl));
                event.preventDefault();
            }
        }
    } else if (clipboardData.getData) {
        clipboardCtrl.colarTexto(clipboardData.getData('text/plain'));
        event.preventDefault();
    }
}

/**
 * Cola um fragmento no DOM.
 * 
 * @param {DocumentFragment} fragmento 
 * @param {EditorArticulacaoController} editorCtrl 
 * @param {ValidacaoController} validacaoCtrl
 */
function colarFragmento(fragmento, editorCtrl, validacaoCtrl) {
    prepararDesfazer(fragmento, editorCtrl);

    let proximaSelecao = fragmento.lastChild;
    let selecao = editorCtrl.getSelection();
    let range = selecao.getRangeAt(0);
    let noInicial = range.startContainer;
    let excluirNoAtual = fragmento.firstChild.nodeType !== Node.TEXT_NODE && noInicial !== editorCtrl._elemento && noInicial.textContent.trim().length === 0;
    let primeiroElementoAValidar;

    // Se a seleção estiver no container, então devemos inserir elementos filhos...
    if (range.collapsed && noInicial === editorCtrl._elemento) {
        // Remove as quebras de linha, usada como placeholder pelos contentEditable.
        for (let itens = editorCtrl._elemento.querySelectorAll('br'), i = 0; i < itens.length; i++) {
            itens[i].remove();
        }

        // Remove os elementos vazios.
        for (let itens = editorCtrl._elemento.querySelectorAll('*:empty'), i = 0; i < itens.length; i++) {
            itens[i].remove();
        }

        // Transforma os nós textuais em parágrafos.
        for (let item = fragmento.firstChild; item && item.nodeType === Node.TEXT_NODE; item = fragmento.firstChild) {
            let p = document.createElement('p');
            p.appendChild(item);
            p.setAttribute('data-tipo', 'artigo');
            fragmento.insertBefore(p, fragmento.firstElementChild);
        }

        primeiroElementoAValidar = fragmento.firstElementChild;
        range.insertNode(fragmento);
    } else {
        if (excluirNoAtual) {
            primeiroElementoAValidar = fragmento.firstElementChild;
        } else {
            primeiroElementoAValidar = noInicial.nodeType === Node.ELEMENT_NODE ? noInicial : noInicial.parentElement;
        }

        // Insere os primeiros nós textuais na própria seleção.
        for (let item = fragmento.firstChild; item && item.nodeType === Node.TEXT_NODE; item = fragmento.firstChild) {
            range.insertNode(item);
        }

        // Insere os elementos em seguida, no container, pois não podem estar aninhados ao elemento da seleção.
        let referencia = range.endContainer;

        while (referencia.parentElement !== editorCtrl._elemento) {
            referencia = referencia.parentElement;
        }
        
        referencia.parentElement.insertBefore(fragmento, referencia.nextSibling);
    }

    if (excluirNoAtual) {
        noInicial.remove();
    }

    // Altera a seleção.
    selecao.removeAllRanges();
    range = document.createRange();

    range.setStartAfter(proximaSelecao);
    selecao.addRange(range);
    
    editorCtrl.atualizarContexto();

    // Realiza validação do fragmento colado.
    let ultimoElementoAValidar = proximaSelecao.nodeType === Node.ELEMENT_NODE ? proximaSelecao : proximaSelecao.parentElement;

    for (let noAlterado = primeiroElementoAValidar; noAlterado && noAlterado !== ultimoElementoAValidar; noAlterado = noAlterado.nextElementSibling) {
        validacaoCtrl.validar(noAlterado);
    }
}

/**
 * Realiza uma cópia do fragmento e monitora ctrl+z para remover fragmentos.
 * 
 * @param {DocumentFragment} fragmento 
 * @param {EditorArticulacaoController} editorCtrl 
 */
function prepararDesfazer(fragmento, editorCtrl) {
    var copia = [];

    for (let i = 0, l = fragmento.childNodes.length; i < l; i++) {
        copia.push(fragmento.childNodes[i]);
    }

    let desfazer = function() {
        let anterior = copia[0].previousSibling;
        let posterior = copia[copia.length - 1].nextSibling;

        // Remove os elementos
        for (let i = 0; i < copia.length; i++) {
            copia[i].remove();
        }

        removerListeners();

        // Restaura o cursor.
        let selecao = editorCtrl.getSelection();
        let range = document.createRange();

        if (anterior) {
            range.setStartAfter(anterior);
            selecao.removeAllRanges();
            selecao.addRange(range);
        } else if (posterior) {
            range.setStartBefore(posterior);
            selecao.removeAllRanges();
            selecao.addRange(range);
        }
    };

    let keyDownListener = function(keyboardEvent) {
        // Desfaz se pressionar o ctrl+z
        if (keyboardEvent.ctrlKey && (keyboardEvent.key === 'z' || keyboardEvent.key === 'Z')) {
            desfazer();
            keyboardEvent.preventDefault();
        }
    };

    let bakExecCommand = document.execCommand;

    let removerListeners = function() {
        editorCtrl._elemento.removeEventListener('keydown', keyDownListener);
        editorCtrl._elemento.removeEventListener('keypress', removerListeners);
        document.execCommand = bakExecCommand;
    };

    editorCtrl._elemento.addEventListener('keydown', keyDownListener);
    editorCtrl._elemento.addEventListener('keypress', removerListeners);

    document.execCommand = function(comando) {
        if (comando === 'undo') {
            desfazer();
        } else {
            return bakExecCommand.apply(document, arguments);
        }
    };
}

/**
 * Insere texto simples no contexto atual.
 * 
 * @param {String} texto  Texto a ser interpretado.
 * @param {String} tipo Tipo atual.
 * @returns {DocumentFragment} Fragmento gerado para colagem.
 */
function transformarTextoPuro(texto, tipo) {
    if (texto.length === 0) {
        return;
    }

    var fragmento = document.createDocumentFragment();
    let primeiraQuebra = texto.indexOf('\n');

    if (primeiraQuebra !== 0) {
        /* A primeira linha deve ser inserida no dispositivo atual.
         * Para tanto, utiliza-se um TextNode.
         */
        let textNode = document.createTextNode(primeiraQuebra > 0 ? texto.substr(0, primeiraQuebra) : texto);
        fragmento.appendChild(textNode);
    }

    if (primeiraQuebra !== -1) {
        // Demais linhas devem ser criadas em novos parágrafos.
        let novoTexto = transformarQuebrasDeLinhaEmP(texto.substr(primeiraQuebra + 1));
        fragmento.appendChild(novoTexto);

        /**
         * Define o tipo para cada P, formatando automaticamente com base na terminação de dois pontos (:) ou ponto final(.).
         */
        let anterior = [];

        if (fragmento.firstChild.nodeType === Node.TEXT_NODE && fragmento.firstChild.textContent.endsWith(':')) {
            let enumeracao = novoTipoEnumeracao(tipo);

            if (tipo !== enumeracao) {
                anterior.push(tipo);
                tipo = enumeracao;
            }
        }

        for (let item = fragmento.firstElementChild; item; item = item.nextElementSibling) {
            item.setAttribute('data-tipo', tipo);

            if (item.textContent.endsWith(':')) {
                let enumeracao = novoTipoEnumeracao(tipo);

                if (tipo !== enumeracao) {
                    anterior.push(tipo);
                    tipo = enumeracao;
                }
            } else if (anterior.length > 0 && item.textContent.endsWith('.')) {
                tipo = anterior.pop();
            }
        }
    }

    return fragmento;
}

function novoTipoEnumeracao(tipoAtual) {
    switch (tipoAtual) {
        case 'artigo':
        case 'paragrafo':
            return 'inciso';

        case 'inciso':
            return 'alinea';

        case 'alinea':
            return 'item';

        default:
            return null;
    }
}

function transformarArticulacao(articulacao) {
    let fragmento = document.createDocumentFragment();

    articulacao.forEach(item => fragmento.appendChild(item.paraEditor()));

    return fragmento;
}

export { ClipboardController, transformarQuebrasDeLinhaEmP, transformarTextoPuro, transformar };
export default ClipboardController;