src/ClipboardController.js
/* 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;