add support to resize or reorder.
This commit is contained in:
parent
de1ba82970
commit
a2fbb7e78a
152
css/grid.scss
152
css/grid.scss
@ -13,6 +13,7 @@
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
|
||||
& {
|
||||
--hover-bg-color: lightyellow;
|
||||
@ -24,6 +25,7 @@
|
||||
--dark-border-color: #666;
|
||||
--split-border-color: #b3b3b3;
|
||||
--dragger-bg-color: #fff;
|
||||
--dragger-cursor-color: #333;
|
||||
--row-bg-color: #fff;
|
||||
--row-active-bg-color: #fafafa;
|
||||
--row-selected-bg-color: #e6f2fb;
|
||||
@ -42,9 +44,10 @@
|
||||
--arrow-size: 4px;
|
||||
--split-width: 8px;
|
||||
--dragger-size: 20px;
|
||||
--dragger-opacity: .4;
|
||||
--dragger-opacity: .6;
|
||||
--dragger-cursor-size: 4px;
|
||||
--dragger-cursor-opacity: .6;
|
||||
--dragger-cursor-pos: -4px;
|
||||
--dragger-cursor-opacity: .3;
|
||||
|
||||
--header-padding: 4px 12px 4px 8px;
|
||||
--spacing-s: 4px;
|
||||
@ -83,65 +86,112 @@
|
||||
table-layout: fixed;
|
||||
position: relative;
|
||||
|
||||
th {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
tr {
|
||||
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;
|
||||
}
|
||||
>th {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
position: relative;
|
||||
|
||||
>.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);
|
||||
>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;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&.desc {
|
||||
border-top: var(--arrow-size) solid var(--dark-border-color);
|
||||
>.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;
|
||||
}
|
||||
}
|
||||
|
||||
&.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: '';
|
||||
>.spliter {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
transition: background-color .12s ease;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background-color: var(--split-border-color);
|
||||
>.dragger {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-width: var(--dragger-size);
|
||||
height: 100%;
|
||||
background-color: var(--dragger-bg-color);
|
||||
opacity: var(--dragger-opacity);
|
||||
display: none;
|
||||
}
|
||||
|
||||
>.dragger-cursor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
border: 1px solid var(--dragger-cursor-color);
|
||||
box-sizing: border-box;
|
||||
margin-left: 0;
|
||||
opacity: var(--dragger-cursor-opacity);
|
||||
display: none;
|
||||
transition: left .12s ease;
|
||||
|
||||
&::before {
|
||||
top: -1px;
|
||||
border-top: var(--dragger-cursor-size) solid;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -1px;
|
||||
border-bottom: var(--dragger-cursor-size) solid;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: var(--dragger-cursor-pos);
|
||||
border-left: var(--dragger-cursor-size) solid transparent;
|
||||
border-right: var(--dragger-cursor-size) solid transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,10 +38,10 @@
|
||||
grid.height = 0;
|
||||
grid.columns = [
|
||||
{ key: 'c1', caption: 'column 1' },
|
||||
{ key: 'c2', caption: 'column 2', allcheck: true, type: Grid.ColumnTypes.Checkbox, enabled: 'enabled' },
|
||||
{ key: 'c2', caption: '选择', allcheck: true, type: Grid.ColumnTypes.Checkbox, enabled: 'enabled' },
|
||||
{
|
||||
key: 'c2a',
|
||||
caption: 'column 2 a',
|
||||
caption: '下拉',
|
||||
type: Grid.ColumnTypes.Dropdown,
|
||||
source: [
|
||||
{ value: 'off', text: 'Off' },
|
||||
@ -53,7 +53,7 @@
|
||||
},
|
||||
{
|
||||
key: 'c2b',
|
||||
caption: 'column 2 b',
|
||||
caption: '自定义',
|
||||
type: statusCol,
|
||||
enabled: 'enabled'
|
||||
},
|
||||
@ -62,7 +62,6 @@
|
||||
];
|
||||
grid.cellClicked = (rId, cId) => {
|
||||
console.log(`row (${rId}), column (${cId}) clicked.`);
|
||||
return false;
|
||||
};
|
||||
grid.rowDblClicked = (rId) => console.log(`row (${rId}) double clicked.`);
|
||||
grid.cellDblClicked = (rId, cId) => console.log(`row (${rId}), column (${cId}) double clicked.`);
|
||||
|
281
lib/ui/grid.js
281
lib/ui/grid.js
@ -17,6 +17,28 @@ const RedumCount = 4;
|
||||
const MiniDragOffset = 4;
|
||||
const MiniColumnWidth = 50;
|
||||
|
||||
function getClientX(e) {
|
||||
if (e == null) {
|
||||
return null;
|
||||
}
|
||||
const cx = e.touches && e.touches[0]?.clientX;
|
||||
return cx ?? e.clientX;
|
||||
}
|
||||
|
||||
function getOffsetLeftFromWindow(element) {
|
||||
let left = 0;
|
||||
while (element != null) {
|
||||
left += element.offsetLeft;
|
||||
element = element.offsetParent;
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function indexOfParent(target) {
|
||||
// return [...target.parentElement.children].indexOf(target);
|
||||
return Array.prototype.indexOf.call(target.parentElement.children, target);
|
||||
}
|
||||
|
||||
class GridColumn {
|
||||
static create() { return document.createElement('span') }
|
||||
|
||||
@ -430,7 +452,7 @@ class Grid {
|
||||
}
|
||||
const direction = this.sortDirection;
|
||||
[...this.#refs.header.children].forEach((th, i) => {
|
||||
const arrow = th.children[1]; // th.querySelector('layer.arrow');
|
||||
const arrow = th.querySelector('.arrow');
|
||||
if (arrow == null) {
|
||||
return;
|
||||
}
|
||||
@ -497,9 +519,9 @@ class Grid {
|
||||
if (col.visible === false) {
|
||||
const hidden = document.createElement('th');
|
||||
hidden.style.display = 'none';
|
||||
if (col.sortable === true) {
|
||||
if (col.sortable !== false) {
|
||||
hidden.dataset.key = col.key;
|
||||
hidden.addEventListener('click', e => this.#onHeaderClicked(col, e, true));
|
||||
hidden.addEventListener('click', e => this.#onHeaderClicked(e, col, true));
|
||||
}
|
||||
header.appendChild(hidden);
|
||||
continue;
|
||||
@ -538,13 +560,19 @@ class Grid {
|
||||
}
|
||||
// element
|
||||
const th = document.createElement('th');
|
||||
th.className = 'column';
|
||||
th.dataset.key = col.key;
|
||||
for (let css of Object.entries(col.style)) {
|
||||
th.style.setProperty(css[0], css[1]);
|
||||
}
|
||||
th.style.cursor = col.sortable ? 'pointer' : 'auto';
|
||||
th.addEventListener('click', e => this.#onHeaderClicked(col, e));
|
||||
th.addEventListener('mousedown', e => this.#onDragStart(col, e));
|
||||
if (col.sortable) {
|
||||
th.style.cursor = 'pointer';
|
||||
th.addEventListener('click', e => this.#onHeaderClicked(e, col));
|
||||
}
|
||||
if (col.orderable !== false) {
|
||||
col.orderable = true;
|
||||
th.addEventListener('mousedown', e => this.#onDragStart(e, col));
|
||||
}
|
||||
const wrapper = document.createElement('div');
|
||||
th.appendChild(wrapper);
|
||||
if (col.enabled !== false && col.allcheck && isCheckbox) {
|
||||
@ -575,7 +603,7 @@ class Grid {
|
||||
if (col.resizable !== false) {
|
||||
const spliter = document.createElement('layer');
|
||||
spliter.className = 'spliter';
|
||||
spliter.addEventListener('mousedown', e => this.#onResizeStart(col, e));
|
||||
spliter.addEventListener('mousedown', e => this.#onResizeStart(e, col));
|
||||
th.appendChild(spliter);
|
||||
}
|
||||
// tooltip
|
||||
@ -621,9 +649,9 @@ class Grid {
|
||||
const bodyContent = document.createElement('table');
|
||||
bodyContent.className = 'grid-body-content';
|
||||
bodyContent.addEventListener('mousedown', e => {
|
||||
let { parent, target } = this.#getRowTarget(e.target);
|
||||
const rowIndex = [...parent.parentElement.children].indexOf(parent);
|
||||
let colIndex = [...parent.children].indexOf(target);
|
||||
let [parent, target] = this.#getRowTarget(e.target);
|
||||
const rowIndex = indexOfParent(parent);
|
||||
let colIndex = indexOfParent(target);
|
||||
if (colIndex >= this.columns.length) {
|
||||
colIndex = -1;
|
||||
}
|
||||
@ -840,6 +868,118 @@ class Grid {
|
||||
// width = this.#refs.bodyContainer.offsetWidth - oldwidth + width;
|
||||
// this.#refs.bodyContainer.style.width = `${width}px`;
|
||||
// }
|
||||
for (let i = 0; i < this.#currentSource.length; i += 1) {
|
||||
const keyid = (i << MaxColumnBit) | index;
|
||||
if (this.#overflows.hasOwnProperty(keyid)) {
|
||||
delete this.#overflows[keyid];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#changingColumnOrder(index, offset, x, offsetLeft) {
|
||||
const children = this.#refs.header.children;
|
||||
let element = children[index];
|
||||
this.#refs.dragger.style.left = `${element.offsetLeft - offsetLeft + offset}px`;
|
||||
this.#refs.dragger.style.width = element.style.width;
|
||||
this.#refs.dragger.style.display = 'block';
|
||||
offset = x - getOffsetLeftFromWindow(element);
|
||||
let idx;
|
||||
if (offset < 0) {
|
||||
offset = -offset;
|
||||
for (let i = index - 1; i >= 0 && offset >= 0; i -= 1) {
|
||||
element = children[i];
|
||||
if (element == null || element.className !== 'column') {
|
||||
break;
|
||||
}
|
||||
if (offset < element.offsetWidth) {
|
||||
idx = (offset > element.offsetWidth / 2) ? i : i + 1;
|
||||
break;
|
||||
}
|
||||
offset -= element.offsetWidth;
|
||||
}
|
||||
idx ??= 0;
|
||||
} else {
|
||||
const count = children.length;
|
||||
for (let i = index; i < count - 1 && offset >= 0; i += 1) {
|
||||
element = children[i];
|
||||
if (element == null || element.className !== 'column') {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
if (offset < element.offsetWidth) {
|
||||
idx = (offset > element.offsetWidth / 2) ? i + 1 : i;
|
||||
break;
|
||||
}
|
||||
offset -= element.offsetWidth;
|
||||
}
|
||||
idx ??= count - 1;
|
||||
}
|
||||
if (idx !== this.#colAttrs.orderIndex) {
|
||||
this.#colAttrs.orderIndex = idx;
|
||||
element = children[idx];
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
this.#refs.draggerCursor.style.left = `${element.offsetLeft - offsetLeft}px`;
|
||||
this.#refs.draggerCursor.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
#changeColumnOrder(index) {
|
||||
this.#refs.dragger.style.display = '';
|
||||
this.#refs.draggerCursor.style.display = '';
|
||||
const orderIndex = this.#colAttrs.orderIndex;
|
||||
if (orderIndex >= 0 && orderIndex !== index) {
|
||||
let targetIndex = orderIndex - index;
|
||||
if (targetIndex >= 0 && targetIndex <= 1) {
|
||||
return;
|
||||
}
|
||||
const header = this.#refs.header;
|
||||
const children = header.children;
|
||||
const rows = this.#refs.bodyContent.children;
|
||||
const columns = this.columns;
|
||||
if (targetIndex > 1) {
|
||||
targetIndex = orderIndex - 1;
|
||||
// const current = columns[index];
|
||||
// for (let i = index; i < targetIndex; i += 1) {
|
||||
// columns[i] = columns[i + 1];
|
||||
// }
|
||||
// columns[targetIndex] = current;
|
||||
const current = columns.splice(index, 1)[0];
|
||||
columns.splice(targetIndex, 0, current);
|
||||
header.insertBefore(children[index], children[targetIndex].nextElementSibling);
|
||||
for (let row of rows) {
|
||||
row.insertBefore(row.children[index], row.children[targetIndex].nextElementSibling);
|
||||
}
|
||||
} else {
|
||||
targetIndex = orderIndex;
|
||||
// const current = columns[index];
|
||||
// for (let i = index; i > targetIndex; i -= 1) {
|
||||
// columns[i] = columns[i - 1];
|
||||
// }
|
||||
// columns[targetIndex] = current;
|
||||
const current = columns.splice(index, 1)[0];
|
||||
columns.splice(targetIndex, 0, current);
|
||||
header.insertBefore(children[index], children[targetIndex]);
|
||||
for (let row of rows) {
|
||||
row.insertBefore(row.children[index], row.children[targetIndex]);
|
||||
}
|
||||
}
|
||||
// refresh sortIndex
|
||||
[...children].forEach((th, i) => {
|
||||
const arrow = th.querySelector('.arrow');
|
||||
if (arrow == null) {
|
||||
return;
|
||||
}
|
||||
if (arrow.className !== 'arrow') {
|
||||
this.sortIndex = i;
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof this.columnChanged === 'function') {
|
||||
this.columnChanged(ColumnChangedType.Reorder, index, targetIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#scrollToTop(top, reload) {
|
||||
@ -877,15 +1017,19 @@ class Grid {
|
||||
while ((parent = target.parentElement) != null && !parent.classList.contains('grid-row')) {
|
||||
target = parent;
|
||||
}
|
||||
return { parent, target };
|
||||
return [parent, target];
|
||||
}
|
||||
|
||||
#onHeaderClicked(col, e, force) {
|
||||
#notHeader(tagName) {
|
||||
return /^(input|label|layer|svg|use)$/i.test(tagName);
|
||||
}
|
||||
|
||||
#onHeaderClicked(e, col, force) {
|
||||
const attr = this.#colAttrs[col.key];
|
||||
if (!force && attr != null && (attr.resizing || attr.dragging)) {
|
||||
return;
|
||||
}
|
||||
if (col.sortable && !/^(label|layer|svg|use)$/i.test(e.target.tagName)) {
|
||||
if (!this.#notHeader(e.target.tagName)) {
|
||||
const index = this.columns.indexOf(col);
|
||||
if (index < 0) {
|
||||
return;
|
||||
@ -902,9 +1046,108 @@ class Grid {
|
||||
}
|
||||
}
|
||||
|
||||
#onDragStart(col, e) { }
|
||||
#onDragStart(e, col) {
|
||||
if (this.#notHeader(e.target.tagName)) {
|
||||
return;
|
||||
}
|
||||
const cx = getClientX(e);
|
||||
const index = indexOfParent(e.currentTarget);
|
||||
const clearEvents = attr => {
|
||||
for (let event of ['mousemove', 'mouseup']) {
|
||||
if (attr.hasOwnProperty(event)) {
|
||||
window.removeEventListener(event, attr[event]);
|
||||
delete attr[event];
|
||||
}
|
||||
}
|
||||
};
|
||||
let attr = this.#colAttrs[col.key];
|
||||
if (attr == null) {
|
||||
attr = this.#colAttrs[col.key] = {};
|
||||
} else {
|
||||
clearEvents(attr);
|
||||
}
|
||||
attr.dragging = true;
|
||||
const offsetLeft = this.#refs.header.querySelector('th:last-child').offsetLeft;
|
||||
const dragmove = e => {
|
||||
const cx2 = getClientX(e);
|
||||
const offset = cx2 - cx;
|
||||
let pos = attr.offset;
|
||||
let dragging;
|
||||
if (pos == null && (offset > MiniDragOffset || offset < -MiniDragOffset)) {
|
||||
dragging = true;
|
||||
} else if (pos !== offset) {
|
||||
dragging = true;
|
||||
}
|
||||
if (dragging) {
|
||||
this.#changingColumnOrder(index, offset, cx2, offsetLeft);
|
||||
attr.offset = offset;
|
||||
}
|
||||
};
|
||||
attr.mousemove = e => throttle(dragmove, RefreshInterval, this, e);
|
||||
attr.mouseup = () => {
|
||||
clearEvents(attr);
|
||||
if (attr.offset == null) {
|
||||
delete attr.dragging;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
delete attr.dragging;
|
||||
delete attr.offset;
|
||||
});
|
||||
this.#changeColumnOrder(index);
|
||||
if (typeof this.columnChanged === 'function') {
|
||||
this.columnChanged(ColumnChangedType.Reorder, index);
|
||||
}
|
||||
}
|
||||
};
|
||||
['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event]));
|
||||
}
|
||||
|
||||
#onResizeStart(col, e) { }
|
||||
#onResizeStart(e, col) {
|
||||
const cx = getClientX(e);
|
||||
const width = col.width;
|
||||
const index = indexOfParent(e.currentTarget.parentElement);
|
||||
const window = this.window ?? global;
|
||||
const clearEvents = attr => {
|
||||
for (let event of ['mousemove', 'mouseup']) {
|
||||
if (attr.hasOwnProperty(event)) {
|
||||
window.removeEventListener(event, attr[event]);
|
||||
delete attr[event];
|
||||
}
|
||||
}
|
||||
};
|
||||
let attr = this.#colAttrs[col.key];
|
||||
if (attr == null) {
|
||||
attr = this.#colAttrs[col.key] = {};
|
||||
} else {
|
||||
clearEvents(attr);
|
||||
}
|
||||
attr.resizing = width;
|
||||
const resizemove = e => {
|
||||
const cx2 = getClientX(e);
|
||||
const val = width + (cx2 - cx);
|
||||
if (val < MiniColumnWidth) {
|
||||
return;
|
||||
}
|
||||
attr.resizing = val;
|
||||
this.#changeColumnWidth(index, val);
|
||||
};
|
||||
attr.mousemove = e => throttle(resizemove, RefreshInterval, this, e);
|
||||
attr.mouseup = e => {
|
||||
clearEvents(attr);
|
||||
const width = attr.resizing;
|
||||
if (width != null) {
|
||||
col.autoResize = false;
|
||||
setTimeout(() => delete attr.resizing);
|
||||
this.#changeColumnWidth(index, width);
|
||||
if (typeof this.columnChanged === 'function') {
|
||||
this.columnChanged(ColumnChangedType.Resize, index, width);
|
||||
}
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event]));
|
||||
}
|
||||
|
||||
#onColumnAllChecked(col, flag) {
|
||||
if (this.#currentSource == null) {
|
||||
@ -934,7 +1177,11 @@ class Grid {
|
||||
}
|
||||
|
||||
#onScroll(e) {
|
||||
this.#scrollLeft = e.target.scrollLeft;
|
||||
const left = e.target.scrollLeft;
|
||||
if (this.#scrollLeft !== left) {
|
||||
this.#scrollLeft = left;
|
||||
this.#refs.header.style.left = `${-left}px`;
|
||||
}
|
||||
if (!this.virtual) {
|
||||
return;
|
||||
}
|
||||
@ -946,7 +1193,7 @@ class Grid {
|
||||
if (e.target.classList.contains('grid-hover-holder')) {
|
||||
return;
|
||||
}
|
||||
let { parent, target } = this.#getRowTarget(e.target);
|
||||
let [parent, target] = this.#getRowTarget(e.target);
|
||||
let keyid = target.keyid;
|
||||
if (parent == null || keyid == null) {
|
||||
delete holder.keyid;
|
||||
|
Loading…
x
Reference in New Issue
Block a user