-
-
Save jdanyow/9782317010240c90fc30e179eeb41064 to your computer and use it in GitHub Desktop.
Aurelia DataGrid Prototype
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>SAMS</title> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css" /> | |
<style> | |
* { | |
color: #333; | |
font-family: Helvetica, Arial, sans-serif; | |
} | |
body { | |
position: fixed; | |
left: 0; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
} | |
</style> | |
</head> | |
<body aurelia-app="src/main"> | |
<script src="https://cdn.jsdelivr.net/gh/aurelia/script@master/scripts/system.js"></script> | |
<script> | |
System.config({ | |
transpiler: 'typescript', | |
typescriptOptions: { | |
"emitDecoratorMetadata": true, | |
"experimentalDecorators": true | |
}, | |
map: { | |
typescript: 'https://cdnjs.cloudflare.com/ajax/libs/typescript/2.0.3/typescript.min.js' | |
}, | |
packages: { | |
"src": { | |
defaultJSExtensions: true, | |
defaultExtension: "ts" | |
} | |
} | |
}); | |
</script> | |
<script src="https://cdn.jsdelivr.net/gh/aurelia/script@master/scripts/aurelia-core.min.js"></script> | |
<script> | |
System.import('aurelia-bootstrapper'); | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<require from="./roster-grid/roster-grid"></require> | |
<style> | |
.slider-label { | |
border: 3px solid #EEE; | |
margin-right: 10px; | |
} | |
</style> | |
<label class="slider-label"> | |
Frozen Columns: | |
<input type="range" value.bind="frozenColumns" min="0" max="30" step="1"> | |
${frozenColumns} | |
</label> | |
<label class="slider-label"> | |
Width: | |
<input type="range" value.bind="width" min="0" max="2000" max.bind="maxWidth" step="1"> | |
${width} | |
</label> | |
<label class="slider-label"> | |
Height: | |
<input type="range" value.bind="height" min="0" max="2000" max.bind="maxHeight" step="1"> | |
${height} | |
</label> | |
<label> | |
Group By: | |
<select value.bind="groupBy"> | |
<option model.bind="null"></option> | |
<option model.bind="'Service'">Service</option> | |
<option model.bind="'ClientFullName'">Consumer</option> | |
</select> | |
</label> | |
<label> | |
Sort By: | |
<select value.bind="sortBy"> | |
<option model.bind="null"></option> | |
<option model.bind="'Service'">Service</option> | |
<option model.bind="'ClientFullName'">Consumer</option> | |
</select> | |
</label> | |
<label> | |
<input type="checkbox" checked.bind="sortDescending"> | |
Descending? | |
</label> | |
<roster-grid items.bind="rosterRecordItems" | |
group-by.bind="groupBy" | |
columns.bind="columns" | |
frozen-columns.bind="frozenColumns" | |
row-height.bind="26" | |
left.bind="25" | |
top.bind="50" | |
width.bind="width" | |
height.bind="height" | |
sort-by.bind="sortBy" | |
sort-descending.bind="sortDescending"> | |
</roster-grid> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {RosterGridColumnDefinition} from './roster-grid/roster-grid-interfaces'; | |
export class App { | |
public rosterRecordItems: any[]; | |
public columns: RosterGridColumnDefinition[] = []; | |
public frozenColumns = 2; | |
public width = document.body.getBoundingClientRect().width - 50; | |
public height = document.body.getBoundingClientRect().height - 100; | |
public maxWidth = document.body.getBoundingClientRect().width - 50; | |
public maxHeight = document.body.getBoundingClientRect().height - 100; | |
public groupBy = 'ClientFullName'; | |
public sortBy = 'Service'; | |
public sortDescending = false; | |
constructor() { | |
this.columns.push({ | |
header: 'Consumer', | |
property: 'ClientFullName', | |
width: 200, | |
type: 'string', | |
editable: false, | |
sortable: true | |
}); | |
this.columns.push({ | |
header: 'Service', | |
property: 'Service', | |
width: 150, | |
type: 'string', | |
editable: false, | |
sortable: true | |
}); | |
const date = new Date(); | |
const daysInMonth = new Date(date.getFullYear(), date.getMonth(), 0).getDate(); | |
for (let day = 1; day <= daysInMonth; day++) { | |
this.columns.push({ | |
header: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][new Date(2016, 9, day).getDay()] | |
+ ' ' + day.toString(10), | |
property: `Day${day}`, | |
width: 80, | |
type: 'number', | |
editable: true, | |
sortable: false | |
}); | |
} | |
} | |
public activate() { | |
const items: any[] = []; | |
const consumers = mockConsumers; | |
const services = mockServices; | |
for (let i = 0; i < consumers.length; i++) { | |
for (let j = 0; j < services.length; j++) { | |
const item: any = { | |
ClientFullName: `${consumers[i].lastName}, ${consumers[i].firstName}`, | |
Service: services[j] | |
}; | |
for (let c = 1; c <= 31; c++) { | |
item[`Day${c}`] = +(Math.random() * 100).toFixed(2); | |
} | |
items.push(item); | |
} | |
} | |
this.rosterRecordItems = items; | |
} | |
} | |
const mockServices = ['Congregate Meals', 'Home Delivered Meals', 'Transportation']; | |
const mockConsumers = [{ | |
"firstName": "Larry", | |
"lastName": "Hunt" | |
}, { | |
"firstName": "Earl", | |
"lastName": "Hamilton" | |
}, { | |
"firstName": "Michelle", | |
"lastName": "Murphy" | |
}, { | |
"firstName": "Carlos", | |
"lastName": "Sims" | |
}, { | |
"firstName": "Sean", | |
"lastName": "Marshall" | |
}, { | |
"firstName": "Karen", | |
"lastName": "Fuller" | |
}, { | |
"firstName": "Ernest", | |
"lastName": "Carroll" | |
}, { | |
"firstName": "Cynthia", | |
"lastName": "Miller" | |
}, { | |
"firstName": "Joan", | |
"lastName": "Stanley" | |
}, { | |
"firstName": "Philip", | |
"lastName": "Ray" | |
}, { | |
"firstName": "Paula", | |
"lastName": "Richards" | |
}, { | |
"firstName": "Juan", | |
"lastName": "Russell" | |
}, { | |
"firstName": "Janet", | |
"lastName": "Peters" | |
}, { | |
"firstName": "Bobby", | |
"lastName": "Shaw" | |
}, { | |
"firstName": "Ralph", | |
"lastName": "Rice" | |
}, { | |
"firstName": "Patricia", | |
"lastName": "Burton" | |
}, { | |
"firstName": "Patricia", | |
"lastName": "Stanley" | |
}, { | |
"firstName": "Edward", | |
"lastName": "Crawford" | |
}, { | |
"firstName": "Phyllis", | |
"lastName": "Watson" | |
}, { | |
"firstName": "Victor", | |
"lastName": "Schmidt" | |
}, { | |
"firstName": "Marie", | |
"lastName": "Scott" | |
}, { | |
"firstName": "Justin", | |
"lastName": "Cruz" | |
}, { | |
"firstName": "Bobby", | |
"lastName": "Duncan" | |
}, { | |
"firstName": "Russell", | |
"lastName": "Price" | |
}, { | |
"firstName": "Heather", | |
"lastName": "Alvarez" | |
}, { | |
"firstName": "Jesse", | |
"lastName": "Howard" | |
}, { | |
"firstName": "Justin", | |
"lastName": "Clark" | |
}, { | |
"firstName": "Joseph", | |
"lastName": "Wood" | |
}, { | |
"firstName": "Annie", | |
"lastName": "Gilbert" | |
}, { | |
"firstName": "Steven", | |
"lastName": "Gonzales" | |
}, { | |
"firstName": "Carl", | |
"lastName": "Hayes" | |
}, { | |
"firstName": "Linda", | |
"lastName": "Schmidt" | |
}, { | |
"firstName": "Martha", | |
"lastName": "Hunt" | |
}, { | |
"firstName": "Todd", | |
"lastName": "Franklin" | |
}, { | |
"firstName": "Tammy", | |
"lastName": "Perry" | |
}, { | |
"firstName": "Carlos", | |
"lastName": "Hansen" | |
}, { | |
"firstName": "Nicholas", | |
"lastName": "Henderson" | |
}, { | |
"firstName": "Debra", | |
"lastName": "Hernandez" | |
}, { | |
"firstName": "Carl", | |
"lastName": "Warren" | |
}, { | |
"firstName": "Diana", | |
"lastName": "Meyer" | |
}, { | |
"firstName": "Marie", | |
"lastName": "Jenkins" | |
}, { | |
"firstName": "Diane", | |
"lastName": "Graham" | |
}, { | |
"firstName": "Jose", | |
"lastName": "Meyer" | |
}, { | |
"firstName": "Louise", | |
"lastName": "Price" | |
}, { | |
"firstName": "Steve", | |
"lastName": "Thompson" | |
}, { | |
"firstName": "Eugene", | |
"lastName": "Brooks" | |
}, { | |
"firstName": "Jacqueline", | |
"lastName": "Morrison" | |
}, { | |
"firstName": "Brenda", | |
"lastName": "Brooks" | |
}, { | |
"firstName": "Emily", | |
"lastName": "Reynolds" | |
}, { | |
"firstName": "Christina", | |
"lastName": "Evans" | |
}, { | |
"firstName": "Jesse", | |
"lastName": "Ellis" | |
}, { | |
"firstName": "Phyllis", | |
"lastName": "Allen" | |
}, { | |
"firstName": "Scott", | |
"lastName": "Ward" | |
}, { | |
"firstName": "Patrick", | |
"lastName": "Day" | |
}, { | |
"firstName": "Kathy", | |
"lastName": "Price" | |
}, { | |
"firstName": "Barbara", | |
"lastName": "Lawrence" | |
}, { | |
"firstName": "Andrew", | |
"lastName": "Butler" | |
}, { | |
"firstName": "Todd", | |
"lastName": "Torres" | |
}, { | |
"firstName": "Rose", | |
"lastName": "Chapman" | |
}, { | |
"firstName": "Mark", | |
"lastName": "Watkins" | |
}, { | |
"firstName": "Victor", | |
"lastName": "Fields" | |
}, { | |
"firstName": "Jean", | |
"lastName": "Alexander" | |
}, { | |
"firstName": "Christine", | |
"lastName": "Arnold" | |
}, { | |
"firstName": "Antonio", | |
"lastName": "Ferguson" | |
}, { | |
"firstName": "Frances", | |
"lastName": "Henderson" | |
}, { | |
"firstName": "Julie", | |
"lastName": "Howard" | |
}, { | |
"firstName": "Samuel", | |
"lastName": "Cole" | |
}, { | |
"firstName": "Randy", | |
"lastName": "Lewis" | |
}, { | |
"firstName": "Diane", | |
"lastName": "Smith" | |
}, { | |
"firstName": "Raymond", | |
"lastName": "Russell" | |
}, { | |
"firstName": "Alice", | |
"lastName": "Foster" | |
}, { | |
"firstName": "Janice", | |
"lastName": "Lawson" | |
}, { | |
"firstName": "Thomas", | |
"lastName": "Little" | |
}, { | |
"firstName": "Frances", | |
"lastName": "Nguyen" | |
}, { | |
"firstName": "Gerald", | |
"lastName": "Armstrong" | |
}, { | |
"firstName": "Jimmy", | |
"lastName": "Schmidt" | |
}, { | |
"firstName": "James", | |
"lastName": "Lopez" | |
}, { | |
"firstName": "Joshua", | |
"lastName": "Torres" | |
}, { | |
"firstName": "Juan", | |
"lastName": "Peters" | |
}, { | |
"firstName": "Deborah", | |
"lastName": "Gibson" | |
}, { | |
"firstName": "Ruby", | |
"lastName": "Gonzales" | |
}, { | |
"firstName": "Samuel", | |
"lastName": "Simmons" | |
}, { | |
"firstName": "Carlos", | |
"lastName": "Gomez" | |
}, { | |
"firstName": "Lori", | |
"lastName": "Ortiz" | |
}, { | |
"firstName": "Ryan", | |
"lastName": "Jones" | |
}, { | |
"firstName": "William", | |
"lastName": "Davis" | |
}, { | |
"firstName": "Virginia", | |
"lastName": "Kim" | |
}, { | |
"firstName": "Joshua", | |
"lastName": "Alexander" | |
}, { | |
"firstName": "Jennifer", | |
"lastName": "Adams" | |
}, { | |
"firstName": "Juan", | |
"lastName": "White" | |
}, { | |
"firstName": "Arthur", | |
"lastName": "Foster" | |
}, { | |
"firstName": "Mary", | |
"lastName": "Edwards" | |
}, { | |
"firstName": "Lisa", | |
"lastName": "Mccoy" | |
}, { | |
"firstName": "Lawrence", | |
"lastName": "Wood" | |
}, { | |
"firstName": "Carol", | |
"lastName": "Parker" | |
}, { | |
"firstName": "Janet", | |
"lastName": "Hansen" | |
}, { | |
"firstName": "William", | |
"lastName": "Hayes" | |
}, { | |
"firstName": "Melissa", | |
"lastName": "Harris" | |
}, { | |
"firstName": "Christine", | |
"lastName": "Coleman" | |
}, { | |
"firstName": "Theresa", | |
"lastName": "Ray" | |
}]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {viewEngineHooks, ViewFactory} from 'aurelia-templating'; | |
@viewEngineHooks() | |
export class CleanBindingCommands { | |
public afterCompile(viewFactory: ViewFactory) { | |
const targets = (<any>viewFactory).template.querySelectorAll('.au-target'); | |
for (let i = 0; i < targets.length; i++) { | |
let el = targets[i]; | |
for (let ii = 0; ii < el.attributes.length; ii++) { | |
let attr = el.attributes[ii]; | |
let parts = attr.name.split('.'); | |
if (parts.length === 2) { | |
el.removeAttribute(attr.name); | |
ii--; | |
} | |
} | |
el.removeAttribute('ref'); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {Aurelia} from 'aurelia-framework'; | |
export function configure(aurelia: Aurelia) { | |
aurelia.use | |
.basicConfiguration() | |
.developmentLogging() | |
.globalResources([ | |
'./src/clean-commands', | |
'./src/set-attribute-binding-behavior', | |
'./src/value-converters/coalesce-value-converter', | |
'./src/value-converters/slice-value-converter', | |
'./src/value-converters/to-fixed-value-converter', | |
]); | |
aurelia.start().then(() => aurelia.setRoot()); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {EventAggregator, Subscription} from 'aurelia-event-aggregator'; | |
import { | |
RosterGridSelectionInfo, | |
RosterGridLayoutInfo, | |
RosterGridColumnDefinition | |
} from './roster-grid-interfaces'; | |
import constants from './roster-grid-constants'; | |
export class RosterGridCellEditor { | |
private input: HTMLInputElement; | |
private selection: RosterGridSelectionInfo; | |
private columns: RosterGridColumnDefinition[]; | |
private items: any[]; | |
private subscriptions: Subscription[]; | |
private mouseIsDown = false; | |
private beginEditOnMouseUp = false; | |
constructor(bus: EventAggregator) { | |
this.createInput(); | |
this.subscriptions = [ | |
bus.subscribe(constants.events.layoutPropertiesChanged, this.layoutPropertiesChanged), | |
bus.subscribe(constants.events.dataViewChanged, this.dataViewChanged), | |
bus.subscribe(constants.events.beforeSelectionChanged, this.beforeSelectionChanged), | |
bus.subscribe(constants.events.selectionChanged, this.selectionChanged), | |
bus.subscribe(constants.events.dom.mousedown, () => this.mouseIsDown = true), | |
bus.subscribe(constants.events.dom.mouseup, this.mouseUp), | |
]; | |
} | |
public dispose() { | |
this.endEdit(); | |
this.subscriptions.forEach(subscription => subscription.dispose()); | |
this.destroyInput(); | |
} | |
private createInput() { | |
const input = this.input = document.createElement('input'); | |
input.type = 'text'; | |
input.maxLength = 10; | |
input.className = 'roster-grid-cell-input'; | |
input.addEventListener('blur', this.inputBlur); | |
input.addEventListener('keydown', this.inputKeyDown); | |
} | |
private destroyInput() { | |
const input = this.input; | |
input.removeEventListener('blur', this.inputBlur); | |
input.removeEventListener('keydown', this.inputKeyDown); | |
} | |
private layoutPropertiesChanged = (layout: RosterGridLayoutInfo) => { | |
this.endEdit(); | |
this.columns = layout.columns; | |
}; | |
private dataViewChanged = (items: any[]) => { | |
this.endEdit(); | |
this.items = items; | |
}; | |
private beforeSelectionChanged = (selection: RosterGridSelectionInfo) => { | |
this.endEdit(); | |
}; | |
private selectionChanged = (selection: RosterGridSelectionInfo) => { | |
this.selection = selection; | |
if (this.mouseIsDown) { | |
this.beginEditOnMouseUp = true; | |
} else { | |
this.beginEditOnMouseUp = false; | |
this.beginEdit(); | |
} | |
}; | |
private mouseUp = () => { | |
this.mouseIsDown = false; | |
if (this.beginEditOnMouseUp) { | |
this.beginEditOnMouseUp = false; | |
this.beginEdit(); | |
} | |
}; | |
private inputBlur = (event: FocusEvent) => { | |
this.endEdit(); | |
}; | |
private inputKeyDown = (event: KeyboardEvent) => { | |
// todo | |
}; | |
private beginEdit() { | |
const cell = this.selection.ranges[this.selection.ranges.length - 1].end; | |
const id = `roster-grid-cell-${cell.col}-${cell.row}`; | |
const cellDiv = <HTMLDivElement>document.getElementById(id); | |
if (!cellDiv) { | |
return; | |
} | |
cellDiv.textContent = ''; | |
cellDiv.style.padding = '0'; | |
cellDiv.style.border = '2px solid #99cfea'; | |
cellDiv.appendChild(this.input); | |
const value = this.items[cell.row - 1][this.columns[cell.col].property]; | |
this.input.value = value.toFixed(2); | |
this.input.focus(); | |
this.input.select(); | |
} | |
private endEdit() { | |
if (!this.items || !this.columns) { | |
return; | |
} | |
const cellDiv = <HTMLDivElement>this.input.parentElement; | |
if (cellDiv) { | |
cellDiv.style.padding = null; | |
cellDiv.style.border = null; | |
cellDiv.removeChild(this.input); | |
const cell = this.selection.ranges[this.selection.ranges.length - 1].end; | |
const value = this.items[cell.row][this.columns[cell.col].property]; | |
cellDiv.textContent = value; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default { | |
minColumnWidth: 10, | |
defaults: { | |
frozenColumns: 0, | |
rowHeight: 26 | |
}, | |
events: { | |
layoutPropertiesChanged: 'layout-properties-changed', | |
layoutApplied: 'layout-applied', | |
resizeColumnRequested: 'resize-column-requested', | |
sortRequested: 'sort-requested', | |
beforeSelectionChanged: 'before-selection-changed', | |
selectionChanged: 'selection-changed', | |
dataViewDefinitionChanged: 'data-view-definition-changed', | |
dataViewChanged: 'data-view-changed', | |
cellChanged: 'cell-changed', | |
dom: { | |
scroll: 'scroll', | |
mousemove: 'mousemove', | |
mouseup: 'mouseup', | |
mousedown: 'mousedown', | |
mouseover: 'mouseover', | |
mouseout: 'mouseout', | |
wheel: 'wheel' | |
} | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {EventAggregator, Subscription} from 'aurelia-event-aggregator'; | |
import {RosterGridDataViewDefinition, RosterGridColumnDefinition} from './roster-grid-interfaces'; | |
import constants from './roster-grid-constants'; | |
interface GroupHeader { | |
isGroupHeader: true; | |
name: string; | |
items: any[]; | |
[propertyName: string]: any; | |
} | |
export class RosterGridDataView { | |
public items: any[]; | |
public footer: any; | |
private definition: RosterGridDataViewDefinition; | |
private groups: GroupHeader[]; | |
private subscriptions: Subscription[]; | |
private initial = false; | |
private animationFrame = 0; | |
constructor(private bus: EventAggregator) { | |
this.subscriptions = [ | |
bus.subscribe(constants.events.dataViewDefinitionChanged, this.definitionChanged), | |
bus.subscribe(constants.events.sortRequested, this.sortRequested), | |
bus.subscribe(constants.events.cellChanged, this.updateGroups) | |
]; | |
} | |
public dispose() { | |
this.subscriptions.forEach(subscription => subscription.dispose()); | |
} | |
public getSortClass(property: string) { | |
if (property !== this.definition.sortBy) { | |
return ''; | |
} | |
return this.definition.sortDescending ? 'roster-grid-descending' : 'roster-grid-ascending'; | |
} | |
private definitionChanged = (definition: RosterGridDataViewDefinition) => { | |
this.definition = definition; | |
if (!this.initial) { | |
this.initial = true; | |
this.update(); | |
return; | |
} | |
this.scheduleUpdate(); | |
} | |
private scheduleUpdate() { | |
cancelAnimationFrame(this.animationFrame); | |
this.animationFrame = requestAnimationFrame(() => this.update()); | |
} | |
private update() { | |
const definition = this.definition; | |
const columns = definition.columns; | |
// sort | |
const sortBy = definition.sortBy === null ? columns[0].property : definition.sortBy; | |
definition.sortBy = sortBy; | |
const groupBy = definition.groupBy; | |
const items = definition.items | |
.slice(0) | |
.sort((a, b) => { | |
let groupByResult = 0; | |
if (groupBy !== null) { | |
groupByResult = stringComparisonOrdinalIgnoreCase(a[groupBy], b[groupBy], false); | |
} | |
if (groupByResult === 0) { | |
return stringComparisonOrdinalIgnoreCase(a[sortBy], b[sortBy], definition.sortDescending); | |
} | |
return groupByResult; | |
}); | |
// make the footer item. | |
const footer: GroupHeader = this.footer = this.makeGroupHeader('footer', columns); | |
footer.items.push(...items); | |
this.groups = [footer]; | |
// insert group headers | |
if (groupBy !== null) { | |
let currentGroup: GroupHeader|null = null; | |
for (let i = 0; i < items.length; i++) { | |
const item = items[i]; | |
let name = item[groupBy]; | |
if (currentGroup !== null && name === currentGroup.name) { | |
currentGroup.items.push(items[i]); | |
continue; | |
} | |
currentGroup = this.makeGroupHeader(name, columns); | |
this.groups.push(currentGroup); | |
items.splice(i, 0, currentGroup); | |
i++; | |
} | |
} | |
// compute totals | |
this.updateGroups(); | |
// update items | |
this.items = items; | |
this.bus.publish(constants.events.dataViewChanged, this.items); | |
} | |
private makeGroupHeader(name: string, columns: RosterGridColumnDefinition[]): GroupHeader { | |
const header: GroupHeader = { isGroupHeader: true, name, items: [] }; | |
for (let i = 0; i < columns.length; i++) { | |
const column = columns[i]; | |
if (column.type !== 'number') { | |
continue; | |
} | |
header[column.property] = null; | |
} | |
return header; | |
} | |
private updateGroups = () => { | |
console.info(`${this.footer.items.length} items, ${this.groups.length} groups`); | |
console.time('updateGroups'); | |
const columns = this.definition.columns; | |
const groups = this.groups; | |
let c = columns.length; | |
while (c--) { | |
const column = columns[c]; | |
if (column.type !== 'number') { | |
continue; | |
} | |
const property = column.property; | |
let g = groups.length; | |
while (g--) { | |
const group = groups[g]; | |
const items = group.items; | |
let i = items.length; | |
let total: number|null = 0; | |
while (i--) { | |
total += (items[i][property] || 0); | |
} | |
if (total === 0) { | |
total = null; | |
} | |
if (total !== group[property]) { | |
group[property] = total; | |
} | |
} | |
} | |
console.timeEnd('updateGroups'); | |
} | |
private sortRequested = (index: number) => { | |
const requestedColumn = this.definition.columns[index]; | |
if (!requestedColumn.sortable) { | |
return; | |
} | |
const requestedProperty = requestedColumn.property; | |
const currentProperty = this.definition.sortBy; | |
if (requestedProperty === currentProperty) { | |
this.definition.sortDescending = !this.definition.sortDescending; | |
} else { | |
this.definition.sortBy = requestedProperty; | |
this.definition.sortDescending = false; | |
} | |
} | |
} | |
function stringComparisonOrdinalIgnoreCase( | |
a: string|null|undefined, | |
b: string|null|undefined, | |
descending: boolean | |
): number { | |
if (a === null || a === undefined) { | |
a = ''; | |
} else { | |
a = a.toLowerCase(); | |
} | |
if (b === null || b === undefined) { | |
b = ''; | |
} else { | |
b = b.toLowerCase(); | |
} | |
if (descending) { | |
return a.localeCompare(b) * -1; | |
} | |
return a.localeCompare(b); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {EventAggregator} from 'aurelia-event-aggregator'; | |
import constants from './roster-grid-constants'; | |
export class RosterGridDomEvents { | |
constructor(private bus: EventAggregator) { | |
document.body.addEventListener(constants.events.dom.mousemove, this.publish); | |
document.body.addEventListener(constants.events.dom.mouseup, this.publish); | |
document.body.addEventListener(constants.events.dom.mousedown, this.publish); | |
document.body.addEventListener(constants.events.dom.mouseover, this.publish); | |
document.body.addEventListener(constants.events.dom.mouseout, this.publish); | |
} | |
public dispose() { | |
document.body.removeEventListener(constants.events.dom.mousemove, this.publish); | |
document.body.removeEventListener(constants.events.dom.mouseup, this.publish); | |
document.body.removeEventListener(constants.events.dom.mousedown, this.publish); | |
document.body.removeEventListener(constants.events.dom.mouseover, this.publish); | |
document.body.removeEventListener(constants.events.dom.mouseout, this.publish); | |
} | |
public publish = (event: UIEvent) => { | |
this.bus.publish(event.type, event); | |
return true; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {EventAggregator, Subscription} from 'aurelia-event-aggregator'; | |
import {RosterGridResizeColumnArgs, RosterGridPositionInfo} from './roster-grid-interfaces'; | |
import constants from './roster-grid-constants'; | |
export class RosterGridHead { | |
private margin = 4; | |
private down: { index: number; leftClientX: number; }|null; | |
private subscriptions: Subscription[]; | |
constructor( | |
private bus: EventAggregator, | |
private getPositionInfo: { (clientX: number, clientY: number): RosterGridPositionInfo; } | |
) { | |
this.subscriptions = [ | |
bus.subscribe(constants.events.dom.mousedown, this.mouseDown), | |
bus.subscribe(constants.events.dom.mouseup, this.mouseUp), | |
bus.subscribe(constants.events.dom.mousemove, this.mouseMove), | |
]; | |
} | |
public dispose() { | |
this.subscriptions.forEach(subscription => subscription.dispose()); | |
} | |
private mouseDown = (event: MouseEvent) => { | |
const { clientX, clientY } = event; | |
const down = this.getPositionInfo(clientX, clientY); | |
const cell = down.cell; | |
if (!cell | |
|| cell.clipped | |
|| cell.row !== 0 | |
) { | |
return; | |
} | |
if (cell.x < this.margin && cell.col !== 0 | |
|| cell.x > cell.width - this.margin | |
) { | |
let index = cell.col; | |
let leftClientX = clientX - cell.x; | |
if (cell.x < this.margin) { | |
index--; | |
leftClientX -= cell.previousColWidth; | |
} | |
this.down = { index, leftClientX }; | |
document.body.style.cursor = 'col-resize'; | |
event.preventDefault(); | |
} else if (down.cell) { | |
this.bus.publish(constants.events.sortRequested, down.cell.col); | |
} | |
} | |
private mouseUp = (event: MouseEvent) => { | |
if (this.down) { | |
document.body.style.cursor = 'auto'; | |
this.down = null; | |
} | |
} | |
private mouseMove = (event: MouseEvent) => { | |
const { clientX, clientY } = event; | |
if (this.down) { | |
const width = clientX - this.down.leftClientX; | |
if (width < constants.minColumnWidth) { | |
return; | |
} | |
const index = this.down.index; | |
const args: RosterGridResizeColumnArgs = { index, width }; | |
this.bus.publish(constants.events.resizeColumnRequested, args); | |
return; | |
} | |
const div = <HTMLDivElement>event.target; | |
const { cell } = this.getPositionInfo(clientX, clientY); | |
if (!cell | |
|| cell.clipped | |
|| cell.row !== 0 | |
) { | |
div.style.cursor = 'default'; | |
return; | |
} | |
if (cell.x < this.margin && cell.col !== 0 | |
|| cell.x > cell.width - this.margin | |
) { | |
div.style.cursor = 'col-resize'; | |
} else { | |
div.style.cursor = 'default'; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface RosterGridColumnDefinition { | |
header: string; | |
property: string; | |
width: number; | |
type: 'number'|'string'; | |
editable: boolean; | |
sortable: boolean; | |
} | |
export interface RosterGridLayoutInfo { | |
itemsCount: number; | |
columns: RosterGridColumnDefinition[]; | |
frozenColumns: number; | |
rowHeight: number; | |
left: number; | |
top: number; | |
width: number; | |
height: number; | |
} | |
export interface RosterGridDataViewDefinition { | |
columns: RosterGridColumnDefinition[]; | |
items: any[]; | |
groupBy: string|null; | |
sortBy: string|null; | |
sortDescending: boolean; | |
} | |
export interface RosterGridSelectionInfo { | |
ranges: { start: { col: number; row: number; }, end: { col: number; row: number; } }[]; | |
} | |
export interface RosterGridResizeColumnArgs { | |
index: number; | |
width: number; | |
} | |
export interface RosterGridPositionInfo { | |
clientX: number; | |
clientY: number; | |
cell: { | |
clipped: boolean; | |
row: number; | |
col: number; | |
left: number; | |
top: number; | |
width: number; | |
height: number; | |
x: number; | |
y: number; | |
previousColWidth: number; | |
}|null; | |
grid: { | |
left: number; | |
top: number; | |
width: number; | |
height: number; | |
x: number; | |
y: number; | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {EventAggregator, Subscription} from 'aurelia-event-aggregator'; | |
import { | |
RosterGridLayoutInfo, | |
RosterGridColumnDefinition, | |
RosterGridResizeColumnArgs, | |
RosterGridPositionInfo | |
} from './roster-grid-interfaces'; | |
import constants from './roster-grid-constants'; | |
export class RosterGridLayout implements RosterGridLayoutInfo { | |
public itemsCount: number; | |
public columns: RosterGridColumnDefinition[]; | |
public frozenColumns: number; | |
public rowHeight: number; | |
public left: number; | |
public top: number; | |
public width: number; | |
public height: number; | |
public contentWidth: number; | |
public contentHeight: number; | |
public frozenContentWidth: number; | |
public scrollContentWidth: number; | |
public visibleItemCount: number; | |
public scrollLeft = 0; | |
public rowOffset = 0; | |
public scrollViewPort: HTMLDivElement; | |
private initialLayout = false; | |
private animationFrame = 0; | |
private scrollRequired: HTMLDivElement|null = null; | |
private columnWidthChanges = new Map<number, number>(); | |
private layoutRequired = false; | |
private subscriptions: Subscription[]; | |
constructor(private bus: EventAggregator) { | |
this.subscriptions = [ | |
bus.subscribe(constants.events.layoutPropertiesChanged, this.layoutPropertiesChanged), | |
bus.subscribe(constants.events.resizeColumnRequested, this.resizeColumnRequested), | |
bus.subscribe(constants.events.dom.scroll, this.domScroll), | |
bus.subscribe(constants.events.dom.wheel, this.domWheel) | |
]; | |
} | |
public dispose() { | |
this.subscriptions.forEach(subscription => subscription.dispose()); | |
cancelAnimationFrame(this.animationFrame); | |
} | |
public getPositionInfo(clientX: number, clientY: number): RosterGridPositionInfo { | |
const x = clientX - this.left; | |
const y = clientY - this.top; | |
const grid = { | |
left: this.left, | |
top: this.top, | |
width: this.width, | |
height: this.height, | |
x, | |
y | |
}; | |
const { scrollLeft, scrollTop, clientWidth, clientHeight } = this.scrollViewPort; | |
const clipped = x < 0 || x > clientWidth || y < 0 || y > clientHeight + this.rowHeight; | |
if (x < 0 || x > this.contentWidth - scrollLeft || y < 0 || y > this.contentHeight + this.rowHeight - scrollTop) { | |
return { clientX, clientY, cell: null, grid }; | |
} | |
const row = y < this.rowHeight ? 0 : Math.floor((y + scrollTop) / this.rowHeight); | |
let col = 0; | |
let left = 0; | |
while (x > left + this.columns[col].width) { | |
left += this.columns[col].width; | |
col++; | |
if (col === this.frozenColumns) { | |
left -= scrollLeft; | |
} | |
} | |
const top = row === 0 ? 0 : (row + 1) * this.rowHeight - scrollTop; | |
return { | |
clientX, | |
clientY, | |
cell: { | |
clipped, | |
row, | |
col, | |
left, | |
top, | |
width: this.columns[col].width, | |
height: this.rowHeight, | |
x: x - left, | |
y: y - top, | |
previousColWidth: col === 0 ? 0 : this.columns[col - 1].width | |
}, | |
grid | |
}; | |
} | |
private layoutPropertiesChanged = (info: RosterGridLayoutInfo) => { | |
this.left = info.left; | |
this.top = info.top; | |
this.width = info.width; | |
this.height = info.height; | |
this.itemsCount = info.itemsCount; | |
this.columns = info.columns; | |
this.frozenColumns = info.frozenColumns; | |
this.rowHeight = info.rowHeight; | |
if (this.initialLayout) { | |
this.layoutRequired = true; | |
this.scheduleAnimation(); | |
} else { | |
this.initialLayout = true; | |
this.layout(); | |
this.bus.publish(constants.events.layoutApplied, this); | |
} | |
}; | |
private resizeColumnRequested = (args: RosterGridResizeColumnArgs) => { | |
this.columnWidthChanges.set(args.index, args.width); | |
this.layoutRequired = true; | |
this.scheduleAnimation(); | |
}; | |
private domWheel = (event: WheelEvent) => { | |
if (event.deltaMode !== 0x00) { | |
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode | |
throw new Error(`Unhandled scroll deltaMode. Expected 0x00 DOM_DELTA_PIXEL, received ${event.deltaMode}`); | |
} | |
this.scrollViewPort.scrollLeft += event.deltaX; | |
this.scrollViewPort.scrollTop += event.deltaY; | |
this.scrollRequired = this.scrollViewPort; | |
this.scheduleAnimation(); | |
}; | |
private domScroll = (event: Event) => { | |
this.scrollRequired = <HTMLDivElement>event.target; | |
this.scheduleAnimation(); | |
}; | |
private layout() { | |
this.columnWidthChanges.forEach((width, index) => this.columns[index].width = width); | |
this.columnWidthChanges.clear(); | |
this.contentWidth = this.columns.reduce((w, c) => w + c.width, 0); | |
this.contentHeight = this.itemsCount * this.rowHeight; | |
this.frozenContentWidth = this.columns | |
.slice(0, this.frozenColumns) | |
.reduce((w, c) => w + c.width, 0); | |
this.scrollContentWidth = this.columns | |
.slice(this.frozenColumns) | |
.reduce((w, c) => w + c.width, 0); | |
this.visibleItemCount = Math.floor(this.height / this.rowHeight); | |
} | |
private scheduleAnimation() { | |
if (this.animationFrame) { | |
return; | |
} | |
this.animationFrame = requestAnimationFrame(() => { | |
this.animationFrame = 0; | |
if (this.layoutRequired) { | |
this.layout(); | |
} | |
if (this.scrollRequired) { | |
this.processScroll(this.scrollRequired); | |
} | |
this.layoutRequired = false; | |
this.scrollRequired = null; | |
this.bus.publish(constants.events.layoutApplied, this); | |
}); | |
} | |
private processScroll(target: HTMLDivElement) { | |
if (target === this.scrollViewPort) { | |
this.scrollLeft = target.scrollLeft; | |
this.rowOffset = Math.floor(this.scrollViewPort.scrollTop / this.rowHeight); | |
} else { | |
this.scrollLeft = this.scrollLeft + target.scrollLeft; | |
this.scrollViewPort.scrollLeft = this.scrollLeft; | |
target.scrollLeft = 0; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { EventAggregator, Subscription } from 'aurelia-event-aggregator'; | |
import { | |
RosterGridPositionInfo, | |
RosterGridLayoutInfo, | |
RosterGridSelectionInfo | |
} from './roster-grid-interfaces'; | |
import constants from './roster-grid-constants'; | |
export class RosterGridSelection implements RosterGridSelectionInfo { | |
public ranges: { start: { col: number; row: number; }, end: { col: number; row: number; } }[] = []; | |
public signal = 0; | |
private mouseDownInCell = false; | |
private mouseClientX: number; | |
private mouseClientY: number; | |
private animationFrame: number; | |
private subscriptions: Subscription[]; | |
constructor( | |
private bus: EventAggregator, | |
private getPositionInfo: { (clientX: number, clientY: number): RosterGridPositionInfo; } | |
) { | |
this.subscriptions = [ | |
bus.subscribe(constants.events.dom.mousedown, this.mouseDown), | |
bus.subscribe(constants.events.dom.mouseup, this.mouseUp), | |
bus.subscribe(constants.events.dom.mousemove, this.mouseMove), | |
bus.subscribe(constants.events.layoutApplied, this.layoutApplied) | |
]; | |
} | |
public dispose() { | |
this.subscriptions.forEach(subscription => subscription.dispose()); | |
} | |
public getClass(col: number, row: number): string { | |
const ranges = this.ranges; | |
for (let i = 0; i < ranges.length; i++) { | |
let { start: { col: col1, row: row1 }, end: { col: col2, row: row2 } } = ranges[i]; | |
if (col1 > col2) { | |
let temp = col2; | |
col2 = col1; | |
col1 = temp; | |
} | |
if (row1 > row2) { | |
let temp = row2; | |
row2 = row1; | |
row1 = temp; | |
} | |
if (col < col1 || col > col2 || row < row1 || row > row2) { | |
continue; | |
} | |
return 'roster-grid-cell-selected'; | |
} | |
return ''; | |
} | |
private mouseDown = (event: MouseEvent) => { | |
const { clientX, clientY } = event; | |
this.mouseClientX = clientX; | |
this.mouseClientY = clientY; | |
const { cell } = this.getPositionInfo(clientX, clientY); | |
// Disable selection on clipped columns and with key combinaison | |
if (cell === null || cell.clipped || event.shiftKey && event.ctrlKey) { | |
this.mouseDownInCell = false; | |
return; | |
} | |
this.mouseDownInCell = true; | |
this.bus.publish(constants.events.beforeSelectionChanged, this); | |
if (event.ctrlKey) { | |
// Does this cell already in the range array? | |
const clickedCell = { start: cell, end: cell }; | |
const found = this.findCellInSelection(clickedCell); | |
if (found !== -1) { | |
this.ranges.splice(found, 1); | |
} else { | |
// Selecting only one cell, add it to range. | |
this.ranges.push({ | |
start: { col: cell.col, row: cell.row }, | |
end: { col: cell.col, row: cell.row } | |
}); | |
} | |
} else { | |
this.ranges = []; | |
this.ranges.push({ | |
start: { col: cell.col, row: cell.row }, | |
end: { col: cell.col, row: cell.row } | |
}); | |
} | |
this.notify(); | |
} | |
private mouseUp = (event: MouseEvent) => { | |
this.mouseDownInCell = false; | |
} | |
private mouseMove = (event: MouseEvent) => { | |
const { clientX, clientY } = event; | |
this.mouseClientX = clientX; | |
this.mouseClientY = clientY; | |
this.handleMove(); | |
} | |
private layoutApplied = (layout: RosterGridLayoutInfo) => { | |
this.handleMove(); | |
} | |
private handleMove() { | |
if (!this.mouseDownInCell) { | |
return; | |
} | |
const { cell } = this.getPositionInfo(this.mouseClientX, this.mouseClientY); | |
if (cell === null) { | |
return; | |
} | |
const end = this.ranges[this.ranges.length - 1].end; | |
if (end.col !== cell.col || end.row !== cell.row) { | |
this.bus.publish(constants.events.beforeSelectionChanged, this); | |
end.col = cell.col; | |
end.row = cell.row; | |
this.notify(); | |
} | |
} | |
private findCellInSelection(candidate: any): number { | |
return this.ranges.findIndex(c => | |
c.start.col === candidate.start.col && | |
c.start.row === candidate.start.row && | |
c.end.col === candidate.end.col && | |
c.end.row === candidate.end.row); | |
} | |
private notify() { | |
this.bus.publish(constants.events.selectionChanged, this); | |
cancelAnimationFrame(this.animationFrame); | |
this.animationFrame = requestAnimationFrame(() => { | |
this.signal++; | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
roster-grid { | |
position: fixed; | |
overflow: hidden; | |
border: 1px solid #EEE; | |
} | |
.roster-grid-head, | |
.roster-grid-head-frozen-content-container, | |
.roster-grid-head-viewport, | |
.roster-grid-head-content-container { | |
position: absolute; | |
overflow: hidden; | |
} | |
.roster-grid-body-scroll-viewport { | |
position: absolute; | |
overflow: auto; | |
} | |
.roster-grid-body, | |
.roster-grid-body-frozen-content-container, | |
.roster-grid-body-viewport, | |
.roster-grid-body-content-container { | |
position: absolute; | |
overflow: hidden; | |
} | |
.roster-grid-body-frozen-content-container, | |
.roster-grid-body-viewport { | |
height: 100%; | |
} | |
.roster-grid-foot, | |
.roster-grid-foot-frozen-content-container, | |
.roster-grid-foot-viewport, | |
.roster-grid-foot-content-container { | |
position: absolute; | |
overflow: hidden; | |
} | |
.roster-grid-cell { | |
display: inline-block; | |
overflow: hidden; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
border-style: solid; | |
border-width: 0 1px 1px 0; | |
border-color: #EEE; | |
box-sizing: border-box; | |
height: 100%; | |
padding-top: 6px; | |
padding-left: 3px; | |
padding-right: 3px; | |
/* do not allow highlighting cell text */ | |
user-select: none; | |
-webkit-user-select: none; | |
-ms-user-select: none; | |
-moz-user-select: none; | |
} | |
/*.roster-grid-data-cell { | |
cursor: cell; | |
}*/ | |
.roster-grid-cell-selected { | |
background-color: #d9edf7; | |
} | |
.roster-grid-head-cell, | |
.roster-grid-foot-cell, | |
.roster-grid-group-header-cell { | |
background-color: #EEE; | |
border-color: #FFF; | |
font-weight: bold; | |
} | |
.roster-grid-cell.number { | |
text-align: right; | |
} | |
.roster-grid-foot > .roster-grid-foot-frozen-content-container > .roster-grid-cell.roster-grid-foot-cell { | |
text-align: right; | |
} | |
.roster-grid-ascending::before { | |
content: '\25B2'; | |
font-size: 75%; | |
} | |
.roster-grid-descending::before { | |
content: '\25BC'; | |
font-size: 75%; | |
} | |
.roster-grid-cell-input { | |
display: block; | |
width: 100%; | |
height: 100%; | |
background: #FFF; | |
border: none; | |
font-size: inherit; | |
padding-left: 3px; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template role="grid" | |
aria-multiselectable="true" | |
aria-colcount.bind="layout.columns.length" | |
aria-rowcount.bind="layout.itemsCount + 1" | |
style.bind="'left:' + layout.left + 'px;top:' + layout.top + 'px;width:' + layout.width + 'px;height:' + layout.height + 'px'" | |
wheel.delegate="domEvents.publish($event)"> | |
<require from="./roster-grid.css"></require> | |
<div class="roster-grid-head" | |
style.bind="'width:' + layout.width + 'px;height:' + layout.rowHeight + 'px'" | |
role="row" aria-rowindex="1"> | |
<div class="roster-grid-head-frozen-content-container" | |
if.bind="frozenColumns" | |
style.bind="'width:' + layout.frozenContentWidth + 'px;height:' + layout.rowHeight + 'px'"> | |
<div repeat.for="c of layout.columns | slice:0:layout.frozenColumns" | |
class-name.bind="'roster-grid-cell roster-grid-head-cell ' + dataView.getSortClass(c.property, dataView.definition.sortBy, dataView.definition.sortDescending) + ' ' + c.type" | |
role="columnheader" aria-colindex.bind="$index + 1" | |
aria-label.bind="c.ariaLabel" | |
style.bind="'width:' + c.width + 'px;height:' + layout.rowHeight + 'px'" | |
text-content.bind="c.header"> | |
</div> | |
</div> | |
<div class="roster-grid-head-viewport" | |
scroll.trigger="domEvents.publish($event)" | |
style.bind="'width:' + (layout.width - layout.frozenContentWidth) + 'px;height:' + layout.rowHeight + 'px;left:' + layout.frozenContentWidth + 'px'"> | |
<div class="roster-grid-head-content-container" | |
style.bind="'width:' + layout.scrollContentWidth + 'px;height:' + layout.rowHeight + 'px;left:-' + layout.scrollLeft + 'px'"> | |
<div repeat.for="c of layout.columns | slice:layout.frozenColumns" | |
class-name.bind="'roster-grid-cell roster-grid-head-cell ' + dataView.getSortClass(c.property, dataView.definition.sortBy, dataView.definition.sortDescending) + ' ' + c.type" | |
role="columnheader" aria-colindex.bind="$index + layout.frozenColumns + 1" | |
aria-label.bind="c.ariaLabel" | |
style.bind="'width:' + c.width + 'px;height:' + layout.rowHeight + 'px'" | |
text-content.bind="c.header"> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="roster-grid-body-scroll-viewport" ref="layout.scrollViewPort" | |
scroll.trigger="domEvents.publish($event)" | |
style.bind="'width:' + layout.width + 'px;height:' + (layout.height - layout.rowHeight) + 'px;top:' + layout.rowHeight + 'px'"> | |
<div style.bind="'width:' + layout.contentWidth + 'px;height:' + (layout.contentHeight + layout.rowHeight * 2) + 'px'"></div> | |
</div> | |
<div class="roster-grid-body" | |
style.bind="'width:' + layout.scrollViewPort.clientWidth + 'px;height:' + (layout.scrollViewPort.clientHeight - layout.rowHeight) + 'px;top:' + layout.rowHeight + 'px'"> | |
<div class="roster-grid-body-frozen-content-container" | |
style.bind="'width:' + layout.frozenContentWidth + 'px'"> | |
<div repeat.for="i of layout.visibleItemCount" | |
class="roster-grid-data-row" | |
role="row" aria-rowindex.bind="i + layout.rowOffset + 2" | |
style.bind="'width:' + layout.frozenContentWidth + 'px;height:' + layout.rowHeight + 'px'"> | |
<div repeat.for="c of layout.columns | slice:0:layout.frozenColumns" | |
class-name.bind="'roster-grid-cell roster-grid-data-cell ' + c.type + ' ' + (dataView.items[i + layout.rowOffset].isGroupHeader ? 'aurelia-hide' : '')" | |
id.bind="'roster-grid-cell-' + $index + '-' + (i + layout.rowOffset + 1)" | |
role="gridcell" | |
aria-colindex.bind="$index + 1" | |
style.bind="'width:' + c.width + 'px'" | |
text-content.bind="dataView.items[i + layout.rowOffset][c.property] | coalesce"> | |
</div> | |
<div class="roster-grid-cell roster-grid-group-header-cell" | |
show.bind="dataView.items[i + layout.rowOffset].isGroupHeader" | |
style.bind="'width:' + layout.frozenContentWidth + 'px;height:' + layout.rowHeight + 'px'" | |
role="gridcell" | |
aria-colindex="0" | |
aria-colspan.bind="layout.frozenColumns" | |
class="roster-grid" | |
text-content.bind="dataView.items[i + layout.rowOffset].name"> | |
</div> | |
</div> | |
</div> | |
<div class="roster-grid-body-viewport" | |
scroll.trigger="domEvents.publish($event)" | |
style.bind="'width:' + (layout.scrollViewPort.clientWidth - layout.frozenContentWidth) + 'px;left:' + layout.frozenContentWidth + 'px'"> | |
<div class="roster-grid-body-content-container" | |
style.bind="'width:' + layout.scrollContentWidth + 'px;left:-' + layout.scrollLeft + 'px'"> | |
<div repeat.for="i of layout.visibleItemCount" | |
class="roster-grid-data-row" | |
role="row" aria-rowindex.bind="i + layout.rowOffset + 2" | |
style.bind="'width:' + layout.scrollContentWidth + 'px;height:' + layout.rowHeight + 'px'"> | |
<div repeat.for="c of layout.columns | slice:layout.frozenColumns" | |
class-name.bind="'roster-grid-cell ' + (dataView.items[i + layout.rowOffset].isGroupHeader ? 'roster-grid-group-header-cell ' : 'roster-grid-data-cell ') + selection.getClass($index + layout.frozenColumns, i + layout.rowOffset + 1, selection.signal) + ' ' + c.type" | |
id.bind="'roster-grid-cell-' + ($index + layout.frozenColumns) + '-' + (i + layout.rowOffset + 1)" | |
role="gridcell" | |
aria-colindex.bind="$index + layout.frozenColumns + 1" | |
style.bind="'width:' + c.width + 'px'" | |
text-content.bind="dataView.items[i + layout.rowOffset][c.property] | toFixed:2 | coalesce"> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="roster-grid-foot" | |
style.bind="'top:' + (layout.scrollViewPort.clientHeight) + 'px;width:' + layout.scrollViewPort.clientWidth + 'px;height:' + layout.rowHeight + 'px'" | |
role="row" aria-rowindex.bind="layout.itemsCount + 1"> | |
<div class="roster-grid-foot-frozen-content-container" | |
if.bind="frozenColumns" | |
style.bind="'width:' + layout.frozenContentWidth + 'px;height:' + layout.rowHeight + 'px'"> | |
<div class="roster-grid-cell roster-grid-foot-cell" | |
style.bind="'width:' + layout.frozenContentWidth + 'px;height:' + layout.rowHeight + 'px'" | |
role="gridcell" | |
aria-colindex.bind="$index + 1" | |
aria-colspan.bind="layout.frozenColumns"> | |
Totals: | |
</div> | |
</div> | |
<div class="roster-grid-foot-viewport" | |
scroll.trigger="domEvents.publish($event)" | |
style.bind="'width:' + (layout.scrollViewPort.clientWidth - layout.frozenContentWidth) + 'px;height:' + layout.rowHeight + 'px;left:' + layout.frozenContentWidth + 'px'"> | |
<div class="roster-grid-foot-content-container" | |
style.bind="'width:' + layout.scrollContentWidth + 'px;height:' + layout.rowHeight + 'px;left:-' + layout.scrollLeft + 'px'"> | |
<div repeat.for="c of layout.columns | slice:layout.frozenColumns" | |
class-name.bind="'roster-grid-cell roster-grid-foot-cell ' + c.type" | |
id.bind="'footer-' + ($index + layout.frozenColumns)" | |
role="gridcell" aria-colindex.bind="$index + layout.frozenColumns + 1" | |
style.bind="'width:' + c.width + 'px;height:' + layout.rowHeight + 'px'" | |
text-content.bind="dataView.footer[c.property] | toFixed:2 | coalesce"> | |
</div> | |
</div> | |
</div> | |
</div> | |
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {EventAggregator} from 'aurelia-event-aggregator'; | |
import {bindable} from 'aurelia-templating'; | |
import {bindingMode} from 'aurelia-binding'; | |
import {RosterGridColumnDefinition, RosterGridLayoutInfo, RosterGridDataViewDefinition} from './roster-grid-interfaces'; | |
import {RosterGridDomEvents} from './roster-grid-dom-events'; | |
import {RosterGridLayout} from './roster-grid-layout'; | |
import {RosterGridHead} from './roster-grid-head'; | |
import {RosterGridSelection} from './roster-grid-selection'; | |
import {RosterGridDataView} from './roster-grid-data-view'; | |
import {RosterGridCellEditor} from './roster-grid-cell-editor'; | |
import constants from './roster-grid-constants'; | |
const twoWay = { defaultBindingMode: bindingMode.twoWay }; | |
export class RosterGrid implements RosterGridLayoutInfo, RosterGridDataViewDefinition { | |
@bindable public items: any[]; | |
@bindable public columns: RosterGridColumnDefinition[]; | |
@bindable(twoWay) public sortBy: string|null = null; | |
@bindable(twoWay) public sortDescending = false; | |
@bindable public groupBy: string|null = null; | |
@bindable public frozenColumns: number = constants.defaults.frozenColumns; | |
@bindable public rowHeight: number = constants.defaults.rowHeight; | |
@bindable public left: number; | |
@bindable public top: number; | |
@bindable public width: number; | |
@bindable public height: number; | |
public domEvents: RosterGridDomEvents; | |
public layout: RosterGridLayout; | |
public dataView: RosterGridDataView; | |
public head: RosterGridHead; | |
public selection: RosterGridSelection; | |
private cellEditor: RosterGridCellEditor; | |
private bus: EventAggregator; | |
constructor() { | |
this.bus = new EventAggregator(); | |
this.domEvents = new RosterGridDomEvents(this.bus); | |
this.layout = new RosterGridLayout(this.bus); | |
this.dataView = new RosterGridDataView(this.bus); | |
this.head = new RosterGridHead(this.bus, this.layout.getPositionInfo.bind(this.layout)); | |
this.selection = new RosterGridSelection(this.bus, this.layout.getPositionInfo.bind(this.layout)); | |
this.cellEditor = new RosterGridCellEditor(this.bus); | |
} | |
get itemsCount() { | |
return this.dataView.items.length; | |
} | |
public bind() { | |
this.bus.publish(constants.events.dataViewDefinitionChanged, <RosterGridDataViewDefinition>this); | |
this.bus.publish(constants.events.layoutPropertiesChanged, <RosterGridLayoutInfo>this); | |
} | |
public unbind() { | |
this.domEvents.dispose(); | |
this.layout.dispose(); | |
this.dataView.dispose(); | |
this.head.dispose(); | |
this.selection.dispose(); | |
this.cellEditor.dispose(); | |
} | |
public propertyChanged(propertyName: string, newValue: any, oldValue: any) { | |
if (['items', 'columns', 'groupBy', 'sortBy', 'sortDescending'].indexOf(propertyName) !== -1) { | |
this.bus.publish(constants.events.dataViewDefinitionChanged, <RosterGridDataViewDefinition>this); | |
} | |
this.bus.publish(constants.events.layoutPropertiesChanged, <RosterGridLayoutInfo>this); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {DataAttributeObserver} from 'aurelia-binding'; | |
export class SetAttributeBindingBehavior { | |
public bind(binding: any, source: any) { | |
binding.targetObserver = new DataAttributeObserver(binding.target, binding.targetProperty); | |
} | |
public unbind() {} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class CoalesceValueConverter { | |
public toView(value: any, fallback: any = '') { | |
return value === null || value === undefined ? fallback : value; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class SliceValueConverter { | |
public toView(value: any[], start: number, end: number|undefined) { | |
return value.slice(start, end); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class ToFixedValueConverter { | |
public toView(value: number|null|undefined, fractionDigits: number = 2) { | |
if (value === null || value === undefined || !value.toFixed) { | |
return value; | |
} | |
return value.toFixed(fractionDigits); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment