1112 lines
34 KiB
JavaScript
1112 lines
34 KiB
JavaScript
/*
|
|
* Table control class
|
|
*
|
|
* Dependences:
|
|
* - Scrollbar (october.scrollbar.js)
|
|
*/
|
|
+function ($) { "use strict";
|
|
|
|
// TABLE CONTROL NAMESPACES
|
|
// ============================
|
|
|
|
if ($.oc === undefined)
|
|
$.oc = {}
|
|
|
|
if ($.oc.table === undefined)
|
|
$.oc.table = {}
|
|
|
|
// TABLE CLASS DEFINITION
|
|
// ============================
|
|
|
|
var Table = function(element, options) {
|
|
this.el = element
|
|
this.$el = $(element)
|
|
|
|
this.options = options
|
|
|
|
//
|
|
// State properties
|
|
//
|
|
|
|
// The data source object
|
|
this.dataSource = null
|
|
|
|
// The cell processors list
|
|
this.cellProcessors = {}
|
|
|
|
// A reference to the currently active cell processor
|
|
this.activeCellProcessor = null
|
|
|
|
// A reference to the currently active table cell
|
|
this.activeCell = null
|
|
|
|
// A reference to the tables container
|
|
this.tableContainer = null
|
|
|
|
// A reference to the data table container
|
|
this.dataTableContainer = null
|
|
|
|
// The key of the row which is being edited at the moment.
|
|
// This key corresponds the data source row key which
|
|
// uniquely identifies the row in the data set. When the
|
|
// table grid notices that a cell in another row is edited it commits
|
|
// the previously edited record to the data source.
|
|
this.editedRowKey = null
|
|
|
|
// A reference to the data table
|
|
this.dataTable = null
|
|
|
|
// A reference to the header table
|
|
this.headerTable = null
|
|
|
|
// A reference to the toolbar
|
|
this.toolbar = null
|
|
|
|
// Event handlers
|
|
this.clickHandler = this.onClick.bind(this)
|
|
this.keydownHandler = this.onKeydown.bind(this)
|
|
this.documentClickHandler = this.onDocumentClick.bind(this)
|
|
this.toolbarClickHandler = this.onToolbarClick.bind(this)
|
|
|
|
if (this.options.postback && this.options.clientDataSourceClass == 'client')
|
|
this.formSubmitHandler = this.onFormSubmit.bind(this)
|
|
|
|
// Navigation helper
|
|
this.navigation = null
|
|
|
|
// Number of records added or deleted during the session
|
|
this.recordsAddedOrDeleted = 0
|
|
|
|
//
|
|
// Initialization
|
|
//
|
|
|
|
this.init()
|
|
}
|
|
|
|
// INTERNAL METHODS
|
|
// ============================
|
|
|
|
Table.prototype.init = function() {
|
|
// Create the data source object
|
|
this.createDataSource()
|
|
|
|
// Create cell processors
|
|
this.initCellProcessors()
|
|
|
|
// Initialize helpers
|
|
this.navigation = new $.oc.table.helper.navigation(this)
|
|
|
|
// Create the UI
|
|
this.buildUi()
|
|
|
|
// Register event handlers
|
|
this.registerHandlers()
|
|
}
|
|
|
|
Table.prototype.disposeCellProcessors = function() {
|
|
// For the performance reasons cell processors are stored
|
|
// in an object structure with keys matching the column names.
|
|
// We can iterate through then with the for cycle if we know
|
|
// the column names. We use the for cycle for the performance
|
|
// reasons: http://jsperf.com/for-vs-foreach/37,
|
|
// http://jonraasch.com/blog/10-javascript-performance-boosting-tips-from-nicholas-zakas
|
|
|
|
for (var i = 0, len = this.options.columns.length; i < len; i++) {
|
|
var column = this.options.columns[i].key
|
|
|
|
this.cellProcessors[column].dispose()
|
|
this.cellProcessors[column] = null
|
|
}
|
|
|
|
this.cellProcessors = null
|
|
this.activeCellProcessor = null
|
|
}
|
|
|
|
Table.prototype.createDataSource = function() {
|
|
var dataSourceClass = this.options.clientDataSourceClass
|
|
|
|
if ($.oc.table.datasource === undefined || $.oc.table.datasource[dataSourceClass] == undefined)
|
|
throw new Error('The table client-side data source class "'+dataSourceClass+'" is not ' +
|
|
'found in the $.oc.table.datasource namespace.')
|
|
|
|
this.dataSource = new $.oc.table.datasource[dataSourceClass](this)
|
|
}
|
|
|
|
Table.prototype.registerHandlers = function() {
|
|
this.el.addEventListener('click', this.clickHandler)
|
|
this.el.addEventListener('keydown', this.keydownHandler)
|
|
document.addEventListener('click', this.documentClickHandler)
|
|
|
|
if (this.options.postback && this.options.clientDataSourceClass == 'client')
|
|
this.$el.closest('form').bind('oc.beforeRequest', this.formSubmitHandler)
|
|
|
|
var toolbar = this.getToolbar()
|
|
if (toolbar)
|
|
toolbar.addEventListener('click', this.toolbarClickHandler);
|
|
}
|
|
|
|
Table.prototype.unregisterHandlers = function() {
|
|
this.el.removeEventListener('click', this.clickHandler);
|
|
document.removeEventListener('click', this.documentClickHandler)
|
|
|
|
this.clickHandler = null
|
|
|
|
this.el.removeEventListener('keydown', this.keydownHandler);
|
|
this.keydownHandler = null
|
|
|
|
var toolbar = this.getToolbar()
|
|
if (toolbar)
|
|
toolbar.removeEventListener('click', this.toolbarClickHandler);
|
|
|
|
this.toolbarClickHandler = null
|
|
|
|
if (this.formSubmitHandler) {
|
|
this.$el.closest('form').unbind('oc.beforeRequest', this.formSubmitHandler)
|
|
this.formSubmitHandler = null
|
|
}
|
|
}
|
|
|
|
Table.prototype.initCellProcessors = function() {
|
|
for (var i = 0, len = this.options.columns.length; i < len; i++) {
|
|
var columnConfiguration = this.options.columns[i],
|
|
column = columnConfiguration.key,
|
|
columnType = columnConfiguration.type
|
|
|
|
// Resolve the default column type to string
|
|
if (columnType === undefined) {
|
|
columnType = 'string'
|
|
this.options.columns[i].type = columnType
|
|
}
|
|
|
|
if ($.oc.table.processor === undefined || $.oc.table.processor[columnType] == undefined)
|
|
throw new Error('The table cell processor for the column type "'+columnType+'" is not ' +
|
|
'found in the $.oc.table.processor namespace.')
|
|
|
|
this.cellProcessors[column] = new $.oc.table.processor[columnType](this, column, columnConfiguration)
|
|
}
|
|
}
|
|
|
|
Table.prototype.getCellProcessor = function(columnName) {
|
|
return this.cellProcessors[columnName]
|
|
}
|
|
|
|
Table.prototype.buildUi = function() {
|
|
this.tableContainer = document.createElement('div')
|
|
this.tableContainer.setAttribute('class', 'table-container')
|
|
|
|
// Build the toolbar
|
|
if (this.options.toolbar)
|
|
this.buildToolbar()
|
|
|
|
// Build the headers table
|
|
this.tableContainer.appendChild(this.buildHeaderTable())
|
|
|
|
// Append the table container to the element
|
|
this.el.insertBefore(this.tableContainer, this.el.children[0])
|
|
|
|
if (!this.options.height)
|
|
this.dataTableContainer = this.tableContainer
|
|
else
|
|
this.dataTableContainer = this.buildScrollbar()
|
|
|
|
// Build the data table
|
|
this.updateDataTable()
|
|
}
|
|
|
|
Table.prototype.buildToolbar = function() {
|
|
if (!this.options.adding && !this.options.deleting)
|
|
return
|
|
|
|
this.toolbar = document.createElement('div')
|
|
this.toolbar.setAttribute('class', 'toolbar')
|
|
|
|
if (this.options.adding) {
|
|
var addBelowButton = document.createElement('a')
|
|
addBelowButton.setAttribute('class', 'btn add-table-row-below')
|
|
addBelowButton.setAttribute('data-cmd', 'record-add-below')
|
|
this.toolbar.appendChild(addBelowButton)
|
|
|
|
if (this.navigation.paginationEnabled() || !this.options.rowSorting) {
|
|
// When the pagination is enabled, or sorting is disabled,
|
|
// new records can only be added to the bottom of the
|
|
// table.
|
|
addBelowButton.textContent = this.options.btnAddRowLabel
|
|
} else {
|
|
addBelowButton.textContent = this.options.btnAddRowBelowLabel
|
|
|
|
var addAboveButton = document.createElement('a')
|
|
addAboveButton.setAttribute('class', 'btn add-table-row-above')
|
|
addAboveButton.textContent = 'Add row above'
|
|
addAboveButton.setAttribute('data-cmd', 'record-add-above')
|
|
this.toolbar.appendChild(addAboveButton)
|
|
}
|
|
}
|
|
|
|
if (this.options.deleting) {
|
|
var deleteButton = document.createElement('a')
|
|
deleteButton.setAttribute('class', 'btn delete-table-row')
|
|
deleteButton.textContent = this.options.btnDeleteRowLabel
|
|
deleteButton.setAttribute('data-cmd', 'record-delete')
|
|
this.toolbar.appendChild(deleteButton)
|
|
}
|
|
|
|
this.tableContainer.appendChild(this.toolbar)
|
|
}
|
|
|
|
Table.prototype.buildScrollbar = function() {
|
|
var scrollbar = document.createElement('div'),
|
|
scrollbarContent = document.createElement('div')
|
|
|
|
scrollbar.setAttribute('class', 'control-scrollbar')
|
|
|
|
if (this.options.dynamicHeight)
|
|
scrollbar.setAttribute('style', 'max-height: ' + this.options.height + 'px')
|
|
else
|
|
scrollbar.setAttribute('style', 'height: ' + this.options.height + 'px')
|
|
|
|
scrollbar.appendChild(scrollbarContent)
|
|
this.tableContainer.appendChild(scrollbar)
|
|
|
|
$(scrollbar).scrollbar({animation: false})
|
|
|
|
return scrollbarContent
|
|
}
|
|
|
|
Table.prototype.buildHeaderTable = function() {
|
|
var headersTable = document.createElement('table'),
|
|
row = document.createElement('tr')
|
|
|
|
headersTable.className = 'headers'
|
|
headersTable.appendChild(row)
|
|
|
|
for (var i = 0, len = this.options.columns.length; i < len; i++) {
|
|
var header = document.createElement('th')
|
|
|
|
if (this.options.columns[i].width)
|
|
header.setAttribute('style', 'width: '+this.options.columns[i].width)
|
|
|
|
header.textContent !== undefined
|
|
? header.textContent = this.options.columns[i].title
|
|
: header.innerText = this.options.columns[i].title
|
|
|
|
row.appendChild(header)
|
|
}
|
|
|
|
this.headerTable = headersTable
|
|
|
|
return headersTable
|
|
}
|
|
|
|
Table.prototype.updateDataTable = function(onSuccess) {
|
|
var self = this
|
|
|
|
this.unfocusTable()
|
|
|
|
this.fetchRecords(function onUpdateDataTableSuccess(records, totalCount){
|
|
self.buildDataTable(records, totalCount)
|
|
|
|
if (onSuccess)
|
|
onSuccess()
|
|
|
|
if (totalCount == 0)
|
|
self.addRecord('above', true)
|
|
|
|
self = null
|
|
})
|
|
}
|
|
|
|
Table.prototype.updateColumnWidth = function() {
|
|
var headerCells = this.headerTable.querySelectorAll('th'),
|
|
dataCells = this.dataTable.querySelectorAll('tr:first-child td')
|
|
|
|
for (var i = 0, len = headerCells.length; i < len; i++) {
|
|
if (dataCells[i])
|
|
dataCells[i].setAttribute('style', headerCells[i].getAttribute('style'))
|
|
}
|
|
}
|
|
|
|
Table.prototype.buildDataTable = function(records, totalCount) {
|
|
var dataTable = document.createElement('table'),
|
|
tbody = document.createElement('tbody'),
|
|
keyColumn = this.options.keyColumn
|
|
|
|
dataTable.setAttribute('class', 'data')
|
|
|
|
for (var i = 0, len = records.length; i < len; i++) {
|
|
var row = document.createElement('tr')
|
|
|
|
if (records[i][keyColumn] === undefined)
|
|
throw new Error('The row attribute '+keyColumn+' is not set for the row #'+i);
|
|
|
|
row.setAttribute('data-row', records[i][keyColumn])
|
|
for (var j = 0, colsLen = this.options.columns.length; j < colsLen; j++) {
|
|
var cell = document.createElement('td'),
|
|
dataContainer = document.createElement('input'),
|
|
cellContentContainer = document.createElement('div'),
|
|
column = this.options.columns[j],
|
|
columnName = column.key,
|
|
cellProcessor = this.getCellProcessor(columnName)
|
|
|
|
cell.setAttribute('data-column', columnName)
|
|
cell.setAttribute('data-column-type', column.type)
|
|
|
|
dataContainer.setAttribute('type', 'hidden')
|
|
dataContainer.setAttribute('data-container', 'data-container')
|
|
dataContainer.value = this.formatDataContainerValue(records[i][columnName])
|
|
|
|
cellContentContainer.setAttribute('class', 'content-container')
|
|
|
|
cell.appendChild(cellContentContainer)
|
|
row.appendChild(cell)
|
|
cell.appendChild(dataContainer)
|
|
|
|
cellProcessor.renderCell(records[i][columnName], cellContentContainer)
|
|
}
|
|
|
|
tbody.appendChild(row)
|
|
}
|
|
|
|
dataTable.appendChild(tbody)
|
|
|
|
// Inject the data table to the DOM or replace the existing table
|
|
if (this.dataTable !== null)
|
|
this.dataTableContainer.replaceChild(dataTable, this.dataTable)
|
|
else
|
|
this.dataTableContainer.appendChild(dataTable)
|
|
|
|
this.dataTable = dataTable
|
|
|
|
// Update column widths
|
|
this.updateColumnWidth()
|
|
|
|
// Update the scrollbar
|
|
this.updateScrollbar()
|
|
|
|
// Update the pagination links
|
|
this.navigation.buildPagination(totalCount)
|
|
}
|
|
|
|
Table.prototype.formatDataContainerValue = function(value) {
|
|
if (value === undefined) {
|
|
return ''
|
|
}
|
|
|
|
if (typeof value === 'boolean') {
|
|
return value ? 1 : ''
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
Table.prototype.fetchRecords = function(onSuccess) {
|
|
this.dataSource.getRecords(
|
|
this.navigation.getPageFirstRowOffset(),
|
|
this.options.recordsPerPage,
|
|
onSuccess
|
|
)
|
|
}
|
|
|
|
Table.prototype.updateScrollbar = function() {
|
|
if (!this.options.height)
|
|
return
|
|
|
|
$(this.dataTableContainer.parentNode).data('oc.scrollbar').update()
|
|
}
|
|
|
|
Table.prototype.scrollCellIntoView = function() {
|
|
if (!this.options.height || !this.activeCell)
|
|
return
|
|
|
|
$(this.dataTableContainer.parentNode).data('oc.scrollbar').gotoElement(this.activeCell)
|
|
}
|
|
|
|
Table.prototype.disposeScrollbar = function() {
|
|
if (!this.options.height)
|
|
return
|
|
|
|
$(this.dataTableContainer.parentNode).data('oc.scrollbar').dispose()
|
|
$(this.dataTableContainer.parentNode).data('oc.scrollbar', null)
|
|
}
|
|
|
|
/*
|
|
* Makes a cell processor active and hides the previously
|
|
* active editor.
|
|
*/
|
|
Table.prototype.setActiveProcessor = function(processor) {
|
|
if (this.activeCellProcessor)
|
|
this.activeCellProcessor.onUnfocus()
|
|
|
|
this.activeCellProcessor = processor
|
|
}
|
|
|
|
Table.prototype.commitEditedRow = function() {
|
|
if (this.editedRowKey === null)
|
|
return
|
|
|
|
var editedRow = this.dataTable.querySelector('tr[data-row="'+this.editedRowKey+'"]')
|
|
if (!editedRow)
|
|
return
|
|
|
|
if (editedRow.getAttribute('data-dirty') != 1)
|
|
return
|
|
|
|
var cells = editedRow.children,
|
|
data = {}
|
|
|
|
for (var i=0, len = cells.length; i < len; i++) {
|
|
var cell = cells[i]
|
|
|
|
data[cell.getAttribute('data-column')] = this.getCellValue(cell)
|
|
}
|
|
|
|
this.dataSource.updateRecord(this.editedRowKey, data)
|
|
editedRow.setAttribute('data-dirty', 0)
|
|
}
|
|
|
|
/*
|
|
* Removes editor from the currently edited cell and commits the row if needed.
|
|
*/
|
|
Table.prototype.unfocusTable = function() {
|
|
this.elementRemoveClass(this.el, 'active')
|
|
|
|
if (this.activeCellProcessor)
|
|
this.activeCellProcessor.onUnfocus()
|
|
|
|
this.commitEditedRow()
|
|
this.activeCellProcessor = null
|
|
|
|
if (this.activeCell)
|
|
this.activeCell.setAttribute('class', '')
|
|
|
|
this.activeCell = null
|
|
}
|
|
|
|
/*
|
|
* Makes the table focused in the UI
|
|
*/
|
|
Table.prototype.focusTable = function() {
|
|
this.elementAddClass(this.el, 'active')
|
|
}
|
|
|
|
/*
|
|
* Calls the onFocus() method for the cell processor responsible for the
|
|
* newly focused cell. Commit the previous edited row to the data source
|
|
* if needed.
|
|
*/
|
|
Table.prototype.focusCell = function(cellElement, isClick) {
|
|
var columnName = cellElement.getAttribute('data-column')
|
|
if (columnName === null)
|
|
return
|
|
|
|
this.focusTable()
|
|
|
|
var processor = this.getCellProcessor(columnName)
|
|
if (!processor)
|
|
throw new Error("Cell processor not found for the column "+columnName)
|
|
|
|
if (this.activeCell !== cellElement) {
|
|
if (this.activeCell)
|
|
this.elementRemoveClass(this.activeCell, 'active')
|
|
|
|
this.setActiveProcessor(processor)
|
|
this.activeCell = cellElement
|
|
|
|
if (processor.isCellFocusable())
|
|
this.elementAddClass(this.activeCell, 'active')
|
|
}
|
|
|
|
// If the cell belongs to other row than the currently edited,
|
|
// commit currently edited row to the data source. Update the
|
|
// currently edited row key.
|
|
var rowKey = this.getCellRowKey(cellElement)
|
|
|
|
if (this.editedRowKey !== null && rowKey != this.editedRowKey)
|
|
this.commitEditedRow()
|
|
|
|
this.editedRowKey = rowKey
|
|
|
|
processor.onFocus(cellElement, isClick)
|
|
|
|
this.scrollCellIntoView()
|
|
}
|
|
|
|
Table.prototype.markCellRowDirty = function(cellElement) {
|
|
cellElement.parentNode.setAttribute('data-dirty', 1)
|
|
}
|
|
|
|
Table.prototype.addRecord = function(placement, noFocus) {
|
|
// If there is no active cell, or the pagination is enabled or
|
|
// row sorting is disabled, add the record to the bottom of
|
|
// the table (last page).
|
|
|
|
if (!this.activeCell || this.navigation.paginationEnabled() || !this.options.rowSorting)
|
|
placement = 'bottom'
|
|
|
|
var relativeToKey = null,
|
|
currentRowIndex = null
|
|
|
|
if (placement == 'above' || placement == 'below') {
|
|
relativeToKey = this.getCellRowKey(this.activeCell)
|
|
currentRowIndex = this.getCellRowIndex(this.activeCell)
|
|
}
|
|
|
|
this.unfocusTable()
|
|
|
|
if (this.navigation.paginationEnabled()) {
|
|
var newPageIndex = this.navigation.getNewRowPage(placement, currentRowIndex)
|
|
|
|
if (newPageIndex != this.navigation.pageIndex) {
|
|
// Validate data on the current page if adding a new record
|
|
// is going to create another page.
|
|
if (!this.validate())
|
|
return
|
|
}
|
|
|
|
this.navigation.pageIndex = newPageIndex
|
|
}
|
|
|
|
this.recordsAddedOrDeleted++
|
|
|
|
// New records have negative keys
|
|
var keyColumn = this.options.keyColumn,
|
|
recordData = {},
|
|
self = this
|
|
|
|
recordData[keyColumn] = -1*this.recordsAddedOrDeleted
|
|
|
|
this.dataSource.createRecord(recordData, placement, relativeToKey,
|
|
this.navigation.getPageFirstRowOffset(),
|
|
this.options.recordsPerPage,
|
|
function onAddRecordDataTableSuccess(records, totalCount) {
|
|
self.buildDataTable(records, totalCount)
|
|
|
|
var row = self.findRowByKey(recordData[keyColumn])
|
|
if (!row)
|
|
throw new Error('New row is not found in the updated table: '+recordData[keyColumn])
|
|
|
|
if (!noFocus)
|
|
self.navigation.focusCell(row, 0)
|
|
|
|
self = null
|
|
}
|
|
)
|
|
}
|
|
|
|
Table.prototype.deleteRecord = function() {
|
|
if (!this.activeCell)
|
|
return
|
|
|
|
var currentRowIndex = this.getCellRowIndex(this.activeCell),
|
|
key = this.getCellRowKey(this.activeCell),
|
|
self = this,
|
|
paginationEnabled = this.navigation.paginationEnabled(),
|
|
currentPageIndex = this.navigation.pageIndex,
|
|
currentCellIndex = this.activeCell.cellIndex
|
|
|
|
if (paginationEnabled)
|
|
this.navigation.pageIndex = this.navigation.getPageAfterDeletion(currentRowIndex)
|
|
|
|
this.recordsAddedOrDeleted++
|
|
|
|
// New records have negative keys
|
|
var keyColumn = this.options.keyColumn,
|
|
newRecordData = {}
|
|
|
|
newRecordData[keyColumn] = -1*this.recordsAddedOrDeleted
|
|
|
|
this.dataSource.deleteRecord(key,
|
|
newRecordData,
|
|
this.navigation.getPageFirstRowOffset(),
|
|
this.options.recordsPerPage,
|
|
function onDeleteRecordDataTableSuccess(records, totalCount) {
|
|
self.buildDataTable(records, totalCount)
|
|
|
|
if (!paginationEnabled)
|
|
self.navigation.focusCellInReplacedRow(currentRowIndex, currentCellIndex)
|
|
else {
|
|
if (currentPageIndex != self.navigation.pageIndex)
|
|
self.navigation.focusCell('bottom', currentCellIndex)
|
|
else
|
|
self.navigation.focusCellInReplacedRow(currentRowIndex, currentCellIndex)
|
|
}
|
|
|
|
self = null
|
|
}
|
|
)
|
|
}
|
|
|
|
Table.prototype.notifyRowProcessorsOnChange = function(cellElement) {
|
|
var columnName = cellElement.getAttribute('data-column'),
|
|
row = cellElement.parentNode
|
|
|
|
for (var i = 0, len = row.children.length; i < len; i++) {
|
|
var column = this.options.columns[i].key
|
|
|
|
this.cellProcessors[column].onRowValueChanged(columnName, row.children[i])
|
|
}
|
|
}
|
|
|
|
Table.prototype.getToolbar = function() {
|
|
return this.tableContainer.querySelector('div.toolbar')
|
|
}
|
|
|
|
/*
|
|
* Validaates data on the current page
|
|
*/
|
|
Table.prototype.validate = function() {
|
|
var rows = this.dataTable.querySelectorAll('tbody tr[data-row]')
|
|
|
|
for (var i = 0, len = rows.length; i < len; i++) {
|
|
var row = rows[i]
|
|
|
|
this.elementRemoveClass(row, 'error')
|
|
}
|
|
|
|
for (var i = 0, rowsLen = rows.length; i < rowsLen; i++) {
|
|
var row = rows[i],
|
|
rowData = this.getRowData(row)
|
|
|
|
for (var j = 0, colsLen = row.children.length; j < colsLen; j++)
|
|
this.elementRemoveClass(row.children[j], 'error')
|
|
|
|
for (var columnName in rowData) {
|
|
var cellProcessor = this.getCellProcessor(columnName),
|
|
message = cellProcessor.validate(rowData[columnName], rowData)
|
|
|
|
if (message !== undefined) {
|
|
var cell = row.querySelector('td[data-column="'+columnName+'"]'),
|
|
self = this
|
|
|
|
this.elementAddClass(row, 'error')
|
|
this.elementAddClass(cell, 'error')
|
|
|
|
$.oc.flashMsg({text: message, 'class': 'error'})
|
|
|
|
window.setTimeout(function(){
|
|
self.focusCell(cell, false)
|
|
cell = null
|
|
self = null
|
|
cellProcessor = null
|
|
}, 100)
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// EVENT HANDLERS
|
|
// ============================
|
|
|
|
Table.prototype.onClick = function(ev) {
|
|
this.focusTable()
|
|
|
|
if (this.navigation.onClick(ev) === false)
|
|
return
|
|
|
|
for (var i = 0, len = this.options.columns.length; i < len; i++) {
|
|
var column = this.options.columns[i].key
|
|
|
|
this.cellProcessors[column].onClick(ev)
|
|
}
|
|
|
|
var target = this.getEventTarget(ev, 'TD')
|
|
|
|
if (!target)
|
|
return
|
|
|
|
if (target.tagName != 'TD')
|
|
return
|
|
|
|
this.focusCell(target, true)
|
|
}
|
|
|
|
Table.prototype.onKeydown = function(ev) {
|
|
if (ev.keyCode == 65 && ev.altKey && this.options.adding) {
|
|
if (!ev.shiftKey) {
|
|
// alt+a - add record below
|
|
this.addRecord('below')
|
|
} else {
|
|
// alt+shift+a - add record above
|
|
this.addRecord('above')
|
|
}
|
|
|
|
this.stopEvent(ev)
|
|
return
|
|
}
|
|
|
|
if (ev.keyCode == 68 && ev.altKey && this.options.deleting) {
|
|
// alt+d - delete record
|
|
this.deleteRecord()
|
|
|
|
this.stopEvent(ev)
|
|
return
|
|
}
|
|
|
|
for (var i = 0, len = this.options.columns.length; i < len; i++) {
|
|
var column = this.options.columns[i].key
|
|
|
|
this.cellProcessors[column].onKeyDown(ev)
|
|
}
|
|
|
|
if (this.navigation.onKeydown(ev) === false)
|
|
return
|
|
}
|
|
|
|
Table.prototype.onFormSubmit = function(ev, data) {
|
|
if (data.handler == this.options.postbackHandlerName) {
|
|
this.unfocusTable()
|
|
|
|
if (!this.validate()) {
|
|
ev.preventDefault()
|
|
return
|
|
}
|
|
|
|
var fieldName = this.options.alias.indexOf('[') > -1 ?
|
|
this.options.alias + '[TableData]' :
|
|
this.options.alias + 'TableData';
|
|
|
|
data.options.data[fieldName] = this.dataSource.getAllData()
|
|
}
|
|
}
|
|
|
|
Table.prototype.onToolbarClick = function(ev) {
|
|
var target = this.getEventTarget(ev),
|
|
cmd = target.getAttribute('data-cmd')
|
|
|
|
switch (cmd) {
|
|
case 'record-add-below':
|
|
this.addRecord('below')
|
|
break
|
|
case 'record-add-above':
|
|
this.addRecord('above')
|
|
break
|
|
case 'record-delete':
|
|
this.deleteRecord()
|
|
break
|
|
}
|
|
|
|
this.stopEvent(ev)
|
|
}
|
|
|
|
Table.prototype.onDocumentClick = function(ev) {
|
|
var target = this.getEventTarget(ev)
|
|
|
|
// Determine if the click was inside the table element
|
|
// and just exit if so
|
|
if (this.parentContainsElement(this.el, target))
|
|
return
|
|
|
|
// Request the active cell processor if the clicked
|
|
// element belongs to any extra-table element created
|
|
// by the processor
|
|
|
|
if (this.activeCellProcessor && this.activeCellProcessor.elementBelongsToProcessor(target))
|
|
return
|
|
|
|
this.unfocusTable()
|
|
}
|
|
|
|
// PUBLIC METHODS
|
|
// ============================
|
|
|
|
Table.prototype.dispose = function() {
|
|
// Remove an editor and commit the data if needed
|
|
this.unfocusTable()
|
|
|
|
// Dispose the data source and clean up the reference
|
|
this.dataSource.dispose()
|
|
this.dataSource = null
|
|
|
|
// Unregister event handlers
|
|
this.unregisterHandlers()
|
|
|
|
// Remove references to DOM elements
|
|
this.dataTable = null
|
|
this.headerTable = null
|
|
this.toolbar = null
|
|
|
|
// Dispose cell processors
|
|
this.disposeCellProcessors()
|
|
|
|
// Dispose helpers and remove references
|
|
this.navigation.dispose()
|
|
this.navigation = null
|
|
|
|
// Delete references to the control HTML elements.
|
|
// The script doesn't remove any DOM elements themselves.
|
|
// If it's needed it should be done by the outer script,
|
|
// we only make sure that the table widget doesn't hold
|
|
// references to the detached DOM tree so that the garbage
|
|
// collector can delete the elements if needed.
|
|
this.disposeScrollbar()
|
|
this.el = null
|
|
this.tableContainer = null
|
|
this.$el = null
|
|
this.dataTableContainer = null
|
|
|
|
// Delete references to other DOM elements
|
|
this.activeCell = null
|
|
}
|
|
|
|
/*
|
|
* Updates row values in the table.
|
|
* rowIndex is an integer value containing the row index on the current page.
|
|
* The rowValues should be a hash object containing only changed
|
|
* columns.
|
|
* Returns false if the row wasn't found. Otherwise returns true.
|
|
*/
|
|
Table.prototype.setRowValues = function(rowIndex, rowValues) {
|
|
var row = this.findRowByIndex(rowIndex)
|
|
|
|
if (!row) {
|
|
return false
|
|
}
|
|
|
|
var dataUpdated = false
|
|
|
|
for (var i = 0, len = row.children.length; i < len; i++) {
|
|
var cell = row.children[i],
|
|
cellColumnName = this.getCellColumnName(cell)
|
|
|
|
for (var rowColumnName in rowValues) {
|
|
if (rowColumnName == cellColumnName) {
|
|
this.setCellValue(cell, rowValues[rowColumnName], true)
|
|
dataUpdated = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if (dataUpdated) {
|
|
var originalEditedRowKey = this.editedRowKey
|
|
|
|
this.editedRowKey = this.getRowKey(row)
|
|
this.commitEditedRow()
|
|
this.editedRowKey = originalEditedRowKey
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// HELPER METHODS
|
|
// ============================
|
|
|
|
Table.prototype.getElement = function() {
|
|
return this.el
|
|
}
|
|
|
|
Table.prototype.getAlias = function() {
|
|
return this.options.alias
|
|
}
|
|
|
|
Table.prototype.getTableContainer = function() {
|
|
return this.tableContainer
|
|
}
|
|
|
|
Table.prototype.getDataTableBody = function() {
|
|
return this.dataTable.children[0]
|
|
}
|
|
|
|
Table.prototype.getEventTarget = function(ev, tag) {
|
|
// TODO: use the foundation library
|
|
|
|
var target = ev.target ? ev.target : ev.srcElement
|
|
|
|
if (tag === undefined)
|
|
return target
|
|
|
|
var tagName = target.tagName
|
|
|
|
while (tagName != tag) {
|
|
target = target.parentNode
|
|
|
|
if (!target)
|
|
return null
|
|
|
|
tagName = target.tagName
|
|
}
|
|
|
|
return target
|
|
}
|
|
|
|
Table.prototype.stopEvent = function(ev) {
|
|
// TODO: use the foundation library
|
|
|
|
if (ev.stopPropagation)
|
|
ev.stopPropagation()
|
|
else
|
|
ev.cancelBubble = true
|
|
|
|
if(ev.preventDefault)
|
|
ev.preventDefault()
|
|
else
|
|
ev.returnValue = false
|
|
}
|
|
|
|
Table.prototype.elementHasClass = function(el, className) {
|
|
// TODO: use the foundation library
|
|
|
|
if (el.classList)
|
|
return el.classList.contains(className);
|
|
|
|
return new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
|
|
}
|
|
|
|
Table.prototype.elementAddClass = function(el, className) {
|
|
// TODO: use the foundation library
|
|
|
|
if (this.elementHasClass(el, className))
|
|
return
|
|
|
|
if (el.classList)
|
|
el.classList.add(className);
|
|
else
|
|
el.className += ' ' + className;
|
|
}
|
|
|
|
Table.prototype.elementRemoveClass = function(el, className) {
|
|
// TODO: use the foundation library
|
|
|
|
if (el.classList)
|
|
el.classList.remove(className);
|
|
else
|
|
el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
|
|
}
|
|
|
|
Table.prototype.parentContainsElement = function(parent, element) {
|
|
while (element && element != parent) {
|
|
element = element.parentNode
|
|
}
|
|
|
|
return element ? true : false
|
|
}
|
|
|
|
Table.prototype.getCellValue = function(cellElement) {
|
|
return cellElement.querySelector('[data-container]').value
|
|
}
|
|
|
|
Table.prototype.getCellRowKey = function(cellElement) {
|
|
return parseInt(cellElement.parentNode.getAttribute('data-row'))
|
|
}
|
|
|
|
Table.prototype.getRowKey = function(rowElement) {
|
|
return parseInt(rowElement.getAttribute('data-row'))
|
|
}
|
|
|
|
Table.prototype.findRowByKey = function(key) {
|
|
return this.dataTable.querySelector('tbody tr[data-row="'+key+'"]')
|
|
}
|
|
|
|
Table.prototype.findRowByIndex = function(index) {
|
|
return this.getDataTableBody().children[index]
|
|
}
|
|
|
|
Table.prototype.getCellRowIndex = function(cellElement) {
|
|
return parseInt(cellElement.parentNode.rowIndex)
|
|
}
|
|
|
|
Table.prototype.getRowCellValueByColumnName = function(row, columnName) {
|
|
var cell = row.querySelector('td[data-column="'+columnName+'"]')
|
|
|
|
if (!cell)
|
|
return cell
|
|
|
|
return this.getCellValue(cell)
|
|
}
|
|
|
|
Table.prototype.getRowData = function(row) {
|
|
var result = {}
|
|
|
|
for (var i = 0, len = row.children.length; i < len; i++) {
|
|
var cell = row.children[i]
|
|
result[cell.getAttribute('data-column')] = this.getCellValue(cell)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
Table.prototype.getCellColumnName = function(cellElement) {
|
|
return cellElement.getAttribute('data-column')
|
|
}
|
|
|
|
Table.prototype.setCellValue = function(cellElement, value, suppressEvents) {
|
|
var dataContainer = cellElement.querySelector('[data-container]')
|
|
|
|
if (dataContainer.value != value) {
|
|
dataContainer.value = value
|
|
|
|
this.markCellRowDirty(cellElement)
|
|
|
|
this.notifyRowProcessorsOnChange(cellElement)
|
|
|
|
if (suppressEvents === undefined || !suppressEvents) {
|
|
this.$el.trigger('oc.tableCellChanged', [
|
|
this.getCellColumnName(cellElement),
|
|
value,
|
|
this.getCellRowIndex(cellElement)
|
|
])
|
|
}
|
|
}
|
|
}
|
|
|
|
Table.DEFAULTS = {
|
|
clientDataSourceClass: 'client',
|
|
keyColumn: 'id',
|
|
recordsPerPage: false,
|
|
data: null,
|
|
postback: true,
|
|
postbackHandlerName: 'onSave',
|
|
adding: true,
|
|
deleting: true,
|
|
toolbar: true,
|
|
rowSorting: false,
|
|
height: false,
|
|
dynamicHeight: false,
|
|
btnAddRowLabel: 'Add row',
|
|
btnAddRowBelowLabel: 'Add row below',
|
|
btnDeleteRowLabel: 'Delete row'
|
|
}
|
|
|
|
// TABLE PLUGIN DEFINITION
|
|
// ============================
|
|
|
|
var old = $.fn.table
|
|
|
|
$.fn.table = function (option) {
|
|
var args = Array.prototype.slice.call(arguments, 1),
|
|
result = undefined
|
|
|
|
this.each(function () {
|
|
var $this = $(this)
|
|
var data = $this.data('oc.table')
|
|
var options = $.extend({}, Table.DEFAULTS, $this.data(), typeof option == 'object' && option)
|
|
if (!data) $this.data('oc.table', (data = new Table(this, options)))
|
|
if (typeof option == 'string') result = data[option].apply(data, args)
|
|
if (typeof result != 'undefined') return false
|
|
})
|
|
|
|
return result ? result : this
|
|
}
|
|
|
|
$.fn.table.Constructor = Table
|
|
|
|
$.oc.table.table = Table
|
|
|
|
// TABLE NO CONFLICT
|
|
// =================
|
|
|
|
$.fn.table.noConflict = function () {
|
|
$.fn.table = old
|
|
return this
|
|
}
|
|
|
|
// TABLE DATA-API
|
|
// ===============
|
|
|
|
$(document).on('render', function(){
|
|
$('div[data-control=table]').table()
|
|
})
|
|
|
|
}(window.jQuery); |