Skip to content

Implementar lista dinâmica de autores e modal de autoria nos artigos #1218

@robertatakenaka

Description

@robertatakenaka

Descrição da tarefa

Relacionado com scieloorg/opac_5#469

Objetivo

Implementar o novo comportamento da lista de autores exibida abaixo do título do artigo e do modal de autoria aberto ao clicar em cada autor.

A implementação deve usar JavaScript para montar:

  1. A lista de autores abaixo do título;
  2. O colapso da lista quando houver muitos autores;
  3. O modal com os dados do autor clicado;
  4. A indicação visual de autor correspondente;
  5. As seções complementares do corpo do artigo:
    • Contribuição de autoria;
    • Conflito de interesses;
    • Editor Responsável.

Estrutura HTML esperada para a lista de autores

Abaixo do título do artigo deve existir um container vazio:

<div class="scielo__contribGroup">
  <ul aria-label="Lista de autores" class="author-list" id="authorList"></ul>
</div>

Deve haver um único modal na página para a exibição dos detalhes de cada um dos autores:

<!-- Modal Authors -->
<div class="modal fade ModalDefault ModalTutors"
     id="ModalTutors"
     tabindex="-1"
     role="dialog"
     aria-modal="true"
     aria-labelledby="ModalTutorsLabel"
     aria-hidden="true">

  <div class="modal-dialog modal-lg">
    <div class="modal-content">

      <div class="modal-header">
        <h2 class="h4 modal-title" id="ModalTutorsLabel">
          Authorship
        </h2>

        <button type="button"
                class="btn-close"
                data-bs-dismiss="modal"
                aria-label="Fechar">
        </button>
      </div>

      <div class="modal-body">

        <div id="authorCardsContainer"></div>

        <div class="correspondence mt-3 mb-4 d-none"
             id="correspondenceSection">

          <div class="section-grid">

            <span class="material-icons-outlined"
                  aria-hidden="true">
              person
            </span>

            <div>
              <p class="section-title mb-1">
                Correspondence:
              </p>

              <div class="section-content">
                <span id="correspondenceText"></span>
                <br>

                <a href=""
                   id="correspondenceEmail">
                </a>
              </div>
            </div>

          </div>

        </div>

      </div>

    </div>
  </div>
</div>

Javascript base esperado

const MAX_VISIBLE_AUTHORS = 25;
const ALWAYS_VISIBLE_FIRST = 2;
const ALWAYS_VISIBLE_LAST = 1;

const authorList = document.getElementById("authorList");

const authorModalElement = document.getElementById("ModalTutors");
const authorCardsContainer = document.getElementById("authorCardsContainer");
const correspondenceSection = document.getElementById("correspondenceSection");
const correspondenceText = document.getElementById("correspondenceText");
const correspondenceEmail = document.getElementById("correspondenceEmail");

const authorModal = authorModalElement
  ? new bootstrap.Modal(authorModalElement)
  : null;

let expanded = false;

Estrutura de dados esperada

const authors = [
  {
    name: "John H. Livingston",
    affiliations: ["1", "2"],
    orcid: "0000-0002-4881-3620",
    email: "[email protected]",
    corresponding: true,
    roles: "Conceptualization · Investigation · Writing – original article"
  },
  {
    name: "Erik A. Petigura",
    affiliations: ["3"],
    orcid: "0000-0003-0967-2893",
    email: "[email protected]",
    corresponding: true,
    roles: "Methodology · Data analysis"
  }
];

As afiliações devem vir de um mapa separado

const affiliationMap = {
  "1": "National Astronomical Observatory of Japan – Tokyo, Japan.",
  "2": "Astrobiology Center – Tokyo, Japan.",
  "3": "Department of Physics and Astronomy, University of California – Los Angeles, USA."
};

Função para criar o botão do autor

function createAuthorButton(author, index) {
  const button = document.createElement("button");

  button.type = "button";
  button.className = "btn-link px-0";
  button.setAttribute("data-author-index", index);
  button.setAttribute("aria-label", `Abrir detalhes de ${author.name}`);

  if (author.corresponding || author.email) {
    const icon = document.createElement("span");

    icon.className = "material-icons-outlined me-1 fs-6";
    icon.setAttribute("aria-hidden", "true");
    icon.textContent = "mail";

    button.appendChild(icon);
  }

  const name = document.createElement("span");
  name.textContent = author.name;

  button.appendChild(name);

  button.addEventListener("click", () => {
    openAuthorModal(index);
  });

  return button;
}

Renderização da lista de autores

function appendAuthorItem(author, index, isLastVisibleItem) {
  const li = document.createElement("li");

  li.appendChild(createAuthorButton(author, index));

  if (!isLastVisibleItem) {
    const separator = document.createElement("span");
    separator.className = "author-separator";
    separator.setAttribute("aria-hidden", "true");
    separator.textContent = ",";

    li.appendChild(separator);
  }

  authorList.appendChild(li);
}

Botão para expandir ou ocultar autores

function appendSummaryItem(hiddenCount, isLastItem = false) {
  const li = document.createElement("li");

  const button = document.createElement("button");
  button.type = "button";
  button.className = "btn btn-secondary btn-sm outlineFadeLink ms-0";

  if (expanded) {
    button.textContent = "Ocultar autores";
    button.setAttribute("aria-label", "Ocultar autores intermediários");
    button.setAttribute("aria-expanded", "true");
  } else {
    button.textContent = `+ ${hiddenCount} autores`;
    button.setAttribute("aria-label", `Mostrar os ${hiddenCount} autores ocultos`);
    button.setAttribute("aria-expanded", "false");
  }

  button.addEventListener("click", () => {
    expanded = !expanded;
    renderAuthors();
  });

  li.appendChild(button);

  if (!isLastItem) {
    const separator = document.createElement("span");
    separator.className = "author-separator";
    separator.setAttribute("aria-hidden", "true");
    separator.textContent = ",";

    li.appendChild(separator);
  }

  authorList.appendChild(li);
}

Lógica de colapso da lista

function renderAuthors() {
  if (!authorList) return;

  authorList.innerHTML = "";

  const total = authors.length;
  const hiddenCount = total - (ALWAYS_VISIBLE_FIRST + ALWAYS_VISIBLE_LAST);
  const canCollapse = total > MAX_VISIBLE_AUTHORS && hiddenCount > 0;

  if (!canCollapse) {
    authors.forEach((author, index) => {
      appendAuthorItem(author, index, index === total - 1);
    });

    return;
  }

  if (expanded) {
    authors.forEach((author, index) => {
      appendAuthorItem(author, index, index === total - 1);
    });

    appendSummaryItem(hiddenCount, true);
    return;
  }

  const firstAuthors = authors.slice(0, ALWAYS_VISIBLE_FIRST);
  const lastAuthors = authors.slice(total - ALWAYS_VISIBLE_LAST);

  firstAuthors.forEach((author, index) => {
    appendAuthorItem(author, index, false);
  });

  appendSummaryItem(hiddenCount, false);

  lastAuthors.forEach((author, idx) => {
    const realIndex = total - ALWAYS_VISIBLE_LAST + idx;
    appendAuthorItem(author, realIndex, true);
  });
}

Montagem do conteúdo do modal

function buildInstitutionText(author) {
  return author.affiliations
    .map(code => affiliationMap[code] || `Afiliação ${code}`)
    .join(" ");
}
function buildAuthorCard(author) {
  const card = document.createElement("div");
  card.className = "author-card";

  const nameRow = document.createElement("div");
  nameRow.className = "author-grid-row";

  const personIcon = document.createElement("span");
  personIcon.className = "material-icons-outlined";
  personIcon.setAttribute("aria-hidden", "true");
  personIcon.textContent = "person";

  const nameWrapper = document.createElement("div");
  nameWrapper.className = "author-name";

  const nameText = document.createElement("span");
  nameText.className = "author-name-text";
  nameText.textContent = author.name;

  nameWrapper.appendChild(nameText);
  nameRow.appendChild(personIcon);
  nameRow.appendChild(nameWrapper);

  card.appendChild(nameRow);

  const roles = document.createElement("div");
  roles.className = "author-roles author-subrow mb-2";
  roles.textContent = author.roles || "Sem funções informadas.";

  card.appendChild(roles);

  const institutionRow = document.createElement("div");
  institutionRow.className = "author-grid-row author-subrow mb-2";

  const schoolIcon = document.createElement("span");
  schoolIcon.className = "material-icons-outlined";
  schoolIcon.setAttribute("aria-hidden", "true");
  schoolIcon.textContent = "school";

  const institution = document.createElement("div");
  institution.className = "author-institution";

  const institutionText = document.createElement("span");
  institutionText.className = "author-inst-text";
  institutionText.textContent = buildInstitutionText(author);

  institution.appendChild(institutionText);
  institutionRow.appendChild(schoolIcon);
  institutionRow.appendChild(institution);

  card.appendChild(institutionRow);

  if (author.orcid) {
    const orcidWrap = document.createElement("div");
    orcidWrap.className = "orcid-button-wrap ms-4";

    const orcidLink = document.createElement("a");
    orcidLink.target = "_blank";
    orcidLink.rel = "noopener noreferrer";
    orcidLink.className = "btn btn-secondary orcid-button";
    orcidLink.href = `https://orcid.org/${author.orcid}`;
    orcidLink.textContent = author.orcid;
    orcidLink.setAttribute(
      "aria-label",
      `Acessar perfil ORCID ${author.orcid}. Abre em nova aba.`
    );

    orcidWrap.appendChild(orcidLink);
    card.appendChild(orcidWrap);
  }

  return card;
}

Abertura do modal

function openAuthorModal(index) {
  if (!authorModal || !authorCardsContainer) return;

  const author = authors[index];

  if (!author) return;

  authorCardsContainer.innerHTML = "";
  authorCardsContainer.appendChild(buildAuthorCard(author));

  if (author.email) {
    correspondenceSection.classList.remove("d-none");
    correspondenceText.textContent = `${author.name}. E-mail: `;
    correspondenceEmail.href = `mailto:${author.email}`;
    correspondenceEmail.textContent = author.email;
  } else {
    correspondenceSection.classList.add("d-none");
    correspondenceText.textContent = "";
    correspondenceEmail.removeAttribute("href");
    correspondenceEmail.textContent = "";
  }

  authorModal.show();
}

renderAuthors();

Subtarefas

  • Seções complementares no corpo do artigo:
    Além da lista de autores e do modal, o artigo deve conter as seguintes seções quando os dados existirem.
    Contribuição de autoria
    Deve aparecer no menu de navegação do artigo:
<a class="list-group-item-action d-block" href="#articleSection3.7">
  Contribuição de autoria
</a>

E no corpo:

<div class="articleSection" id="articleSection3.7">
  <h2 class="h5">Contribuição de autoria</h2>
</div>

Essa seção deve usar os mesmos dados da lista de autores e do modal, principalmente:

author.name
author.roles
  • Conflito de interesses.
    Deve aparecer no menu:
<a class="list-group-item-action d-block" href="#articleSection3.8">
  Conflito de interesses
</a>

E no corpo:

<a class="list-group-item-action d-block" href="#articleSection3.8">
  Conflito de interesses
</a>

O texto deve refletir o dado real do artigo.

  • Editor Responsável.
    Deve aparecer no menu:
<a class="list-group-item-action d-block" href="#articleSection3.9">
  Editor Responsável
</a>

E no corpo:

<div class="articleSection" id="articleSection3.9">
  <h2 class="h5">Editor Responsável</h2>
  <p>Nome do editor responsável</p>
</div>

Requisitos de acessibilidade:

  • O modal deve seguir o padrão ModalDefault usado no SciELO.
  • O modal deve possuir role="dialog".
  • O modal deve possuir aria-modal="true".
  • O modal deve usar aria-labelledby apontando para o título.
  • O botão de fechar deve possuir aria-label.
  • O gerenciamento de foco deve ser feito pelo Bootstrap.
  • Não remover foco manualmente via JavaScript.
  • Os nomes dos autores devem ser botões.
  • Não usar href="#" para abrir modal.
  • O botão + N autores deve usar aria-expanded.
  • Ícones decorativos devem usar aria-hidden="true".
  • Links ORCID devem usar target="_blank" e rel="noopener noreferrer".
  • Links ORCID devem informar no aria-label que abrem em nova aba.

Critérios de aceite

  • A lista de autores é montada dinamicamente em #authorList.
  • O modal usa o ID ModalTutors.
  • O modal segue o padrão acessível do SciELO.
  • Cada autor abre o modal correto ao ser clicado.
  • Autores correspondentes exibem ícone de envelope.
  • O ícone de envelope usa material-icons-outlined me-1 fs-6.
  • A lista colapsa quando passa do limite configurado.
  • O botão + N autores expande a lista.
  • O botão Ocultar autores recolhe a lista.
  • O modal exibe nome, contribuição, afiliação, ORCID e e-mail quando existirem.
  • A seção “Contribuição de autoria” usa os mesmos dados dos autores.
  • A seção “Conflito de interesses” aparece no corpo e no menu quando existir.
  • A seção “Editor Responsável” aparece no corpo e no menu quando existir.
  • A navegação por teclado funciona corretamente.
  • Não há erros no console.

Considerações e notas

  • Não devem ser carregadas no packtools classes css dentro de <style>. Todo o CSS deve vir do Design System.
  • Verifique este link como referencia de html funcional.

Metadata

Metadata

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions