add support to resize or reorder.

This commit is contained in:
Tsanie Lily 2023-04-04 15:30:38 +08:00
parent de1ba82970
commit a2fbb7e78a
3 changed files with 368 additions and 72 deletions

View File

@ -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;
}
}
}
}

View File

@ -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.`);

View File

@ -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;