# Arquivo (Drive) — JS do Drive

> **Complemento de `04-VIEWS-E-UX.md`.** Este arquivo detalha o JavaScript
> do Drive (`v5/public/assets/js/custom/contents/midias/index.js`): upload
> com cropper e categoria, listagem com scroll infinito, busca,
> reordenação por drag-and-drop e exclusão.

---

## 1. JS do Drive (`v5/public/assets/js/custom/contents/midias/index.js`)

### 1.1 Upload com categoria e edição de imagem

```js
$("#multipleupload").uploadFile({
    url: "/cms/galeria",
    fileName: "file",
    multiple: true,
    showPreview: true,
    autoSubmit: false,
    acceptFiles: "image/jpeg, image/png, image/jpg, application/pdf, text/plain, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document",

    onSelect: function (files) {
        const originalFile = files[0];
        const isImage = originalFile.type.startsWith("image/");
        const processFile = isImage
            ? abrirModalEdicao(originalFile) // abre cropper se for imagem
            : Promise.resolve(originalFile);

        processFile.then( async (editedFile) => {
            categories = await myFetch('/cms/drive/categorias');                    

            // Abre modal para nome e categoria
            Swal.fire({
                title: "Informações do arquivo",
                html: `
                <input type="text" id="swal-nameFile" class="swal2-input" placeholder="Digite o nome do arquivo">
                <select id="swal-category" class="swal2-input">
                    <option value="">Selecione a categoria</option>
                    ${categories.map(cat => `<option value="${cat.id}">${cat.name}</option>`).join('')}
                </select>
            `,
                focusConfirm: false,
                showCancelButton: true,
                confirmButtonText: "Enviar",
                cancelButtonText: "Cancelar",
                preConfirm: () => {
                    const inputName = document.getElementById("swal-nameFile").value.trim();
                    const inputCategory = document.getElementById("swal-category").value;
                    if (!inputName || !inputCategory) {
                        Swal.showValidationMessage("Preencha todos os campos!");
                        return false;
                    }
                    return { inputName, inputCategory };
                }
            })
```

O `FormData` enviado:

```js
const formData = new FormData();
formData.append("_token", $('meta[name="csrf-token"]').attr('content'));
formData.append("id", window.id ?? null);
formData.append("table", window.table ?? null);
formData.append("nameFile", nameFile);
formData.append("category", category);
formData.append("file", editedFile, editedFile.name);
```

Regras de UI:

- **Nome e categoria são obrigatórios** quando o modo avançado está ligado
  (`Swal.showValidationMessage`).
- A categoria é **obrigatória no front, opcional no back** — o controller
  aceita `category` ausente.
- O botão "Adicionar Categoria" só aparece se
  `configurations.advanced_drive_settings_enabled != 0`
  (`v5/resources/views/prefecture/contents/midias/form.blade.php` injeta
  `App\Prefecture\Configuration` e lê a chave). Na base analisada o valor
  é `0` → **o botão fica oculto** e o upload_avançado fica inacessível,
  embora o JS do uploadAvançado ainda esteja no arquivo.

### 1.2 Listagem, busca e scroll infinito

```js
async function chargeGallery(id = null, column = null) {
    let index = 0;
    let batchSize = 50;
    let area = document.querySelector('.drive');
    let route = id ? `/cms/galeria/${column}/${id}` : `/cms/galeria/`
    let info = await myFetch(route);

    async function loadBatch() {
        document.getElementById('loadingIndicator').style.display = 'flex';
        let batch = info.data.slice(index, index + batchSize);
        await verifyAndLoadImages(batch, area);
        index += batchSize;
        filterImages(currentSearchText);
        document.getElementById('loadingIndicator').style.display = 'none';
        setupInfiniteScrolling();
    }

    function setupInfiniteScrolling() {
        let sentinel = document.querySelector('.sentinel') || create_element({ tag: 'div', classes: 'sentinel' });
        area.appendChild(sentinel);

        let observer = new IntersectionObserver(entries => {
            if (entries[0].isIntersecting) {
                observer.disconnect();
                sentinel.remove();
                loadBatch();
            }
        }, { threshold: 1.0 });

        observer.observe(sentinel);
    }

    loadBatch();
}
```

- O endpoint **devolve a lista inteira** de uma vez; o "scroll infinito" é
  apenas um recorte de 50 em 50 no cliente (`info.data.slice(...)`) —
  **não há paginação no servidor**.
- Cada item é validado com um `HEAD` no arquivo antes de ser exibido:

```js
const response = await fetch(`${window.location.origin}/${element.path}`, { method: 'HEAD' });
return { exists: response.redirected ? false : response.ok, ...element };
```

  → **N requisições HEAD por página** (50 por lote).

### 1.3 Item da galeria

```js
function createGalleryItem(path, name, id, extension, order, category) {
    let div = create_element({ tag: 'div', classes: `gallery-item id_${id} order_${order} text-center p-2`, estilos: 'width: 20%; margin: 0; cursor: default;' });
    let imageContainer = create_element({ tag: 'div', classes: 'image-container-box' });

    let content;

    if (extension != 'png' && extension != 'jpeg' && extension != 'jpg') {
        content = create_element({ tag: 'i', classes: 'fa fa-file-archive-o fa-4x', estilos: 'font-size: 95px' });
    } else {
        content = create_element({ tag: 'img', classes: 'img-thumbnail', estilos: 'object-fit: cover;', params: [{ key: 'src', value: `/${path}` }] });
    }

    let titleContainer = create_element({ tag: 'div', classes: 'image-title-box', conteudo: textLimit(name, 30) });
    let categoryContainer = create_element({ tag: 'div', classes: 'image-category-box', conteudo: category ? '<p class="font-weight-bold">' + category.name + '</p>' : '' });
    let buttonContainer = createButtonContainer(path, id);

    imageContainer.appendChild(content);
    div.append(imageContainer, titleContainer, categoryContainer, buttonContainer);
    return div;
}
```

- Somente `png/jpeg/jpg` mostram miniatura; o resto vira ícone de arquivo.
- O nome aparece **truncado em 30 caracteres** (`textLimit`).
- A **categoria aparece sob o nome** — é a única sinalização visual da
  categorização na galeria.

### 1.4 Busca

```js
function filterImages(searchText) {
    const text = searchText.toLowerCase().trim();
    document.querySelectorAll('.gallery-item').forEach(item => {
        const title = item.querySelector('.image-title-box').textContent.toLowerCase();
        const category = item.querySelector('.image-category-box').textContent.toLowerCase();
        const match = title.includes(text) || category.includes(text);
        item.style.display = text === '' || match ? '' : 'none';
    });
}
```

A busca é **100 % client-side** e filtra por **nome OU categoria**. Debounce
de 300 ms (`setupSearch`).

### 1.5 Reordenação por drag-and-drop

```js
$(function () {
    $("#imageBox").sortable({
        items: ".gallery-item",
        cursor: "move",
        opacity: 0.8,
        placeholder: "sortable-placeholder",

        start: function (event, ui) {
            ui.item.data("startIndex", ui.item.index());
        },

        update: function (event, ui) {
            const token = $('meta[name="csrf-token"]').attr('content');

            const startIndex = ui.item.data("startIndex");
            const newIndex = ui.item.index();

            if (startIndex === newIndex) return; // não mudou, sai

            const dragged = ui.item;
            const draggedId = dragged.attr("class").match(/id_(\d+)/)[1];

            // recupera todos os itens (sem o item arrastado)
            const allItems = $("#imageBox .gallery-item").not(dragged);

            // determina qual era o item alvo com base na direção
            let target;
            if (newIndex > startIndex) {
                // arrastou pra baixo → o alvo é o item **depois** da nova posição
                target = allItems.eq(newIndex - 1);
            } else {
                // arrastou pra cima → o alvo é o item **na nova posição**
                target = allItems.eq(newIndex);
            }

            if (!target.length) return; // segurança

            const targetId = target.attr("class").match(/id_(\d+)/)[1];

            if (draggedId === targetId) return; // segurança extra            

            $.ajax({
                url: '/cms/drive/atualizar-ordem',
                type: 'POST',
                contentType: 'application/json',
                headers: { 'X-CSRF-TOKEN': token },
                data: JSON.stringify({
                    dragged_id: draggedId,
                    target_id: targetId
                }),
                success: function (res) {
                },
                error: function (xhr) {
                    alert('Erro ao atualizar ordem.');
                }
            });
        }
    });
});
```

O alvo é deduzido do índice do item arrastado (`allItems.eq(newIndex)` /
`eq(newIndex - 1)`), e o `id` é extraído do nome da classe por regex:
`dragged.attr("class").match(/id_(\d+)/)[1]`.

> O `sortable` do jQuery UI **exige** `jquery-ui` carregado — o
> `form.blade.php` da v5 inclui
> `https://code.jquery.com/ui/1.13.2/themes/base/jquery-ui.css` (CDN), mas o
> **JS** do jQuery UI não vem desse include; ele existe localmente em
> `v5/public/assets/plugins/jquery-ui/jquery-ui.min.js` e precisa estar
> carregado pelo layout AdminLTE para que `$.fn.sortable` exista.

### 1.6 Exclusão

```js
async function deleteArchive(id, event) {
    event.preventDefault();
    let result = await myFetch(`/cms/galeria/delete/${id}`, 'post');
    if (result.success) {
        let elementToRemove = document.querySelector(`.id_${id}`);
        if (elementToRemove) elementToRemove.remove();
        toastr.success(result.success);
    } else {
        toastr.error(result.erro);
    }
}
```

> `myFetch` (`v5/public/assets/js/custom/utilities/functions.js:64-88`) envia
> `Content-type: application/json` e o header `X-CSRF-TOKEN`, mas **não**
> envia `X-Requested-With`. O `catch` da função **retorna `undefined`**
> (sem `return`), o que faz `result.success` lançar `TypeError` se a
> requisição falhar (rede/down) — o erro aparece como "Cannot read
> properties of undefined" no console em vez de uma mensagem ao usuário.
