Initial commit,修正
This commit is contained in:
Vendored
+374
@@ -0,0 +1,374 @@
|
||||
$.fn.editableTable = function(options) {
|
||||
// Default options
|
||||
let defaultOptions = {
|
||||
cloneProperties: ['padding', 'padding-top', 'padding-bottom', 'padding-left', 'padding-right',
|
||||
'text-align', 'font', 'font-size', 'font-family', 'font-weight',
|
||||
'border', 'border-top', 'border-bottom', 'border-left', 'border-right', 'color', 'background-color', 'border-radius'
|
||||
],
|
||||
columns: []
|
||||
}
|
||||
|
||||
// Overlay default options on user options
|
||||
options = $.extend({}, defaultOptions, options);
|
||||
|
||||
// The instance reference we'll pass to the user
|
||||
let _instance;
|
||||
|
||||
// Class to put on the editor to denote an error
|
||||
let errorClass = 'error';
|
||||
|
||||
// Something to tag the hidden input with
|
||||
let identifierAttribute = 'table-editor-input';
|
||||
|
||||
// Keycodes
|
||||
let ARROW_LEFT = 37,
|
||||
ARROW_UP = 38,
|
||||
ARROW_RIGHT = 39,
|
||||
ARROW_DOWN = 40,
|
||||
ENTER = 13,
|
||||
ESC = 27,
|
||||
TAB = 9,
|
||||
CONTROL = 17,
|
||||
LEFT_WINDOWS = 91,
|
||||
SELECT_KEY = 93;
|
||||
|
||||
// The table element
|
||||
let element = $(this);
|
||||
|
||||
// Show all columns and then hide any hidden ones
|
||||
element.find('th').show();
|
||||
options.columns.forEach((col, i) => {
|
||||
if (col.isHidden !== undefined && col.isHidden) element.find('th').eq(i).hide();
|
||||
});
|
||||
|
||||
// If there are actions, add a final blank table header
|
||||
if (options.actions !== undefined && options.actions.length > 0) {
|
||||
// Add header if we don't already have one
|
||||
if (!element.find('thead tr th[actions]').length) {
|
||||
element.find('thead tr').append($('<th actions=""></th>'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// The textbox allowing user input. Only add if there's not already an editor input control around
|
||||
let editor;
|
||||
let existingEditor = element.parent().find(`input[${identifierAttribute}]`);
|
||||
if (existingEditor.length) {
|
||||
editor = existingEditor.first();
|
||||
} else {
|
||||
editor = $('<input>');
|
||||
}
|
||||
|
||||
// Add it to the DOM
|
||||
editor.attr(identifierAttribute, '')
|
||||
.css('position', 'absolute')
|
||||
.hide()
|
||||
.appendTo(element.parent());
|
||||
|
||||
// The `td` being "edited"
|
||||
let activeCell;
|
||||
|
||||
// Function to show the editor
|
||||
function showEditor(select) {
|
||||
// Set the active cell
|
||||
activeCell = element.find('td:focus:not([action])');
|
||||
if (activeCell.length) {
|
||||
// Get column def
|
||||
let currentColIndex = $(activeCell).parent().children('td').index($(activeCell));
|
||||
let columnDef = options.columns[currentColIndex];
|
||||
|
||||
// Prepare input control
|
||||
editor.val(activeCell.text()) // Throw the value in
|
||||
.removeClass(errorClass) // remove any error classes
|
||||
.show() // show it
|
||||
.offset(activeCell.offset()) // position it
|
||||
.css(activeCell.css(options.cloneProperties)) // make it look similar by cloning properties
|
||||
.width(activeCell.width()) // size it
|
||||
.height(activeCell.height()) // size it
|
||||
.focus(); // focus user input into it
|
||||
|
||||
// Set max length, if it's specified
|
||||
if (columnDef.maxLength !== undefined) editor.attr('maxlength', columnDef.maxLength);
|
||||
else editor.removeAttr('maxlength');
|
||||
|
||||
if (select) {
|
||||
editor.select();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function setActiveText() {
|
||||
let text = editor.val(),
|
||||
evt = $.Event('change'),
|
||||
originalContent;
|
||||
if (activeCell.text() === text || editor.hasClass(errorClass)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
originalContent = activeCell.html();
|
||||
activeCell.text(text).trigger(evt, text);
|
||||
if (evt.result === false) {
|
||||
activeCell.html(originalContent);
|
||||
}
|
||||
};
|
||||
|
||||
// For traversing the table up/down/left/right by returning the adjacent cell
|
||||
function handleMovement(element, keycode) {
|
||||
if (keycode === ARROW_RIGHT) {
|
||||
return element.next('td');
|
||||
} else if (keycode === ARROW_LEFT) {
|
||||
return element.prev('td');
|
||||
} else if (keycode === ARROW_UP) {
|
||||
return element.parent().prev().children().eq(element.index());
|
||||
} else if (keycode === ARROW_DOWN) {
|
||||
return element.parent().next().children().eq(element.index());
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
// On the editor losing focus, hide the input
|
||||
editor.blur(function() {
|
||||
setActiveText();
|
||||
editor.hide();
|
||||
});
|
||||
|
||||
// Handle typing into the input
|
||||
editor.keydown(function(e) {
|
||||
if (e.which === ENTER) {
|
||||
setActiveText();
|
||||
editor.hide();
|
||||
activeCell.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
} else if (e.which === ESC) {
|
||||
editor.val(activeCell.text());
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
editor.hide();
|
||||
activeCell.focus();
|
||||
} else if (e.which === TAB) {
|
||||
activeCell.focus();
|
||||
} else if (this.selectionEnd - this.selectionStart === this.value.length) {
|
||||
let possibleMove = handleMovement(activeCell, e.which);
|
||||
if (possibleMove.length > 0) {
|
||||
possibleMove.focus();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Validate cell input on typing or pasting
|
||||
editor.on('input paste', function() {
|
||||
let evt = $.Event('validate');
|
||||
activeCell.trigger(evt, editor.val());
|
||||
if (evt.result !== undefined) {
|
||||
if (evt.result === false) {
|
||||
editor.addClass(errorClass);
|
||||
} else {
|
||||
editor.removeClass(errorClass);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// On table clicking, move around cells
|
||||
element.on('click keypress dblclick', showEditor);
|
||||
element.keydown(function(e) {
|
||||
let prevent = true,
|
||||
possibleMove = handleMovement($(e.target), e.which);
|
||||
if (possibleMove.length > 0) {
|
||||
possibleMove.focus();
|
||||
} else if (e.which === ENTER) {
|
||||
showEditor(false);
|
||||
} else if (e.which === CONTROL || e.which === LEFT_WINDOWS || e.which === SELECT_KEY) {
|
||||
showEditor(true);
|
||||
prevent = false;
|
||||
} else {
|
||||
prevent = false;
|
||||
}
|
||||
if (prevent) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
element.find('td').prop('tabindex', 1);
|
||||
|
||||
$(window).on('resize', function() {
|
||||
if (editor.is(':visible')) {
|
||||
editor.offset(activeCell.offset())
|
||||
.width(activeCell.width())
|
||||
.height(activeCell.height());
|
||||
}
|
||||
});
|
||||
|
||||
function refresh() {
|
||||
$(element).editableTable(options);
|
||||
}
|
||||
|
||||
// Validate based on options
|
||||
$('table td').on('validate', function(evt, newValue) {
|
||||
let currentColIndex = $(evt.currentTarget).parent().children('td').index($(evt.currentTarget));
|
||||
let columnDef = options.columns[currentColIndex];
|
||||
let currentData = _instance.getData({
|
||||
convert: false
|
||||
}); // current data to allow user to validate based on existing data
|
||||
let isValid = columnDef.isValid && columnDef.isValid(newValue, currentData);
|
||||
return isValid;
|
||||
});
|
||||
|
||||
$('table td').on('change', function(evt, newValue) {
|
||||
let td = $(this);
|
||||
let currentColIndex = $(evt.currentTarget).parent().children('td').index($(evt.currentTarget));
|
||||
let columnDef = options.columns[currentColIndex];
|
||||
|
||||
if (columnDef.removeRowIfCleared && newValue == '') {
|
||||
td.parent('tr').remove();
|
||||
}
|
||||
|
||||
// Bind user-specified events if they exist
|
||||
if (typeof columnDef.afterChange == 'function') {
|
||||
columnDef.afterChange(newValue, td);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Set up the instance reference
|
||||
_instance = {
|
||||
// Get table back out as JSON
|
||||
getData: function(opts) {
|
||||
opts = $.extend({}, {
|
||||
convert: true
|
||||
}, opts);
|
||||
|
||||
let rowData = [];
|
||||
element.find('tbody tr').toArray().forEach(row => {
|
||||
let newRow = {};
|
||||
|
||||
$(row).find('td:not([action])').toArray().forEach(col => {
|
||||
let columnsDef = options.columns[
|
||||
$(col).parent().children('td').index($(col)) // only index at cells, or "td"s
|
||||
];
|
||||
|
||||
let value = $(col).text();
|
||||
|
||||
// Check if the cell was marked as having a null value, and if so, extract as null and not
|
||||
// as a blank string
|
||||
let isNull = $(col).attr('data-is-null');
|
||||
if (typeof isNull !== 'undefined' && isNull !== false) {
|
||||
value = null;
|
||||
}
|
||||
|
||||
// Convert if requried
|
||||
if (opts.convert && typeof columnsDef.convertOut == 'function') {
|
||||
value = columnsDef.convertOut(value);
|
||||
}
|
||||
|
||||
newRow[columnsDef.name] = value;
|
||||
});
|
||||
|
||||
rowData.push(newRow);
|
||||
});
|
||||
return rowData;
|
||||
},
|
||||
|
||||
// Add a new row with JSON
|
||||
addRow: function(row) {
|
||||
let newRow = $(`<tr></tr>`);
|
||||
|
||||
if (row !== undefined && row !== null) {
|
||||
let props = Object.keys(row);
|
||||
|
||||
let columnsToAdd = [];
|
||||
props.forEach(prop => {
|
||||
let columnDef = options.columns.filter(col => col.name === prop);
|
||||
if (columnDef.length) {
|
||||
columnDef = columnDef[0];
|
||||
columnsToAdd.push({
|
||||
order: columnDef.index,
|
||||
value: row[prop],
|
||||
prop: prop,
|
||||
def: columnDef
|
||||
});
|
||||
}
|
||||
})
|
||||
columnsToAdd.sort((a, b) => a.order - b.order).forEach((colToAdd, index) => {
|
||||
let newCell;
|
||||
if (colToAdd.value !== null)
|
||||
newCell = $(`<td>${colToAdd.value}</td>`);
|
||||
else
|
||||
newCell = $(`<td data-is-null></td>`);
|
||||
|
||||
// Apply any classes
|
||||
if (colToAdd.def.classes !== undefined && colToAdd.def.classes.length) {
|
||||
colToAdd.def.classes.forEach(classToAdd => newCell.addClass(classToAdd));
|
||||
}
|
||||
|
||||
// Apply any style
|
||||
if (colToAdd.def.style !== undefined && colToAdd.def.style.length) {
|
||||
newCell.attr("style", newCell.attr("style") + "; " + colToAdd.def.style);
|
||||
}
|
||||
|
||||
// Hide if hidden
|
||||
if (colToAdd.def.isHidden !== undefined && colToAdd.def.isHidden) {
|
||||
newCell.hide();
|
||||
}
|
||||
|
||||
// Add to the column
|
||||
newRow.append(newCell);
|
||||
|
||||
// Trigger any events
|
||||
let columnDef = options.columns.filter(col => col.name === colToAdd.prop)[0];
|
||||
if (typeof columnDef.afterAdd == 'function') {
|
||||
columnDef.afterAdd(colToAdd.value, newCell);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
newRow = $(`<tr></tr>`);
|
||||
activeOptions.columns.forEach(x => {
|
||||
newRow.append(`<td></td>`);
|
||||
});
|
||||
}
|
||||
|
||||
// Add actions if any
|
||||
if (options.actions !== undefined && options.actions.length > 0) {
|
||||
let actionsCell = $(`<td action></td>`);
|
||||
options.actions.forEach(a => {
|
||||
let actionElement = $(a.label);
|
||||
actionElement.css('cursor', 'pointer');
|
||||
actionElement.click(e => a.action(e, newRow))
|
||||
actionsCell.append(actionElement);
|
||||
});
|
||||
newRow.append(actionsCell);
|
||||
}
|
||||
|
||||
// Add the new row
|
||||
let lastRow = element.find('tbody tr:last');
|
||||
if (lastRow.length > 0)
|
||||
lastRow.after(newRow);
|
||||
else
|
||||
element.find('tbody').append(newRow);
|
||||
|
||||
refresh();
|
||||
},
|
||||
|
||||
// Clear the table
|
||||
clear: function() {
|
||||
element.find('tbody tr').remove();
|
||||
},
|
||||
|
||||
// Set the table's data with JSON
|
||||
setData: function(data) {
|
||||
if (data) {
|
||||
this.clear();
|
||||
data.forEach(datum => {
|
||||
this.addRow(datum);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
return _instance;
|
||||
};
|
||||
Reference in New Issue
Block a user