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:
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:
/********************************* 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
- Inicialização Rápida
- Estrutura de Configuração
- Exemplos de Uso Avançado
- Métodos da API Pública
- 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
<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
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(numberoufunction):
Indica como o plugin deve encontrar a chave primária da linha que está sendo editada. Se fornumber, ele lerá o texto da coluna correspondente (ex:0para a primeira coluna). Se forfunction, 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(arrayde 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).
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.
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. SeforceInitfortrue, descarta configurações anteriores e recomeça o ciclo de vida. -
show(args):
Abre o modo de edição manualmente. Oargsexige a propriedadecellcontendo o HTMLElement<td>alvo. Exemplo:gridColumnEditable.show({ cell: tdElement }). (Suporta também as propriedadesx,y,widtheheightpara 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):
{
"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.