app/assets/javascripts/app/controllers/_application_controller/table.coffee
###
# table based on model
rowClick = (id, e) ->
e.preventDefault()
console.log('rowClick', id)
rowMouseover = (id, e) ->
e.preventDefault()
console.log('rowMouseover', id)
rowMouseout = (id, e) ->
e.preventDefault()
console.log('rowMouseout', id)
rowDblClick = (id, e) ->
e.preventDefault()
console.log('rowDblClick', id)
colClick = (id, e) ->
e.preventDefault()
console.log('colClick', e.target)
checkboxClick = (id, e) ->
e.preventDefault()
console.log('checkboxClick', e.target)
callbackHeader = (headers) ->
console.log('current header is', headers)
# add new header item
attribute =
name: 'some name'
display: 'Some Name'
headers.push attribute
console.log('new header is', headers)
headers
callbackAttributes = (value, object, attribute, header) ->
console.log('data of item col', value, object, attribute, header)
value = 'New Data To Show'
value
new App.ControllerTable(
tableId: 'some_id_to_idientify_user_based_table_preferences'
el: element
overview: ['host', 'user', 'adapter', 'active']
model: App.Channel
objects: data
groupBy: 'adapter'
checkbox: false
radio: false
class: 'some-css-class'
bindRow:
events:
'click': rowClick
'mouseover': rowMouseover
'mouseout': rowMouseout
'dblclick': rowDblClick
bindCol:
host:
events:
'click': colClick
bindCheckbox:
events:
'click': rowClick
'mouseover': rowMouseover
'mouseout': rowMouseout
'dblclick': rowDblClick
callbackHeader: [callbackHeader]
callbackAttributes:
attributeName: [
callbackAttributes
]
dndCallback: =>
items = @el.find('table > tbody > tr')
console.log('all effected items', items)
)
new App.ControllerTable(
el: element
overview: ['time', 'area', 'level', 'browser', 'location', 'data']
attribute_list: [
{ name: 'time', display: 'Time', tag: 'datetime' },
{ name: 'area', display: 'Area', type: 'text' },
{ name: 'level', display: 'Level', type: 'text' },
{ name: 'browser', display: 'Browser', type: 'text' },
{ name: 'location', display: 'Location', type: 'text' },
{ name: 'data', display: 'Data', type: 'text' },
]
objects: data
)
###
class App.ControllerTable extends App.Controller
minColWidth: 30
baseColWidth: 130
minTableWidth: 612
checkBoxColWidth: 30
radioColWidth: 22
sortableColWidth: 36
events:
'click .js-sort': 'sortByColumn'
'click .js-page': 'paginate'
overviewAttributes: undefined
#model: App.TicketPriority,
objects: []
checkbox: false
radio: false
renderState: undefined
groupBy: undefined
groupDirection: undefined
pagerEnabled: true
pagerItemsPerPage: 150
pagerShownPage: 0
customActions: []
columnsLength: undefined
headers: undefined
headerWidth: {}
currentRows: []
orderDirection: 'ASC'
orderBy: undefined
lastOrderDirection: undefined
lastOrderBy: undefined
lastOverview: undefined
customOrderDirection: undefined
customOrderBy: undefined
frontendTimeUpdateExecute: true
bindCol: {}
bindRow: {}
constructor: ->
super
if !@model
@model = {}
@overviewAttributes ||= @overview || @model.configure_overview || []
@attributesListRaw ||= @attribute_list || @model.configure_attributes || {}
@attributesList = App.Model.attributesGet(false, @attributesListRaw)
@destroy = if _.isNull(@destroy) or _.isUndefined(@destroy) then @model.configure_delete else @destroy
@clone = if _.isNull(@clone) or _.isUndefined(@clone) then @model.configure_clone else @clone
@setAsDefault = @model.configure_set_as_default
@unsetDefault = @model.configure_unset_default
throw 'overviewAttributes needed' if _.isEmpty(@overviewAttributes)
throw 'attributesList needed' if _.isEmpty(@attributesList)
# apply personal preferences
data = {}
if @tableId
data = @preferencesGet()
if data.order
for key, value of data.order
@[key] = value
if data.headerWidth
for key, value of data.headerWidth
@headerWidth[key] = value
if !@availableWidth
@availableWidth = @el.width()
if @availableWidth is 0
@availableWidth = @minTableWidth
@renderQueue()
show: =>
return if @windowIsResized isnt true
@windowIsResized = false
@onResize()
hide: ->
release: =>
$(window).off('resize.table', @onResize)
update: (params) =>
if params.sync is true
for key, value of params
@[key] = value
return @render()
@renderQueue(params)
renderQueue: (params) =>
localeRender = =>
for key, value of params
@[key] = value
@render()
App.QueueManager.add('tableRender', localeRender)
App.QueueManager.run('tableRender')
renderPager: (el, find = false) =>
return if !@pagerEnabled
if @pagerAjax
@renderPagerAjax(el, find)
else
@renderPagerStatic(el, find)
renderPagerAjax: (el, find = false) =>
pages = parseInt((@pagerTotalCount - 1) / @pagerPerPage)
if pages < 1
if find
el.find('.js-pager').html('')
else
el.filter('.js-pager').html('')
return
pager = App.view('generic/table_pager')(
page: @pagerSelected - 1
pages: pages
)
if find
el.find('.js-pager').html(pager)
else
el.filter('.js-pager').html(pager)
renderPagerStatic: (el, find = false) =>
pages = parseInt(((@objects.length - 1) / @pagerItemsPerPage))
if pages < 1
if find
el.find('.js-pager').html('')
else
el.filter('.js-pager').html('')
return
pager = App.view('generic/table_pager')(
page: @pagerShownPage
pages: pages
)
if find
el.find('.js-pager').html(pager)
else
el.filter('.js-pager').html(pager)
render: =>
@setMaxPage()
# always render pager in case of ajax pagination
if @pagerTotalCount
@renderPager(@el, true)
if @renderState is undefined
# check if table is empty
if _.isEmpty(@objects)
@renderState = 'emptyList'
@el.html(@renderEmptyList())
$(window).on('resize.table', @onResize)
return ['emptyList.new']
else
@renderState = 'List'
@renderTableFull()
$(window).on('resize.table', @onResize)
return ['fullRender.new']
else if @renderState is 'emptyList' && !_.isEmpty(@objects)
@renderState = 'List'
@renderTableFull()
return ['fullRender']
else if @renderState isnt 'emptyList' && _.isEmpty(@objects)
@renderState = 'emptyList'
@el.html(@renderEmptyList())
return ['emptyList']
else
# check if header has changed
if @tableHeadersHasChanged()
@renderTableFull()
return ['fullRender.overviewAttributesChanged']
# check for changes
newRows = @renderTableRows(true)
removedRows = _.difference(@currentRows, newRows)
addedRows = _.difference(newRows, @currentRows)
@log 'debug', 'table newRows', newRows
@log 'debug', 'table removedRows', removedRows
@log 'debug', 'table addedRows', addedRows
# if only rows are removed
if (!_.isEmpty(addedRows) || !_.isEmpty(removedRows)) && addedRows.length < 10 && removedRows.length < 15 && removedRows.length < newRows.length && !_.isEmpty(newRows)
newCurrentRows = []
removePositions = []
for position in [0..@currentRows.length-1]
if _.contains(removedRows, @currentRows[position])
removePositions.push position
else
newCurrentRows.push @currentRows[position]
addPositions = []
for position in [0..newRows.length-1]
if _.contains(addedRows, newRows[position])
addPositions.push position
newCurrentRows.splice(position,0,newRows[position])
# check if order is still correct
if @_isSame(newRows, newCurrentRows) is true
for position in removePositions.reverse()
@$("tbody > tr:nth-child(#{position+1})").remove()
for position in addPositions
if position is 0
if @$('tbody tr:nth-child(1)').get(0)
@$('tbody tr:nth-child(1)').before(newCurrentRows[position])
else
@$('tbody').append(newCurrentRows[position])
else
@$("tbody > tr:nth-child(#{position})").after(newCurrentRows[position])
@currentRows = newCurrentRows
@log 'debug', 'table.fullRender.contentRemoved', removePositions, addPositions
@renderPager(@el, true)
@frontendTimeUpdateElement(@el) if @frontendTimeUpdateExecute is true
return ['fullRender.contentRemoved', removePositions, addPositions]
if newRows.length isnt @currentRows.length
result = ['fullRender.lenghtChanged', @currentRows.length, newRows.length]
@renderTableFull(newRows)
@log 'debug', 'table.fullRender.lenghtChanged', result
return result
# compare rows
result = @_isSame(newRows, @currentRows)
if result isnt true
@renderTableFull(newRows)
@log 'debug', "table.fullRender.contentChanged|row(#{result})"
return ['fullRender.contentChanged', result]
@log 'debug', 'table.noChanges'
return ['noChanges']
renderEmptyList: =>
App.view('generic/admin/empty')(
explanation: @explanation
)
renderTableFull: (rows, options = {}) =>
@log 'debug', 'table.renderTableFull', @orderBy, @orderDirection
@tableHeaders(options)
@sortList()
bulkIds = @getBulkSelected()
container = @renderTableContainer()
table = container.filter('.table')
if !rows
rows = @renderTableRows()
@currentRows = clone(rows)
else
@currentRows = clone(rows)
container.find('.js-tableBody').html(rows)
@frontendTimeUpdateElement(container) if @frontendTimeUpdateExecute is true
@renderPager(container)
cursorMap =
click: 'pointer'
dblclick: 'pointer'
#mouseover: 'alias'
# bind col.
if !_.isEmpty(@bindCol)
for name, item of @bindCol
if item.events
position = 0
if @dndCallback
position += 1
if @checkbox
position += 1
hit = false
for headerName in @headers
if !hit
position += 1
if headerName.name is name || headerName.name is "#{name}_id" || headerName.name is "#{name}_bulkIds"
hit = true
if hit
for event, callback of item.events
do (table, event, callback) ->
if cursorMap[event]
table.find("tbody > tr > td:nth-child(#{position})").css('cursor', cursorMap[event])
table.on(event, "tbody > tr > td:nth-child(#{position})",
(e) ->
e.stopPropagation()
id = $(e.target).parents('tr').data('id')
callback(id, e, e.currentTarget)
)
# bind row
if !_.isEmpty(@bindRow)
if @bindRow.events
for event, callback of @bindRow.events
do (table, event, callback) ->
if cursorMap[event]
table.find('tbody > tr').css( 'cursor', cursorMap[event] )
table.on(event, 'tbody > tr',
(e) ->
id = $(e.target).parents('tr').data('id')
callback(id, e)
)
# bind bindCheckbox
if @bindCheckbox
if @bindCheckbox.events
for event, callback of @bindCheckbox.events
do (table, event, callback) ->
table.on(event, 'input[name="bulk"]', (e) ->
e.stopPropagation()
id = $(e.currentTarget).parents('tr').data('id')
checked = $(e.currentTarget).prop('checked')
callback(id, checked, e)
)
# if we have a personalised table
if @tableId
# enable resize column
table.on('mousedown touchstart', '.js-col-resize', @onColResizeStart)
table.on('click', '.js-col-resize', @stopPropagation)
# enable checkbox bulk selection
if @checkbox
# click first tr>td, catch click
table.on('click', 'tr > td:nth-child(1)', (e) ->
e.stopPropagation()
)
# bind on full bulk click
table.on('change', 'input[name="bulk_all"]', (e) =>
e.stopPropagation()
clicks = []
if $(e.currentTarget).prop('checked')
$(e.currentTarget).parents('table').find('[name="bulk"]').each( ->
$element = $(@)
return if $element.prop('checked')
$element.prop('checked', true)
id = $element.parents('tr').data('id')
clicks.push [id, true]
)
else
$(e.currentTarget).parents('table').find('[name="bulk"]').each( ->
$element = $(@)
return if !$element.prop('checked')
$element.prop('checked', false)
id = $element.parents('tr').data('id')
clicks.push [id, false]
)
return if !@bindCheckbox
return if !@bindCheckbox.events
return if _.isEmpty(clicks)
# If a select_all callback exists, then trigger it once insteading of triggering the callback once for each checkbox
if @bindCheckbox.select_all
@bindCheckbox.select_all(clicks[0]..., e)
return
for event, callback of @bindCheckbox.events
if event == 'click' || event == 'change'
for click in clicks
callback(click..., e)
)
if @dndCallback && !App.Config.get('translation_inline')
dndOptions =
tolerance: 'pointer'
distance: 15
opacity: 0.6
forcePlaceholderSize: true
items: 'tr'
helper: (e, tr) ->
originals = tr.children()
helper = tr.clone()
helper.children().each (index) ->
# Set helper cell sizes to match the original sizes
$(@).width( originals.eq(index).outerWidth() )
return helper
update: @dndCallback
table.find('tbody').sortable(dndOptions)
@el.html(container)
@setBulkSelected(bulkIds)
renderTableContainer: =>
$(App.view('generic/table')(
tableId: @tableId
headers: @headers
checkbox: @checkbox
radio: @radio
class: @class
sortable: @dndCallback
))
sortObjectKeys: (objects, direction) ->
sorted = Object.keys(objects).sort()
switch direction
when 'DESC'
sorted.reverse()
else
sorted
renderTableRows: (sort = false) =>
if sort is true
@sortList()
position = 0
columnsLength = @headers.length
if @checkbox || @radio
columnsLength++
groupLast = ''
groupLastName = ''
tableBody = []
objectsToShow = @objectsOfPage(@pagerShownPage)
if @groupBy
# group by raw (and not printable) value so dates work also
objectsGrouped = _.groupBy(objectsToShow, (object) => @groupObjectName(object, @groupBy, excludeTags: ['date', 'datetime']))
else
objectsGrouped = { '': objectsToShow }
for groupValue in @sortObjectKeys(objectsGrouped, @groupDirection)
groupObjects = objectsGrouped[groupValue]
for object in groupObjects
objectActions = []
if object
position++
if @groupBy
groupByName = @groupObjectName(object, @groupBy)
if groupLastName isnt groupByName
groupLastName = groupByName
tableBody.push @renderTableGroupByRow(object, position, groupByName)
for action in @actions
# Check if the available key is used, it can be a Boolean or a function which should be called.
if !action.available? || action.available == true
objectActions.push action
else if typeof action.available is 'function' && action.available(object) == true
objectActions.push action
tableBody.push @renderTableRow(object, position, objectActions)
tableBody
renderTableGroupByRow: (object, position, groupByName) =>
ui_table_group_by_show_count = @Config.get('ui_table_group_by_show_count')
groupByCount = undefined
if ui_table_group_by_show_count is true
groupBy = @groupBy
groupLast = @groupObjectName(object, @groupBy)
groupByCount = 0
if @objects
for localObject in @objects
if @groupObjectName(localObject, groupBy) is groupLast
groupByCount += 1
App.view('generic/table_row_group_by')(
position: position
groupByName: groupByName
groupByCount: groupByCount
columnsLength: @columnsLength
)
renderTableRow: (object, position, actions) =>
App.view('generic/table_row')(
headers: @headers
attributes: @attributesList
checkbox: @checkbox
radio: @radio
callbacks: @callbackAttributes
sortable: @dndCallback
position: position
object: object
actions: actions
)
tableHeadersHasChanged: =>
return true if @overviewAttributes isnt @lastOverview
false
tableHeaders: (options = {}) =>
orderBy = @customOrderBy || @orderBy
orderDirection = @customOrderDirection || @orderDirection
if @headers && @lastOrderBy is orderBy && @lastOrderDirection is orderDirection && !@tableHeadersHasChanged()
@log 'debug', 'table.Headers: same overviewAttributes just return headers', @headers
return ['headers are the same', @headers]
@lastOverview = @overviewAttributes
# get header data
@headers = []
@actions = [].concat @customActions
availableWidth = @availableWidth
for item in @overviewAttributes
headerFound = false
for attributeName, attribute of @attributesList
# remove group by attribute from header
if !@groupBy || @groupBy isnt item
if !attribute.style
attribute.style = {}
if attributeName is item || (attributeName is "#{item}_id" || attributeName is "#{item}_ids")
# e.g. column: owner
headerFound = true
if !options.skipHeadersResize
if @headerWidth[attribute.name]
attribute.displayWidth = @headerWidth[attribute.name] * availableWidth
else if !attribute.width
attribute.displayWidth = @baseColWidth
else
# convert percentages to pixels
value = parseInt attribute.width, 10
unit = attribute.width.match(/[px|%]+/)[0]
if unit is '%'
attribute.displayWidth = value / 100 * availableWidth
else
attribute.displayWidth = value
@headers.push attribute
# execute header callback
if @callbackHeader
for callback in @callbackHeader
@headers = callback(@headers)
# update headers to align to trailing edge
if @autoAlignLastColumn
lastColumnConfig = _.last(@headers)
for attr in @headers
if attr.autoAligned
delete attr.autoAligned
delete attr.align
if ['datetime', 'date'].includes(lastColumnConfig?.tag)
if _.isEmpty(lastColumnConfig.align)
lastColumnConfig.align = 'right'
lastColumnConfig.autoAligned = true # set a flag to track which column was automatically aligned
throw 'no headers found' if _.isEmpty(@headers)
if @clone
@actions.push
name: 'clone'
display: __('Clone')
icon: 'clipboard'
class: 'create js-clone'
callback: (id) =>
item = @model.find(id)
item.name = "Clone: #{item.name}"
if @cloneCallback
@cloneCallback(item)
return
new App.ControllerGenericNew(
item: item
pageData:
object: item.constructor.className
callback: =>
@renderTableFull()
genericObject: item.constructor.className
container: @container
)
if @destroy
@actions.push
name: 'delete'
display: __('Delete')
icon: 'trash'
class: 'danger js-delete'
callback: (id) =>
item = @model.find(id)
new App.ControllerGenericDestroyConfirm
item: item
container: @container
if @setAsDefault
@actions.push
name: 'setAsDefault'
icon: 'reload'
display: __('Set as default')
class: 'js-set-as-default'
available: (item) => !@model.is_default(item)
callback: (id) =>
item = @model.find(id)
new App.ControllerConfirm
message: App.i18n.translatePlain('Are you sure you want to set "%s" as default?', item.name)
item: item
container: @container
callback: =>
@model.set_as_default(item)
if @unsetDefault
@actions.push
name: 'unsetDefault'
icon: 'inactive-reload'
display: __('Unset default')
class: 'js-unset-default'
available: (item) => @model.is_default(item)
callback: (id) =>
item = @model.find(id)
new App.ControllerConfirm
message: App.i18n.translatePlain('Are you sure you want to unset "%s" as default?', item.name)
item: item
container: @container
callback: =>
@model.unset_default(item)
if @actions.length
@headers.push
name: 'action'
display: __('Action')
width: '50px'
displayWidth: 50
align: 'right'
parentClass: 'noTruncate no-padding'
unresizable: true
unsortable: true
@bindCol['action'] =
events:
click: @toggleActionDropdown
@calculateHeaderWidths()
@storeHeaderWidths()
@columnsLength = @headers.length
if @checkbox || @radio
@columnsLength++
@log 'debug', 'table.Headers: new headers', @headers
['new headers', @headers]
setMaxPage: =>
return if !@pagerEnabled
pages = parseInt(((@objects.length - 1) / @pagerItemsPerPage))
if parseInt(@pagerShownPage) > pages
@pagerShownPage = pages
objectsOfPage: (page = 0) =>
return @objects if !@pagerEnabled
page = parseInt(page)
@objects.slice(page * @pagerItemsPerPage, (page + 1) * @pagerItemsPerPage)
paginate: (e) =>
e.stopPropagation()
page = $(e.currentTarget).attr('data-page')
if @pagerAjax
@navigate "#{@pagerBaseUrl}#{(parseInt(page) + 1)}"
else
render = =>
@pagerShownPage = page
@renderTableFull()
App.QueueManager.add('tableRender', render)
App.QueueManager.run('tableRender')
sortList: =>
return if _.isEmpty(@objects)
orderBy = @customOrderBy || @orderBy
orderDirection = @customOrderDirection || @orderDirection
@log 'debug', 'table.order', @orderBy, @orderDirection
@log 'debug', 'table.customOrder', @customOrderBy, @customOrderDirection
return if _.isEmpty(orderBy) && _.isEmpty(@groupBy)
return if @lastSortedobjects is @objects && @lastOrderDirection is orderDirection && @lastOrderBy is orderBy
@lastOrderDirection = orderDirection
@lastOrderBy = orderBy
# sorting for ajax pagination will be made in the backend
# so we only set the arrow for the sort direction
if @pagerAjax
for header in @headers
if header.name is orderBy || "#{header.name}_id" is orderBy || header.name is "#{orderBy}_id"
if orderDirection is 'DESC'
header.sortOrderIcon = ['arrow-down', 'table-sort-arrow']
else
header.sortOrderIcon = ['arrow-up', 'table-sort-arrow']
else
header.sortOrderIcon = undefined
return
# Underscore's sortBy cannot deal with null values, so we replace null values with a place holder string
sortBy = (list, iteratee) ->
_.sortBy(
list
(item) ->
res = iteratee(item)
# Checking for a falsey value would overide 0 or false to placeholder.
# UnderscoreJS sorter breaks when \uFFFF is sorted together with non-string values.
return res if res != null and res != undefined and res != ''
# null values are considered lexicographically "last"
'\uFFFF'
)
modelAttributes = App[@model]?.configure_attributes || []
localObjects is undefined
if orderBy
for header in @headers
if header.name is orderBy || "#{header.name}_id" is orderBy || header.name is "#{orderBy}_id"
localObjects = sortBy(
@objects
(item) ->
# error handling
if !item
console.log('Got empty object in order by with header _.sortBy')
return ''
if _.includes(['multiselect', 'select'], header.tag)
rawValue = item[header.name]
if !_.isArray(rawValue)
rawValue = [rawValue]
sortValue = if _.isArray(header.options)
rawValue
.map (elem) -> _.findIndex(header.options, (option) -> option.value == elem)
.sort()
else if _.isObject(header.options)
rawValue
.map (elem) ->
displayValue = header.options[elem]
if displayValue && header.translate
displayValue = App.i18n.translateInline(displayValue)
value = displayValue || elem
if typeof value is 'string'
value = value.toLocaleLowerCase()
value
.sort()
else if header.relation
rawValue
.map (elem) ->
relatedItem = App[header.relation].findNative(item[header.name])
return '' if !relatedItem
displayValue = relatedItem.displayName?() || relatedItem.name || ''
if displayValue && header.translate
displayValue = App.i18n.translateInline(displayValue)
value = displayValue || elem
if typeof value is 'string'
value = value.toLocaleLowerCase()
value
.sort()
else
rawValue
return sortValue
# if we need to sort translated col.
if header.translate
return App.i18n.translateInline(item[header.name])
# if we need to sort by relation name
if header.relation
if item[header.name]
localItem = App[header.relation].findNative(item[header.name])
if localItem
if localItem.displayName
localItem = localItem.displayName().toLowerCase()
if localItem.name
localItem = localItem.name.toLowerCase()
return localItem
return ''
if header.tag is 'float' || modelAttributes[header.name]?.tag is 'float'
return parseFloat(item[header.name])
if header.tag is 'integer' || modelAttributes[header.name]?.tag is 'integer'
return parseInt(item[header.name])
item[header.name]
)
if orderDirection is 'DESC'
header.sortOrderIcon = ['arrow-down', 'table-sort-arrow']
localObjects = localObjects.reverse()
else
header.sortOrderIcon = ['arrow-up', 'table-sort-arrow']
else
header.sortOrderIcon = undefined
# in case order by is not in show column, use orderBy attribute
if !localObjects
for attributeName, attribute of @attributesList
if attributeName is orderBy || "#{attributeName}_id" is orderBy || attributeName is "#{orderBy}_id"
# order by
localObjects = _.sortBy(
@objects
(item) ->
# error handling
if !item
console.log('Got empty object in order by in attribute _.sortBy')
return ''
# if we need to sort translated col.
if attribute.translate
return App.i18n.translateInline(item[attribute.name])
# if we need to sort by relation name
if attribute.relation
if item[attribute.name]
localItem = App[attribute.relation].findNative(item[attribute.name])
if localItem
if localItem.displayName
localItem = localItem.displayName().toLowerCase()
if localItem.name
localItem = localItem.name.toLowerCase()
return localItem
return ''
if attribute.tag is 'float' || modelAttributes[attribute.name]?.tag is 'float'
return parseFloat(item[attribute.name])
if attribute.tag is 'integer' || modelAttributes[attribute.name]?.tag is 'integer'
return parseInt(item[attribute.name])
item[attribute.name]
)
if orderDirection is 'DESC'
localObjects = localObjects.reverse()
if localObjects
@objects = localObjects
else
console.log("Unable to orderBy objects, no attribute found with name #{orderBy}")
# group by
if @groupBy
# get groups
groupObjects = {}
for object in @objects
group = @groupObjectName(object, @groupBy)
groupObjects[group] ||= []
groupObjects[group].push object
groupsSorted = []
for key of groupObjects
groupsSorted.push key
groupsSorted = groupsSorted.sort()
# Reverse the sorted groups depending on the groupDirection
if @groupDirection == 'DESC'
groupsSorted.reverse()
# get new order
localObjects = []
for group in groupsSorted
localObjects = localObjects.concat groupObjects[group]
groupObjects[group] = [] # release old array
return if localObjects is undefined
@objects = localObjects
@lastSortedobjects = localObjects
groupObjectName: (object, key = undefined, options = {}) ->
group = object
if key
if key not of object
key += '_id'
# return internal value if needed
return object[key] if options.excludeTags && _.find(@attributesList, (attr) -> attr.name == key && _.contains(options.excludeTags, attr.tag))
group = App.viewPrint(object, key, @attributesList)
if _.isEmpty(group)
group = ''
if group.displayName
group = group.displayName().toLowerCase()
else if group.name
group = group.name.toLowerCase()
group
onActionButtonClicked: (e) =>
id = $(e.currentTarget).parents('tr').data('id')
name = e.currentTarget.getAttribute('data-table-action')
@runAction(name, id, e)
runAction: (name, id, e = undefined) =>
action = _.findWhere @actions, name: name
action.callback(id, e)
toggleActionDropdown: (id, e, td) =>
e.stopPropagation()
$dropdown = $(td).find('.js-table-action-menu')
if $dropdown.length
# open dropdown
$dropdown.dropdown('toggle')
# check if bind to open dropdown is already done
return if $dropdown.prop('rendered')
# remember that bind to open dropdown is already done
$dropdown.prop('rendered', true)
# bind on click now that the dropdown is open
$dropdown.on('click.dropdown', '[data-table-action]', @onActionButtonClicked)
else
# only one action - directly fire that action
name = $(td).find('[data-table-action]').attr('data-table-action')
@runAction(name, id, e)
calculateHeaderWidths: ->
return if !@tableId
return if !@headers
availableWidth = @availableWidth
# ensure all widths are integers
@headers = _.map @headers, (col) ->
col.displayWidth = Math.floor(col.displayWidth)
return col
shrinkBy = Math.ceil (@getHeaderWidths() - availableWidth) / @getShrinkableHeadersCount()
# make all cols evenly smaller
@headers = _.map @headers, (col) =>
if !col.unresizable
col.displayWidth = Math.max(@minColWidth, col.displayWidth - shrinkBy)
return col
# give left-over space from rounding to last column to get to 100%
roundingLeftOver = availableWidth - @getHeaderWidths()
# but only if there is something left over (will get negative when there are too many columns for each column to stay in their min width)
if roundingLeftOver > 0 && roundingLeftOver < 10
@headers[@headers.length - 1].displayWidth = @headers[@headers.length - 1].displayWidth + roundingLeftOver
true
getShrinkableHeadersCount: ->
_.reduce @headers, (memo, col) ->
return if col.unresizable then memo else memo+1
, 0
getHeaderWidths: ->
widths = _.reduce @headers, (memo, col, i) ->
return memo + col.displayWidth
, 0
if @checkbox
widths += @checkBoxColWidth
if @radio
widths += @radioColWidth
if @dndCallback
widths += @sortableColWidth
widths
setHeaderWidths: =>
return if !@calculateHeaderWidths()
@$('.js-tableHead').each (i, el) =>
el.style.width = @headers[i].displayWidth + 'px'
@storeHeaderWidths()
storeHeaderWidths: ->
widths = {}
for header in @headers
widths[header.name] = header.displayWidth / @availableWidth
@headerWidth = widths
App.LocalStorage.set(@preferencesStoreKey(), { headerWidth: widths }, @Session.get('id'))
onResize: =>
localWidth = @el.width()
if localWidth is 0
@windowIsResized = true
return
@availableWidth = localWidth
localDelay = =>
localSetHeaderWidths = =>
@availableWidth = @el.width()
@setHeaderWidths()
App.QueueManager.add('tableRender', localSetHeaderWidths)
App.QueueManager.run('tableRender')
@delay(localDelay, 200, 'table-resize-finish')
stopPropagation: (event) ->
event.stopPropagation()
getPageX: (event) ->
return event.pageX if event.originalEvent instanceof MouseEvent
return event.targetTouches[0].pageX if event.targetTouches[0]
event.changedTouches[event.changedTouches.length - 1].pageX
onColResizeStart: (event) =>
@resizeTargetLeft = $(event.currentTarget).parents('th')
@resizeTargetRight = @resizeTargetLeft.next()
@resizeStartX = @getPageX(event)
@resizeLeftStartWidth = @resizeTargetLeft.width()
@resizeRightStartWidth = @resizeTargetRight.width()
$(document).on('mousemove.resizeCol touchmove.resizeCol', @onColResizeMove)
$(document).one('mouseup touchend', @onColResizeEnd)
@tableWidth = @el.width()
onColResizeMove: (event) =>
pageX = @getPageX(event)
# use pixels while moving for max precision
if App.i18n.dir() is 'rtl'
difference = @resizeStartX - pageX
else
difference = pageX - @resizeStartX
if @resizeLeftStartWidth + difference < @minColWidth
difference = - (@resizeLeftStartWidth - @minColWidth)
if @resizeRightStartWidth - difference < @minColWidth
difference = @resizeRightStartWidth - @minColWidth
@resizeTargetLeft.width @resizeLeftStartWidth + difference
@resizeTargetRight.width @resizeRightStartWidth - difference
onColResizeEnd: =>
$(document).off('mousemove.resizeCol touchmove.resizeCol')
# switch to percentage
resizeBaseWidth = @resizeTargetLeft.parents('table').width()
leftWidth = @resizeTargetLeft.outerWidth() / resizeBaseWidth
rightWidth = @resizeTargetRight.outerWidth() / resizeBaseWidth
leftColumnKey = @resizeTargetLeft.attr('data-column-key')
rightColumnKey = @resizeTargetRight.attr('data-column-key')
# update store and runtime @headerWidth
@preferencesStore('headerWidth', leftColumnKey, leftWidth)
@headerWidth[leftColumnKey] = leftWidth
# set header at runtime
for header in @headers
if header.name is leftColumnKey
header.displayWidth = @resizeTargetLeft.outerWidth()
# update store and runtime @headerWidth
if rightColumnKey
@preferencesStore('headerWidth', rightColumnKey, rightWidth)
@headerWidth[rightColumnKey] = rightWidth
# set header at runtime
for header in @headers
if header.name is rightColumnKey
header.displayWidth = @resizeTargetRight.outerWidth()
sortByColumn: (event) =>
column = $(event.currentTarget).closest('[data-column-key]').attr('data-column-key')
# for ajax pagination we only accept valid attributes for sorting
if @model && @pagerAjax
return if !@attributesList[column]
orderBy = @customOrderBy || @orderBy
orderDirection = @customOrderDirection || @orderDirection
# sort, update runtime @orderBy and @orderDirection
if orderBy isnt column
orderBy = column
orderDirection = 'ASC'
else
if orderDirection is 'ASC'
orderDirection = 'DESC'
else
orderDirection = 'ASC'
@orderBy = orderBy
@orderDirection = orderDirection
@customOrderBy = orderBy
@customOrderDirection = orderDirection
# update store
@preferencesStore('order', 'customOrderBy', @orderBy)
@preferencesStore('order', 'customOrderDirection', @orderDirection)
if @sortClickCallback
@sortClickCallback(@)
if @sortRenderCallback
App.QueueManager.add('tableRender', @sortRenderCallback)
else
render = =>
@renderTableFull(false, skipHeadersResize: true)
App.QueueManager.add('tableRender', render)
App.QueueManager.run('tableRender')
preferencesStore: (type, key, value) ->
data = @preferencesGet()
if !data[type]
data[type] = {}
if !data[type][key]
data[type][key] = {}
data[type][key] = value
App.LocalStorage.set(@preferencesStoreKey(), data, @Session.get('id'))
preferencesGet: =>
data = App.LocalStorage.get(@preferencesStoreKey(), @Session.get('id'))
return {} if !data
data
preferencesStoreKey: =>
"tablePrefs:#{@tableId}"
getBulkSelected: =>
ids = []
@$('[name="bulk"]:checked').each( (index, element) ->
id = $(element).val()
ids.push id
)
ids
setBulkSelected: (ids) ->
@$('[name="bulk"]').each( (index, element) ->
id = $(element).val()
for idSelected in ids
if idSelected is id
$(element).prop('checked', true)
)
_isSame: (array1, array2) ->
for position in [0..array1.length-1]
if array1[position] isnt array2[position]
return position
true