feature: drag support in sort panel.

This commit is contained in:
2024-05-13 16:46:55 +08:00
parent f676ec76db
commit a946012a33
14 changed files with 777 additions and 566 deletions

View File

@ -26,7 +26,9 @@ function fillCheckbox(container, type = 'fa-regular', label, tabindex = -1, char
container.appendChild(
createElement('span', span => {
span.innerText = label;
span.title = title;
if (title != null) {
span.title = title;
}
})
);
}

View File

@ -705,6 +705,12 @@
}
}
.ui-popup-mask .ui-popup-container .ui-popup-footer {
>.ui-sort-layout {
flex: 1 1 auto;
}
}
/*@media (prefers-color-scheme: dark) {
.ui-grid {
--cell-hover-bg-color: yellow;

View File

@ -76,43 +76,47 @@
--icon-size: 16px;
>.ui-video-wrapper {
position: relative;
display: inline-block;
>.ui-video-content {
height: 100%;
>video {
width: 100%;
height: 100%;
}
>.ui-video-wrapper {
position: relative;
display: inline-block;
>.ui-video-waiting {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .3);
display: flex;
justify-content: center;
align-items: center;
transition: opacity .2s;
>svg {
fill: #eee;
width: 30px;
height: 30px;
top: calc(50% - 15px);
left: calc(50% - 15px);
animation: spinner 1.2s infinite linear;
>video {
width: 100%;
height: 100%;
}
@keyframes spinner {
0% {
transform: rotate(0deg);
>.ui-video-waiting {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .3);
display: flex;
justify-content: center;
align-items: center;
transition: opacity .2s;
>svg {
fill: #eee;
width: 30px;
height: 30px;
top: calc(50% - 15px);
left: calc(50% - 15px);
animation: spinner 1.2s infinite linear;
}
100% {
transform: rotate(359deg);
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
}
}
@ -120,12 +124,13 @@
>.ui-video-control {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
height: 70px;
bottom: 0;
opacity: 0;
background: linear-gradient(transparent calc(100% - 100px), rgba(50, 50, 50, .7) calc(100% - 65px), #000);
background: linear-gradient(transparent, #000);
// calc(100% - 100px), rgba(50, 50, 50, .7) calc(100% - 65px)
transition: opacity .5s;
&:hover,

View File

@ -204,7 +204,8 @@ export class Dropdown {
let label;
if (options.input) {
label = createElement('input', 'ui-drop-text');
label.setAttribute('type', 'text');
label.type = 'text';
label.draggable = false;
options.placeholder && label.setAttribute('placeholder', options.placeholder);
isPositive(options.maxLength) && label.setAttribute('maxlength', options.maxLength);
isPositive(options.tabIndex) && label.setAttribute('tabindex', options.tabIndex);
@ -383,7 +384,7 @@ export class Dropdown {
if (!options.input && options.search) {
const search = createElement('div', 'ui-drop-search');
const input = createElement('input');
input.setAttribute('type', 'text');
input.type = 'text';
isPositive(options.tabIndex) && input.setAttribute('tabindex', options.tabIndex);
!nullOrEmpty(options.searchPlaceholder) && input.setAttribute('placeholder', options.searchPlaceholder);
input.addEventListener('input', e => {
@ -503,7 +504,7 @@ export class Dropdown {
}
const li = createElement('li');
li.dataset.value = val;
li.setAttribute('title', item[textkey]);
li.title = item[textkey];
let label;
const html = item[htmlkey];
if (html instanceof HTMLElement) {

View File

@ -71,13 +71,13 @@ let r = lang;
/**
* 键值字典
* @template T
* @typedef {Map<string, T>} KeyMap
* @typedef {{[key: string]: T}} KeyMap
*/
/**
* 索引字典
* @template T
* @typedef {Map<number, T>} IndexMap
* @typedef {{[index: number]: T}} IndexMap
*/
/**
@ -134,6 +134,7 @@ let r = lang;
* @param {GridRowItem} item - 行数据对象
* @param {boolean} editing - 是否处于编辑状态
* @param {HTMLElement} [body] - Grid 控件的 `<tbody>` 部分
* @param {number} [index] - 所在行索引(不可依赖此值,除了呈现时,其他时候该值不会传递)
* @returns {ValueItem} - 返回过滤后的显示或编辑值
* @this GridColumnDefinition
*/
@ -455,6 +456,9 @@ const GridColumnDirection = {
* @property {string} [addLevel] - Add Level
* @property {string} [deleteLevel] - Delete Level
* @property {string} [copyLevel] - Copy Level
* @property {string} [sortBy] - Sort by
* @property {string} [thenBy] - Then by
* @property {string} [updateLayout] - Update Layout
* @property {string} [asc] - Ascending
* @property {string} [desc] - Descending
* @property {string} [column] - Column
@ -939,6 +943,13 @@ export class Grid {
* @ignore
*/
headerWrap = true;
/**
* 是否允许行间拖拽
* @type {boolean}
* @default false
* @ignore
*/
rowDraggable = false;
/**
* 监听事件的窗口载体
* @type {(Window | HTMLElement)}
@ -1060,6 +1071,16 @@ export class Grid {
* @this Grid
*/
onRowCollapsed;
/**
* 行发生变化时触发的事件
* @event
* @param {("update" | "add" | "remove")} action - 变动类型
* @param {GridRowItem[]} items - 发生变动的行对象
* @param {(number | number[])} indexes - 变动的索引集合
* @this Grid
* @since 1.0.3
*/
onRowChanged;
/**
* 列类型枚举
@ -1099,6 +1120,7 @@ export class Grid {
* @property {boolean} [tooltipDisabled=false] - 单元格 tooltip 是否禁用
* @property {boolean} [headerVisible=true] - 列头是否显示
* @property {boolean} [headerWrap=true] - 列头是否允许换行
* @property {boolean} [rowDraggable=false] - 是否允许行间拖拽 @since 1.0.3
* @property {(Window | HTMLElement)} [window=global] - 监听事件的窗口载体
* @property {number} [sortIndex=-1] - 排序列的索引
* @property {GridColumnDirection} [sortDirection=GridColumnDirection.Ascending] - 排序方式,正数升序,负数倒序
@ -1332,6 +1354,9 @@ export class Grid {
addLevel: r('addLevel', 'Add level'),
deleteLevel: r('deleteLevel', 'Delete level'),
copyLevel: r('copyLevel', 'Copy level'),
sortBy: r('sortBy', 'Sort by'),
thenBy: r('thenBy', 'Then by'),
updateLayout: r('updateLayout', 'Update Layout'),
asc: r('asc', 'Ascending'),
desc: r('desc', 'Descending'),
column: r('column', 'Column'),
@ -1408,6 +1433,30 @@ export class Grid {
}
this._onRowClicked(e, rowIndex, colIndex);
});
if (this.rowDraggable) {
grid.addEventListener('dragover', e => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = e.ctrlKey ? 'copy' : 'move';
});
grid.addEventListener('drop', e => {
e.preventDefault();
e.stopPropagation();
const src = Number(e.dataTransfer.getData('text'));
if (isNaN(src) || src < 0) {
return;
}
const target = this._var.currentSource?.length ?? 0;
if (target == null || src === target) {
return;
}
const row = e.ctrlKey ?
Object.assign({}, this._var.currentSource[src]?.values) :
this.removeItem(src);
this.addItem(row, target);
this.selectedIndexes = [e.ctrlKey ? target : target - 1];
});
}
container.replaceChildren(grid);
const sizer = createElement('span', 'ui-grid-sizer');
grid.appendChild(sizer);
@ -1719,24 +1768,35 @@ export class Grid {
/**
* 显示多列排序设置面板
* @param {Function} [callback] - 更新回调函数 @since 1.0.3
* @param {boolean} [layout] - 是否显示更新 layout 复选框 @since 1.0.3
* @since 1.0.1
*/
showSortPanel() {
showSortPanel(callback, layout) {
const content = createElement('div', 'ui-sort-panel-content');
const buttonWrapper = createElement('div', 'ui-sort-panel-buttons');
const grid = new Grid(null, r);
grid.rowDraggable = true;
grid.langs = this.langs;
const rowChanged = index => {
buttonWrapper.querySelector('.ui-button-delete').disabled = index < 0;
buttonWrapper.querySelector('.ui-button-copy').disabled = index < 0;
buttonWrapper.querySelector('.ui-button-move-up').disabled = index < 1;
buttonWrapper.querySelector('.ui-button-move-down').disabled = index >= grid.source.length - 1;
// buttonWrapper.querySelector('.ui-button-move-up').disabled = index < 1;
// buttonWrapper.querySelector('.ui-button-move-down').disabled = index >= grid.source.length - 1;
};
grid.onSelectedRowChanged = rowChanged;
grid.onRowChanged = () => {
const layout = pop.container.querySelector('.ui-sort-layout');
if (layout != null) {
layout.classList.remove('disabled');
layout.querySelector('input').disabled = false;
}
};
const reload = index => {
grid.selectedIndexes = [index];
grid.scrollTop = index * (grid.rowHeight + 1);
rowChanged(index);
grid.onRowChanged();
}
buttonWrapper.append(
createElement('span', 'button',
@ -1793,6 +1853,7 @@ export class Grid {
});
})
),
/*
createElement('span', button => {
button.className = 'button ui-button-move-up';
const icon = createIcon('fa-light', 'chevron-up');
@ -1829,23 +1890,43 @@ export class Grid {
});
button.appendChild(icon);
})
//*/
);
const gridWrapper = createElement('div', 'ui-sort-panel-grid');
content.append(buttonWrapper, gridWrapper);
const columnSource = this.columns.filter(c => c.sortable !== false && c.visible !== false);
const columnSource = this.columns.filter(c => c.sortable !== false); // ticket 56389, && c.visible !== false
columnSource.sort((a, b) => a.caption > b.caption ? 1 : -1);
grid.columns = [
{
key: 'column',
width: 80,
sortable: false,
orderable: false,
resizable: false,
filter: (_item, _editing, _body, index) => index === 0 ? this.langs.sortBy : this.langs.thenBy
},
{
key: 'caption',
caption: this.langs.column,
width: 270,
type: GridColumnTypeEnum.Dropdown,
dropOptions: {
textKey: 'caption',
valueKey: 'key'
valueKey: 'caption',
input: true
},
source: columnSource,
dropRestrictCase: true,
sourceCache: false,
source: () => columnSource.filter(c => grid.source?.find(s => s.column === c.key) == null),
sortable: false,
orderable: false
orderable: false,
onChanged: (item, _value, _oldValue, e) => {
item.column = e.key;
const layout = pop.container.querySelector('.ui-sort-layout');
if (layout != null) {
layout.classList.remove('disabled');
layout.querySelector('input').disabled = false;
}
}
},
{
key: 'order',
@ -1857,7 +1938,14 @@ export class Grid {
{ value: 'desc', text: this.langs.desc }
],
sortable: false,
orderable: false
orderable: false,
onChanged: () => {
const layout = pop.container.querySelector('.ui-sort-layout');
if (layout != null) {
layout.classList.remove('disabled');
layout.querySelector('input').disabled = false;
}
}
}
];
const pop = new Popup({
@ -1871,6 +1959,14 @@ export class Grid {
const source = grid.source;
if (source == null || source.length === 0) {
this.sortArray = null;
// arrow icon
[...this._headerCells].forEach((th, i) => {
const arrow = th.querySelector('.arrow');
if (arrow == null) {
return;
}
arrow.className = 'arrow';
});
} else {
const dict = {};
for (let i = 0; i < source.length; ++i) {
@ -1895,12 +1991,12 @@ export class Grid {
}
dict[it.column] = true;
}
this.sortArray = source;
this.sortArray = source.map(s => ({ column: s.column, order: s.order }));
this.sortDirection = 1;
this.sort();
}
if (typeof this.onSorted === 'function') {
this.onSorted(this.sortArray);
if (typeof callback === 'function') {
callback.call(this, this.sortArray, pop.container.querySelector('.ui-sort-layout>input')?.checked);
}
return true;
}
@ -1909,11 +2005,29 @@ export class Grid {
],
onResizeEnded: () => grid.resize()
});
const source = this.sortArray || [{ column: '', order: 'asc' }];
let source = this.sortArray;
if (source == null && this.sortIndex >= 0) {
const col = this.columns[this.sortIndex];
if (col != null) {
source = [{ column: col.key, order: this.sortDirection > 0 ? 'asc' : 'desc' }];
}
}
source ??= [{ column: '', order: 'asc' }];
pop.show(this._var.el).then(() => {
pop.container.style.cssText += 'width: 520px; height: 400px';
pop.container.style.cssText += 'width: 600px; height: 460px';
if (layout) {
const footer = pop.container.querySelector('.ui-popup-footer');
footer.insertBefore(createCheckbox({
label: this.langs.updateLayout,
className: 'ui-sort-layout',
enabled: false
}), footer.children[0]);
}
grid.init(gridWrapper);
grid.source = source.filter(s => s.column === '' || columnSource.find(c => c.key === s.column) != null);
grid.source = source
.map(s => (s.c = columnSource.find(c => c.key === s.column), s))
.filter(s => s.column === '' || s.c != null)
.map(s => ({ column: s.column, caption: s.c?.caption ?? '', order: s.order }));
grid.selectedIndexes = [0];
grid.refresh();
rowChanged(0);
@ -1942,6 +2056,9 @@ export class Grid {
} else {
this.refresh();
}
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('update', [item], index);
}
}
/**
@ -1980,6 +2097,9 @@ export class Grid {
} else {
this.reload();
}
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('add', [item], index);
}
return item;
}
@ -2024,6 +2144,9 @@ export class Grid {
} else {
this.reload();
}
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('add', array, index);
}
return array;
}
@ -2053,6 +2176,9 @@ export class Grid {
this._var.selectedIndexes = [];
}
this.reload();
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('remove', [it.values], index);
}
return it.values;
}
@ -2098,11 +2224,14 @@ export class Grid {
it.__index -= offset;
}
const index = indexes[0];
if (index < 1) {
if (index > 0) {
this._var.selectedIndexes = [index - 1];
} else {
this._var.selectedIndexes = [];
}
if (typeof this.onRowChanged === 'function') {
this.onRowChanged('remove', array, indexes);
}
this.reload();
return array;
}
@ -2144,7 +2273,7 @@ export class Grid {
align: c.align,
visible: c.visible
})),
source: this.source?.map(s => {
source: this.source?.map((s, i) => {
const item = Object.create(null);
for (let c of this.columns) {
if (c.key == null) {
@ -2154,7 +2283,7 @@ export class Grid {
if (c.text != null) {
val = c.text;
} else if (typeof c.filter === 'function') {
val = c.filter(s, false, this._var.refs.body);
val = c.filter(s, false, this._var.refs.body, i);
} else {
val = s[c.key];
if (val != null) {
@ -2676,6 +2805,34 @@ export class Grid {
const readonly = this.readonly;
for (let i = 0; i < count; ++i) {
const row = createElement('tr', 'ui-grid-row');
if (this.rowDraggable) {
row.draggable = true;
row.addEventListener('dragstart', e => {
e.dataTransfer.setData('text', String(this._var.startIndex + exists + i));
});
row.addEventListener('dragover', e => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = e.ctrlKey ? 'copy' : 'move';
});
row.addEventListener('drop', e => {
e.preventDefault();
e.stopPropagation();
const src = Number(e.dataTransfer.getData('text'));
if (isNaN(src) || src < 0) {
return;
}
const target = this._var.startIndex + exists + i;
if (src === target) {
return;
}
const row = e.ctrlKey ?
Object.assign({}, this._var.currentSource[src]?.values) :
this.removeItem(src);
this.addItem(row, target);
this.selectedIndexes = [target];
});
}
const virtualRow = { cells: {} };
this._var.virtualRows[exists + i] = virtualRow;
let left = this.expandable ? ExpandableWidth : 0;
@ -2852,7 +3009,7 @@ export class Grid {
if (col.text != null) {
val = col.text;
} else if (typeof col.filter === 'function') {
val = col.filter(item, selected, this._var.refs.body);
val = col.filter(item, selected, this._var.refs.body, startIndex + i);
} else {
val = item[col.key];
if (val != null) {

View File

@ -158,6 +158,7 @@ export function createFile(url, icon = 'file-alt') {
* @param {boolean} options.[autoPlay] - 是否自动播放
* @param {boolean} options.[autoFullScreen] - 是否自动全屏
* @param {boolean} options.[autoLoop] - 是否循环播放
* @param {Function} options.[onLoaded] - 视频加载完成回调
* @param {Function} [callback] - 视频元素处理回调函数
* @returns {HTMLDivElement} 返回联动视频元素
*/
@ -330,6 +331,9 @@ export function createVideoList(urls, options, callback) {
controller.classList.add('no-fullscreen');
}
const content = createElement('div', 'ui-video-content');
container.append(content, controller);
urls.forEach((url, i) => {
const video = createElement('video');
videos[i] = video;
@ -349,17 +353,22 @@ export function createVideoList(urls, options, callback) {
video.volume = 0;
}
prepared += 1;
if (options?.autoPlay && prepared >= length) {
// auto play
videos.forEach(v => v.play().catch(() => { }));
if (options?.autoFullScreen && length === 1 && document.fullscreenElement == null) {
video.requestFullscreen().catch(() => { });
if (prepared >= length) {
if (options?.autoPlay) {
// auto play
videos.forEach(v => v.play().catch(() => { }));
if (options?.autoFullScreen && length === 1 && document.fullscreenElement == null) {
video.requestFullscreen().catch(() => { });
}
}
if (typeof options?.onLoaded === 'function') {
options.onLoaded();
}
}
});
video.addEventListener('progress', () => {
const buffered = video.buffered;
for (let i = 0; i < buffered.length; i += 1) {
for (let i = 0; i < buffered.length; ++i) {
let buffer = seekBufferBar.children[i];
if (buffer == null) {
seekBufferBar.append(buffer = createElement('div', 'ui-video-buffer'));
@ -435,9 +444,8 @@ export function createVideoList(urls, options, callback) {
if (typeof callback === 'function') {
callback(wrapper);
}
container.append(wrapper);
content.append(wrapper);
});
container.append(controller);
return container;
}