portal_publico/static/js/ajuda-aplicacao.js

246 lines
9.0 KiB
JavaScript

// Botão "?" reutilizável ao lado do nome de uma aplicação — ao passar o
// mouse mostra "Mais informações" (ver .info-tooltip em components.css); ao
// clicar, abre um modal com o texto cadastrado em GET /api/ajuda-aplicacoes/{app_key}/.
// Visualizar é livre a qualquer autenticado; editar é restrito a quem tem o
// perfil Inovação (me.eh_perfil_inovacao, já calculado no servidor) — regra
// fixa, não uma flag na árvore de permissões (ver CLAUDE.md).
//
// Um único modal é criado sob demanda e reaproveitado por todos os botões da
// página — só um pode estar aberto por vez. Para adicionar o botão a uma
// aplicação nova: no HTML, ao lado do nome da aplicação, `pidCriarBotaoAjuda(
// "<id do botão>", "<app_key>", "<nome de exibição>")` na página que a usa.
//
// O texto aceita imagens embutidas (colar/arrastar), mesmo mecanismo das
// Observações de Acessos Gerais (`.ag-richtext` em acessos-gerais.js) — o
// HTML digitado aqui só é sanitizado no servidor (`AjudaAplicacaoSerializer.
// validate_texto`), então o editor em si não precisa filtrar nada.
const PID_AJUDA_IMAGE_MAX_BYTES = 2 * 1024 * 1024;
function pidAjudaAplicacaoModal() {
let modal = document.getElementById("ajuda-aplicacao-modal");
if (modal) return modal;
modal = document.createElement("div");
modal.className = "modal-overlay";
modal.id = "ajuda-aplicacao-modal";
modal.hidden = true;
modal.dataset.editing = "false";
modal.innerHTML = `
<div class="modal-card ajuda-modal__card">
<h2 class="ajuda-modal__titulo"></h2>
<div class="ajuda-modal__texto"></div>
<div class="ajuda-modal__editor" contenteditable="true" hidden data-placeholder="Escreva aqui — cole (Ctrl+V) ou arraste uma imagem para incluí-la no texto"></div>
<p class="ajuda-modal__editor-hint" hidden>Cole (Ctrl+V) ou arraste uma imagem para incluí-la no texto.</p>
<p class="modal-error" hidden></p>
<div class="modal-actions">
<button type="button" class="btn-outline ajuda-modal__editar-btn" hidden>Editar</button>
<button type="button" class="btn-outline ajuda-modal__cancelar-btn" hidden>Cancelar</button>
<button type="button" class="btn-solid ajuda-modal__salvar-btn" hidden>Salvar</button>
<button type="button" class="btn-outline ajuda-modal__fechar-btn">Fechar</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener("click", (event) => {
if (event.target === modal) pidFecharAjudaAplicacaoModal();
});
modal.querySelector(".ajuda-modal__fechar-btn").addEventListener("click", pidFecharAjudaAplicacaoModal);
const editor = modal.querySelector(".ajuda-modal__editor");
const erro = modal.querySelector(".modal-error");
function placeCaretAtPoint(x, y) {
const selection = window.getSelection();
if (document.caretRangeFromPoint) {
const range = document.caretRangeFromPoint(x, y);
if (range) {
selection.removeAllRanges();
selection.addRange(range);
}
} else if (document.caretPositionFromPoint) {
const pos = document.caretPositionFromPoint(x, y);
if (pos) {
const range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
}
}
function insertImageFile(file, atPoint) {
if (!file || !file.type.startsWith("image/")) return;
if (file.size > PID_AJUDA_IMAGE_MAX_BYTES) {
erro.textContent = "A imagem deve ter no máximo 2MB.";
erro.hidden = false;
return;
}
erro.hidden = true;
const reader = new FileReader();
reader.onload = () => {
editor.focus();
if (atPoint) placeCaretAtPoint(atPoint.x, atPoint.y);
document.execCommand("insertImage", false, reader.result);
};
reader.readAsDataURL(file);
}
editor.addEventListener("paste", (event) => {
const items = Array.from((event.clipboardData && event.clipboardData.items) || []);
const imageItem = items.find((item) => item.type.startsWith("image/"));
if (!imageItem) return;
event.preventDefault();
insertImageFile(imageItem.getAsFile());
});
editor.addEventListener("dragover", (event) => {
event.preventDefault();
});
editor.addEventListener("drop", (event) => {
const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
if (!file || !file.type.startsWith("image/")) return;
event.preventDefault();
insertImageFile(file, { x: event.clientX, y: event.clientY });
});
return modal;
}
async function pidFecharAjudaAplicacaoModal() {
const modal = document.getElementById("ajuda-aplicacao-modal");
if (!modal) return;
if (modal.dataset.editing === "true" && pidAjudaTemAlteracao(modal)) {
const confirmado = await pidConfirm("Sair sem salvar as alterações?", {
titulo: "Sair sem salvar",
textoConfirmar: "Sair sem salvar",
perigoso: true,
});
if (!confirmado) return;
}
modal.dataset.editing = "false";
modal.hidden = true;
}
/** Compara o HTML atual do editor com o da abertura da edição (guardado em
* `modal.dataset.textoOriginal` por entrarEdicao, já depois de o navegador
* normalizar o HTML), pra só avisar de "sair sem salvar" quando algo mudou. */
function pidAjudaTemAlteracao(modal) {
const editor = modal.querySelector(".ajuda-modal__editor");
return editor.innerHTML !== (modal.dataset.textoOriginal || "");
}
function pidAjudaEditorValue(editor) {
const hasImage = editor.querySelector("img") !== null;
const hasText = editor.textContent.trim() !== "";
return hasImage || hasText ? editor.innerHTML : "";
}
async function pidAbrirAjudaAplicacaoModal(appKey, tituloApp) {
const modal = pidAjudaAplicacaoModal();
const titulo = modal.querySelector(".ajuda-modal__titulo");
const textoEl = modal.querySelector(".ajuda-modal__texto");
const editor = modal.querySelector(".ajuda-modal__editor");
const editorHint = modal.querySelector(".ajuda-modal__editor-hint");
const erro = modal.querySelector(".modal-error");
const editarBtn = modal.querySelector(".ajuda-modal__editar-btn");
const cancelarBtn = modal.querySelector(".ajuda-modal__cancelar-btn");
const salvarBtn = modal.querySelector(".ajuda-modal__salvar-btn");
modal.dataset.editing = "false";
titulo.textContent = `Mais informações — ${tituloApp}`;
textoEl.textContent = "Carregando…";
textoEl.hidden = false;
editor.hidden = true;
editorHint.hidden = true;
erro.hidden = true;
editarBtn.hidden = true;
cancelarBtn.hidden = true;
salvarBtn.hidden = true;
modal.hidden = false;
let ajuda = null;
let podeEditar = false;
try {
const me = await pidGetMe();
podeEditar = !!me.eh_perfil_inovacao;
ajuda = await pidApiRequest(`/ajuda-aplicacoes/${encodeURIComponent(appKey)}/`);
} catch (err) {
textoEl.textContent = `Não foi possível carregar o texto de ajuda. (${err.message || "erro desconhecido"})`;
return;
}
function renderVisualizacao() {
modal.dataset.editing = "false";
if (ajuda.texto) {
textoEl.innerHTML = ajuda.texto;
} else {
textoEl.textContent = "Ainda não há um texto cadastrado para esta ferramenta.";
}
textoEl.hidden = false;
editor.hidden = true;
editorHint.hidden = true;
editarBtn.hidden = !podeEditar;
cancelarBtn.hidden = true;
salvarBtn.hidden = true;
erro.hidden = true;
}
function entrarEdicao() {
modal.dataset.editing = "true";
editor.innerHTML = ajuda.texto || "";
// Lido de volta do editor (não de ajuda.texto): o navegador pode normalizar o HTML ao atribuir.
modal.dataset.textoOriginal = editor.innerHTML;
textoEl.hidden = true;
editor.hidden = false;
editorHint.hidden = false;
editarBtn.hidden = true;
cancelarBtn.hidden = false;
salvarBtn.hidden = false;
erro.hidden = true;
editor.focus();
}
editarBtn.onclick = entrarEdicao;
cancelarBtn.onclick = async () => {
if (!pidAjudaTemAlteracao(modal)) {
renderVisualizacao();
return;
}
const confirmado = await pidConfirm("Sair sem salvar as alterações?", {
titulo: "Sair sem salvar",
textoConfirmar: "Sair sem salvar",
perigoso: true,
});
if (confirmado) renderVisualizacao();
};
salvarBtn.onclick = async () => {
if (salvarBtn.disabled) return;
salvarBtn.disabled = true;
salvarBtn.textContent = "Salvando…";
try {
ajuda = await pidApiRequest(`/ajuda-aplicacoes/${encodeURIComponent(appKey)}/`, {
method: "PATCH",
body: { texto: pidAjudaEditorValue(editor) },
});
renderVisualizacao();
} catch (err) {
erro.textContent = err.message || "Não foi possível salvar o texto.";
erro.hidden = false;
} finally {
salvarBtn.disabled = false;
salvarBtn.textContent = "Salvar";
}
};
renderVisualizacao();
}
function pidCriarBotaoAjuda(botaoId, appKey, tituloApp) {
const botao = document.getElementById(botaoId);
if (!botao) return;
botao.addEventListener("click", () => pidAbrirAjudaAplicacaoModal(appKey, tituloApp));
}