diff --git a/css/functions/func.scss b/css/functions/func.scss
index 185817b..02b909a 100644
--- a/css/functions/func.scss
+++ b/css/functions/func.scss
@@ -1,3 +1,10 @@
+@mixin inset($top, $right, $bottom, $left) {
+ top: $top;
+ right: $right;
+ bottom: $bottom;
+ left: $left;
+}
+
@mixin scrollbar() {
&::-webkit-scrollbar {
width: 8px;
@@ -8,4 +15,4 @@
background-color: rgba(168, 168, 168, 0.9);
border-radius: 4px;
}
-}
+}
\ No newline at end of file
diff --git a/css/grid.scss b/css/grid.scss
new file mode 100644
index 0000000..5df77d4
--- /dev/null
+++ b/css/grid.scss
@@ -0,0 +1,250 @@
+@keyframes loading-spinner {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(359deg);
+ }
+}
+
+.grid {
+ position: relative;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+
+ & {
+ --hover-bg-color: lightyellow;
+ --header-border-color: #adaba9;
+ --header-bg-color: #fafafa;
+ --header-fore-color: #000;
+ --cell-border-color: #f0f0f0;
+ --cell-fore-color: #333;
+ --dark-border-color: #666;
+ --split-border-color: #b3b3b3;
+ --dragger-bg-color: #fff;
+ --row-bg-color: #fff;
+ --row-active-bg-color: #fafafa;
+ --row-selected-bg-color: #e6f2fb;
+ --text-disabled-color: gray;
+ --loading-bg-color: hsla(0, 0%, 100%, .4);
+ --loading-fore-color: rgba(0, 0, 0, .2);
+
+ --font-size: .8125rem;
+ --line-height: 36px;
+ --header-line-height: 26px;
+ --text-indent: 8px;
+
+ --loading-size: 40px;
+ --loading-border-radius: 20px;
+
+ --arrow-size: 4px;
+ --split-width: 8px;
+ --dragger-size: 20px;
+ --dragger-opacity: .4;
+ --dragger-cursor-size: 4px;
+ --dragger-cursor-opacity: .6;
+
+ --header-padding: 4px 12px 4px 8px;
+ --spacing-s: 4px;
+ --spacing-cell: 5px 4px 5px 8px;
+ }
+
+ &:focus,
+ &:focus-visible {
+ outline: none;
+ }
+
+ &,
+ input[type="text"] {
+ font-size: var(--font-size);
+ }
+
+ >.grid-sizer {
+ position: absolute;
+ white-space: nowrap;
+ font-weight: bold;
+ visibility: hidden;
+ }
+
+ >.grid-header {
+ width: 100%;
+ min-width: 100%;
+ margin: 0;
+ border-bottom: 1px solid var(--header-border-color);
+ background-color: var(--header-bg-color);
+ color: var(--header-fore-color);
+ user-select: none;
+ border-collapse: collapse;
+ border-spacing: 0;
+ table-layout: fixed;
+ position: relative;
+
+ th {
+ padding: 0;
+ margin: 0;
+ word-wrap: break-word;
+ white-space: normal;
+ position: relative;
+
+ >div {
+ line-height: var(--header-line-height);
+ min-height: var(--line-height);
+ display: flex;
+ align-items: center;
+ padding: var(--header-padding);
+ box-sizing: border-box;
+ }
+
+ >.arrow {
+ width: 0;
+ height: 0;
+ top: 50%;
+ margin-top: calc(0px - var(--arrow-size) / 2);
+ right: calc(var(--arrow-size) / 2);
+ position: absolute;
+
+ &.asc {
+ border-bottom: var(--arrow-size) solid var(--dark-border-color);
+ }
+
+ &.desc {
+ border-top: var(--arrow-size) solid var(--dark-border-color);
+ }
+
+ &.asc,
+ &.desc {
+ border-left: var(--arrow-size) solid transparent;
+ border-right: var(--arrow-size) solid transparent;
+ }
+ }
+
+ >.spliter {
+ position: absolute;
+ height: 100%;
+ top: 0;
+ right: calc(0px - var(--split-width) /2);
+ width: var(--split-width);
+ cursor: ew-resize;
+ z-index: 1;
+
+ &::after {
+ content: '';
+ height: 100%;
+ width: 1px;
+ display: block;
+ margin: 0 auto;
+ transition: background-color .12s ease;
+ }
+
+ &:hover::after {
+ background-color: var(--split-border-color);
+ }
+ }
+ }
+ }
+
+ >.grid-body {
+ flex: 1 1 auto;
+ overflow: auto;
+ color: var(--cell-fore-color);
+ @include scrollbar();
+
+ .grid-body-content {
+ position: absolute;
+ min-width: 100%;
+ table-layout: fixed;
+ border-collapse: collapse;
+ border-spacing: 0;
+
+ >.grid-row {
+ line-height: var(--line-height);
+ white-space: nowrap;
+ background-color: var(--row-bg-color);
+ border-bottom: 1px solid var(--cell-border-color);
+ box-sizing: border-box;
+
+ &:hover {
+ background-color: var(--row-active-bg-color);
+ }
+
+ &.selected {
+ background-color: var(--row-selected-bg-color);
+ }
+
+ >td {
+ padding: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: pre;
+
+ >span {
+ padding: var(--spacing-cell);
+ }
+
+ >input[type="text"] {
+ border: none;
+ box-sizing: border-box;
+ height: calc(var(--line-height) + 2px);
+ width: 100%;
+ text-indent: var(--text-indent);
+
+ &:focus,
+ &:focus-visible {
+ outline: none;
+ }
+
+ &:disabled {
+ color: var(--text-disabled-color);
+ }
+ }
+
+ .checkbox-wrapper .check-box-inner {
+
+ &,
+ >svg {
+ transition: none;
+ }
+ }
+ }
+ }
+ }
+
+ .grid-hover-holder {
+ box-sizing: border-box;
+ position: absolute;
+ line-height: var(--line-height);
+ padding: var(--spacing-cell);
+ background-color: var(--hover-bg-color);
+ white-space: pre;
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ >.grid-loading {
+ position: absolute;
+ @include inset(0, 0, 0, 0);
+ visibility: hidden;
+ opacity: 0;
+ transition: visibility 0s linear .12s, opacity .12s ease;
+ background-color: var(--loading-bg-color);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1;
+
+ >div {
+ background-color: var(--loading-fore-color);
+ border-radius: var(--loading-border-radius);
+
+ >svg {
+ width: var(--loading-size);
+ height: var(--loading-size);
+ padding: 20px;
+ animation: loading-spinner 1.2s infinite linear;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/css/ui.scss b/css/ui.scss
index 6109287..847a1ab 100644
--- a/css/ui.scss
+++ b/css/ui.scss
@@ -1,3 +1,4 @@
@import './checkbox.scss';
@import './dropdown.scss';
-@import './tooltip.scss';
\ No newline at end of file
+@import './tooltip.scss';
+@import './grid.scss';
\ No newline at end of file
diff --git a/index.html b/index.html
index 88d5099..a820516 100644
--- a/index.html
+++ b/index.html
@@ -21,6 +21,7 @@
checkbox
tooltip
dropdown
+ grid
lib-utility
diff --git a/lib/ui/grid.html b/lib/ui/grid.html
new file mode 100644
index 0000000..05f26ae
--- /dev/null
+++ b/lib/ui/grid.html
@@ -0,0 +1,49 @@
+
+
grid
+
+
+ 创建一个统一样式的滚动列表元素。
+
+
+
示例
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/ui/grid.js b/lib/ui/grid.js
index 2f4e649..5bade01 100644
--- a/lib/ui/grid.js
+++ b/lib/ui/grid.js
@@ -21,23 +21,35 @@ class GridColumn {
static setValue(element, val) { element.innerText = val }
static getValue(element) { return element.innerText }
+
+ static setStyle(element, style) {
+ for (let css of Object.entries(style)) {
+ element.style.setProperty(css[0], css[1]);
+ }
+ }
}
class GridInputColumn extends GridColumn {
static createEdit(trigger) {
const input = document.createElement('input');
- input.setAttribute('type', 'input');
+ input.setAttribute('type', 'text');
if (typeof trigger === 'function') {
input.addEventListener('change', trigger);
}
return input;
}
- static setValue(element, val) { element.value = val }
+ static setValue(element, val) {
+ if (element.tagName !== 'INPUT') {
+ super.setValue(element, val);
+ } else {
+ element.value = val;
+ }
+ }
- static getValue(element) { return element.value }
+ static getValue(e) { return e.target.value }
- static setEnabled(element, enabled) { element.disabled = enabled !== false }
+ static setEnabled(element, enabled) { element.disabled = enabled === false }
}
class GridDropdownColumn extends GridColumn {
@@ -53,9 +65,9 @@ class GridCheckboxColumn extends GridColumn {
static setValue(element, val) { element.querySelector('input').checked = val }
- static getValue(element) { return element.querySelector('input').checked }
+ static getValue(e) { return e.target.checked }
- static setEnabled(element, enabled) { element.querySelector('input').disabled = enabled !== false }
+ static setEnabled(element, enabled) { element.querySelector('input').disabled = enabled === false }
}
const ColumnTypes = {
@@ -81,6 +93,9 @@ class Grid {
#rowCount = -1;
#overflows;
#scrollTop;
+ #scrollLeft;
+ #colTypes = {};
+ #colAttrs = {};
columns = [];
langs = {
@@ -89,15 +104,17 @@ class Grid {
reset: r('reset', 'Reset')
};
virtualCount = 100;
- rowHeight = 27;
- filterRowHeight = 26;
+ rowHeight = 39;
+ filterRowHeight = 30;
height;
+ readonly;
multiSelect = false;
+ fullrowClick = true;
allowHtml = false;
holderDisabled = false;
window = global;
sortIndex = -1;
- sortDirection = 'asc';
+ sortDirection = 1;
willSelect;
selectedRowChanged;
@@ -110,30 +127,32 @@ class Grid {
Common: 0,
Input: 1,
Dropdown: 2,
- Checkbox: 3
+ Checkbox: 3,
+ isCheckbox(type) { return type === 3 }
};
constructor(container) {
this.#parent = container;
}
- get source() { return this.#source; }
+ get source() { return this.#source?.map(s => s.values) }
set source(list) {
if (this.#el == null) {
throw new Error('grid has not been initialized.')
}
+ if (!Array.isArray(list)) {
+ throw new Error('source is not an Array.')
+ }
+ list = list.map(i => { return { values: i } });
this.#source = list;
// TODO: filter to currentSource;
this.#currentSource = list;
- this.#containerHeight = list.length * this.rowHeight;
this.#overflows = {};
this.#selectedColumnIndex = -1;
this.#selectedIndexes = [];
this.#startIndex = 0;
this.#scrollTop = 0;
- this.#refs.body.scrollTop = 0;
- this.#refs.bodyContent.style.top = '0px';
- this.#refs.bodyContainer.style.height = `${this.#containerHeight}px`;
+ this.#scrollLeft = 0;
this.#rowCount = -1;
if (this.sortIndex >= 0) {
@@ -142,11 +161,38 @@ class Grid {
this.resize();
}
}
+
get virtual() { return this.#currentSource?.length > this.virtualCount }
- get sortKey() { }
+
+ get sortKey() {
+ if (this.columns == null) {
+ return null;
+ }
+ return this.columns[this.sortIndex]?.key;
+ }
+
get selectedIndexes() { return this.#selectedIndexes }
- set selectedIndexes(indexes) { }
- get selectedIndex() { }
+ set selectedIndexes(indexes) {
+ const startIndex = this.#startIndex;
+ this.#selectedIndexes.splice(0, this.#selectedIndexes.length, ...indexes);
+ if (this.readonly !== true) {
+ this.refresh();
+ } else {
+ [...this.#refs.bodyContent.children].forEach((row, i) => {
+ if (indexes.indexOf(startIndex + i) >= 0) {
+ row.classList.add('selected');
+ } else if (row.classList.contains('selected')) {
+ row.classList.remove('selected');
+ }
+ });
+ }
+ if (typeof this.selectedRowChanged === 'function') {
+ this.selectedRowChanged();
+ }
+ }
+
+ get selectedIndex() { return (this.#selectedIndexes && this.#selectedIndexes[0]) ?? -1 }
+
get loading() { return this.#refs.loading?.style.visibility === 'visible' }
set loading(flag) {
if (this.#refs.loading == null) {
@@ -160,6 +206,7 @@ class Grid {
this.#refs.loading.style.opacity = 1;
}
}
+
get scrollTop() { return this.#refs.body?.scrollTop; }
set scrollTop(top) {
if (this.#refs.body == null) {
@@ -173,7 +220,6 @@ class Grid {
this.#el = null;
this.#refs = {};
this.#rendering = true;
- this.#currentSource = this.source;
if (!(container instanceof HTMLElement)) {
throw new Error('no specified parent.');
}
@@ -186,26 +232,21 @@ class Grid {
let flag = false;
if (e.key === 'ArrowUp') {
// up
- flag = true;
- if (index > 1) {
- delete this.#currentSource[index].__selected;
+ if (index > 0) {
+ flag = true;
index -= 1;
- } else {
- index = 0;
}
} else if (e.key === 'ArrowDown') {
// down
- flag = true;
const count = this.#currentSource?.length ?? 0;
if (index < count - 1) {
- delete this.#currentSource[index].__selected;
+ flag = true;
index += 1;
}
}
if (flag) {
- this.selectedIndexes = [index];
+ this.#selectedIndexes = [index];
this.scrollToIndex(index);
- this.#currentSource[index].__selected = true;
this.refresh();
if (typeof this.selectedRowChanged === 'function') {
this.selectedRowChanged(index);
@@ -228,21 +269,22 @@ class Grid {
// loading
const loading = document.createElement('div');
loading.className = 'grid-loading';
- loading.appendChild(createIcon('fa-regular', 'spinner-third'));
+ const loadingHolder = document.createElement('div');
+ loadingHolder.appendChild(createIcon('fa-regular', 'spinner-third'));
+ loading.appendChild(loadingHolder);
this.#refs.loading = loading;
grid.appendChild(loading);
this.#el = grid;
this.#rendering = false;
if (this.sortIndex >= 0) {
- this.sortColumn(true);
- } else {
- this.resize();
+ this.sortColumn();
}
}
scrollToIndex(index) {
- this.#scrollToTop(index * this.rowHeight, true);
+ const top = this.#scrollToTop(index * this.rowHeight, true);
+ this.#refs.body.scrollTop = top;
}
resize(force) {
@@ -250,21 +292,18 @@ class Grid {
return;
}
const body = this.#refs.body;
- let height = this.#refs.header.offsetHeight + 2;
- let top = body.offsetTop;
- if (top !== height) {
- body.style.top = `${height}px`;
- top = height;
- }
+ // let height = this.#refs.header.offsetHeight + 2;
+ // let top = body.offsetTop;
+ // if (top !== height) {
+ // body.style.top = `${height}px`;
+ // top = height;
+ // }
+ const top = this.#refs.header.offsetHeight;
- height = this.height;
- if (isNaN(height)) {
+ let height = this.height;
+ if (isNaN(height) || height <= 0) {
height = this.#el.offsetHeight - top;
- } else if (height === 0) {
- height = this.#refs.bodyContent.offsetHeight;
- this.#el.style.height = `${top + height}px`;
}
- body.style.height = `${height}px`;
const count = truncate((height - 1) / this.rowHeight) * (RedumCount * 2) + 1;
if (force || count !== this.#rowCount) {
this.#rowCount = count;
@@ -275,6 +314,10 @@ class Grid {
reload() {
this.#containerHeight = this.#currentSource.length * this.rowHeight;
+ this.#refs.body.scrollTop = 0;
+ this.#refs.body.scrollLeft = 0;
+ this.#refs.bodyContent.style.top = '0px';
+ this.#refs.bodyContainer.style.height = `${this.#containerHeight}px`;
this.#adjustRows(this.#refs.bodyContent);
this.refresh();
}
@@ -292,7 +335,10 @@ class Grid {
if (!col.autoResize) {
return;
}
- const width = widths[i];
+ let width = widths[i];
+ if (width < col.width) {
+ width = col.width;
+ }
if (width > 0) {
this.#changeColumnWidth(i, width);
}
@@ -300,7 +346,79 @@ class Grid {
}
}
- sortColumn(auto, reload) { }
+ resetChange() {
+ if (this.#currentSource == null) {
+ return;
+ }
+ for (let row of this.#currentSource) {
+ delete row.__changed;
+ }
+ }
+
+ sortColumn(reload) {
+ const index = this.sortIndex;
+ const col = this.columns[index];
+ if (col == null) {
+ return;
+ }
+ const direction = this.sortDirection;
+ [...this.#refs.header.children].forEach((th, i) => {
+ const arrow = th.children[1]; // th.querySelector('layer.arrow');
+ if (arrow == null) {
+ return;
+ }
+ if (i === index) {
+ arrow.className = `arrow ${(direction !== 1 ? 'desc' : 'asc')}`;
+ } else if (arrow.className !== 'arrow') {
+ arrow.className = 'arrow';
+ }
+ });
+ let comparer;
+ if (typeof col.sortFilter !== 'function') {
+ const direction = this.sortDirection;
+ if (isNaN(direction)) {
+ direction = 1;
+ }
+ comparer = (a, b) => {
+ const ta = a.values[col.key];
+ const tb = b.values[col.key];
+ if ((ta == null || tb == null) && typeof col.filter === 'function') {
+ a = col.filter(a.values);
+ b = col.filter(b.values);
+ } else {
+ a = ta;
+ b = tb;
+ }
+ if (a?.value != null) {
+ a = a.value;
+ }
+ if (b?.value != null) {
+ b = b.value;
+ }
+ if (a == null && typeof b === 'number') {
+ a = 0;
+ } else if (typeof a === 'number' && b == null) {
+ b = 0;
+ } else if (a != null && b == null) {
+ return direction;
+ } else if (typeof a === 'string' && typeof b === 'string') {
+ a = a.toLowerCase();
+ b = b.toLowerCase();
+ }
+ return a === b ? 0 : (a > b ? 1 : -1) * direction;
+ };
+ } else {
+ comparer = (a, b) => col.sortFilter(a, b) * direction;
+ }
+ this.#source.sort(comparer);
+ // TODO: filter to currentSource;
+ this.#currentSource = this.#source;
+ if (reload) {
+ this.reload();
+ } else {
+ this.refresh();
+ }
+ }
#createHeader() {
const thead = document.createElement('table');
@@ -320,29 +438,34 @@ class Grid {
continue;
}
// style
+ const isCheckbox = Grid.ColumnTypes.isCheckbox(col.type);
if (col.width > 0 || col.shrink) {
col.autoResize = false;
} else {
col.autoResize = true;
this.#needResize = true;
sizer.innerText = col.caption;
- let width = sizer.offsetWidth + 20;
+ let width = sizer.offsetWidth + 22;
+ if (col.allcheck && isCheckbox) {
+ width += 32;
+ }
if (width < MiniColumnWidth) {
width = MiniColumnWidth;
}
col.width = width;
}
- col.align ??= 'left';
+ col.align ??= isCheckbox ? 'center' : 'left';
if (col.sortable !== false) {
col.sortable = true;
}
if (col.shrink) {
col.style = { 'text-align': col.align };
} else {
+ const w = `${col.width}px`;
col.style = {
- 'width': col.width,
- 'max-width': col.width,
- 'min-width': col.width,
+ 'width': w,
+ 'max-width': w,
+ 'min-width': w,
'text-align': col.align
};
}
@@ -357,14 +480,19 @@ class Grid {
th.addEventListener('mousedown', e => this.#onDragStart(col, e));
const wrapper = document.createElement('div');
th.appendChild(wrapper);
- if (col.enabled !== false && col.allcheck && col.type === Grid.ColumnTypes.Checkbox) {
+ if (col.enabled !== false && col.allcheck && isCheckbox) {
const check = createCheckbox({
onchange: e => this.#onColumnAllChecked(col, e.target.checked)
});
wrapper.appendChild(check);
}
const caption = document.createElement('span');
- caption = col.caption;
+ if (col.textStyle != null) {
+ for (let css of Object.entries(col.textStyle)) {
+ caption.style.setProperty(css[0], css[1]);
+ }
+ }
+ caption.innerText = col.caption;
wrapper.appendChild(caption);
// order arrow
if (col.sortable) {
@@ -407,25 +535,17 @@ class Grid {
body.className = 'grid-body';
body.addEventListener('scroll', e => throttle(this.#onScroll, RefreshInterval, this, e), { passive: true });
const cols = this.columns;
- let height = this.#currentSource.length * this.rowHeight;
- let width;
- if (height === 0) {
- height = 1;
- width = 0;
- for (let col of cols) {
- if (col.visible !== false && !isNaN(col.width)) {
- width += col.width + 1;
- }
+ let width = 1;
+ for (let col of cols) {
+ if (col.visible !== false && !isNaN(col.width)) {
+ width += col.width + 1;
}
- width += 1;
}
- this.#containerHeight = height;
// body container
const bodyContainer = document.createElement('div');
bodyContainer.style.position = 'relative';
bodyContainer.style.minWidth = '100%';
bodyContainer.style.minHeight = '1px';
- bodyContainer.style.height = `${height}px`;
if (width > 0) {
bodyContainer.style.width = `${width}px`;
}
@@ -434,7 +554,7 @@ class Grid {
const bodyContent = document.createElement('table');
bodyContent.className = 'grid-body-content';
bodyContainer.appendChild(bodyContent);
- this.#adjustRows();
+ // this.#adjustRows();
// events
if (!this.holderDisabled) {
const holder = document.createElement('div');
@@ -464,7 +584,7 @@ class Grid {
for (let i = 0; i < count; i += 1) {
const row = document.createElement('tr');
row.className = 'grid-row';
- row.addEventListener('mousedown', e => this.#onRowClicked(e, exists + 1));
+ row.addEventListener('mousedown', e => this.#onRowClicked(e, exists + i));
row.addEventListener('dblclick', e => this.#onRowDblClicked(e));
cols.forEach((col, j) => {
const cell = document.createElement('td');
@@ -480,13 +600,25 @@ class Grid {
cell.style.setProperty(css[0], css[1]);
}
}
- if (col.type === Grid.ColumnTypes.Checkbox) {
+ if (Grid.ColumnTypes.isCheckbox(col.type)) {
cell.appendChild(GridCheckboxColumn.createEdit(e => this.#onRowChanged(e, exists + i, col, e.target.checked)));
- } else if (this.allowHtml && col.type != null && isNaN(col.type)) {
- cell.appendChild(col.type.create());
+ // this.#colTypes[col.key] = GridCheckboxColumn;
} else {
- cell.appendChild(GridColumn.create());
+ let type = this.#colTypes[col.key];
+ if (type == null) {
+ if (isNaN(col.type)) {
+ if (this.allowHtml && col.type != null) {
+ type = col.type;
+ }
+ } else {
+ type = ColumnTypes[col.type];
+ }
+ type ??= GridColumn;
+ this.#colTypes[col.key] = type;
+ }
+ cell.appendChild(type.create());
}
+
}
row.appendChild(cell);
});
@@ -502,10 +634,9 @@ class Grid {
}
#fillRows(rows, cols, widths) {
- const startIndex = this.startIndex;
- const selected = this.#selectedIndexes;
- const allowHtml = this.allowHtml;
- rows.forEach((row, i) => {
+ const startIndex = this.#startIndex;
+ const selectedIndexes = this.#selectedIndexes;
+ [...rows].forEach((row, i) => {
const vals = this.#currentSource[startIndex + i];
if (vals == null) {
return;
@@ -514,13 +645,19 @@ class Grid {
return;
}
const item = vals.values;
- if (selected.indexOf(startIndex + i) < 0) {
- row.classList.remove('selected');
- } else {
+ const selected = selectedIndexes.indexOf(startIndex + i) >= 0;
+ if (selected) {
row.classList.add('selected');
+ } else if (row.classList.contains('selected')) {
+ row.classList.remove('selected');
}
// data
- const selected = row.dataset.selected === '1';
+ const selectChanged = vals.__selected ^ selected;
+ if (selected) {
+ vals.__selected = true;
+ } else {
+ delete vals.__selected;
+ }
cols.forEach((col, j) => {
if (col.visible === false) {
return;
@@ -539,19 +676,18 @@ class Grid {
val ??= '';
// fill
const cell = row.children[j];
- const custom = allowHtml && col.type != null && isNaN(col.type);
+ if (typeof col.bgFilter === 'function') {
+ const bgColor = col.bgFilter(item);
+ cell.style.backgroundColor = bgColor ?? '';
+ }
+ const isCheckbox = Grid.ColumnTypes.isCheckbox(col.type);
+ const type = isCheckbox ? GridCheckboxColumn : this.#colTypes[col.key] ?? GridColumn;
let element;
- if (vals.__selected ^ selected) {
- if (custom) {
- element = selected ?
- col.type.createEdit(e => this.#onRowChanged(e, startIndex + i, col, col.type.getValue(element))) :
- col.type.create();
- cell.replaceChildren(element);
- // } else if (col.type !== Grid.ColumnTypes.Checkbox) {
- // // TODO:
- } else {
- element = cell.children[0];
- }
+ if (!isCheckbox && selectChanged) {
+ element = selected && typeof type.createEdit === 'function' ?
+ type.createEdit(e => this.#onRowChanged(e, startIndex + i, col, type.getValue(e))) :
+ type.create();
+ cell.replaceChildren(element);
} else {
element = cell.children[0];
}
@@ -559,44 +695,322 @@ class Grid {
if (typeof enabled === 'string') {
enabled = item[enabled];
}
- if (custom) {
- col.type.setValue(element, item);
- col.type.setEnabled(element, enabled);
- } else if (col.type === Grid.ColumnTypes.Checkbox) {
- GridCheckboxColumn.setValue(element, val);
- GridCheckboxColumn.setEnabled(element, enabled);
- } else {
- // TODO: input, dropdown, etc...
- GridColumn.setValue(element, val);
+ type.setValue(element, val, item);
+ if (typeof type.setEnabled === 'function') {
+ type.setEnabled(element, enabled);
}
- })
+ // auto resize
+ if (this.#needResize && col.autoResize) {
+ const width = cell.scrollWidth + 12;
+ if (width > 0 && widths != null && (isNaN(widths[j]) || widths[j] < width)) {
+ widths[j] = width;
+ widths.flag = true;
+ }
+ }
+ if (typeof col.styleFilter === 'function') {
+ const style = col.styleFilter(item);
+ if (style != null) {
+ type.setStyle(element, style);
+ }
+ }
+ if (col.events != null) {
+ for (let ev of Object.entries(col.events)) {
+ element[ev[0]] = ev[1].bind(item);
+ }
+ }
+ if (col.attrs != null) {
+ let attrs = col.attrs;
+ if (typeof attrs === 'function') {
+ attrs = attrs(item);
+ }
+ for (let attr of Object.entries(attrs)) {
+ element.setAttribute(attr[0], attr[1]);
+ }
+ }
+ });
});
}
- #changeColumnWidth(i, width) { }
+ #changeColumnWidth(index, width) {
+ const col = this.columns[index];
+ // const oldwidth = col.width;
+ const w = `${width}px`;
+ col.width = width;
+ col.style.width = w;
+ col.style['max-width'] = w;
+ col.style['min-width'] = w;
+ let element = this.#refs.header.children[index];
+ element.style.width = w;
+ element.style.maxWidth = w;
+ element.style.minWidth = w;
+ const body = this.#refs.bodyContent;
+ for (let row of body.children) {
+ element = row.children[index];
+ if (element != null) {
+ element.style.width = w;
+ element.style.maxWidth = w;
+ element.style.minWidth = w;
+ }
+ }
+ // } else {
+ // width = this.#refs.bodyContainer.offsetWidth - oldwidth + width;
+ // this.#refs.bodyContainer.style.width = `${width}px`;
+ // }
+ }
- #scrollToTop(top, reload) { }
+ #scrollToTop(top, reload) {
+ const rowHeight = this.rowHeight;
+ top -= (top % (rowHeight * 2)) + (RedumCount * rowHeight);
+ if (top < 0) {
+ top = 0;
+ } else {
+ let bottomTop = this.#containerHeight - (reload ? 0 : this.#rowCount * rowHeight);
+ if (bottomTop < 0) {
+ bottomTop = 0;
+ }
+ if (top > bottomTop) {
+ top = bottomTop;
+ }
+ }
+ if (this.#scrollTop !== top) {
+ this.#scrollTop = top;
+ if (this.virtual) {
+ this.#startIndex = top / rowHeight;
+ }
+ this.refresh();
+ if (this.virtual) {
+ this.#refs.bodyContent.style.top = `${top}px`;
+ }
+ } else if (reload) {
+ this.refresh();
+ }
+
+ return top;
+ }
+
+ #getColumnIndex(target) {
+ if (target == null) {
+ return -1;
+ }
+ let parent;
+ while ((parent = target.parentElement) != null && !parent.classList.contains('grid-row')) {
+ target = parent;
+ }
+ if (parent == null) {
+ return -1;
+ }
+ const index = [...parent.children].indexOf(target);
+ return index >= this.columns.length ? -1 : index;
+ }
+
+ #onHeaderClicked(col, e, force) {
+ const attr = this.#colAttrs[col.key];
+ if (!force && attr != null && (attr.resizing || attr.dragging)) {
+ return;
+ }
+ if (col.sortable && ['LABEL', 'LAYER', 'SVG', 'USE'].indexOf(e.target.tagName) < 0) {
+ const index = this.columns.indexOf(col);
+ if (index < 0) {
+ return;
+ }
+ if (this.sortIndex === index) {
+ this.sortDirection = this.sortDirection === 1 ? -1 : 1;
+ } else {
+ this.sortIndex = index;
+ }
+ this.sortColumn(true);
+ if (typeof this.columnChanged === 'function') {
+ this.columnChanged(ColumnChangedType.Sort, index, this.sortDirection);
+ }
+ }
+ }
- #onHeaderClicked(col, e, force) { }
#onDragStart(col, e) { }
+
#onResizeStart(col, e) { }
- #onColumnAllChecked(col, flag) { }
- #onScroll(e) { }
- #onBodyMouseMove(e, holder) { }
- #onRowClicked(e, index, colIndex) { }
- #onRowDblClicked(e) { }
+
+ #onColumnAllChecked(col, flag) {
+ if (this.#currentSource == null) {
+ return;
+ }
+ const key = col.key;
+ const test = typeof col.enabled === 'string';
+ if (typeof col.onallchecked === 'function') {
+ col.onallchecked.call(this, col, flag);
+ } else {
+ for (let row of this.#currentSource) {
+ const item = row.values;
+ if (item == null) {
+ continue;
+ }
+ const enabled = test ? item[col.enabled] : col.enabled;
+ if (enabled !== false) {
+ item[key] = flag;
+ row.__changed = true;
+ if (typeof col.onchanged === 'function') {
+ col.onchanged.call(this, item, flag);
+ }
+ }
+ }
+ this.refresh();
+ }
+ }
+
+ #onScroll(e) {
+ this.#scrollLeft = e.target.scrollLeft;
+ if (!this.virtual) {
+ return;
+ }
+ const top = e.target.scrollTop;
+ this.#scrollToTop(top);
+ }
+
+ #onBodyMouseMove(e, holder) {
+ let target = e.target;
+ if (target.className === 'grid-hover-holder') {
+ return;
+ }
+ let parent;
+ while ((parent = target.parentElement) != null && !parent.classList.contains('grid-row')) {
+ target = parent;
+ }
+ let keyid = target.keyid;
+ if (parent == null || keyid == null) {
+ delete holder.keyid;
+ if (holder.style.display !== 'none') {
+ holder.style.display = 'none';
+ }
+ return;
+ }
+ const oldkeyid = holder.keyid;
+ keyid += this.#startIndex << MaxColumnBit;
+ if (keyid === oldkeyid) {
+ return;
+ }
+ let overflow = this.#overflows[keyid];
+ if (overflow == null) {
+ overflow = target.scrollWidth > target.offsetWidth;
+ this.#overflows[keyid] = overflow;
+ }
+ if (overflow) {
+ holder.keyid = keyid;
+ holder.innerText = target.innerText;
+ const top = this.#refs.bodyContent.offsetTop + target.offsetTop + 1;
+ let left = target.offsetLeft;
+ let width = holder.offsetWidth;
+ if (width > this.#bodyClientWidth) {
+ width = this.#bodyClientWidth;
+ }
+ const maxleft = this.#bodyClientWidth + this.#scrollLeft - width;
+ if (left > maxleft) {
+ left = maxleft;
+ }
+ const height = target.offsetHeight;
+ holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this.#bodyClientWidth}px; height: ${height - 2}px`;
+ } else {
+ if (oldkeyid != null) {
+ delete holder.keyid;
+ }
+ if (holder.style.display !== 'none') {
+ holder.style.display = 'none';
+ }
+ }
+ }
+
+ #onRowClicked(e, index, colIndex) {
+ const startIndex = this.#startIndex;
+ const selectedIndex = startIndex + index;
+ if (typeof this.willSelect === 'function' && !this.willSelect(selectedIndex, colIndex)) {
+ return;
+ }
+ // multi-select
+ let flag = false;
+ const selectedIndexes = this.#selectedIndexes;
+ if (this.multiSelect) {
+ if (e.ctrlKey) {
+ const i = selectedIndexes.indexOf(selectedIndex);
+ if (i < 0) {
+ selectedIndexes.push(selectedIndex);
+ } else {
+ selectedIndexes.splice(i, 1);
+ }
+ flag = true;
+ } else if (e.shiftKey && selectedIndexes.length > 0) {
+ if (selectedIndexes.length > 1 || selectedIndexes[0] !== selectedIndex) {
+ let start = selectedIndexes[selectedIndexes.length - 1];
+ let end;
+ if (start > selectedIndex) {
+ end = start;
+ start = selectedIndex;
+ } else {
+ end = selectedIndex;
+ }
+ selectedIndexes.splice(0);
+ for (let i = start; i <= end; i += 1) {
+ selectedIndexes.push(i);
+ }
+ flag = true;
+ }
+ }
+ }
+ if (!flag && selectedIndexes.length !== 1 || selectedIndexes[0] !== selectedIndex) {
+ selectedIndexes.splice(0, selectedIndexes.length, selectedIndex);
+ flag = true;
+ }
+ // apply style
+ if (flag) {
+ if (this.readonly !== true) {
+ this.refresh();
+ } else {
+ [...this.#refs.bodyContent.children].forEach((row, i) => {
+ if (selectedIndexes.indexOf(startIndex + i) >= 0) {
+ row.classList.add('selected');
+ } else if (row.classList.contains('selected')) {
+ row.classList.remove('selected');
+ }
+ });
+ }
+ if (typeof this.selectedRowChanged === 'function') {
+ this.selectedRowChanged(selectedIndex);
+ }
+ }
+ colIndex ??= this.#getColumnIndex(e.target);
+ this.#selectedColumnIndex = colIndex;
+ if ((this.fullrowClick || colIndex >= 0) && e.buttons === 1 && typeof this.cellClicked === 'function') {
+ if (this.cellClicked(selectedIndex, colIndex) === false) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ }
+ #onRowDblClicked(e) {
+ if (e.target.tagName === 'INPUT') {
+ return;
+ }
+ const index = this.selectedIndex;
+ if (typeof this.rowDblClicked === 'function') {
+ this.rowDblClicked(index);
+ }
+ if (typeof this.cellDblClicked === 'function') {
+ const colIndex = this.#selectedColumnIndex;
+ if ((this.fullrowClick || colIndex >= 0) && e.buttons === 1) {
+ this.cellDblClicked(index, colIndex);
+ }
+ }
+ }
#onRowChanged(_e, index, col, value) {
if (this.#currentSource == null) {
return;
}
- const item = this.#currentSource[this.#startIndex + index].values;
+ const row = this.#currentSource[this.#startIndex + index];
+ const item = row.values;
if (item == null) {
return;
}
const enabled = typeof col.enabled === 'string' ? item[col.enabled] : col.enabled;
if (enabled !== false) {
item[col.key] = value;
- item.__changed = true;
+ row.__changed = true;
if (typeof col.onchanged === 'function') {
col.onchanged.call(this, item, value);
}