# Plugin para incluir edição em linha para Grids

**URL:** <https://community.latromi.com.br/t/plugin-para-incluir-edicao-em-linha-para-grids/1871>\
**Category:** Tutoriais\
**Tags:** latromi-web, javascript, form\
**Created:** [Julho 23, 2026, 2:14pm UTC](https://community.latromi.com.br/t/plugin-para-incluir-edicao-em-linha-para-grids/1871 "2026-07-23T14:14:39Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![Diego\_Ramos](https://community.latromi.com.br/user_avatar/community.latromi.com.br/diego_ramos/32/1819_2.png) [@Diego\_Ramos](https://community.latromi.com.br/u/Diego_Ramos)\
**Post date:** [Julho 23, 2026, 2:14pm UTC](https://community.latromi.com.br/t/plugin-para-incluir-edicao-em-linha-para-grids/1871/1 "2026-07-23T14:14:39Z")

</div>

O `gridColumnEditable` é um plugin JavaScript leve e autônomo projetado para adicionar o recurso de **edição in-line** a tabelas HTML existentes. Ele suporta a inicialização de múltiplas tabelas simultaneamente, injeção de inputs customizados e rastreamento de alterações por linha e coluna.

## Código fonte

- **commons.js:**

```js
function __deepMerge(target, ...sources) {
    sources.forEach(source => {
        if (source) {
            Object.keys(source).forEach(key => {
                if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
                    if (!target[key]) {
                        target[key] = {};
                    }
                    deepMerge(target[key], source[key]); // Recursivamente mescla objetos
                } else {
                    target[key] = source[key]; // Cópia simples para valores não-objetos
                }
            });
        }
    });
    return target;
}

function __uuidv4() {
    return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
        (+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
    );
}

```

- **gridColumnEditable.js:**

```js
/ *********************************Column Grid Editable********************************** /
/*
    Control instances data change (Support for multiple grids)
    Expected structure of instancesDataMap:
    {
        '[instanceId]': {
            'gridName': 'myGrid',
            'indexId': 0, // Column index that holds the row ID
            'editableColumns': [{ index: 1 }, { index: 2, inputElement: customInputElement }],
            'DataChanged': {
                '[rowId]': {
                    [columnIndex]: 'new data value',
                    // ... other changed columns for this row
                },
                // ... other rows
            }
        }
    }
*/

var gridColumnEditable = (function () {
    var instancesDataMap = {};
    var settings = {};
    var initialized = false;
    var lastIndexChanged;
    
    // 'me' is used to maintain a consistent reference to the plugin's active state
    var me = {
        element: undefined,
        activeCell: undefined // NEW: Tracks the logical cell being edited, regardless of DOM placement
    };

    function initialize(self, options) {
        settings = __deepMerge({
            editableGrids: [
                {
                    name: undefined,
                    wrapperElement: 'table',
                    contentElement: 'tbody',
                    columnsInfo: {
                        idIndex: 0,
                        valueDelegate: undefined,
                        editables: []
                    }
                }
            ],
            confirmKey: "Enter",
            onConfirm: undefined,
        }, options);

        if (!settings.editableGrids || !Array.isArray(settings.editableGrids) || settings.editableGrids.length === 0)
            return;

        _ensureSettingsDefaultValues();

        for (var gridIndex = 0; gridIndex < settings.editableGrids.length; gridIndex++) {
            var gridConfig = settings.editableGrids[gridIndex];
            var wrapperElement = _tryGetElement(gridConfig.wrapperElement);

            if (gridConfig.columnsInfo.editables && gridConfig.columnsInfo.editables.length > 0) {
                var instanceId = __uuidv4();

                wrapperElement.addEventListener("click", function (ev) {
                    var currentInstanceId = this.getAttribute("data-instance");
                    var cell;

                    if (ev.target.tagName.toLowerCase() === "td") 
                        cell = ev.target;
                    else if (ev.target.closest("td"))
                        cell = ev.target.closest("td");

                    if (!cell) return;

                    if (gridColumnEditable.getColumnsEditableIndex(currentInstanceId).indexOf(cell.cellIndex) === -1)
                        return;
                    
                    gridColumnEditable.show({ cell: cell });
                });

                instancesDataMap[instanceId] = {
                    gridName: gridConfig.name,
                    indexId: gridConfig.columnsInfo.idIndex,
                    editableColumns: gridConfig.columnsInfo.editables,
                    wrapperElement: gridConfig.wrapperElement,
                    DataChanged: {}
                };

                wrapperElement.setAttribute("data-instance", instanceId);
            }

            wrapperElement.setAttribute("data-la-editable-grid", "true");
        }

        initialized = true;
    }

    function _tryGetElement(elementOrSelector) {
        if (elementOrSelector instanceof HTMLElement)
            return elementOrSelector;
        return document.querySelector(elementOrSelector);
    }

    function _tryGetRowId(rowElement, idIndex, wrapperElement) {
        if (typeof idIndex === "number")
            return rowElement.querySelector(`td:nth-child(${idIndex + 1})`)?.textContent;
        else if (typeof idIndex === "function")
            return idIndex(wrapperElement, rowElement.rowIndex);
        
        return undefined;
    }

    function _tryGetValue(element, delegate) {
        if (typeof delegate === "function")
            return delegate();
        return element.value;
    }

    function _ensureInitialized() {
        if (!initialized)
            throw new Error("gridColumnEditable is not initialized. Please call initialize method before using it.");
    }

    function _ensureSettingsDefaultValues() {
        if (!settings.editableGrids || !Array.isArray(settings.editableGrids))
            settings.editableGrids = [{}];

        for (var editableGrid of settings.editableGrids) {
            if (!editableGrid.wrapperElement) editableGrid.wrapperElement = "table";
            if (!editableGrid.contentElement) editableGrid.contentElement = "tbody";
            if (!editableGrid.columnsInfo) editableGrid.columnsInfo = {};
            if (!editableGrid.columnsInfo.gridName) editableGrid.columnsInfo.name = undefined;
            if (!editableGrid.columnsInfo.idIndex) editableGrid.columnsInfo.idIndex = 0;
            if (!editableGrid.columnsInfo.editables || !Array.isArray(editableGrid.columnsInfo.editables))
                editableGrid.columnsInfo.editables = [];
        }
    }

    function renderByElement(parent) {
        if (!(parent instanceof HTMLElement))
            throw new Error("Element must be an instance of HTMLElement");

        if (me.element !== undefined) return; 
        
        if (parent.tagName.toLowerCase() !== "td") // TODO: Considerar adicionar o suporte para N tipos de element.
            throw new Error(`I'm sorry but that resource is not implemented to tag '${parent.tagName}'.`);

        var wrapperElement = parent.closest("[data-la-editable-grid=true]");
        if (!wrapperElement)
            throw new Error("Parent element is not inside an editable grid wrapper.");

        var instanceId = wrapperElement.getAttribute("data-instance");
        var columnData = instancesDataMap[instanceId].editableColumns.find(function (e) { return e.index == parent.cellIndex; });

        if (columnData && columnData.inputElement) {
            me.element = columnData.inputElement;
        } else {
            me.element = document.createElement("input");
            me.element.id = `la-editable-input`;
            me.element.name = `la-editable-input`;
        }
        
        me.activeCell = parent; // Lock the logical parent reference

        var styleElement = document.head.querySelector('style#la-editable-input-style');
        if (!styleElement) {
            styleElement = document.createElement('style');
            styleElement.id = "la-editable-input-style";
            styleElement.innerHTML = `
            .la-editable-input input,
            /* LATROMI Support */
            .la-editable-input .fieldWrapper,
            .la-editable-input .fieldWrapper > div {
                position: absolute !important;
                left: 0 !important; 
                top: 0 !important;
                width: 100% !important; 
                height: 100% !important;
                font-size: inherit !important;
                font-family: inherit !important;
                padding-top: 0 !important; 
                padding-bottom: 0 !important;
                padding-left: inherit !important; 
                padding-right: inherit !important;
                margin: 0 !important; 
                box-sizing: border-box !important;
                display: inherit !important;
            }`;
            document.head.appendChild(styleElement);
        }

        _configureEvent();
        
        parent.classList.add("la-editable-input");
        parent.style.position = "relative"; 
        parent.appendChild(me.element);
        me.element.focus();
    }

    function renderByPosition(args) {
        if (args.x === undefined || args.y === undefined)
            throw new Error("Position must have x and y properties");

        if (!args.cell)
            throw new Error("A logical cell reference (args.cell) is required even when rendering by position.");

        if (me.element !== undefined) return;

        var style = {
            "position": "absolute",
            "left": args.x + "px",
            "top": args.y + "px"
        };

        if (args.width) style.width = args.width + "px";
        if (args.height) style.height = args.height + "px";

        // FIX: Replaced 'this.element' with 'me.element' to maintain scope
        me.element = document.createElement("input");
        me.element.name = `la-editable-input`;
        me.element.style = Object.entries(style).map(([key, value]) => `${key}: ${value};`).join(" ");

        me.activeCell = args.cell; // Lock the logical cell reference

        _configureEvent();

        document.body.appendChild(me.element);
        me.element.focus();
    }

    function _confirm() {
        // FIX: Now relies on me.activeCell instead of me.element's physical DOM parent
        var cell = me.activeCell;
        var row = cell?.closest("tr");
        var instanceId = cell?.closest("[data-la-editable-grid=true]")?.getAttribute("data-instance");

        if (!instanceId || !row || cell?.cellIndex === undefined) {
            console.error("Invalid operation: Cannot identify the grid or cell context.");
            return; 
        }

        var dataInfo = instancesDataMap[instanceId];
        
        if (!dataInfo) {
            console.warn(`No data info found for instance ID '${instanceId}'`);
            return; 
        }

        var editableColumn = dataInfo.editableColumns.find(e => e.index == cell.cellIndex);

        if (!editableColumn) {
            console.warn(`Column index '${cell.cellIndex}' is not editable for instance ID '${instanceId}'`);
            return; 
        }
        
        // FIX: Passing the correct gridConfig context
        var rowId = _tryGetRowId(row, dataInfo.indexId, dataInfo.wrapperElement);
        
        if (!(rowId in dataInfo.DataChanged))
            dataInfo.DataChanged[rowId] = {};
        
        lastIndexChanged = rowId;
        dataInfo.DataChanged[rowId][cell.cellIndex] = _tryGetValue(me.element, editableColumn.valueDelegate);
        
        if (settings.onConfirm) {
            settings.onConfirm({
                elementName: dataInfo.gridName,
                valueChanged: dataInfo.DataChanged[rowId][cell.cellIndex],
                parent: cell
            });
        }
    }

    function _configureEvent() {
        var clickListener = function (event) {
            if (me.element && me.element.contains(event.target)) {
                return; 
            }

            if (me.activeCell) {
                me.activeCell.classList.remove("la-editable-input");
                me.activeCell.style.position = ""; 
            }

            me.element?.remove();
            me.element = undefined;
            me.activeCell = undefined;

            document.removeEventListener("keydown", keyPressListener, { capture: true }); 
            document.removeEventListener("click", clickListener, { capture: true }); 
        }

        var keyPressListener = function (event) {
            if (event.key === "Escape" || event.key === "Tab" || event.key === settings.confirmKey) {
                event.preventDefault();

                if (event.key === settings.confirmKey) 
                    _confirm();

                if (me.activeCell) {
                    me.activeCell.classList.remove("la-editable-input");
                    me.activeCell.style.position = ""; 
                }

                me.element?.remove();
                me.element = undefined;
                me.activeCell = undefined;

                document.removeEventListener("keydown", keyPressListener, { capture: true }); 
                document.removeEventListener("click", clickListener, { capture: true }); 
            }
        }

        document.addEventListener("click", clickListener, { capture: true });
        document.addEventListener("keydown", keyPressListener, { capture: true });
    }

    function __reset() {
        settings = {};
        instancesDataMap = {};
        initialized = false;
        lastIndexChanged = undefined;
        me.element = undefined;
        me.activeCell = undefined;
    }

    return {
        initialize: function (options, forceInit) {
            if (forceInit) __reset();
            if (initialized) return;

            initialize(this, options);

            return this;
        },
        show: function (args) {
            _ensureInitialized();

            // The plugin now requires 'cell' to be passed in args in all scenarios
            if (args.x !== undefined && args.y !== undefined)
                renderByPosition(args);
            else if (args.cell)
                renderByElement(args.cell);
        },
        getDataChanged: function (instanceId) {
            _ensureInitialized();

            if (!instanceId) throw new Error("Instance ID is required to get data changed");

            return instancesDataMap[instanceId].DataChanged;
        },
        getColumnsEditableIndex: function (instanceId) {
            _ensureInitialized();

            if (!instanceId) 
                throw new Error("Instance ID is required to get columns editable index");
            
            if (!(instancesDataMap[instanceId].editableColumns) || instancesDataMap[instanceId].editableColumns?.length === 0)
                return [];

            return instancesDataMap[instanceId].editableColumns.map(function (data) { return data.index; });
        },
        getLastIndexChanged: function () {
            return lastIndexChanged;
        }
    };
})();

```

## Índice

1. [Inicialização Rápida](#p-3830-inicializa%C3%A7%C3%A3o-r%C3%A1pida)
2. [Estrutura de Configuração](#p-3830-estrutura-de-configura%C3%A7%C3%A3o-options)
3. [Exemplos de Uso Avançado](#p-3830-exemplos-de-uso-avan%C3%A7ado)
4. [Métodos da API Pública](#p-3830-m%C3%A9todos-da-api-p%C3%BAblica)
5. [Como Funciona o Rastreamento de Dados](#p-3830-como-funciona-o-rastreamento-de-dados)

* * *

## Inicialização Rápida

Para iniciar o plugin, basta chamar o método `initialize`, passando um objeto de configuração detalhando quais tabelas devem receber o comportamento de edição e quais colunas serão afetadas.

### Exemplo Básico de HTML

```html
<table id="minhaTabela">
    <thead>
        <tr><th>ID</th><th>Nome</th><th>Idade (Editável)</th></tr>
    </thead>
    <tbody>
        <tr><td>101</td><td>João</td><td>25</td></tr>
        <tr><td>102</td><td>Maria</td><td>30</td></tr>
    </tbody>
</table>

```

### Script de Inicialização

```javascript
gridColumnEditable.initialize({
    editableGrids: [
        {
            name: "GridUsuarios",
            wrapperElement: "#minhaTabela", // Seletor CSS ou HTMLElement
            columnsInfo: {
                idIndex: 0, // A coluna 0 guarda o ID único da linha (101, 102...)
                editables: [
                    { index: 2 } // Torna a coluna 2 (Idade) editável
                ]
            }
        }
    ],
    confirmKey: "Enter",
    onConfirm: function(eventData) {
        console.log(`Grid: ${eventData.elementName} atualizada! Novo valor: ${eventData.valueChanged}`);
    }
});

```

## Estrutura de Configuração

O método `initialize` espera um objeto contendo as seguintes propriedades globais:

| Propriedade | Tipo | Padrão | Descrição |
| --- | --- | --- | --- |
| `confirmKey` | `string` | `"Enter"` | Tecla que, ao ser pressionada, confirma e salva a edição atual. Ex: `"Enter"`, `"Tab"`. |
| `onConfirm` | `function` | `undefined` | Callback disparado imediatamente após uma edição ser confirmada. Recebe como parâmetro um objeto `eventData`. |
| `editableGrids` | `array` | `[]` | Lista de configurações individuais para cada tabela que será gerenciada pelo plugin. |

### Configuração Interna de editableGrids

Cada objeto dentro do array `editableGrids` deve seguir esta estrutura:

| Propriedade | Tipo | Obrigatório | Descrição |
| --- | --- | --- | --- |
| `name` | `string` | Não | Nome de identificação da Grid (útil para logs no callback `onConfirm`). |
| `wrapperElement` | `string` ou `HTMLElement` | Não (Padrão: `"table"`) | O elemento raiz da tabela onde o evento de clique será ancorado (Delegação de Eventos). |
| `contentElement` | `string` ou `HTMLElement` | Não (Padrão `"tbody"`) | Onde as células de conteúdo residem. _(Atualmente não bloqueante, reservado para expansões)_. |
| `columnsInfo` | `object` | Sim | Objeto definindo as regras das colunas (veja detalhes abaixo). |

#### Objeto `columnsInfo`

- **`idIndex`** (`number` ou `function`):  
Indica como o plugin deve encontrar a chave primária da linha que está sendo editada. Se for `number`, ele lerá o texto da coluna correspondente (ex: `0` para a primeira coluna). Se for `function`, permite extrair o ID via script.

- **`valueDelegate`** (`function`):  
Função customizada para extrair o valor final a ser salvo (útil se você estiver usando componentes complexos em vez de um `<input>` simples).

- **`editables`** (`array` de objetos):  
Define as colunas editáveis. Ex: `[{ index: 1 }, { index: 2 }]`.

## Exemplos de Uso Avançado

### Exemplo A: Usando Inputs Customizados

Se você não quiser o `<input type="text">` padrão gerado pelo plugin, você pode injetar seu próprio elemento (como um `<select>` ou um input com máscaras).

```javascript
var meuSelect = document.createElement('select');
meuSelect.innerHTML = '<option value="A">Ativo</option><option value="I">Inativo</option>';

gridColumnEditable.initialize({
    editableGrids: [
        {
            wrapperElement: document.querySelector('#gridStatus'),
            columnsInfo: {
                idIndex: 0,
                editables: [
                    // Na coluna 3, o plugin usará o select criado acima
                    { index: 3, inputElement: meuSelect } 
                ]
            }
        }
    ]
});

```

### Exemplo B: Extraindo ID da linha via Delegate (Função)

Caso o ID da sua linha não esteja visível em uma coluna (ex: ele fica em um atributo `data-row-id="15"` do `<tr>`), você pode usar uma função para ensiná-lo a capturar o ID.

```javascript
columnsInfo: {
    // Recebe o wrapperElement e o rowIndex do <tr> clicado
    idIndex: function(wrapper, rowIndex) {
        var linha = wrapper.querySelectorAll("tr")[rowIndex];
        return linha.getAttribute("data-row-id");
    },
    editables: [{ index: 1 }]
}

```

## Métodos da API Pública

O `gridColumnEditable` expõe uma API para que você interaja com ele externamente.

- `initialize(options, forceInit)`:  
Inicializa o plugin. Se `forceInit` for `true`, descarta configurações anteriores e recomeça o ciclo de vida.

- `show(args)`:  
Abre o modo de edição manualmente. O `args` exige a propriedade `cell` contendo o HTMLElement `<td>` alvo. Exemplo: `gridColumnEditable.show({ cell: tdElement })`. _(Suporta também as propriedades `x`, `y`, `width` e `height` para renderização em coordenadas absolutas sobrepostas na tela)._

- `getDataChanged(instanceId)`:  
Retorna o dicionário de dados alterados para uma grid específica. Requer o ID da instância da grid.

- `getColumnsEditableIndex(instanceId)`:  
Retorna um array com os índices de todas as colunas que estão configuradas como editáveis.

- `getLastIndexChanged()`:  
Retorna o ID da última linha editada no sistema.

## Como Funciona o Rastreamento de Dados

O plugin não altera silenciosamente o valor visual do HTML (o `innerText` do `<td>`) após o “Enter”. A filosofia do plugin é armazenar temporariamente o _estado_ das modificações na memória (no dicionário `DataChanged`).

Sempre que a tecla de confirmação é acionada, o plugin monta a seguinte estrutura, que você pode recuperar usando `getDataChanged(instanceId)`:

```json
{
  "101": { // ID da Linha (rowId)
    "2": "Novo valor da idade", // Chave é o Index da coluna, valor é o dado editado
    "3": "Ativo"
  },
  "105": {
    "2": "Nova idade"
  }
}

```

É responsabilidade do desenvolvedor capturar essas alterações (seja através do evento `onConfirm` ou lendo o mapa completo no momento de salvar o formulário) e processar a atualização no backend ou redesenhar a tabela na tela conforme necessário.
