diff --git a/css/grid.scss b/css/grid.scss index 0b1a1da..2d33fd8 100644 --- a/css/grid.scss +++ b/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; + } } } } diff --git a/lib/ui/grid.html b/lib/ui/grid.html index e88f541..d82a4d2 100644 --- a/lib/ui/grid.html +++ b/lib/ui/grid.html @@ -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.`); diff --git a/lib/ui/grid.js b/lib/ui/grid.js index 7ac0b27..c7adced 100644 --- a/lib/ui/grid.js +++ b/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;