diff --git a/packages/devui-vue/devui/dragdrop-new/index.ts b/packages/devui-vue/devui/dragdrop-new/index.ts new file mode 100644 index 0000000000..fc9bbdddaa --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/index.ts @@ -0,0 +1,43 @@ +import { App } from 'vue'; +import { DragDropService } from './src/drag-drop.service'; +import { default as Draggable } from './src/draggable.directive'; +import { default as Droppable } from './src/droppable.directive'; +import { default as Sortable } from './src/sortable.directive'; +import { default as DropScrollEnhanced, DropScrollEnhancedSide } from './src/drop-scroll-enhance.directive'; +import { default as BatchDraggable } from './src/batch-draggable.directive'; +import { default as DragPreview } from './src/drag-preview.directive'; +import { DragPreviewTemplate } from './src/drag-preview.component'; +import { default as DragPreviewCloneDomRef } from './src/drag-preview-clone-dom-ref.component'; +import { default as useDragDropSort } from './src/sync'; + +export * from './src/drag-drop.service'; +export * from './src/draggable.directive'; +export * from './src/droppable.directive'; +export * from './src/sortable.directive'; +export * from './src/drop-scroll-enhance.directive'; +export * from './src/batch-draggable.directive'; +export * from './src/drag-preview.component'; +export * from './src/drag-preview.directive'; +export * from './src/drag-preview-clone-dom-ref.component'; +export * from './src/sync'; + +export { Draggable, Droppable, Sortable, DropScrollEnhanced }; + +export default { + title: 'DragDrop 2.0 拖拽', + category: '通用', + status: '100%', + install(app: App): void { + app.directive('dDraggable', Draggable); + app.directive('dDroppable', Droppable); + app.directive('dSortable', Sortable); + app.directive('dDropScrollEnhanced', DropScrollEnhanced); + app.directive('dDropScrollEnhancedSide', DropScrollEnhancedSide); + app.directive('dDraggableBatchDrag', BatchDraggable); + app.directive('dDragPreview', DragPreview); + app.component('DDragPreviewTemplate', DragPreviewTemplate); + app.component(DragPreviewCloneDomRef.name, DragPreviewCloneDomRef); + app.provide(DragDropService.TOKEN, new DragDropService()); + app.use(useDragDropSort); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/batch-draggable.directive.ts b/packages/devui-vue/devui/dragdrop-new/src/batch-draggable.directive.ts new file mode 100644 index 0000000000..f668ac59af --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/batch-draggable.directive.ts @@ -0,0 +1,204 @@ +import { EventEmitter } from './preserve-next-event-emitter'; +import { DragDropService } from './drag-drop.service'; +import { NgDirectiveBase, NgSimpleChanges } from './directive-base'; +import { DraggableDirective } from './draggable.directive'; +import { DirectiveBinding } from 'vue'; +import { injectFromContext } from './utils'; +export type BatchDragStyle = 'badge' | 'stack' | string; + +export interface IBatchDraggableBinding { + batchDragGroup?: string; + batchDragActive?: boolean; + batchDragLastOneAutoActiveEventKeys?: Array; + batchDragStyle?: string | Array; +} +export interface IBatchDraggableListener { + '@batchDragActiveEvent'?: (_: any) => void; +} + +export class BatchDraggableDirective extends NgDirectiveBase { + static INSTANCE_KEY = '__vueDevuiBatchDraggableDirectiveInstance'; + batchDragGroup = 'default'; + batchDragActive = false; + batchDragLastOneAutoActiveEventKeys = ['ctrlKey']; + batchDragStyle: Array = ['badge', 'stack']; + batchDragActiveEvent = new EventEmitter(); + dragData?: any; + needToRestore = false; + + constructor(private draggable: DraggableDirective, private dragDropService: DragDropService) { + super(); + this.draggable.batchDraggable = this; + } + + ngOnInit() { + this.initDragDataByIdentity(); + } + + ngOnDestroy() { + this.draggable.batchDraggable = undefined; + if (this.dragData) { + if (this.dragData.draggable === this.draggable) { + this.dragData.draggable = undefined; + if (!this.dragData.identity) { + this.removeFromBatchGroup(); + } + } + } + } + + ngOnChanges(changes: NgSimpleChanges): void { + if (changes['batchDragActive']) { + if (!this.initDragDataByIdentity()) { + if (this.batchDragActive) { + if (!this.dragData && this.allowAddToBatchGroup()) { + this.addToBatchGroup(); + } + } else { + this.removeFromBatchGroup(); + } + } + } + } + ngAfterViewInit() { + if (this.needToRestore) { + this.restoreDragDataViewAfterViewInit(); + this.needToRestore = false; + } + } + initDragDataByIdentity() { + const dragData = this.findInBatchDragDataByIdentities(); + if (dragData) { + if (this.batchDragActive) { + if (!this.dragData) { + this.addToBatchGroup(dragData); + this.registerRestoreDragDataViewAfterViewInitWhiteDragging(); + } + } else { + this.removeFromBatchGroup(dragData); + } + } + return dragData; + } + + registerRestoreDragDataViewAfterViewInitWhiteDragging() { + if ( + this.dragDropService.draggedEl && + this.dragDropService.draggedElIdentity && + this.dragDropService.draggedEl !== this.draggable.el.nativeElement + ) { + this.needToRestore = true; + } + } + restoreDragDataViewAfterViewInit() { + const draggable = this.draggable; + if (draggable.originPlaceholder && draggable.originPlaceholder.show !== false) { + draggable.insertOriginPlaceholder(true, false); + } + draggable.el.nativeElement.style.display = 'none'; + } + + allowAddToBatchGroup() { + if (!this.dragDropService.batchDragGroup) { + return true; + } else { + return this.batchDragGroup === this.dragDropService.batchDragGroup; + } + } + addToBatchGroup(dragData?: any) { + this.dragDropService.batchDragGroup = this.dragDropService.batchDragGroup || this.batchDragGroup; + if (dragData) { + dragData.draggable = this.draggable; + dragData.dragData = this.draggable.dragData; + this.dragData = dragData; + } else { + this.dragData = this.dragData || { + identity: this.draggable.dragIdentity || undefined, + draggable: this.draggable, + dragData: this.draggable.dragData, + }; + this.dragDropService.batchDragData = this.addToArrayIfNotExist(this.dragDropService.batchDragData!, this.dragData); + } + } + removeFromBatchGroup(dragData?: any) { + this.deleteFromArrayIfExist(this.dragDropService.batchDragData!, dragData || this.dragData); + this.dragData = undefined; + if (!(this.dragDropService.batchDragData && this.dragDropService.batchDragData.length)) { + this.dragDropService.batchDragGroup = undefined; + } + } + + private addToArrayIfNotExist(array: any[], target: any) { + array = array || []; + if (array.indexOf(target) === -1) { + array.push(target); + } + return array; + } + + private deleteFromArrayIfExist(array: any[], target: any) { + if (!array) { + return; + } + if (array.length > 0) { + const index = array.indexOf(target); + if (index > -1) { + array.splice(index, 1); + } + } + return array; + } + + private findInBatchDragDataByIdentities() { + if (!this.draggable.dragIdentity) { + return null; + } else if (!this.dragDropService.batchDragData) { + return undefined; + } else { + return this.dragDropService.batchDragData.filter((dragData) => dragData.identity === this.draggable.dragIdentity).pop(); + } + } + + active() { + this.batchDragActiveEvent.emit({ el: this.draggable.el.nativeElement, data: this.draggable.dragData }); + } + + public updateDragData() { + // 选中状态才更新 + if (!this.dragData) { + return; + } + // 需要维持内存地址不变 + Object.assign(this.dragData, { + identity: this.draggable.dragIdentity || undefined, + draggable: this.draggable, + dragData: this.draggable.dragData, + }); + } +} + +export default { + mounted(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding, vNode) { + const context = vNode['ctx'].provides; + const dragDropService = injectFromContext(DragDropService.TOKEN, context) as DragDropService; + const draggableDirective = injectFromContext(DraggableDirective.TOKEN, context) as DraggableDirective; + const batchDraggableDirective = (el[BatchDraggableDirective.INSTANCE_KEY] = new BatchDraggableDirective( + draggableDirective, + dragDropService + )); + batchDraggableDirective.setInput(binding.value); + batchDraggableDirective.mounted(); + batchDraggableDirective.ngOnInit?.(); + setTimeout(() => { + batchDraggableDirective.ngAfterViewInit?.(); + }, 0); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const batchDraggableDirective = el[BatchDraggableDirective.INSTANCE_KEY] as BatchDraggableDirective; + batchDraggableDirective.updateInput(binding.value, binding.oldValue!); + }, + beforeUnmount(el: HTMLElement & { [props: string]: any }) { + const batchDraggableDirective = el[BatchDraggableDirective.INSTANCE_KEY] as BatchDraggableDirective; + batchDraggableDirective.ngOnDestroy?.(); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/directive-base.ts b/packages/devui-vue/devui/dragdrop-new/src/directive-base.ts new file mode 100644 index 0000000000..70a1b078eb --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/directive-base.ts @@ -0,0 +1,158 @@ +import { Subscription } from 'rxjs'; +import { EventEmitter } from './preserve-next-event-emitter'; + +export interface ISimpleChange { + previousValue: any; + currentValue: any; + firstChange: boolean; +} +export class NgSimpleChange { + constructor(public previousValue: any, public currentValue: any, public firstChange: boolean) {} + isFirstChange(): boolean { + return this.firstChange; + } +} +export type NgSimpleChanges = Record; +export class NgDirectiveBase< + IInput extends { [prop: string]: any } = { [prop: string]: any }, + IOutput = { [prop: string]: (e: any) => void } +> { + private __eventListenerMap = new Map(); + mounted() { + if (this.hostBindingMap && this.el.nativeElement) { + Object.keys(this.hostBindingMap).forEach((key) => { + if ((this as any)[key] !== undefined) { + this.hostBinding(this.hostBindingMap![key], key); + } + }); + } + if (this.hostListenerMap && this.el.nativeElement) { + Object.keys(this.hostListenerMap).forEach((key) => { + if ((this as any)[key]) { + this.hostListener(this.hostListenerMap![key], key); + } + }); + } + } + el: { nativeElement: any } = { nativeElement: null }; + setInput(props: IInput & IOutput & { [props: string]: any }) { + if (!props) { + return; + } + const changes: Map = new Map(); + Object.keys(props).forEach((key) => { + if (key.startsWith('@')) { + const outputKey = this.getOutputKey(key.slice(1)); + this.eventListener(outputKey, props[key]); + } else { + const inputKey = this.getInputKey(key); + const previousValue = (this as any)[inputKey]; + (this as any)[inputKey] = props[key]; + changes.set(inputKey, { + previousValue, + currentValue: props[key], + firstChange: true, + }); + } + }); + this.notifyOnChanges(changes); + if (this.hostBindingMap && this.el.nativeElement) { + Object.keys(this.hostBindingMap).forEach((key) => { + if (props[key]) { + this.hostBinding(this.hostBindingMap![key], key); + } + }); + } + } + updateInput(props: IInput & IOutput, old: IInput & IOutput) { + const changes: Map = new Map(); + props && + Object.keys(props).forEach((key) => { + const inputKey = this.getInputKey(key); + if (props[key] !== old?.[key]) { + changes.set(inputKey, { + previousValue: old[key], + currentValue: props[key], + firstChange: old[key] === undefined, + }); + } + }); + old && + Object.keys(old) + .filter((key) => !Object.keys(props).includes(key)) + .forEach((key) => { + if (old[key] !== props?.[key]) { + const inputKey = this.getInputKey(key); + changes.set(inputKey, { + previousValue: old[key], + currentValue: props[key], + firstChange: old[key] === undefined, + }); + } + }); + changes.forEach((value, key) => { + if (key.startsWith('@')) { + this.eventListener(key.slice(1), value['currentValue']); + } else { + (this as any)[key] = value['currentValue']; + } + }); + this.notifyOnChanges(changes); + if (this.hostBindingMap && this.el.nativeElement) { + Object.keys(this.hostBindingMap).forEach((key) => { + if (changes.get(key)) { + this.hostBinding(this.hostBindingMap![key], key); + } + }); + } + } + hostBinding(key: string, valueKey: string) { + const element = this.el.nativeElement as HTMLElement; + const value = (this as any)[valueKey]; + element.setAttribute(key, value); + } + + hostListener(key: string, functionKey: string) { + const element = this.el.nativeElement as HTMLElement; + element.addEventListener(key, (this as any)[functionKey].bind(this)); + } + eventListener(key: string, userFunction: (e: any) => void) { + const subscription = ((this as any)[key] as EventEmitter).subscribe((e: any) => { + userFunction(e); + }); + if (this.__eventListenerMap.get(key)) { + this.__eventListenerMap.get(key)?.unsubscribe(); + this.__eventListenerMap.delete(key); + } + this.__eventListenerMap.set(key, subscription); + } + + ngOnChanges?(changes: NgSimpleChanges): void; + hostBindingMap?: { [key: string]: string } = undefined; + hostListenerMap?: { [key: string]: string } = undefined; + inputNameMap?: { [key: string]: string } = undefined; + outputNameMap?: { [key: string]: string } = undefined; + + getInputKey(key: string) { + return (this.inputNameMap && this.inputNameMap[key]) || key; + } + + getOutputKey(key: string) { + return (this.outputNameMap && this.outputNameMap[key]) || key; + } + + notifyOnChanges(changes: Map) { + if (this.ngOnChanges) { + const simpleChanges = [...changes.entries()] + .filter(([key, value]) => !key.startsWith('@')) + .reduce((obj: NgSimpleChanges, [key, value]) => { + const { previousValue, currentValue, firstChange } = value; + obj[key] = new NgSimpleChange(previousValue, currentValue, firstChange); + return obj; + }, {}); + if (Object.keys(simpleChanges).length) { + this.ngOnChanges(simpleChanges); + } + } + } +} diff --git a/packages/devui-vue/devui/dragdrop-new/src/drag-drop.service.ts b/packages/devui-vue/devui/dragdrop-new/src/drag-drop.service.ts new file mode 100644 index 0000000000..57e55b41d3 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/drag-drop.service.ts @@ -0,0 +1,316 @@ +import { Subject, Subscription } from 'rxjs'; +import { DraggableDirective } from './draggable.directive'; +import { Utils } from './utils'; +import { InjectionKey, provide } from 'vue'; +import { DragPreviewDirective } from './drag-preview.directive'; +import { DragDropTouch } from './touch-support/dragdrop-touch'; + +export class DragDropService { + static TOKEN: InjectionKey = Symbol('DRAG_DROP_SERVICE_TOKEN'); + dragData: any; + draggedEl: any; + draggedElIdentity: any; + batchDragData?: Array<{ + identity?: any; + draggable: DraggableDirective; + dragData: any; + }>; + batchDragGroup?: string; + batchDragStyle?: Array; + batchDragging?: boolean; + scope?: string | Array; + dropTargets: Array<{ nativeElement: any }> = []; + dropEvent: Subject = new Subject(); + dragEndEvent = new Subject(); + dragStartEvent = new Subject(); + dropOnItem?: boolean; + dragFollow?: boolean; + dragFollowOptions?: { + appendToBody?: boolean; + }; + dropOnOrigin?: boolean; + draggedElFollowingMouse?: boolean; + dragOffset?: { + top: number; + left: number; + offsetLeft: number | null; + offsetTop: number | null; + width?: number; + height?: number; + }; + subscription: Subscription = new Subscription(); + + private _dragEmptyImage?: HTMLImageElement; + get dragEmptyImage() { + if (!this._dragEmptyImage) { + this._dragEmptyImage = new Image(); + // safari的img必须要有src + this._dragEmptyImage.src = + 'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=='; + } + return this._dragEmptyImage; + } + + dragCloneNode: any; + dragOriginPlaceholder: any; + dragItemContainer: any; + dragItemParentName = ''; + dragItemChildrenName = ''; + intersectionObserver: any = null; + sub?: Subscription; + dragOriginPlaceholderNextSibling: any; + touchInstance: any; + + /* 协同拖拽需要 */ + dragElShowHideEvent = new Subject(); + dragSyncGroupDirectives?: any; + /* 预览功能 */ + dragPreviewDirective?: DragPreviewDirective; + get document() { + return window.document; + } + constructor() { + this.touchInstance = DragDropTouch.getInstance(); + } + + newSubscription() { + this.subscription.unsubscribe(); + // eslint-disable-next-line no-return-assign + return (this.subscription = new Subscription()); + } + + enableDraggedCloneNodeFollowMouse() { + if (!this.dragCloneNode) { + this.dragItemContainer = this.draggedEl.parentElement; + if (this.dragPreviewDirective && this.dragPreviewDirective.dragPreviewTemplate) { + this.dragPreviewDirective.createPreview(); + this.dragCloneNode = this.dragPreviewDirective.getPreviewElement(); + this.dragItemContainer = this.document.body; + } else { + this.dragCloneNode = this.draggedEl.cloneNode(true); + } + + this.dragCloneNode.style.margin = '0'; + if (this.dragFollowOptions && this.dragFollowOptions.appendToBody) { + this.dragItemContainer = this.document.body; + this.copyStyle(this.draggedEl, this.dragCloneNode); + } + + if (this.dragItemChildrenName !== '') { + const parentElement = this.dragItemParentName === '' ? this.dragCloneNode : this.document.querySelector(this.dragItemParentName); + const dragItemChildren = parentElement.querySelectorAll(this.dragItemChildrenName); + this.interceptChildNode(parentElement, dragItemChildren); + } + // 拷贝canvas的内容 + const originCanvasArr = this.draggedEl.querySelectorAll('canvas'); + const targetCanvasArr = this.dragCloneNode.querySelectorAll('canvas'); + [].forEach.call(targetCanvasArr, (canvas: HTMLCanvasElement, index: number) => { + canvas.getContext('2d')!.drawImage(originCanvasArr[index], 0, 0); + }); + + this.document.addEventListener('dragover', this.followMouse4CloneNode, { capture: true, passive: true }); + + this.dragCloneNode.style.width = this.dragOffset!.width + 'px'; + this.dragCloneNode.style.height = this.dragOffset!.height + 'px'; + + if ( + !( + this.dragPreviewDirective && + this.dragPreviewDirective.dragPreviewTemplate && + this.dragPreviewDirective.dragPreviewOptions && + this.dragPreviewDirective.dragPreviewOptions.skipBatchPreview + ) + ) { + // 批量拖拽样式 + if (this.batchDragging && this.batchDragData && this.batchDragData.length > 1) { + // 创建一个节点容器 + const node = this.document.createElement('div'); + node.appendChild(this.dragCloneNode); + node.classList.add('batch-dragged-node'); + + /* 计数样式定位 */ + if (this.batchDragStyle && this.batchDragStyle.length && this.batchDragStyle.indexOf('badge') > -1) { + const badge = this.document.createElement('div'); + badge.innerText = String(this.batchDragData.length); + badge.classList.add('batch-dragged-node-count'); + node.style.position = 'relative'; + const style = { + position: 'absolute', + right: '5px', + top: '-12px', + height: '24px', + width: '24px', + borderRadius: '12px', + fontSize: '14px', + lineHeight: '24px', + textAlign: 'center', + color: '#fff', + background: ['#5170ff', 'var(--brand-1, #5170ff)'], + }; + Utils.addElStyles(badge, style); + node.appendChild(badge); + } + + /* 层叠感样式定位 */ + if (this.batchDragStyle && this.batchDragStyle.length && this.batchDragStyle.indexOf('stack') > -1) { + let stack = 2; + if (this.batchDragData.length === 2) { + stack = 1; + } + for (let i = 0; i < stack; i++) { + const stackNode = this.dragCloneNode.cloneNode(false); + const stackStyle = { + position: 'absolute', + left: -5 * (i + 1) + 'px', + top: -5 * (i + 1) + 'px', + zIndex: String(-(i + 1)), + width: this.dragOffset!.width + 'px', + height: this.dragOffset!.height + 'px', + background: '#fff', + border: ['1px solid #5170ff', '1px solid var(--brand-1, #5170ff)'], + }; + Utils.addElStyles(stackNode, stackStyle); + node.appendChild(stackNode); + } + } + this.dragCloneNode = node; + } + } + + this.dragCloneNode.classList.add('drag-clone-node'); + if (!(this.dragPreviewDirective && this.dragPreviewDirective.dragPreviewTemplate)) { + this.dragCloneNode.style.width = this.dragOffset!.width + 'px'; + this.dragCloneNode.style.height = this.dragOffset!.height + 'px'; + } + this.dragCloneNode.style.position = 'fixed'; + this.dragCloneNode.style.zIndex = '1090'; + this.dragCloneNode.style.pointerEvents = 'none'; + this.dragCloneNode.style.top = this.dragOffset!.top + 'px'; + this.dragCloneNode.style.left = this.dragOffset!.left + 'px'; + this.dragCloneNode.style.willChange = 'left, top'; + this.dragItemContainer.appendChild(this.dragCloneNode); + + setTimeout(() => { + if (this.draggedEl) { + this.draggedEl.style.display = 'none'; + this.dragElShowHideEvent.next(false); + if (this.dragOriginPlaceholder) { + this.dragOriginPlaceholder.style.display = 'block'; + } + } + }); + } + } + + disableDraggedCloneNodeFollowMouse() { + if (this.dragCloneNode) { + this.document.removeEventListener('dragover', this.followMouse4CloneNode, { capture: true }); + this.dragItemContainer.removeChild(this.dragCloneNode); + this.draggedEl.style.display = ''; + this.dragElShowHideEvent.next(true); + } + if (this.dragPreviewDirective && this.dragPreviewDirective.dragPreviewTemplate) { + this.dragPreviewDirective.destroyPreview(); + } + this.dragCloneNode = undefined; + this.dragItemContainer = undefined; + + if (this.intersectionObserver) { + this.intersectionObserver.disconnect(); + } + } + + interceptChildNode(parentNode: Node, childNodeList: NodeListOf) { + const interceptOptions = { + root: parentNode, + } as any; + this.intersectionObserver = new IntersectionObserver(this.setChildNodeHide, interceptOptions); + [].forEach.call(childNodeList, (childNode) => { + this.intersectionObserver.observe(childNode); + }); + } + + setChildNodeHide(entries: any) { + entries.forEach((element: any) => { + const { isIntersecting, target: childNode } = element; + if (isIntersecting) { + childNode.style.display = 'block'; + } else { + childNode.style.display = 'none'; + } + }); + } + + followMouse4CloneNode = (event: DragEvent) => { + const { offsetLeft, offsetTop } = this.dragOffset!; + const { clientX, clientY } = event; + requestAnimationFrame(() => { + if (!this.dragCloneNode) { + return; + } + this.dragCloneNode.style.left = clientX - offsetLeft! + 'px'; + this.dragCloneNode.style.top = clientY - offsetTop! + 'px'; + }); + }; + + getBatchDragData(identity?: any, order: ((a: any, b: any) => number) | 'select' | 'draggedElFirst' = 'draggedElFirst') { + const result = this.batchDragData!.map((dragData) => dragData.dragData); + if (typeof order === 'function') { + result.sort(<(a: any, b: any) => number>order); + } else if (order === 'draggedElFirst') { + let dragData = this.dragData; + if (identity) { + const realDragData = this.batchDragData!.filter((dd) => dd.identity === identity).pop()!.dragData; + dragData = realDragData; + } + result.splice(result.indexOf(dragData), 1); + result.splice(0, 0, dragData); + } + return result; + } + + /** usage: + * constructor(..., private dragDropService: DragDropService) {} + * cleanBatchDragData() { this.dragDropService.cleanBatchDragData(); } + */ + public cleanBatchDragData() { + const batchDragData = this.batchDragData; + if (this.batchDragData) { + this.batchDragData + .filter((dragData) => dragData.draggable) + .map((dragData) => dragData.draggable) + .forEach((draggable) => { + draggable.batchDraggable.dragData = undefined; + }); + this.batchDragData = undefined; + this.batchDragGroup = undefined; + } + return batchDragData; + } + + public copyStyle(source: HTMLElement, target: HTMLElement) { + ['id', 'class', 'style', 'draggable'].forEach(function (att) { + target.removeAttribute(att); + }); + + // copy style (without transitions) + const computedStyle = getComputedStyle(source); + for (let i = 0; i < computedStyle.length; i++) { + const key = computedStyle[i] as any; + if (key.indexOf('transition') < 0) { + target.style[key] = computedStyle[key]; + } + } + target.style.pointerEvents = 'none'; + // and repeat for all children + for (let i = 0; i < source.children.length; i++) { + this.copyStyle(source.children[i] as HTMLElement, target.children[i] as HTMLElement); + } + } +} + +export function useDragDropService() { + const dragDropService = new DragDropService(); + provide(DragDropService.TOKEN, new DragDropService()); + return dragDropService; +} diff --git a/packages/devui-vue/devui/dragdrop-new/src/drag-preview-clone-dom-ref.component.tsx b/packages/devui-vue/devui/dragdrop-new/src/drag-preview-clone-dom-ref.component.tsx new file mode 100644 index 0000000000..a1ae6d1ac8 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/drag-preview-clone-dom-ref.component.tsx @@ -0,0 +1,111 @@ +import { DragDropService } from './drag-drop.service'; +import { NgDirectiveBase, NgSimpleChanges } from './directive-base'; +import { + PropType, + defineComponent, + getCurrentInstance, + inject, + nextTick, + onBeforeUnmount, + onMounted, + onUnmounted, + onUpdated, + watch, +} from 'vue'; + +export interface IDragPreviewCloneDomRefBinding { + domRef?: HTMLElement; + copyStyle?: boolean; + [props: string]: any; +} + +export class DragPreviewCloneDomRefComponent extends NgDirectiveBase { + static NAME = 'd-drag-preview-clone-dom-ref'; + domRef: HTMLElement; + copyStyle = true; + cloneNode; + constructor(public el: { nativeElement: any }, private dragDropService: DragDropService) { + super(); + } + ngAfterViewInit() { + if (!this.cloneNode) { + this.createView(); + } + } + ngOnChanges(changes: NgSimpleChanges) { + if (changes['domRef']) { + if (this.cloneNode) { + this.destroyView(); + this.createView(); + } else { + this.createView(); + } + } + } + ngOnDestroy() { + if (this.cloneNode) { + this.destroyView(); + } + } + + createView() { + if (this.domRef) { + this.cloneNode = this.domRef.cloneNode(true); + if (this.copyStyle) { + this.dragDropService.copyStyle(this.domRef, this.cloneNode); + } + setTimeout(() => { + this.el.nativeElement.appendChild(this.cloneNode); + }, 0); + } + } + destroyView() { + if (this.cloneNode) { + if (this.el.nativeElement.contains(this.cloneNode)) { + this.el.nativeElement.removeChild(this.cloneNode); + } + this.cloneNode = undefined; + } + } + + public updateTemplate() { + // do nothing 保持api一致 + } +} +export default defineComponent({ + name: 'DDragPreviewCloneDomRef', + props: { + domRef: Object as PropType, + copyStyle: { + type: Boolean, + default: true, + }, + }, + setup(props, { expose }) { + const el: { nativeElement: any } = { nativeElement: null }; + const dragDropService = inject(DragDropService.TOKEN); + const instance = new DragPreviewCloneDomRefComponent(el, dragDropService!); + + instance.setInput(props as any); + onMounted(() => { + instance.mounted(); + nextTick(() => { + instance.ngAfterViewInit?.(); + }); + }); + watch( + () => props, + (binding, oldBinding) => { + instance.updateInput(binding as any, oldBinding as any); + } + ); + onBeforeUnmount(() => { + instance.ngOnDestroy?.(); + }); + + expose({ + instance, + }); + return () =>
(el.nativeElement = e)}>
; + }, +}); diff --git a/packages/devui-vue/devui/dragdrop-new/src/drag-preview.component.tsx b/packages/devui-vue/devui/dragdrop-new/src/drag-preview.component.tsx new file mode 100644 index 0000000000..a0ea370799 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/drag-preview.component.tsx @@ -0,0 +1,35 @@ +import { PropType, VNodeChild, defineComponent } from 'vue'; + +export interface IDragPreviewContext { + data: any; // dragPreviewData + draggedEl: HTMLElement; + dragData: any; + batchDragData?: any[]; + dragSyncDOMElements?: HTMLElement[]; +} + +export interface IDragPreviewTemplate { + template: (context: IDragPreviewContext) => VNodeChild; +} +export const DragPreviewTemplate = defineComponent({ + name: 'DDragPreviewTemplate', + setup(props, { slots, expose }) { + expose({ + template: slots.default, + } as IDragPreviewTemplate); + + return () => null; + }, +}); + +export const DragPreviewComponent = defineComponent({ + name: 'DDragPreviewContainer', + props: { + template: Function as PropType<(context: IDragPreviewContext) => VNodeChild>, + context: Object as PropType, + }, + setup(props) { + return () => props.template?.(props.context!); + }, +}); +export default DragPreviewComponent; diff --git a/packages/devui-vue/devui/dragdrop-new/src/drag-preview.directive.ts b/packages/devui-vue/devui/dragdrop-new/src/drag-preview.directive.ts new file mode 100644 index 0000000000..9ff21c3436 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/drag-preview.directive.ts @@ -0,0 +1,88 @@ +import { InjectionKey, createApp, DirectiveBinding, getCurrentInstance, createBaseVNode, render } from 'vue'; +import { NgDirectiveBase } from './directive-base'; +import { DragDropService } from './drag-drop.service'; +import DragPreview, { IDragPreviewTemplate } from './drag-preview.component'; +import { injectFromContext, provideToContext } from './utils'; + +export interface IDragPreviewBinding { + dragPreview?: IDragPreviewTemplate; + dragPreviewData?: any; + dragPreviewOptions?: { + skipBatchPreview: boolean; + }; + [props: string]: any; + context; +} +export class DragPreviewDirective extends NgDirectiveBase { + static INSTANCE_KEY = '__vueDevuiDragPreviewDirectiveInstance'; + static TOKEN: InjectionKey = Symbol('DRAG_PREVIEW_DIRECTIVE_TOKEN'); + inputNameMap?: { [key: string]: string } | undefined = { + dragPreview: 'dragPreviewTemplate', + }; + dragPreviewTemplate: IDragPreviewTemplate; + dragPreviewData; + dragPreviewOptions = { + skipBatchPreview: false, + }; + public previewRef; + public context; + el: { nativeElement: any } = { nativeElement: null }; + constructor(el: HTMLElement, private dragDropService: DragDropService) { + super(); + this.el.nativeElement = el; + } + + public createPreview() { + const context = { + data: this.dragPreviewData, + draggedEl: this.dragDropService.draggedEl, + dragData: this.dragDropService.dragData, + batchDragData: this.dragDropService.batchDragData && this.dragDropService.getBatchDragData(), + dragSyncDOMElements: this.dragDropService.dragSyncGroupDirectives && this.getDragSyncDOMElements(), + }; + const app = createApp(DragPreview, { context, template: this.dragPreviewTemplate?.template }); + // 这里用hack的手法来讲当前的上下文川给新的模板组件 + app._context.provides = Object.create(this.context); + const element = document.createElement('div'); + const instance = app.mount(element); + const unmount = () => { + app.unmount(); + }; + + this.previewRef = { + instance, + element, + unmount, + }; + } + + public destroyPreview() { + if (this.previewRef) { + this.previewRef.unmount(); + this.previewRef = undefined; + } + } + + public getPreviewElement() { + return this.previewRef && this.previewRef.element; + } + private getDragSyncDOMElements() { + return this.dragDropService.dragSyncGroupDirectives.map((dir) => dir.el.nativeElement); + } +} + +export default { + mounted(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding, vNode) { + const context = vNode['ctx'].provides; + const dragDropService = injectFromContext(DragDropService.TOKEN, context) as DragDropService; + const dragPreviewDirective = (el[DragPreviewDirective.INSTANCE_KEY] = new DragPreviewDirective(el, dragDropService)); + provideToContext(DragPreviewDirective.TOKEN, dragPreviewDirective, context); + dragPreviewDirective.setInput({ context }); + dragPreviewDirective.setInput(binding.value); + dragPreviewDirective.mounted(); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const dragPreviewDirective = el[DragPreviewDirective.INSTANCE_KEY] as DragPreviewDirective; + dragPreviewDirective.updateInput(binding.value, binding.oldValue!); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/draggable.directive.ts b/packages/devui-vue/devui/dragdrop-new/src/draggable.directive.ts new file mode 100644 index 0000000000..cdf662a55e --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/draggable.directive.ts @@ -0,0 +1,532 @@ +import { Subject, Subscription, fromEvent } from 'rxjs'; +import { EventEmitter, PreserveNextEventEmitter } from './preserve-next-event-emitter'; +import { DragDropService } from './drag-drop.service'; +import { Utils, injectFromContext, provideToContext } from './utils'; +import { DirectiveBinding, InjectionKey, VNode } from 'vue'; +import { NgDirectiveBase } from './directive-base'; +import { DragPreviewDirective } from './drag-preview.directive'; + +export interface IDraggableBinding { + dragData?: any; + dragHandle?: string; + dragEffect?: string; + dragScope?: string | Array; + dragHandleClass?: string; + dragOverClass?: string; + disabled?: boolean; + enableDragFollow?: boolean; + dragFollowOptions?: { + appendToBody?: boolean; + }; + originPlaceholder?: { + show?: boolean; + tag?: string; + style?: { [cssProperties: string]: string }; + text?: string; + removeDelay?: number; // 单位: ms + }; + dragIdentity?: any; + dragItemParentName?: string; + dragItemChildrenName?: string; +} +export interface IDraggableListener { + '@dragStartEvent'?: (_: any) => void; + '@dragEvent'?: (_: any) => void; + '@dragEndEvent'?: (_: any) => void; + '@dropEndEvent'?: (_: any) => void; +} + +export class DraggableDirective extends NgDirectiveBase { + static INSTANCE_KEY = '__vueDevuiDraggableDirectiveInstance'; + static TOKEN: InjectionKey = Symbol('DRAGGABLE_DIRECTIVE_TOKEN'); + hostBindingMap?: { [key: string]: string } | undefined = { + draggable: 'draggable', + 'data-drag-handle-selector': 'dragHandle', + }; + draggable: boolean = true; + dragData?: any; + dragScope: string | Array = 'default'; + dragHandle?: string; + dragHandleClass = 'drag-handle'; + dragOverClass?: string; + dragEffect = 'move'; + public get disabled(): boolean { + return this._disabled; + } + + public set disabled(value: boolean) { + this._disabled = value; + this.draggable = !this._disabled; + } + private _disabled: boolean = false; + + dragStartEvent: EventEmitter = new EventEmitter(); + dragEvent: PreserveNextEventEmitter = new PreserveNextEventEmitter(); + dragEndEvent: EventEmitter = new EventEmitter(); + dropEndEvent: PreserveNextEventEmitter = new PreserveNextEventEmitter(); + document = window.document; + private mouseOverElement: any; + + enableDragFollow = false; // 默认false使用浏览器H5API拖拽, 否则使用原dom定位偏移 + dragFollowOptions?: { + appendToBody?: boolean; + }; + originPlaceholder?: { + show?: boolean; + tag?: string; + style?: { [cssProperties: string]: string }; + text?: string; + removeDelay?: number; // 单位: ms + }; + dragIdentity: any; // 用于虚拟滚动的恢复 + + dragItemParentName = ''; // 当前拖拽元素的类名或元素名称(类名需要加.),主要用于子节点的截取操作 + dragItemChildrenName = ''; // 当前拖拽元素的子节点类名或元素名称(类名需要加.) + + dragsSub: Subscription = new Subscription(); + destroyDragEndSub?: Subscription = new Subscription(); + isDestroyed?: boolean; + private delayRemoveOriginPlaceholderTimer?: number; + public batchDraggable: undefined | any; + private dragOriginPlaceholder?: HTMLElement; + private dragOriginPlaceholderNextSibling?: Element; + public dragElShowHideEvent = new Subject(); + public beforeDragStartEvent = new Subject(); + + el: { nativeElement: any } = { nativeElement: null }; + dragDropService: DragDropService; + dragPreviewDirective?: DragPreviewDirective; + + constructor(el: HTMLElement, dragDropService: DragDropService, dragPreviewDirective?: DragPreviewDirective) { + super(); + this.el.nativeElement = el; + this.dragDropService = dragDropService; + this.dragPreviewDirective = dragPreviewDirective; + } + + ngOnInit() { + this.dragsSub.add(fromEvent(this.el.nativeElement, 'mouseover').subscribe((event) => this.mouseover(event))); + this.dragsSub.add(fromEvent(this.el.nativeElement, 'dragstart').subscribe((event) => this.dragStart(event))); + this.dragsSub.add(fromEvent(this.el.nativeElement, 'dragend').subscribe((event) => this.dragEnd(event))); + } + + dropSubscription() { + const dragDropSub = this.dragDropService.newSubscription(); + dragDropSub.add( + this.dragDropService.dropEvent.subscribe((event) => { + this.mouseOverElement = undefined; + Utils.removeClass(this.el.nativeElement, this.dragOverClass!); + this.dropEndEvent.emit(event); + // 兼容虚拟滚动后被销毁 + if (this.isDestroyed) { + if (this.dropEndEvent.schedulerFns && this.dropEndEvent.schedulerFns.size > 0) { + this.dropEndEvent.forceCallback(event, true); + } + } + if (this.dragDropService.dragOriginPlaceholder) { + if (this.originPlaceholder && this.originPlaceholder.removeDelay! > 0 && !this.dragDropService.dropOnOrigin) { + // 非drop到自己的情况 + this.delayRemoveOriginPlaceholder(); + } else { + this.removeOriginPlaceholder(); + } + this.dragDropService.draggedElIdentity = undefined; + } + this.dragDropService.subscription.unsubscribe(); + }) + ); + dragDropSub.add(this.dragDropService.dragElShowHideEvent.subscribe(this.dragElShowHideEvent)); + } + + ngAfterViewInit() { + this.applyDragHandleClass(); + if (this.dragIdentity) { + if (this.dragDropService.draggedEl && this.dragIdentity === this.dragDropService.draggedElIdentity) { + if (this.originPlaceholder && this.originPlaceholder.show !== false) { + this.insertOriginPlaceholder(); + } + this.dragDropService.draggedEl = this.el.nativeElement; + this.el.nativeElement.style.display = 'none'; // recovery don't need to emit event + } + } + } + + ngOnDestroy() { + // 兼容虚拟滚动后被销毁 + this.isDestroyed = true; + if (this.dragDropService.draggedEl === this.el.nativeElement) { + this.destroyDragEndSub = new Subscription(); + this.destroyDragEndSub.add( + fromEvent(this.el.nativeElement, 'dragend').subscribe((event) => { + this.dragEnd(event); + if (this.dropEndEvent.schedulerFns && this.dropEndEvent.schedulerFns.size > 0) { + this.dropEndEvent.forceCallback(event, true); + } + this.destroyDragEndSub!.unsubscribe(); + this.destroyDragEndSub = undefined; + }) + ); + if ( + this.originPlaceholder && + this.originPlaceholder.show !== false && + this.dragDropService.dragOriginPlaceholder && + this.dragDropService.draggedElIdentity + ) { + // 如果有originPlaceholder 则销毁 + this.removeOriginPlaceholder(); + } + } + this.dragsSub.unsubscribe(); + } + + dragStart(e: DragEvent) { + if (this.allowDrag(e)) { + Utils.addClass(this.el.nativeElement, this.dragOverClass!); + this.dragDropService.dragData = this.dragData; + this.dragDropService.scope = this.dragScope; + this.dragDropService.draggedEl = this.el.nativeElement; + this.dragDropService.draggedElIdentity = this.dragIdentity; + this.dragDropService.dragFollow = this.enableDragFollow; + this.dragDropService.dragFollowOptions = this.dragFollowOptions; + this.dragDropService.dragItemParentName = this.dragItemParentName; + this.dragDropService.dragItemChildrenName = this.dragItemChildrenName; + this.beforeDragStartEvent.next(true); + if (this.dragPreviewDirective && this.dragPreviewDirective?.dragPreviewTemplate) { + this.dragDropService.dragFollow = true; + this.dragDropService.dragPreviewDirective = this.dragPreviewDirective; + } + if (this.batchDraggable) { + if (this.batchDraggable.dragData) { + // 有dragData证明被加入到了group里 + if (this.dragDropService.batchDragData && this.dragDropService.batchDragData.length > 1) { + this.dragDropService.batchDragging = true; + this.dragDropService.batchDragStyle = this.batchDraggable.batchDragStyle; + } + } else if (this.batchDraggable.batchDragLastOneAutoActiveEventKeys) { + const batchActiveAble = this.batchDraggable.batchDragLastOneAutoActiveEventKeys + .map((key: any) => (e as any)[key]) + .some((eventKey: any) => eventKey === true); + if (batchActiveAble) { + if (this.dragDropService.batchDragData && this.dragDropService.batchDragData.length > 0) { + this.batchDraggable.active(); + if (!this.batchDraggable.dragData) { + // 如果用户没做任何处理把项目加到组里则加到组里 + this.batchDraggable.addToBatchGroup(); + } + if (this.dragDropService.batchDragData.some((dragData) => dragData.draggable === this)) { + this.dragDropService.batchDragging = true; + this.dragDropService.batchDragStyle = this.batchDraggable.batchDragStyle; + } + } + } + } + } + const targetOffset = this.el.nativeElement.getBoundingClientRect(); + if (this.dragDropService.dragFollow) { + const mousePositionXY = this.mousePosition(e); + // 用于出现transform的场景position:fixed相对位置变更 + const transformOffset = this.checkAndGetViewPointChange(this.el.nativeElement); + this.dragDropService.dragOffset = { + left: targetOffset.left, + top: targetOffset.top, + offsetLeft: mousePositionXY.x - targetOffset.left + transformOffset!.offsetX, + offsetTop: mousePositionXY.y - targetOffset.top + transformOffset!.offsetY, + width: targetOffset.width, + height: targetOffset.height, + }; + this.dragDropService.enableDraggedCloneNodeFollowMouse(); + } else { + this.dragDropService.dragOffset = { + left: targetOffset.left, + top: targetOffset.top, + offsetLeft: null, + offsetTop: null, + width: targetOffset.width, + height: targetOffset.height, + }; + } + if (this.originPlaceholder && this.originPlaceholder.show !== false) { + this.insertOriginPlaceholder(false); + } + if (this.dragDropService.batchDragging && this.dragDropService.batchDragData && this.dragDropService.batchDragData.length > 1) { + this.dragDropService.batchDragData + .map((dragData) => dragData.draggable) + .filter((draggable) => draggable && draggable !== this) + .forEach((draggable) => { + if (draggable.originPlaceholder && draggable.originPlaceholder.show !== false) { + draggable.insertOriginPlaceholder(true, false); + draggable.el.nativeElement.style.display = 'none'; + } else { + setTimeout(() => { + draggable.el.nativeElement.style.display = 'none'; + }); + } + }); + } + // Firefox requires setData() to be called otherwise the drag does not work. + if (e.dataTransfer !== null) { + e.dataTransfer.setData('text', ''); + } + e.dataTransfer!.effectAllowed = this.dragEffect as unknown as DataTransfer['effectAllowed']; + this.dropSubscription(); + if (this.dragDropService.dragFollow) { + if (typeof DataTransfer.prototype.setDragImage === 'function') { + e.dataTransfer!.setDragImage(this.dragDropService.dragEmptyImage, 0, 0); + } else { + // 兼容老浏览器 + (e.srcElement! as HTMLElement).style.display = 'none'; + this.dragDropService.dragElShowHideEvent.next(false); + } + } + e.stopPropagation(); + this.dragStartEvent.emit(e); + this.dragDropService.dragStartEvent.next(e); + } else { + e.preventDefault(); + } + } + + dragEnd(e: DragEvent) { + Utils.removeClass(this.el.nativeElement, this.dragOverClass!); + this.dragDropService.dragEndEvent.next(e); + this.mouseOverElement = undefined; + if (this.dragDropService.draggedEl) { + // 当dom被清除的的时候不会触发dragend,所以清理工作部分交给了drop,但是内部排序的时候dom不会被清理,dragend防止和drop重复操作清理动作 + if (this.dragDropService.dragFollow) { + this.dragDropService.disableDraggedCloneNodeFollowMouse(); + } + if (this.dragDropService.dragOriginPlaceholder) { + this.removeOriginPlaceholder(); + } + if (this.dragDropService.batchDragging && this.dragDropService.batchDragData && this.dragDropService.batchDragData.length > 1) { + this.dragDropService.batchDragData + .map((dragData) => dragData.draggable) + .filter((draggable) => draggable && draggable !== this) + .forEach((draggable) => { + if (draggable.originPlaceholder && draggable.originPlaceholder.show !== false) { + draggable.el.nativeElement.style.display = ''; + draggable.removeOriginPlaceholder(); + } else { + draggable.el.nativeElement.style.display = ''; + } + }); + } + if (this.batchDraggable && !this.batchDraggable.batchDragActive) { + this.batchDraggable.removeFromBatchGroup(); + this.dragDropService.batchDragging = false; + this.dragDropService.batchDragStyle = undefined; + } + if (this.dragDropService.subscription) { + this.dragDropService.subscription.unsubscribe(); + } + this.dragDropService.dragData = undefined; + this.dragDropService.scope = undefined; + this.dragDropService.draggedEl = undefined; + this.dragDropService.dragFollow = undefined; + this.dragDropService.dragFollowOptions = undefined; + this.dragDropService.dragOffset = undefined; + this.dragDropService.draggedElIdentity = undefined; + this.dragDropService.dragPreviewDirective = undefined; + } + e.stopPropagation(); + e.preventDefault(); + this.dragEndEvent.emit(e); + } + + mouseover(e: MouseEvent) { + this.mouseOverElement = e.target; + } + + private allowDrag(e: DragEvent & { fromTouch?: boolean }) { + if (!this.draggable) { + return false; + } + if (this.batchDraggable && !this.batchDraggable.allowAddToBatchGroup()) { + // 批量拖拽判断group是否相同 + return false; + } + if (this.dragHandle) { + if (e && e.fromTouch) { + return true; + } // from touchstart dispatch event + if (!this.mouseOverElement) { + return false; + } + return Utils.matches(this.mouseOverElement, this.dragHandle); + } else { + return true; + } + } + + private applyDragHandleClass() { + const dragElement = this.getDragHandleElement(); + if (!dragElement) { + return; + } + if (this.draggable) { + Utils.addClass(dragElement, this.dragHandleClass); + } else { + Utils.removeClass(this.el, this.dragHandleClass); + } + } + + private getDragHandleElement() { + let dragElement = this.el; + if (this.dragHandle) { + dragElement = this.el.nativeElement.querySelector(this.dragHandle); + } + return dragElement; + } + + private mousePosition(event: MouseEvent) { + return { + x: event.clientX, + y: event.clientY, + }; + } + public insertOriginPlaceholder = (directShow = true, updateService = true) => { + if (this.delayRemoveOriginPlaceholderTimer) { + clearTimeout(this.delayRemoveOriginPlaceholderTimer); + this.delayRemoveOriginPlaceholderTimer = undefined; + } + + const node = this.document.createElement(this.originPlaceholder?.tag || 'div'); + const rect = this.el.nativeElement.getBoundingClientRect(); + if (directShow) { + node.style.display = 'block'; + } else { + node.style.display = 'none'; + } + + node.style.width = rect.width + 'px'; + node.style.height = rect.height + 'px'; + node.classList.add('drag-origin-placeholder'); + if (this.originPlaceholder?.text) { + node.innerText = this.originPlaceholder.text; + } + if (this.originPlaceholder?.style) { + Utils.addElStyles(node, this.originPlaceholder.style); + } + if (updateService) { + this.dragDropService.dragOriginPlaceholder = node; + this.dragDropService.dragOriginPlaceholderNextSibling = this.el.nativeElement.nextSibling; + } else { + node.classList.add('side-drag-origin-placeholder'); + const originCloneNode = this.el.nativeElement.cloneNode(true); + originCloneNode.style.margin = 0; + originCloneNode.style.pointerEvents = 'none'; + originCloneNode.style.opacity = '0.3'; + node.appendChild(originCloneNode); + } + this.dragOriginPlaceholder = node; + this.dragOriginPlaceholderNextSibling = this.el.nativeElement.nextSibling; + this.el.nativeElement.parentElement.insertBefore(node, this.el.nativeElement.nextSibling); + }; + + public removeOriginPlaceholder = (updateService = true) => { + if (this.dragOriginPlaceholder) { + this.dragOriginPlaceholder.parentElement?.removeChild(this.dragOriginPlaceholder); + } + if (updateService) { + this.dragDropService.dragOriginPlaceholder = undefined; + this.dragDropService.dragOriginPlaceholderNextSibling = undefined; + } + this.dragOriginPlaceholder = undefined; + this.dragOriginPlaceholderNextSibling = undefined; + }; + public delayRemoveOriginPlaceholder = (updateService = true) => { + const timeout = this.originPlaceholder?.removeDelay; + const delayOriginPlaceholder = this.dragOriginPlaceholder; + const dragOriginPlaceholderNextSibling = this.findNextSibling(this.dragOriginPlaceholderNextSibling!); + + // 需要临时移动位置,保证被ngFor刷新之后位置是正确的 + // ngFor刷新的原理是有变化的部分都刷新,夹在变化部分中间的内容将被刷到变化部分之后的位置,所以需要恢复位置 + // setTimeout是等ngFor的View刷新, 后续需要订阅sortContainer的view的更新才需要重新恢复位置 + if (delayOriginPlaceholder?.parentElement?.contains(dragOriginPlaceholderNextSibling)) { + delayOriginPlaceholder.parentElement.insertBefore(delayOriginPlaceholder, dragOriginPlaceholderNextSibling); + } + setTimeout(() => { + if (delayOriginPlaceholder?.parentElement?.contains(dragOriginPlaceholderNextSibling)) { + delayOriginPlaceholder.parentElement.insertBefore(delayOriginPlaceholder, dragOriginPlaceholderNextSibling); + } + delayOriginPlaceholder?.classList.add('delay-deletion'); + this.delayRemoveOriginPlaceholderTimer = setTimeout(() => { + delayOriginPlaceholder?.parentElement?.removeChild(delayOriginPlaceholder); + if (this.document.body.contains(this.el.nativeElement)) { + this.el.nativeElement.style.display = ''; + this.dragDropService.dragElShowHideEvent.next(false); + } + }, timeout) as unknown as number; + if (updateService) { + this.dragDropService.dragOriginPlaceholder = undefined; + this.dragDropService.dragOriginPlaceholderNextSibling = undefined; + } + this.dragOriginPlaceholder = undefined; + this.dragOriginPlaceholderNextSibling = undefined; + }); + }; + findNextSibling(currentNextSibling: Element) { + if (!this.dragDropService.batchDragData) { + return currentNextSibling; + } else { + if ( + this.dragDropService.batchDragData + .map((dragData) => dragData.draggable && dragData.draggable.el.nativeElement) + .indexOf(currentNextSibling) > -1 + ) { + currentNextSibling = currentNextSibling.nextSibling as Element; + } + return currentNextSibling; + } + } + + private checkAndGetViewPointChange(element: HTMLElement) { + if (!element.parentNode) { + return null; + } + // 模拟一个元素测预测位置和最终位置是否符合,如果不符合则是有transform等造成的偏移 + const elementPosition = element.getBoundingClientRect(); + const testEl = this.document.createElement('div'); + Utils.addElStyles(testEl, { + opacity: '0', + position: 'fixed', + top: elementPosition.top + 'px', + left: elementPosition.left + 'px', + width: '1px', + height: '1px', + zIndex: '-999999', + }); + element.parentNode.appendChild(testEl); + const testElPosition = testEl.getBoundingClientRect(); + element.parentNode.removeChild(testEl); + return { + offsetX: testElPosition.left - elementPosition.left, + offsetY: testElPosition.top - elementPosition.top, + }; + } +} + +export default { + mounted(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding, vNode: VNode) { + const context = vNode.ctx?.provides; + const dragDropService = injectFromContext(DragDropService.TOKEN, context) as DragDropService; + let dragPreviewDirective = injectFromContext(DragPreviewDirective.TOKEN, context) as DragPreviewDirective | undefined; + if (dragPreviewDirective?.el.nativeElement !== el) { + dragPreviewDirective = undefined; + } + const draggableDirective = (el[DraggableDirective.INSTANCE_KEY] = new DraggableDirective(el, dragDropService, dragPreviewDirective)); + provideToContext(DraggableDirective.TOKEN, draggableDirective, context); + draggableDirective.setInput(binding.value); + draggableDirective.mounted(); + draggableDirective.ngOnInit?.(); + draggableDirective.ngAfterViewInit?.(); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const draggableDirective = el[DraggableDirective.INSTANCE_KEY] as DraggableDirective; + draggableDirective.updateInput(binding.value, binding.oldValue!); + }, + beforeUnmount(el: HTMLElement & { [props: string]: any }) { + const draggableDirective = el[DraggableDirective.INSTANCE_KEY] as DraggableDirective; + draggableDirective.ngOnDestroy?.(); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/drop-scroll-enhance.directive.ts b/packages/devui-vue/devui/dragdrop-new/src/drop-scroll-enhance.directive.ts new file mode 100644 index 0000000000..f02351e602 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/drop-scroll-enhance.directive.ts @@ -0,0 +1,469 @@ +import { Subscription, fromEvent, merge, tap, throttleTime } from 'rxjs'; +import { DragDropService } from './drag-drop.service'; +import { Utils, injectFromContext } from './utils'; +import { DirectiveBinding } from 'vue'; +import { NgDirectiveBase } from './directive-base'; + +// types +export type DropScrollEdgeDistancePercent = number; // 单位 px / px +export type DropScrollSpeed = number; // 单位 px/ s +export type DropScrollSpeedFunction = (x: DropScrollEdgeDistancePercent) => DropScrollSpeed; +export type DropScrollDirection = 'h' | 'v'; // 'both' 暂不支持双向滚动 +export enum DropScrollOrientation { + forward, // 进, 右/下 + backward, // 退, 左/上 +} +export interface DropScrollAreaOffset { + left?: number; + right?: number; + top?: number; + bottom?: number; + widthOffset?: number; + heightOffset?: number; +} +export type DropScrollTriggerEdge = 'left' | 'right' | 'top' | 'bottom'; + +export const DropScrollEnhanceTimingFunctionGroup = { + default: (x: number) => Math.ceil((1 - x) * 18) * 100, +}; + +export interface IDropScrollEnhancedBinding { + minSpeed?: DropScrollSpeed; + maxSpeed?: DropScrollSpeed; + responseEdgeWidth?: string | ((total: number) => string); + speedFn?: DropScrollSpeedFunction; + direction?: DropScrollDirection; + viewOffset?: { + forward?: DropScrollAreaOffset; // 仅重要边和次要边有效 + backward?: DropScrollAreaOffset; + }; + dropScrollScope?: string | Array; + backSpaceDroppable?: boolean; +} +export interface IDropScrollEnhancedListener {} + +export class DropScrollEnhancedDirective extends NgDirectiveBase { + static INSTANCE_KEY = '__vueDevuiDropScrollEnhancedDirectiveInstance'; + + minSpeed: DropScrollSpeed = 50; + maxSpeed: DropScrollSpeed = 1000; + responseEdgeWidth: string | ((total: number) => string) = '100px'; + speedFn: DropScrollSpeedFunction = DropScrollEnhanceTimingFunctionGroup.default; + direction: DropScrollDirection = 'v'; + viewOffset?: { + forward?: DropScrollAreaOffset; // 仅重要边和次要边有效 + backward?: DropScrollAreaOffset; + }; + dropScrollScope?: string | Array; + backSpaceDroppable = true; + + private forwardScrollArea?: HTMLElement; + private backwardScrollArea?: HTMLElement; + private subscription: Subscription = new Subscription(); + private forwardScrollFn?: (event: DragEvent) => void; + private backwardScrollFn?: (event: DragEvent) => void; + private lastScrollTime?: number; + private animationFrameId?: number; + document: Document; + el: { nativeElement: any } = { nativeElement: null }; + private dragDropService: DragDropService; + + constructor(el: HTMLElement, dragDropService: DragDropService) { + super(); + this.el.nativeElement = el; + this.dragDropService = dragDropService; + this.document = window.document; + } + + ngAfterViewInit() { + // 设置父元素 + this.el.nativeElement.parentNode.style.position = 'relative'; + this.el.nativeElement.parentNode.style.display = 'block'; + // 创建后退前进区域和对应的滚动函数 + this.forwardScrollArea = this.createScrollArea(this.direction, DropScrollOrientation.forward); + this.backwardScrollArea = this.createScrollArea(this.direction, DropScrollOrientation.backward); + this.forwardScrollFn = this.createScrollFn(this.direction, DropScrollOrientation.forward, this.speedFn); + this.backwardScrollFn = this.createScrollFn(this.direction, DropScrollOrientation.backward, this.speedFn); + + // 拖拽到其上触发滚动 + this.subscription.add( + fromEvent(this.forwardScrollArea!, 'dragover') + .pipe( + tap((event) => { + event.preventDefault(); + event.stopPropagation(); + }), + throttleTime(100, undefined, { leading: true, trailing: false }) + ) + .subscribe((event) => this.forwardScrollFn!(event)) + ); + this.subscription.add( + fromEvent(this.backwardScrollArea!, 'dragover') + .pipe( + tap((event) => { + event.preventDefault(); + event.stopPropagation(); + }), + throttleTime(100, undefined, { leading: true, trailing: false }) + ) + .subscribe((event) => this.backwardScrollFn!(event)) + ); + // 拖拽放置委托 + this.subscription.add( + merge(fromEvent(this.forwardScrollArea, 'drop'), fromEvent(this.backwardScrollArea, 'drop')).subscribe( + (event) => this.delegateDropEvent(event) + ) + ); + // 拖拽离开清除参数 + this.subscription.add( + merge( + fromEvent(this.forwardScrollArea, 'dragleave', { passive: true }), + fromEvent(this.backwardScrollArea, 'dragleave', { passive: true }) + ).subscribe((event) => this.cleanLastScrollTime()) + ); + // 滚动过程计算区域有效性,滚动条贴到边缘的时候无效,无效的时候设置鼠标事件可用为none + this.subscription.add( + fromEvent(this.el.nativeElement, 'scroll', { passive: true }) + .pipe(throttleTime(300, undefined, { leading: true, trailing: true })) + .subscribe((event) => { + this.toggleScrollToOneEnd(this.el.nativeElement, this.forwardScrollArea!, this.direction, DropScrollOrientation.forward); + this.toggleScrollToOneEnd(this.el.nativeElement, this.backwardScrollArea!, this.direction, DropScrollOrientation.backward); + }) + ); + // 窗口缩放的时候重绘有效性区域 + this.subscription.add( + fromEvent(window, 'resize', { passive: true }) + .pipe(throttleTime(300, undefined, { leading: true, trailing: true })) + .subscribe((event) => this.resizeArea()) + ); + // dragstart的时候显示拖拽滚动边缘面板 + this.subscription.add( + this.dragDropService.dragStartEvent.subscribe(() => { + if (!this.allowScroll()) { + return; + } + setTimeout(() => { + // 立马出现会打断边缘元素的拖拽 + this.forwardScrollArea!.style.display = 'block'; + this.backwardScrollArea!.style.display = 'block'; + }); + }) + ); + // dragEnd或drop的时候结束了拖拽,滚动区域影藏起来 + this.subscription.add( + merge(this.dragDropService.dragEndEvent, this.dragDropService.dropEvent).subscribe(() => { + this.forwardScrollArea!.style.display = 'none'; + this.backwardScrollArea!.style.display = 'none'; + this.lastScrollTime = undefined; + }) + ); + setTimeout(() => { + this.resizeArea(); + }, 0); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + createScrollFn(direction: DropScrollDirection, orientation: DropScrollOrientation, speedFn: DropScrollSpeedFunction) { + if (typeof window === 'undefined') { + return; + } + const scrollAttr = direction === 'v' ? 'scrollTop' : 'scrollLeft'; + const eventAttr = direction === 'v' ? 'clientY' : 'clientX'; + const scrollWidthAttr = direction === 'v' ? 'scrollHeight' : 'scrollWidth'; + const offsetWidthAttr = direction === 'v' ? 'offsetHeight' : 'offsetWidth'; + const clientWidthAttr = direction === 'v' ? 'clientHeight' : 'clientWidth'; + const rectWidthAttr = direction === 'v' ? 'height' : 'width'; + const compareTarget = orientation === DropScrollOrientation.forward ? this.forwardScrollArea : this.backwardScrollArea; + const targetAttr = this.getCriticalEdge(direction, orientation); + const scrollElement = this.el.nativeElement; + + return (event: DragEvent) => { + const compareTargetRect = compareTarget!.getBoundingClientRect(); + const distance = event[eventAttr] - compareTargetRect[targetAttr]; + let speed = speedFn(Math.abs(distance / (compareTargetRect[rectWidthAttr] || 1))); + if (speed < this.minSpeed) { + speed = this.minSpeed; + } + if (speed > this.maxSpeed) { + speed = this.maxSpeed; + } + if (distance < 0) { + speed = -speed; + } + if (this.animationFrameId) { + window.cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = undefined; + } + this.animationFrameId = requestAnimationFrame(() => { + const time = new Date().getTime(); + const moveDistance = Math.ceil((speed * (time - (this.lastScrollTime || time))) / 1000); + scrollElement[scrollAttr] -= moveDistance; + this.lastScrollTime = time; + // 判断是不是到尽头 + if ( + (scrollElement[scrollAttr] === 0 && orientation === DropScrollOrientation.backward) || + (scrollElement[scrollAttr] + + scrollElement.getBoundingClientRect()[rectWidthAttr] - + scrollElement[offsetWidthAttr] + + scrollElement[clientWidthAttr] === + scrollElement[scrollWidthAttr] && + orientation === DropScrollOrientation.forward) + ) { + compareTarget!.style.pointerEvents = 'none'; + this.toggleActiveClass(compareTarget!, false); + } + this.animationFrameId = undefined; + }); + if (this.backSpaceDroppable) { + Utils.dispatchEventToUnderElement(event); + } + }; + } + delegateDropEvent(event: DragEvent) { + if (this.backSpaceDroppable) { + const ev = Utils.dispatchEventToUnderElement(event); + if (ev.defaultPrevented) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + getCriticalEdge(direction: DropScrollDirection, orientation: DropScrollOrientation): DropScrollTriggerEdge { + return ( + (direction === 'v' && orientation === DropScrollOrientation.forward && 'bottom') || + (direction === 'v' && orientation === DropScrollOrientation.backward && 'top') || + (direction !== 'v' && orientation === DropScrollOrientation.forward && 'right') || + (direction !== 'v' && orientation === DropScrollOrientation.backward && 'left') || + 'bottom' + ); + } + getSecondEdge(direction: DropScrollDirection): DropScrollTriggerEdge { + return (direction === 'v' && 'left') || (direction !== 'v' && 'top') || 'left'; + } + + createScrollArea(direction: DropScrollDirection, orientation: DropScrollOrientation) { + const area = this.document.createElement('div'); + area.className = `dropover-scroll-area dropover-scroll-area-${this.getCriticalEdge(direction, orientation)}`; + // 处理大小 + area.classList.add('active'); + this.setAreaSize(area, direction, orientation); + // 处理位置 + area.style.position = 'absolute'; + this.setAreaStyleLayout(area, direction, orientation); + + // 默认不展示 + area.style.display = 'none'; + // 附着元素 + this.el.nativeElement.parentNode.appendChild(area, this.el.nativeElement); + return area; + } + + setAreaSize(area: HTMLElement, direction: DropScrollDirection, orientation: DropScrollOrientation) { + const rect = this.el.nativeElement.getBoundingClientRect(); + const containerAttr = direction === 'v' ? 'height' : 'width'; + const responseEdgeWidth = + typeof this.responseEdgeWidth === 'string' ? this.responseEdgeWidth : this.responseEdgeWidth(rect[containerAttr]); + const settingOffset = + this.viewOffset && (orientation === DropScrollOrientation.forward ? this.viewOffset.forward : this.viewOffset.backward); + let width = direction === 'v' ? rect.width + 'px' : responseEdgeWidth; + let height = direction === 'v' ? responseEdgeWidth : rect.height + 'px'; + if (settingOffset) { + if (settingOffset.widthOffset) { + width = 'calc(' + width + ' + ' + settingOffset.widthOffset + 'px)'; + } + if (settingOffset.heightOffset) { + height = 'calc(' + height + ' + ' + settingOffset.heightOffset + 'px)'; + } + } + area.style.width = width; + area.style.height = height; + } + + setAreaStyleLayout(area: HTMLElement, direction: DropScrollDirection, orientation: DropScrollOrientation) { + const target = this.el.nativeElement; + const relatedTarget = this.el.nativeElement.parentNode; + const defaultOffset = { left: 0, right: 0, top: 0, bottom: 0 }; + const settingOffset = + (this.viewOffset && (orientation === DropScrollOrientation.forward ? this.viewOffset.forward : this.viewOffset.backward)) || + defaultOffset; + + const criticalEdge = this.getCriticalEdge(direction, orientation); + const secondEdge = this.getSecondEdge(direction); + [criticalEdge, secondEdge].forEach((edge) => { + area.style[edge] = this.getRelatedPosition(target, relatedTarget, edge, settingOffset[edge]); + }); + } + + getRelatedPosition(target: HTMLElement, relatedTarget: HTMLElement, edge: DropScrollTriggerEdge, offsetValue?: number) { + if (typeof window === 'undefined') { + return '0px'; + } + const relatedComputedStyle = window.getComputedStyle(relatedTarget) as any; + const relatedRect = relatedTarget.getBoundingClientRect() as any; + const selfRect = target.getBoundingClientRect() as any; + const helper = { + left: ['left', 'Left'], + right: ['right', 'Right'], + top: ['top', 'Top'], + bottom: ['bottom', 'Bottom'], + }; + let factor = 1; + if (edge === 'right' || edge === 'bottom') { + factor = -1; + } + return ( + (selfRect[helper[edge][0]] - + relatedRect[helper[edge][0]] + + parseInt(relatedComputedStyle['border' + helper[edge][1] + 'Width'], 10)) * + factor + + (offsetValue || 0) + + 'px' + ); + } + + resizeArea() { + [ + { area: this.forwardScrollArea!, orientation: DropScrollOrientation.forward }, + { area: this.backwardScrollArea!, orientation: DropScrollOrientation.backward }, + ].forEach((item) => { + this.setAreaSize(item.area, this.direction, item.orientation); + this.setAreaStyleLayout(item.area, this.direction, item.orientation); + }); + } + + toggleScrollToOneEnd(scrollElement: any, toggleElement: HTMLElement, direction: DropScrollDirection, orientation: DropScrollOrientation) { + const scrollAttr = direction === 'v' ? 'scrollTop' : 'scrollLeft'; + const scrollWidthAttr = direction === 'v' ? 'scrollHeight' : 'scrollWidth'; + const offsetWidthAttr = direction === 'v' ? 'offsetHeight' : 'offsetWidth'; + const clientWidthAttr = direction === 'v' ? 'clientHeight' : 'clientWidth'; + const rectWidthAttr = direction === 'v' ? 'height' : 'width'; + if ( + (scrollElement[scrollAttr] === 0 && orientation === DropScrollOrientation.backward) || + (Math.abs( + scrollElement[scrollAttr] + + scrollElement.getBoundingClientRect()[rectWidthAttr] - + scrollElement[scrollWidthAttr] - + scrollElement[offsetWidthAttr] + + scrollElement[clientWidthAttr] + ) < 1 && + orientation === DropScrollOrientation.forward) + ) { + toggleElement.style.pointerEvents = 'none'; + this.toggleActiveClass(toggleElement, false); + } else { + toggleElement.style.pointerEvents = 'auto'; + this.toggleActiveClass(toggleElement, true); + } + } + + cleanLastScrollTime() { + if (this.animationFrameId && typeof window !== 'undefined') { + window.cancelAnimationFrame(this.animationFrameId); + this.animationFrameId = undefined; + } + this.lastScrollTime = undefined; + } + + toggleActiveClass(target: HTMLElement, active: boolean) { + if (active) { + target.classList.remove('inactive'); + target.classList.add('active'); + } else { + target.classList.remove('active'); + target.classList.add('inactive'); + } + } + + allowScroll(): boolean { + if (!this.dropScrollScope) { + return true; + } + let allowed = false; + if (typeof this.dropScrollScope === 'string') { + if (typeof this.dragDropService.scope === 'string') { + allowed = this.dragDropService.scope === this.dropScrollScope; + } + if (this.dragDropService.scope instanceof Array) { + allowed = this.dragDropService.scope.indexOf(this.dropScrollScope) > -1; + } + } + if (this.dropScrollScope instanceof Array) { + if (typeof this.dragDropService.scope === 'string') { + allowed = this.dropScrollScope.indexOf(this.dragDropService.scope) > -1; + } + if (this.dragDropService.scope instanceof Array) { + allowed = + this.dropScrollScope.filter((item) => { + return this.dragDropService.scope!.indexOf(item) !== -1; + }).length > 0; + } + } + return allowed; + } +} + +export default { + mounted( + el: HTMLElement & { [props: string]: any }, + binding: DirectiveBinding, + vNode + ) { + const context = vNode['ctx'].provides; + const dragDropService = injectFromContext(DragDropService.TOKEN, context) as DragDropService; + const droppableDirective = (el[DropScrollEnhancedDirective.INSTANCE_KEY] = new DropScrollEnhancedDirective(el, dragDropService)); + droppableDirective.setInput(binding.value); + droppableDirective.mounted(); + setTimeout(() => { + droppableDirective.ngAfterViewInit?.(); + }, 0); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const droppableDirective = el[DropScrollEnhancedDirective.INSTANCE_KEY] as DropScrollEnhancedDirective; + droppableDirective.updateInput(binding.value, binding.oldValue!); + }, + beforeUnmount(el: HTMLElement & { [props: string]: any }) { + const droppableDirective = el[DropScrollEnhancedDirective.INSTANCE_KEY] as DropScrollEnhancedDirective; + droppableDirective.ngOnDestroy?.(); + }, +}; + +export class DropScrollEnhancedSideDirective extends DropScrollEnhancedDirective { + inputNameMap?: { [key: string]: string } | undefined = { + direction: 'sideDirection', + }; + sideDirection: DropScrollDirection = 'h'; + direction: DropScrollDirection = 'v'; + ngOnInit() { + this.direction = this.sideDirection === 'v' ? 'h' : 'v'; + } +} +export const DropScrollEnhancedSide = { + mounted( + el: HTMLElement & { [props: string]: any }, + binding: DirectiveBinding, + vNode + ) { + const context = vNode['ctx'].provides; + const dragDropService = injectFromContext(DragDropService.TOKEN, context) as DragDropService; + const droppableDirective = (el[DropScrollEnhancedSideDirective.INSTANCE_KEY] = new DropScrollEnhancedSideDirective( + el, + dragDropService + )); + droppableDirective.setInput(binding.value); + droppableDirective.mounted(); + setTimeout(() => { + droppableDirective.ngAfterViewInit?.(); + }, 0); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const droppableDirective = el[DropScrollEnhancedSideDirective.INSTANCE_KEY] as DropScrollEnhancedSideDirective; + droppableDirective.updateInput(binding.value, binding.oldValue!); + }, + beforeUnmount(el: HTMLElement & { [props: string]: any }) { + const droppableDirective = el[DropScrollEnhancedSideDirective.INSTANCE_KEY] as DropScrollEnhancedSideDirective; + droppableDirective.ngOnDestroy?.(); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/droppable.directive.ts b/packages/devui-vue/devui/dragdrop-new/src/droppable.directive.ts new file mode 100644 index 0000000000..936babfbb0 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/droppable.directive.ts @@ -0,0 +1,839 @@ +import { Subject, Subscription, distinctUntilChanged, filter, fromEvent } from 'rxjs'; +import { EventEmitter } from './preserve-next-event-emitter'; +import { DragDropService } from './drag-drop.service'; +import { Utils, injectFromContext, provideToContext } from './utils'; +import { DirectiveBinding, InjectionKey, VNode } from 'vue'; +import { NgDirectiveBase } from './directive-base'; +import { DraggableDirective } from './draggable.directive'; + +export type DropIndexFlag = 'beforeAll' | 'afterAll'; +export interface IDroppableBinding { + dragOverClass?: string; + dropScope?: string | Array; + placeholderTag?: string; + placeholderStyle?: Record; + placeholderText?: string; + allowDropOnItem?: boolean; + dragOverItemClass?: string; + nestingTargetRect?: { + height?: number; + width?: number; + }; + switchWhileCrossEdge?: boolean; + defaultDropPosition?: 'closest' | 'before' | 'after'; + dropSortCountSelector: string; + dropSortVirtualScrollOption?: { + totalLength?: number; + startIndex?: number; + // innerSortContainer?: HTMLElement | string; // 用于虚拟滚动列表结构发生内嵌 + }; +} +export interface IDroppableListener { + '@dragEnterEvent'?: (_: any) => void; + '@dragOverEvent'?: (_: any) => void; + '@dragLeaveEvent'?: (_: any) => void; + '@dropEvent'?: (_: any) => void; +} +export class DropEvent { + nativeEvent: any; + dragData: any; + batchDragData: any; + dropSubject: Subject; + dropIndex?: number; + dragFromIndex?: number; + dropOnItem?: boolean; + dropOnOrigin?: boolean; + constructor( + event: any, + data: any, + dropSubject: Subject, + dropIndex?: number, + dragFromIndex?: number, + dropOnItem?: boolean, + dropOnOrigin?: boolean, + batchDragData?: Array + ) { + this.nativeEvent = event; + this.dragData = data; + this.dropSubject = dropSubject; + this.dropIndex = dropIndex; + this.dragFromIndex = dragFromIndex; + this.dropOnItem = dropOnItem; + this.dropOnOrigin = dropOnOrigin; + this.batchDragData = batchDragData; + } +} +export interface DragPlaceholderInsertionEvent { + command: 'insertBefore' | 'append' | 'remove'; + container?: HTMLElement; + relatedEl?: Element; +} +export interface DragPlaceholderInsertionIndexEvent { + command: 'insertBefore' | 'append' | 'remove'; + index?: number; +} +export class DroppableDirective extends NgDirectiveBase { + static INSTANCE_KEY = '__vueDevuiDroppableDirectiveInstance'; + static TOKEN: InjectionKey = Symbol('DROPPABLE_DIRECTIVE_TOKEN'); + hostListenerMap?: { [key: string]: string } | undefined = { + drop: 'drop', + }; + dragEnterEvent: EventEmitter = new EventEmitter(); + dragOverEvent: EventEmitter = new EventEmitter(); + dragLeaveEvent: EventEmitter = new EventEmitter(); + dropEvent: EventEmitter = new EventEmitter(); // 注意使用了虚拟滚动后,DropEvent中的dragFromIndex无效 + + dragOverClass?: string; + dropScope: string | Array = 'default'; + placeholderTag = 'div'; + placeholderStyle: any = { backgroundColor: ['#859bff', `var(--devui-brand-foil, #859bff)`], opacity: '.4' }; + placeholderText = ''; + allowDropOnItem = false; + dragOverItemClass?: string; + nestingTargetRect?: { height?: number; width?: number }; + switchWhileCrossEdge = false; + + defaultDropPosition: 'closest' | 'before' | 'after' = 'closest'; + dropSortCountSelector?: string; + dropSortVirtualScrollOption?: { + totalLength?: number; + startIndex?: number; + // innerSortContainer?: HTMLElement | string; // 用于虚拟滚动列表结构发生内嵌 + }; + private dropFlag?: DropIndexFlag; + + private sortContainer: any; + private sortDirection?: 'v' | 'h'; + private sortDirectionZMode?: boolean; + private placeholder: any; + + // 用于修复dragleave多次触发 + private dragCount = 0; + + private dropIndex?: number = undefined; + + private dragStartSubscription?: Subscription; + private dragEndSubscription?: Subscription; + private dropEndSubscription?: Subscription; + + // 记录上一次悬停的元素,用于对比悬停的元素等是否发生变化 + private overElement?: any; + + private dragPartEventSub?: Subscription; + private allowDropCache?: boolean; + private dragElIndex?: number; + /* 协同拖拽需要 */ + placeholderInsertionEvent = new Subject(); + placeholderRenderEvent = new Subject(); + document: Document; + el: { nativeElement: any } = { nativeElement: null }; + private dragDropService: DragDropService; + + constructor(el: HTMLElement, dragDropService: DragDropService) { + super(); + this.document = window.document; + this.el.nativeElement = el; + this.dragDropService = dragDropService; + } + + ngOnInit() { + this.placeholder = this.document.createElement(this.placeholderTag); + this.placeholder.className = 'drag-placeholder'; + this.placeholder.innerText = this.placeholderText; + this.dragStartSubscription = this.dragDropService.dragStartEvent.subscribe(() => this.setPlaceholder()); + if (this.dragDropService.draggedEl) { + this.setPlaceholder(); // 虚拟滚动生成元素过程中 + } + this.dropEndSubscription = this.dragDropService.dropEvent.subscribe(() => { + if (this.dragDropService.draggedEl) { + if (!this.dragDropService.dragFollow) { + Utils.addElStyles(this.dragDropService.draggedEl, { display: '' }); + this.dragDropService.dragElShowHideEvent.next(true); + } + } + this.removePlaceholder(); + this.overElement = undefined; + this.allowDropCache = undefined; + this.dragElIndex = undefined; + this.dropIndex = undefined; + }); + this.dragEndSubscription = this.dragDropService.dragEndEvent.subscribe(() => { + if (this.dragDropService.draggedEl) { + if (!this.dragDropService.dragFollow) { + Utils.addElStyles(this.dragDropService.draggedEl, { display: '' }); + this.dragDropService.dragElShowHideEvent.next(true); + } + } + this.removePlaceholder(); + this.dragCount = 0; + this.overElement = undefined; + this.allowDropCache = undefined; + this.dragElIndex = undefined; + this.dropIndex = undefined; + }); + + this.dragPartEventSub = new Subscription(); + this.dragPartEventSub.add( + fromEvent(this.el.nativeElement, 'dragover') + .pipe( + filter((event) => this.allowDrop(event)), + distinctUntilChanged((prev, current) => { + const bool = prev.clientX === current.clientX && prev.clientY === current.clientY && prev.target === current.target; + if (bool) { + current.preventDefault(); + current.stopPropagation(); + } + return bool; + }) + ) + .subscribe((event) => this.dragOver(event)) + ); + this.dragPartEventSub.add(fromEvent(this.el.nativeElement, 'dragenter').subscribe((event) => this.dragEnter(event))); + this.dragPartEventSub.add(fromEvent(this.el.nativeElement, 'dragleave').subscribe((event) => this.dragLeave(event))); + } + + ngAfterViewInit() { + if (this.el.nativeElement.hasAttribute('d-sortable')) { + this.sortContainer = this.el.nativeElement; + } else { + this.sortContainer = this.el.nativeElement.querySelector('[d-sortable]'); + } + this.sortDirection = this.sortContainer ? this.sortContainer.getAttribute('dsortable') || 'v' : 'v'; + this.sortDirectionZMode = this.sortContainer ? this.sortContainer.getAttribute('d-sortable-zmode') === 'true' || false : false; + } + + ngOnDestroy() { + this.dragStartSubscription?.unsubscribe(); + this.dragEndSubscription?.unsubscribe(); + this.dropEndSubscription?.unsubscribe(); + if (this.dragPartEventSub) { + this.dragPartEventSub.unsubscribe(); + } + } + + dragEnter(e: DragEvent) { + this.dragCount++; + e.preventDefault(); // ie11 dragenter需要preventDefault否则dragover无效 + this.dragEnterEvent.emit(e); + } + + dragOver(e: DragEvent) { + if (this.allowDrop(e)) { + if (this.dragDropService.dropTargets.indexOf(this.el) === -1) { + this.dragDropService.dropTargets.forEach((el) => { + const placeHolderEl = el!.nativeElement.querySelector('.drag-placeholder'); + if (placeHolderEl) { + placeHolderEl.parentElement.removeChild(placeHolderEl); + } + Utils.removeClass(el, this.dragOverClass!); + this.removeDragoverItemClass(el.nativeElement); + }); + this.dragDropService.dropTargets = [this.el]; + this.overElement = undefined; // 否则会遇到上一次position= 这一次的然后不刷新和插入。 + } + Utils.addClass(this.el, this.dragOverClass!); + const hitPlaceholder = this.dragDropService.dragOriginPlaceholder && this.dragDropService.dragOriginPlaceholder.contains(e.target); + if ( + this.sortContainer && + ((hitPlaceholder && this.overElement === undefined) || + !((e.target as HTMLElement).contains(this.placeholder) || hitPlaceholder) || + (this.switchWhileCrossEdge && !this.placeholder.contains(e.target) && !hitPlaceholder) || // 越边交换回折的情况需要重新计算 + (!this.sortContainer.contains(e.target) && this.defaultDropPosition === 'closest')) // 就近模式需要重新计算 + ) { + const overElement = this.findSortableEl(e); + if ( + !(this.overElement && overElement) || + this.overElement.index !== overElement.index || + (this.allowDropOnItem && + this.overElement.position !== overElement.position && + (this.overElement.position === 'inside' || overElement.position === 'inside')) + ) { + // overElement的参数有刷新的时候才进行插入等操作 + this.overElement = overElement; + + this.insertPlaceholder(overElement); + + this.removeDragoverItemClass(this.sortContainer, overElement); + if (overElement.position === 'inside' && this.dragOverItemClass) { + Utils.addClass(overElement.el, this.dragOverItemClass); + } + } else { + this.overElement = overElement; + } + } else { + if (this.sortContainer && this.overElement && this.overElement.el) { + if (!this.overElement.el.contains(e.target)) { + this.overElement.realEl = e.target; + } else { + this.overElement.realEl = undefined; + } + } + } + if (this.dragDropService.draggedEl) { + if (!this.dragDropService.dragFollow) { + Utils.addElStyles(this.dragDropService.draggedEl, { display: 'none' }); + this.dragDropService.dragElShowHideEvent.next(false); + if (this.dragDropService.dragOriginPlaceholder) { + Utils.addElStyles(this.dragDropService.dragOriginPlaceholder, { display: 'block' }); + } + } + } + e.preventDefault(); + e.stopPropagation(); + this.dragOverEvent.emit(e); + } + } + + dragLeave(e: DragEvent) { + // 用于修复包含子元素时,多次触发dragleave + this.dragCount--; + + if (this.dragCount === 0) { + if (this.dragDropService.dropTargets.indexOf(this.el) !== -1) { + this.dragDropService.dropTargets = []; + } + Utils.removeClass(this.el, this.dragOverClass!); + this.removePlaceholder(); + this.removeDragoverItemClass(this.el.nativeElement); + this.overElement = undefined; + this.dragElIndex = undefined; + this.dropIndex = undefined; + } + e.preventDefault(); + this.dragLeaveEvent.emit(e); + } + + // @HostListener('drop', ['$event']) + drop(e: DragEvent) { + if (!this.allowDrop(e)) { + return; + } + this.dragCount = 0; + Utils.removeClass(this.el, this.dragOverClass!); + this.removeDragoverItemClass(this.sortContainer); + this.removePlaceholder(); + e.preventDefault(); + e.stopPropagation(); + this.dragDropService.dropOnOrigin = this.isDragPlaceholderPosition(this.dropIndex!); + const draggedElIdentity = this.dragDropService.draggedElIdentity; + this.dragDropService.draggedElIdentity = undefined; // 需要提前清除,避免新生成的节点复用了id 刷新了dragOriginPlaceholder + let batchDraggble: Array = []; + if (this.dragDropService.batchDragData && this.dragDropService.batchDragData.length > 1) { + batchDraggble = this.dragDropService.batchDragData + .map((dragData) => dragData.draggable) + .filter((draggable) => draggable && draggable.el.nativeElement !== this.dragDropService.draggedEl); + } + this.dropEvent.emit( + new DropEvent( + e, + this.dragDropService.dragData, + this.dragDropService.dropEvent, + this.dropSortVirtualScrollOption ? this.getRealIndex(this.dropIndex!, this.dropFlag) : this.dropIndex, + this.sortContainer ? this.checkSelfFromIndex(this.dragDropService.draggedEl) : -1, + this.dragDropService.dropOnItem, + this.dragDropService.dropOnOrigin, + this.dragDropService.batchDragging ? this.dragDropService.getBatchDragData(draggedElIdentity) : undefined + ) + ); + // 如果drop之后drag元素被删除,则不会发生dragend事件,需要代替dragend清理 + if (this.dragDropService.dragFollow) { + this.dragDropService.disableDraggedCloneNodeFollowMouse(); + } else { + Utils.addElStyles(this.dragDropService.draggedEl, { display: undefined }); + this.dragDropService.dragElShowHideEvent.next(false); + } + if (batchDraggble.length > 0 && this.dragDropService.batchDragging) { + batchDraggble.forEach((draggable) => { + if (!draggable.originPlaceholder || draggable.originPlaceholder.show === false) { + draggable.el.nativeElement.style.display = ''; + } else if (draggable.originPlaceholder.removeDelay! > 0 && !this.dragDropService.dropOnOrigin) { + draggable.delayRemoveOriginPlaceholder(false); + } else { + draggable.el.nativeElement.style.display = ''; + draggable.removeOriginPlaceholder(false); + } + }); + } + this.dragDropService.dropEvent.next(e); + this.dragDropService.dragData = undefined; + this.dragDropService.scope = undefined; + this.dragDropService.draggedEl = undefined; + this.dragDropService.dragFollow = undefined; + this.dragDropService.dragFollowOptions = undefined; + this.dragDropService.dragOffset = undefined; + this.dragDropService.dropOnOrigin = undefined; + this.dragDropService.batchDragging = false; + this.dragDropService.batchDragStyle = undefined; + this.dragDropService.dragPreviewDirective = undefined; + } + + allowDrop(e: DragEvent): boolean { + if (!e) { + return false; + } + if (this.allowDropCache !== undefined) { + return this.allowDropCache; + } + let allowed = false; + if (typeof this.dropScope === 'string') { + if (typeof this.dragDropService.scope === 'string') { + allowed = this.dragDropService.scope === this.dropScope; + } + if (this.dragDropService.scope instanceof Array) { + allowed = this.dragDropService.scope.indexOf(this.dropScope) > -1; + } + } + if (this.dropScope instanceof Array) { + if (typeof this.dragDropService.scope === 'string') { + allowed = this.dropScope.indexOf(this.dragDropService.scope) > -1; + } + if (this.dragDropService.scope instanceof Array) { + allowed = + this.dropScope.filter((item) => { + return this.dragDropService.scope!.indexOf(item) !== -1; + }).length > 0; + } + } + this.allowDropCache = allowed; + return allowed; + } + + private dropSortCountSelectorFilterFn = (value: HTMLElement) => { + return ( + Utils.matches(value, this.dropSortCountSelector!) || + value.contains(this.placeholder) || + value === this.dragDropService.dragOriginPlaceholder + ); + }; + + // 查询需要插入placeholder的位置 + /* eslint-disable-next-line complexity*/ + private findSortableEl(event: DragEvent) { + const moveElement = event.target; + let overElement: any = null; + if (!this.sortContainer) { + return overElement; + } + overElement = { index: 0, el: null, position: 'before' }; + this.dropIndex = 0; + this.dropFlag = undefined; + let childEls: Array = Utils.slice(this.sortContainer.children); + // 删除虚拟滚动等的额外元素不需要计算的元素 + if (this.dropSortCountSelector) { + childEls = childEls.filter(this.dropSortCountSelectorFilterFn); + } + // 如果没有主动删除则删除多余的originplaceholder + if (childEls.some((el) => el !== this.dragDropService.dragOriginPlaceholder && el.classList.contains('drag-origin-placeholder'))) { + childEls = childEls.filter( + (el) => !(el.classList.contains('drag-origin-placeholder') && el !== this.dragDropService.dragOriginPlaceholder) + ); + } + // 要先删除clonenode否则placeholderindex是错的 + if (this.dragDropService.dragFollow && this.dragDropService.dragCloneNode) { + const cloneNodeIndex = childEls.findIndex((value) => value === this.dragDropService.dragCloneNode); + if (-1 !== cloneNodeIndex) { + childEls.splice(cloneNodeIndex, 1); + } + } + // 计算index数组需要删除源占位符 + if (this.dragDropService.dragOriginPlaceholder) { + const dragOriginPlaceholderIndex = childEls.findIndex((value) => value === this.dragDropService.dragOriginPlaceholder); + if (-1 !== dragOriginPlaceholderIndex) { + this.dragElIndex = dragOriginPlaceholderIndex - 1; + childEls.splice(dragOriginPlaceholderIndex, 1); + } else { + this.dragElIndex = -1; + } + } else { + this.dragElIndex = -1; + } + // 查询是否已经插入了placeholder + const placeholderIndex = childEls.findIndex((value) => value.contains(this.placeholder)); + // 删除placeholder + if (-1 !== placeholderIndex) { + childEls.splice(placeholderIndex, 1); + } + // 如果还有placeholder在前面 dragElIndex得再减一 + if (-1 !== placeholderIndex && -1 !== this.dragElIndex && placeholderIndex < this.dragElIndex) { + this.dragElIndex--; + } + const positionIndex = -1 !== placeholderIndex ? placeholderIndex : this.dragElIndex; + const currentIndex = childEls.findIndex( + (value) => + value.contains(moveElement as Node) || + (value.nextElementSibling === moveElement && value.nextElementSibling!.classList.contains('drag-origin-placeholder')) + ); + if (this.switchWhileCrossEdge && !this.allowDropOnItem && childEls.length && -1 !== positionIndex && currentIndex > -1) { + // 越过元素边界立即交换位置算法 + const lastIndex = positionIndex; + // 解决抖动 + const realEl = this.overElement && (this.overElement.realEl || this.overElement.el); + if (-1 !== currentIndex && realEl === childEls[currentIndex]) { + this.dropIndex = this.overElement.index; + return this.overElement; + } + + overElement = { + index: lastIndex > currentIndex ? currentIndex : currentIndex + 1, + el: childEls[currentIndex], + position: lastIndex > currentIndex ? 'before' : 'after', + }; + + this.dragDropService.dropOnItem = false; + this.dropIndex = overElement.index; + return overElement; + } + + if ( + moveElement === this.sortContainer || + (moveElement as HTMLElement).classList?.contains('drag-origin-placeholder') || + moveElement === (this.dragDropService && this.dragDropService.dragOriginPlaceholder) || + (!this.sortContainer.contains(moveElement) && this.defaultDropPosition === 'closest') + ) { + if (!childEls.length) { + this.dropIndex = 0; + this.dragDropService.dropOnItem = false; + return overElement; + } + // 落入A元素和B元素的间隙里 + let findInGap = false; + for (let i = 0; i < childEls.length; i++) { + const targetElement = childEls[i]; + // 处理非越边的落到side-origin-placeholder + if (childEls[i].nextSibling === moveElement && (moveElement as HTMLElement).classList.contains('drag-origin-placeholder')) { + const position = this.calcPosition(event, moveElement); + this.dragDropService.dropOnItem = position === 'inside'; + overElement = { index: position === 'after' ? i + 1 : i, el: childEls[i], position: position }; + this.dropIndex = overElement.index; + return overElement; + } + const positionOutside = this.calcPositionOutside(event, targetElement); + if (positionOutside === 'before') { + this.dragDropService.dropOnItem = false; + overElement = { index: i, el: childEls[i], position: positionOutside, realEl: moveElement }; + this.dropIndex = overElement.index; + findInGap = true; + break; + } else { + // for 'notsure' + } + } + if (!findInGap) { + this.dragDropService.dropOnItem = false; + overElement = { index: childEls.length, el: childEls[childEls.length - 1], position: 'after', realEl: moveElement }; + this.dropIndex = childEls.length; + } + return overElement; + } + if (!this.sortContainer.contains(moveElement)) { + if (this.defaultDropPosition === 'before') { + overElement = { index: 0, el: childEls.length ? childEls[0] : null, position: 'before', realEl: moveElement }; + this.dropFlag = 'beforeAll'; + } else { + overElement = { + index: childEls.length, + el: childEls.length ? childEls[childEls.length - 1] : null, + position: 'after', + realEl: moveElement, + }; + this.dropFlag = 'afterAll'; + } + this.dropIndex = overElement.index; + return overElement; + } + let find = false; + for (let i = 0; i < childEls.length; i++) { + if (childEls[i].contains(moveElement as HTMLElement)) { + const targetElement = childEls[i]; + const position = this.calcPosition(event, targetElement); + this.dragDropService.dropOnItem = position === 'inside'; + overElement = { index: position === 'after' ? i + 1 : i, el: childEls[i], position: position }; + this.dropIndex = overElement.index; + find = true; + break; + } + } + if (!find) { + if (childEls.length) { + overElement = { index: childEls.length, el: childEls[childEls.length - 1], position: 'after' }; + } + this.dropIndex = childEls.length; + this.dragDropService.dropOnItem = false; + } + return overElement; + } + + private calcPosition(event: any, targetElement: any) { + const rect = targetElement.getBoundingClientRect(); + const relY = event.clientY - (rect.y || rect.top); + const relX = event.clientX - (rect.x || rect.left); + + // 处理允许drop到元素自己 + if (this.allowDropOnItem) { + const dropOnItemEdge = { + // 有内嵌列表的时候需要修正元素的高度活宽度 + height: (this.nestingTargetRect && this.nestingTargetRect.height) || rect.height, + width: (this.nestingTargetRect && this.nestingTargetRect.width) || rect.width, + }; + const threeQuartersOfHeight = (dropOnItemEdge.height * 3) / 4; + const threeQuartersOfWidth = (dropOnItemEdge.width * 3) / 4; + const AQuarterOfHeight = Number(dropOnItemEdge.height) / 4; + const AQuarterOfWidth = Number(dropOnItemEdge.width) / 4; + + if (this.sortDirectionZMode) { + const slashPosition = relY / dropOnItemEdge.height + relX / dropOnItemEdge.width; + if (slashPosition > 0.3 && slashPosition <= 0.7) { + return 'inside'; + } else if (slashPosition > 0.7) { + const slashPositionNesting = + (relY - rect.height + dropOnItemEdge.height) / dropOnItemEdge.height + + (relX - rect.width + dropOnItemEdge.width) / dropOnItemEdge.width; + if (slashPositionNesting <= 0.7) { + return 'inside'; + } + } + } + if ( + (this.sortDirection === 'v' && relY > AQuarterOfHeight && relY <= threeQuartersOfHeight) || + (this.sortDirection !== 'v' && relX > AQuarterOfWidth && relX <= threeQuartersOfWidth) + ) { + // 高度的中间1/4 - 3/4 属于drop到元素自己 + return 'inside'; + } else if ( + (this.sortDirection === 'v' && relY > threeQuartersOfHeight && relY <= rect.height - AQuarterOfHeight) || + (this.sortDirection !== 'v' && relX > threeQuartersOfWidth && relX <= rect.width - AQuarterOfWidth) + ) { + // 内嵌列表后中间区域都属于inside + return 'inside'; + } + } + + if (this.sortDirectionZMode) { + if (relY / rect.height + relX / rect.width < 1) { + return 'before'; + } + return 'after'; + } + // 其他情况保持原来的属于上半部分或者下半部分 + if ((this.sortDirection === 'v' && relY > rect.height / 2) || (this.sortDirection !== 'v' && relX > rect.width / 2)) { + return 'after'; + } + return 'before'; + } + + private calcPositionOutside(event: any, targetElement: any) { + // targetElement 获取 getBoundingClientRect + const rect = this.getBoundingRectAndRealPosition(targetElement); + const relY = event.clientY - (rect.y || rect.top); + const relX = event.clientX - (rect.x || rect.left); + + if (this.sortDirectionZMode) { + if ( + (this.sortDirection === 'v' && (relY < 0 || (relY < rect.height && relX < 0))) || + (this.sortDirection !== 'v' && (relX < 0 || (relX < rect.width && relY < 0))) + ) { + return 'before'; + } + return 'notsure'; + } + + if ((this.sortDirection === 'v' && relY < rect.height / 2) || (this.sortDirection !== 'v' && relX < rect.width / 2)) { + return 'before'; + } + return 'notsure'; + } + setPlaceholder = () => { + this.placeholder.style.width = this.dragDropService.dragOffset!.width + 'px'; + this.placeholder.style.height = this.dragDropService.dragOffset!.height + 'px'; // ie下clientHeight为0 + Utils.addElStyles(this.placeholder, this.placeholderStyle); + this.placeholderRenderEvent.next({ width: this.dragDropService.dragOffset!.width, height: this.dragDropService.dragOffset!.height }); + }; + + // 插入placeholder + private insertPlaceholder(overElement: any) { + const tempScrollTop = this.sortContainer.scrollTop; + const tempScrollLeft = this.sortContainer.scrollLeft; + let hitPlaceholder = false; + let cmd: DragPlaceholderInsertionIndexEvent; + const getIndex = (arr: Array, el: HTMLElement, defaultValue: number) => { + const index = arr.indexOf(el); + return index > -1 ? index : defaultValue; + }; + if (null !== overElement) { + const sortContainerChildren = Utils.slice(this.sortContainer.children).filter( + (el) => el !== this.dragDropService.dragCloneNode + ); + + if (overElement.el === null) { + cmd = { + command: 'append', + }; + this.sortContainer.appendChild(this.placeholder); + } else { + if (overElement.position === 'inside') { + cmd = { + command: 'remove', + }; + this.removePlaceholder(); + } else if (this.dragDropService.dragOriginPlaceholder && this.isDragPlaceholderPosition(overElement.index)) { + cmd = { + command: 'remove', + }; + this.removePlaceholder(); + hitPlaceholder = true; + } else if (overElement.position === 'after') { + if ( + overElement.el.nextSibling && + overElement.el.nextSibling.classList && + overElement.el.nextSibling.classList.contains('drag-origin-placeholder') + ) { + // 针对多源占位符场景 + cmd = { + command: 'insertBefore', + index: getIndex(sortContainerChildren, overElement.el.nextSibling, sortContainerChildren.length) + 1, + }; + this.sortContainer.insertBefore(this.placeholder, overElement.el.nextSibling.nextSibling); + } else { + cmd = { + command: 'insertBefore', + index: getIndex(sortContainerChildren, overElement.el, sortContainerChildren.length) + 1, + }; + this.sortContainer.insertBefore(this.placeholder, overElement.el.nextSibling); + } + } else { + cmd = { + command: 'insertBefore', + index: getIndex(sortContainerChildren, overElement.el, sortContainerChildren.length), + }; + this.sortContainer.insertBefore(this.placeholder, overElement.el); + } + } + } + this.placeholderInsertionEvent.next(cmd!); + this.sortContainer.scrollTop = tempScrollTop; + this.sortContainer.scrollLeft = tempScrollLeft; + if (this.dragDropService.dragOriginPlaceholder) { + if (hitPlaceholder) { + this.hitDragOriginPlaceholder(); + } else { + this.hitDragOriginPlaceholder(false); + } + } + } + + private isDragPlaceholderPosition(index: number) { + if (this.dragElIndex! > -1 && (index === this.dragElIndex || index === this.dragElIndex! + 1)) { + return true; + } else { + return false; + } + } + private hitDragOriginPlaceholder(bool = true) { + const placeholder = this.dragDropService.dragOriginPlaceholder; + if (bool) { + placeholder.classList.add('hit-origin-placeholder'); + } else { + placeholder.classList.remove('hit-origin-placeholder'); + } + } + + private removePlaceholder() { + if (this.sortContainer && this.sortContainer.contains(this.placeholder)) { + this.sortContainer.removeChild(this.placeholder); + this.placeholderInsertionEvent.next({ + command: 'remove', + }); + } + } + + private removeDragoverItemClass(container: HTMLElement, overElement?: any) { + if (this.dragOverItemClass) { + const dragOverItemClassGroup = container.querySelectorAll('.' + this.dragOverItemClass); + if (dragOverItemClassGroup && dragOverItemClassGroup.length > 0) { + for (const element of dragOverItemClassGroup as unknown as Set) { + if (overElement) { + if (element !== overElement.el || overElement.position !== 'inside') { + Utils.removeClass(element, this.dragOverItemClass); + } + } else { + Utils.removeClass(element, this.dragOverItemClass); + } + } + } + } + } + + private checkSelfFromIndex(el: any) { + let fromIndex = -1; + if (!this.sortContainer.contains(el)) { + return fromIndex; + } + let children: Array = Utils.slice(this.sortContainer.children); + if (this.dropSortCountSelector) { + children = children.filter(this.dropSortCountSelectorFilterFn); + } + for (let i = 0; i < children.length; i++) { + if (children[i].contains(this.dragDropService.draggedEl)) { + fromIndex = i; + break; + } + } + return this.getRealIndex(fromIndex); + } + private getRealIndex(index: number, flag?: DropIndexFlag): number { + let realIndex; + const startIndex = (this.dropSortVirtualScrollOption && this.dropSortVirtualScrollOption.startIndex) || 0; + const totalLength = this.dropSortVirtualScrollOption && this.dropSortVirtualScrollOption.totalLength; + if (flag === 'beforeAll') { + realIndex = 0; + } else if (flag === 'afterAll') { + realIndex = totalLength || index; + } else { + realIndex = startIndex + index; + } + return realIndex; + } + + getBoundingRectAndRealPosition(targetElement: HTMLElement) { + // 用于修复部分display none的元素获取到的top和left是0, 取它下一个元素的左上角为坐标 + let rect: any = targetElement.getBoundingClientRect(); + const { bottom, right, width, height } = rect; + if ( + rect.width === 0 && + rect.height === 0 && + (targetElement.style.display === 'none' || getComputedStyle(targetElement).display === 'none') + ) { + if (targetElement.nextElementSibling) { + const { top: realTop, left: realLeft } = targetElement.nextElementSibling.getBoundingClientRect(); + rect = { x: realLeft, y: realTop, top: realTop, left: realLeft, bottom, right, width, height }; + } + } + return rect; + } + getSortContainer() { + return this.sortContainer; + } +} + +export default { + mounted(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding, vNode: VNode) { + const context = vNode['ctx'].provides; + const dragDropService = injectFromContext(DragDropService.TOKEN, context) as DragDropService; + const droppableDirective = (el[DroppableDirective.INSTANCE_KEY] = new DroppableDirective(el, dragDropService)); + provideToContext(DroppableDirective.TOKEN, droppableDirective, context); + droppableDirective.setInput(binding.value); + droppableDirective.mounted(); + droppableDirective.ngOnInit?.(); + setTimeout(() => { + droppableDirective.ngAfterViewInit?.(); + }, 0); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const droppableDirective = el[DroppableDirective.INSTANCE_KEY] as DroppableDirective; + droppableDirective.updateInput(binding.value, binding.oldValue!); + }, + beforeUnmount(el: HTMLElement & { [props: string]: any }) { + const droppableDirective = el[DroppableDirective.INSTANCE_KEY] as DroppableDirective; + droppableDirective.ngOnDestroy?.(); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/preserve-next-event-emitter.ts b/packages/devui-vue/devui/dragdrop-new/src/preserve-next-event-emitter.ts new file mode 100644 index 0000000000..e494eedbee --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/preserve-next-event-emitter.ts @@ -0,0 +1,137 @@ +import { Subject, Subscription } from 'rxjs'; + +export class EventEmitter extends Subject { + protected __isAsync: boolean; // tslint:disable-line + + constructor(isAsync: boolean = false) { + super(); + this.__isAsync = isAsync; + } + + emit(value?: any) { + super.next(value); + } + + subscribe(generatorOrNext?: any, error?: any, complete?: any): Subscription { + let schedulerFn: (t: any) => any; + let errorFn = (err: any): any => null; + let completeFn = (): any => null; + + if (generatorOrNext && typeof generatorOrNext === 'object') { + schedulerFn = this.__isAsync + ? (value: any) => { + setTimeout(() => generatorOrNext.next(value)); + } + : (value: any) => { + generatorOrNext.next(value); + }; + + if (generatorOrNext.error) { + errorFn = this.__isAsync + ? (err) => { + setTimeout(() => generatorOrNext.error(err)); + } + : (err) => { + generatorOrNext.error(err); + }; + } + + if (generatorOrNext.complete) { + errorFn = this.__isAsync + ? () => { + setTimeout(() => generatorOrNext.complete()); + } + : () => { + generatorOrNext.complete(); + }; + } + } else { + schedulerFn = this.__isAsync + ? (value: any) => { + setTimeout(() => generatorOrNext(value)); + } + : (value: any) => { + generatorOrNext(value); + }; + + if (error) { + errorFn = this.__isAsync + ? (err) => { + setTimeout(() => error(err)); + } + : (err) => { + error(err); + }; + } + + if (complete) { + completeFn = this.__isAsync + ? () => { + setTimeout(() => complete()); + } + : () => { + complete(); + }; + } + } + + const sink = super.subscribe(schedulerFn, errorFn, completeFn); + + if (generatorOrNext instanceof Subscription) { + generatorOrNext.add(sink); + } + + return sink; + } +} + +export class PreserveNextEventEmitter extends EventEmitter { + // 保留注册的 generatorOrNext 构成的函数 + private _schedulerFns: Set | undefined; + private _isAsync: boolean = false; + get schedulerFns() { + return this._schedulerFns; + } + forceCallback(value: T, once = false) { + if (this.schedulerFns && this.schedulerFns.size) { + this.schedulerFns.forEach((fn) => { + fn(value); + }); + if (once) { + this.cleanCallbackFn(); + } + } + } + + cleanCallbackFn() { + this._schedulerFns = undefined; + } + + subscribe(generatorOrNext?: any, error?: any, complete?: any): any { + let schedulerFn: (t: any) => any; + + if (generatorOrNext && typeof generatorOrNext === 'object') { + schedulerFn = this._isAsync + ? (value: any) => { + setTimeout(() => generatorOrNext.next(value)); + } + : (value: any) => { + generatorOrNext.next(value); + }; + } else { + schedulerFn = this._isAsync + ? (value: any) => { + setTimeout(() => generatorOrNext(value)); + } + : (value: any) => { + generatorOrNext(value); + }; + } + if (!this._schedulerFns) { + this._schedulerFns = new Set(); + } + this._schedulerFns.add(schedulerFn); + + return super.subscribe(generatorOrNext, error, complete); + } +} diff --git a/packages/devui-vue/devui/dragdrop-new/src/sortable.directive.ts b/packages/devui-vue/devui/dragdrop-new/src/sortable.directive.ts new file mode 100644 index 0000000000..67deefc5ce --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/sortable.directive.ts @@ -0,0 +1,41 @@ +import { DirectiveBinding } from 'vue'; +import { NgDirectiveBase } from './directive-base'; + +export interface ISortableBinding { + dSortableZMode?: any; + dSortable?: 'v' | 'h'; + [props: string]: any; +} + +export class SortableDirective extends NgDirectiveBase { + static INSTANCE_KEY = '__vueDevuiSortableDirectiveInstance'; + dSortDirection = 'v'; + dSortableZMode = false; + dSortable = true; + + hostBindingMap?: { [key: string]: string } | undefined = { + dSortDirection: 'dsortable', + dSortableZMode: 'd-sortable-zmode', + dSortable: 'd-sortable', + }; + inputNameMap?: { [key: string]: string } | undefined = { + dSortable: 'dSortDirection', + }; + el: { nativeElement: any } = { nativeElement: null }; + constructor(el: HTMLElement) { + super(); + this.el.nativeElement = el; + } +} + +export default { + mounted(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const sortableDirective = (el[SortableDirective.INSTANCE_KEY] = new SortableDirective(el)); + sortableDirective.setInput(binding.value); + sortableDirective.mounted(); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const sortableDirective = el[SortableDirective.INSTANCE_KEY] as SortableDirective; + sortableDirective.updateInput(binding.value, binding.oldValue!); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/sync/desc-reg.service.ts b/packages/devui-vue/devui/dragdrop-new/src/sync/desc-reg.service.ts new file mode 100644 index 0000000000..597be2ea6e --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/sync/desc-reg.service.ts @@ -0,0 +1,71 @@ +import { BehaviorSubject, Observable, Subject, Subscription, debounceTime } from 'rxjs'; +import { QueryList } from './query-list'; +import { NgDirectiveBase } from '../directive-base'; + +export class DescendantRegisterService { + protected _result: Array = []; + protected changeSubject: Subject> = new BehaviorSubject>([]); + public changes: Observable> = this.changeSubject.asObservable().pipe(debounceTime(200)); + public register(t: T) { + if (!t) { + return; + } + const index = this._result.indexOf(t); + if (index === -1) { + this._result.push(t); + this.changeSubject.next(this._result); + } + } + public unregister(t: T) { + if (!t) { + return; + } + const index = this._result.indexOf(t); + if (index > -1) { + this._result.splice(index, 1); + this.changeSubject.next(this._result); + } + } + public queryResult() { + return this._result.concat([]); + } +} + +export class DescendantChildren< + T, + I extends { [prop: string]: any } = { [prop: string]: any }, + O = { [prop: string]: (e: any) => void } +> extends NgDirectiveBase { + constructor(private drs: DescendantRegisterService) { + super(); + } + protected descendantItem: T; + ngOnInit() { + this.drs.register(this.descendantItem); + } + ngOnDestroy() { + this.drs.unregister(this.descendantItem); + } +} + +export class DescendantRoot extends QueryList { + protected sub: Subscription; + constructor(private drs: DescendantRegisterService) { + super(); + } + public on() { + if (this.sub) { + return; + } + this.reset(this.drs.queryResult()); + this.sub = this.drs.changes.subscribe((result) => { + this.reset(result); + }); + } + + public off() { + if (this.sub) { + this.sub.unsubscribe(); + } + } +} diff --git a/packages/devui-vue/devui/dragdrop-new/src/sync/drag-drop-descendant-sync.service.ts b/packages/devui-vue/devui/dragdrop-new/src/sync/drag-drop-descendant-sync.service.ts new file mode 100644 index 0000000000..6c29ab5d84 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/sync/drag-drop-descendant-sync.service.ts @@ -0,0 +1,11 @@ +import { DragSyncDirective } from './drag-sync.directive'; +import { DropSortSyncDirective } from './drop-sort-sync.directive'; +import { DescendantRegisterService } from './desc-reg.service'; +import { InjectionKey } from 'vue'; + +export class DragSyncDescendantRegisterService extends DescendantRegisterService { + static TOKEN: InjectionKey = Symbol('DRAG_SYNC_DR_SERVICE'); +} +export class DropSortSyncDescendantRegisterService extends DescendantRegisterService { + static TOKEN: InjectionKey = Symbol('DROP_SORT_SYNC_DR_SERVICE'); +} diff --git a/packages/devui-vue/devui/dragdrop-new/src/sync/drag-drop-sync-box.directive.ts b/packages/devui-vue/devui/dragdrop-new/src/sync/drag-drop-sync-box.directive.ts new file mode 100644 index 0000000000..9e99c2feed --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/sync/drag-drop-sync-box.directive.ts @@ -0,0 +1,80 @@ +import { DirectiveBinding, inject } from 'vue'; +import { Subscription } from 'rxjs'; +import { DescendantRoot } from './desc-reg.service'; +import { DragSyncDescendantRegisterService, DropSortSyncDescendantRegisterService } from './drag-drop-descendant-sync.service'; +import { DragDropSyncService } from './drag-drop-sync.service'; +import { DragSyncDirective } from './drag-sync.directive'; +import { DropSortSyncDirective } from './drop-sort-sync.directive'; +import { NgDirectiveBase } from '../directive-base'; +import { injectFromContext, provideToContext } from '../utils'; + +export class DragDropSyncBoxDirective extends NgDirectiveBase { + static INSTANCE_KEY = '__vueDevuiDragDropSyncBoxDirectiveInstance'; + sub = new Subscription(); + dragSyncList: DescendantRoot; + dropSyncList: DescendantRoot; + constructor( + private dragDropSyncService: DragDropSyncService, + private dragSyncDrs: DragSyncDescendantRegisterService, + private dropSortSyncDrs: DropSortSyncDescendantRegisterService + ) { + super(); + } + + ngOnInit() { + this.dragSyncList = new DescendantRoot(this.dragSyncDrs); + this.dropSyncList = new DescendantRoot(this.dropSortSyncDrs); + } + ngAfterViewInit() { + this.dragSyncList.on(); + this.dropSyncList.on(); + this.dragDropSyncService.updateDragSyncList(this.dragSyncList); + this.dragDropSyncService.updateDropSyncList(this.dropSyncList); + this.sub.add(this.dragSyncList.changes.subscribe((list) => this.dragDropSyncService.updateDragSyncList(list))); + this.sub.add(this.dropSyncList.changes.subscribe((list) => this.dragDropSyncService.updateDropSyncList(list))); + } + ngOnDestroy() { + if (this.sub) { + this.sub.unsubscribe(); + } + this.dragSyncList.off(); + this.dropSyncList.off(); + } +} + +export default { + created(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding<{}>, vNode) { + const context = vNode['ctx'].provides; + const dragDropSyncService = new DragDropSyncService(); + const dragSyncDrs = new DragSyncDescendantRegisterService(); + const dropSortSyncDrs = new DropSortSyncDescendantRegisterService(); + provideToContext(DragDropSyncService.TOKEN, dragDropSyncService, context); + provideToContext(DragSyncDescendantRegisterService.TOKEN, dragSyncDrs, context); + provideToContext(DropSortSyncDescendantRegisterService.TOKEN, dropSortSyncDrs, context); + }, + mounted(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding<{}>, vNode) { + const context = vNode['ctx'].provides; + const dragDropSyncService = injectFromContext(DragDropSyncService.TOKEN, context)!; + const dragSyncDrs = injectFromContext(DragSyncDescendantRegisterService.TOKEN, context)!; + const dropSortSyncDrs = injectFromContext(DropSortSyncDescendantRegisterService.TOKEN, context)!; + const dragDropSyncBoxDirective = (el[DragDropSyncBoxDirective.INSTANCE_KEY] = new DragDropSyncBoxDirective( + dragDropSyncService, + dragSyncDrs, + dropSortSyncDrs + )); + dragDropSyncBoxDirective.setInput(binding.value); + dragDropSyncBoxDirective.mounted(); + dragDropSyncBoxDirective.ngOnInit?.(); + setTimeout(() => { + dragDropSyncBoxDirective.ngAfterViewInit?.(); + }, 0); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding<{}>) { + const dragDropSyncBoxDirective = el[DragDropSyncBoxDirective.INSTANCE_KEY] as DragDropSyncBoxDirective; + dragDropSyncBoxDirective.updateInput(binding.value, binding.oldValue!); + }, + beforeUnmount(el: HTMLElement & { [props: string]: any }) { + const dragDropSyncBoxDirective = el[DragDropSyncBoxDirective.INSTANCE_KEY] as DragDropSyncBoxDirective; + dragDropSyncBoxDirective.ngOnDestroy?.(); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/sync/drag-drop-sync.service.ts b/packages/devui-vue/devui/dragdrop-new/src/sync/drag-drop-sync.service.ts new file mode 100644 index 0000000000..d1012303b7 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/sync/drag-drop-sync.service.ts @@ -0,0 +1,30 @@ +import { InjectionKey } from 'vue'; +import { DragSyncDirective } from './drag-sync.directive'; +import { DropSortSyncDirective } from './drop-sort-sync.directive'; +import { QueryList } from './query-list'; + +export class DragDropSyncService { + static TOKEN: InjectionKey = Symbol('DRAG_DROP_SYNC_SERVICE'); + dragSyncList: QueryList; + dropSortSyncList: QueryList; + + public updateDragSyncList(list: QueryList) { + this.dragSyncList = list; + } + public getDragSyncByGroup(groupName: string) { + if (groupName) { + return []; + } + return this.dragSyncList ? this.dragSyncList.filter((dragSync) => dragSync.dragSyncGroup === groupName) : []; + } + + public updateDropSyncList(list: QueryList) { + this.dropSortSyncList = list; + } + public getDropSyncByGroup(groupName: string) { + if (groupName) { + return []; + } + return this.dropSortSyncList ? this.dropSortSyncList.filter((dragSync) => dragSync.dropSyncGroup === groupName) : []; + } +} diff --git a/packages/devui-vue/devui/dragdrop-new/src/sync/drag-sync.directive.ts b/packages/devui-vue/devui/dragdrop-new/src/sync/drag-sync.directive.ts new file mode 100644 index 0000000000..31f5d5f4b5 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/sync/drag-sync.directive.ts @@ -0,0 +1,104 @@ +import { DirectiveBinding } from 'vue'; +import { Subscription } from 'rxjs'; +import { DescendantChildren } from './desc-reg.service'; +import { DragSyncDescendantRegisterService } from './drag-drop-descendant-sync.service'; +import { DragDropSyncService } from './drag-drop-sync.service'; +import { DragDropService } from '../drag-drop.service'; +import { DraggableDirective } from '../draggable.directive'; +import { injectFromContext } from '../utils'; + +export interface IDragSyncBinding { + dragSync?: string; + dragSyncGroup?: string; + [props: string]: any; +} +export class DragSyncDirective extends DescendantChildren { + static INSTANCE_KEY = '__vueDevuiDragSyncDirectiveInstance'; + inputNameMap?: { [key: string]: string } | undefined = { + dragSync: 'dragSyncGroup', // 保持原有api可以正常使用 + dragSyncGroup: 'dragSyncGroup', // 别名,更好理解 + }; + dragSyncGroup = ''; + subscription: Subscription = new Subscription(); + syncGroupDirectives?: Array; + public el: { nativeElement: any } = { nativeElement: null }; + constructor( + el: HTMLElement, + // @Optional() @Self() + private draggable: DraggableDirective, + private dragDropSyncService: DragDropSyncService, + private dragDropService: DragDropService, + dragSyncDrs: DragSyncDescendantRegisterService + ) { + super(dragSyncDrs); + this.el.nativeElement = el; + this.descendantItem = this; + } + + ngOnInit() { + if (this.draggable) { + this.subscription.add(this.draggable.dragElShowHideEvent.subscribe(this.subDragElEvent)); + this.subscription.add( + this.draggable.beforeDragStartEvent.subscribe(() => { + this.syncGroupDirectives = this.dragDropSyncService + .getDragSyncByGroup(this.dragSyncGroup) + .filter((directive) => directive !== this); + this.dragDropService.dragSyncGroupDirectives = this.syncGroupDirectives; + }) + ); + this.subscription.add( + this.draggable.dropEndEvent.subscribe(() => { + this.dragDropService.dragSyncGroupDirectives = undefined; + this.syncGroupDirectives = undefined; + }) + ); + } + super.ngOnInit(); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + super.ngOnDestroy(); + } + + subDragElEvent = (bool: boolean) => { + this.syncGroupDirectives!.forEach((dir) => this.renderDisplay(dir.el.nativeElement, bool)); + }; + + renderDisplay(nativeEl: HTMLElement, bool: boolean) { + nativeEl.style.display = bool ? '' : 'none'; + } +} + +export default { + mounted(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding, vNode) { + const context = vNode['ctx'].provides; + let draggable = injectFromContext(DraggableDirective.TOKEN, context) as DraggableDirective | undefined; + if (draggable?.el.nativeElement !== el) { + draggable = undefined; + } + const dragDropSyncService = injectFromContext(DragDropSyncService.TOKEN, context) as DragDropSyncService; + const dragDropService = injectFromContext(DragDropService.TOKEN, context) as DragDropService; + const dragSyncDrs = injectFromContext(DragSyncDescendantRegisterService.TOKEN, context) as DragSyncDescendantRegisterService; + const dragSyncDirective = (el[DragSyncDirective.INSTANCE_KEY] = new DragSyncDirective( + el, + draggable!, + dragDropSyncService, + dragDropService, + dragSyncDrs + )); + dragSyncDirective.setInput(binding.value); + dragSyncDirective.mounted(); + dragSyncDirective.ngOnInit?.(); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const dragSyncDirective = el[DragSyncDirective.INSTANCE_KEY] as DragSyncDirective; + dragSyncDirective.updateInput(binding.value, binding.oldValue!); + }, + beforeUnmount(el: HTMLElement & { [props: string]: any }) { + const dragSyncDirective = el[DragSyncDirective.INSTANCE_KEY] as DragSyncDirective; + dragSyncDirective.ngOnDestroy?.(); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/sync/drop-sort-sync.directive.ts b/packages/devui-vue/devui/dragdrop-new/src/sync/drop-sort-sync.directive.ts new file mode 100644 index 0000000000..343f38a3c9 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/sync/drop-sort-sync.directive.ts @@ -0,0 +1,147 @@ +import { DirectiveBinding } from 'vue'; +import { Subscription } from 'rxjs'; +import { DescendantChildren } from './desc-reg.service'; +import { DropSortSyncDescendantRegisterService } from './drag-drop-descendant-sync.service'; +import { DragDropSyncService } from './drag-drop-sync.service'; +import { Utils, injectFromContext } from '../utils'; +import { DroppableDirective, type DragPlaceholderInsertionEvent, type DragPlaceholderInsertionIndexEvent } from '../droppable.directive'; + +export interface IDropSortSyncBinding { + dropSortSync?: string; + dropSyncGroup?: string; + dropSyncDirection?: 'v' | 'h'; + [props: string]: any; +} +export class DropSortSyncDirective extends DescendantChildren { + static INSTANCE_KEY = '__vueDevuiDropSortSyncDirectiveInstance'; + inputNameMap?: { [key: string]: string } | undefined = { + dropSortSync: 'dropSyncGroup', + dropSyncGroup: 'dropSyncGroup', + dropSyncDirection: 'direction', + }; + dropSyncGroup = ''; + direction: 'v' | 'h' = 'v'; // 与sortContainer正交的方向 + subscription: Subscription = new Subscription(); + syncGroupDirectives: Array; + placeholder: HTMLElement; + sortContainer: HTMLElement; + public el: { nativeElement: any } = { nativeElement: null }; + constructor( + el: HTMLElement, + // @Optional() @Self() + private droppable: DroppableDirective, + private dragDropSyncService: DragDropSyncService, + dropSortSyncDrs: DropSortSyncDescendantRegisterService + ) { + super(dropSortSyncDrs); + this.el.nativeElement = el; + this.descendantItem = this; + } + + ngOnInit() { + this.sortContainer = this.el.nativeElement; + if (this.droppable) { + this.sortContainer = this.droppable.getSortContainer(); + this.subscription.add(this.droppable.placeholderInsertionEvent.subscribe(this.subInsertionEvent)); + this.subscription.add(this.droppable.placeholderRenderEvent.subscribe(this.subRenderEvent)); + } + super.ngOnInit(); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + super.ngOnDestroy(); + } + subRenderEvent = (nativeStyle: { width: number; height: number }) => { + this.syncGroupDirectives = this.dragDropSyncService.getDropSyncByGroup(this.dropSyncGroup).filter((directive) => directive !== this); + this.syncGroupDirectives.forEach((dir) => { + dir.renderPlaceholder(nativeStyle, this.droppable); + }); + }; + + subInsertionEvent = (cmd: DragPlaceholderInsertionIndexEvent) => { + this.syncGroupDirectives = this.dragDropSyncService.getDropSyncByGroup(this.dropSyncGroup).filter((directive) => directive !== this); + this.syncGroupDirectives.forEach((dir) => { + dir.insertPlaceholderCommand({ + command: cmd.command, + container: dir.sortContainer, + relatedEl: dir.getChildrenElByIndex(dir.sortContainer, cmd.index)!, + }); + }); + }; + getChildrenElByIndex(target, index?) { + if (index === undefined || (target && target.children && target.children.length < index) || index < 0) { + return null; + } + return this.sortContainer.children.item(index); + } + + renderPlaceholder(nativeStyle: { width: number; height: number }, droppable) { + if (!this.placeholder) { + this.placeholder = document.createElement(droppable.placeholderTag); + this.placeholder.className = 'drag-placeholder'; + this.placeholder.classList.add('drag-sync-placeholder'); + this.placeholder.innerText = droppable.placeholderText; + } + const { width, height } = nativeStyle; + if (this.direction === 'v') { + this.placeholder.style.width = width + 'px'; + this.placeholder.style.height = this.sortContainer.getBoundingClientRect().height + 'px'; + } else { + this.placeholder.style.height = height + 'px'; + this.placeholder.style.width = this.sortContainer.getBoundingClientRect().width + 'px'; + } + Utils.addElStyles(this.placeholder, droppable.placeholderStyle); + } + + insertPlaceholderCommand(cmd: DragPlaceholderInsertionEvent) { + if (cmd.command === 'insertBefore' && cmd.container) { + cmd.container.insertBefore(this.placeholder, cmd.relatedEl!); + return; + } + if (cmd.command === 'append' && cmd.container) { + cmd.container.appendChild(this.placeholder); + return; + } + if (cmd.command === 'remove' && cmd.container) { + if (cmd.container.contains(this.placeholder)) { + cmd.container.removeChild(this.placeholder); + } + return; + } + } +} + +export default { + mounted(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding, vNode) { + const context = vNode['ctx'].provides; + let droppable = injectFromContext(DroppableDirective.TOKEN, context) as DroppableDirective | undefined; + if (droppable?.el.nativeElement !== el) { + droppable = undefined; + } + const dragDropSyncService = injectFromContext(DragDropSyncService.TOKEN, context) as DragDropSyncService; + const dropSortSyncDrs = injectFromContext( + DropSortSyncDescendantRegisterService.TOKEN, + context + ) as DropSortSyncDescendantRegisterService; + const dropSortSyncDirective = (el[DropSortSyncDirective.INSTANCE_KEY] = new DropSortSyncDirective( + el, + droppable!, + dragDropSyncService, + dropSortSyncDrs + )); + dropSortSyncDirective.setInput(binding.value); + dropSortSyncDirective.mounted(); + dropSortSyncDirective.ngOnInit?.(); + }, + updated(el: HTMLElement & { [props: string]: any }, binding: DirectiveBinding) { + const dropSortSyncDirective = el[DropSortSyncDirective.INSTANCE_KEY] as DropSortSyncDirective; + dropSortSyncDirective.updateInput(binding.value, binding.oldValue!); + }, + beforeUnmount(el: HTMLElement & { [props: string]: any }) { + const dropSortSyncDirective = el[DropSortSyncDirective.INSTANCE_KEY] as DropSortSyncDirective; + dropSortSyncDirective.ngOnDestroy?.(); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/sync/index.ts b/packages/devui-vue/devui/dragdrop-new/src/sync/index.ts new file mode 100644 index 0000000000..95976f9fb6 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/sync/index.ts @@ -0,0 +1,23 @@ +import { App } from 'vue'; +import { default as DragDropSyncBox } from './drag-drop-sync-box.directive'; +import { default as DragSync } from './drag-sync.directive'; +import { default as DropSortSync } from './drop-sort-sync.directive'; + +export * from './desc-reg.service'; +export * from './drag-drop-descendant-sync.service'; +export * from './drag-drop-sync.service'; +export * from './drag-drop-sync-box.directive'; +export * from './drag-sync.directive'; +export * from './drop-sort-sync.directive'; + +export { DragDropSyncBox, DragSync, DropSortSync }; + +export default { + title: 'DragDropSync 拖拽同步', + category: '基础组件', + install(app: App): void { + app.directive('dDragDropSyncBox', DragDropSyncBox); + app.directive('dDragSync', DragSync); + app.directive('dDropSortSync', DropSortSync); + }, +}; diff --git a/packages/devui-vue/devui/dragdrop-new/src/sync/query-list.ts b/packages/devui-vue/devui/dragdrop-new/src/sync/query-list.ts new file mode 100644 index 0000000000..4b9c270099 --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/sync/query-list.ts @@ -0,0 +1,115 @@ +import { Observable } from 'rxjs'; +import { EventEmitter } from '../preserve-next-event-emitter'; + +/** + * Flattens an array. + */ +export function flatten(list: any[], dst?: any[]): any[] { + if (dst === undefined) dst = list; + for (let i = 0; i < list.length; i++) { + let item = list[i]; + if (Array.isArray(item)) { + // we need to inline it. + if (dst === list) { + // Our assumption that the list was already flat was wrong and + // we need to clone flat since we need to write to it. + dst = list.slice(0, i); + } + flatten(item, dst); + } else if (dst !== list) { + dst.push(item); + } + } + return dst; +} + +function symbolIterator(this: QueryList): Iterator { + return ((this as any as { _results: Array })._results as any)[Symbol.iterator](); +} + +export class QueryList implements Iterable { + public readonly dirty = true; + private _results: Array = []; + public readonly changes: Observable = new EventEmitter(); + + readonly length: number = 0; + readonly first!: T; + readonly last!: T; + + constructor() { + const symbol = Symbol.iterator; + const proto = QueryList.prototype as any; + if (!proto[symbol]) proto[symbol] = symbolIterator; + } + + // proxy array method to property '_results' + map(fn: (item: T, index: number, array: T[]) => U): U[] { + return this._results.map(fn); + } + filter(fn: (item: T, index: number, array: T[]) => boolean): T[] { + return this._results.filter(fn); + } + find(fn: (item: T, index: number, array: T[]) => boolean): T | undefined { + return this._results.find(fn); + } + reduce(fn: (prevValue: U, curValue: T, curIndex: number, array: T[]) => U, init: U): U { + return this._results.reduce(fn, init); + } + forEach(fn: (item: T, index: number, array: T[]) => void): void { + this._results.forEach(fn); + } + some(fn: (value: T, index: number, array: T[]) => boolean): boolean { + return this._results.some(fn); + } + + /** + * Returns a copy of the internal results list as an Array. + */ + toArray(): T[] { + return this._results.slice(); + } + + toString(): string { + return this._results.toString(); + } + + /** + * Updates the stored data of the query list, and resets the `dirty` flag to `false`, so that + * on change detection, it will not notify of changes to the queries, unless a new change + * occurs. + * + * @param resultsTree The query results to store + */ + reset(resultsTree: Array): void { + this._results = flatten(resultsTree); + (this as { dirty: boolean }).dirty = false; + (this as { length: number }).length = this._results.length; + (this as { last: T }).last = this._results[this.length - 1]; + (this as { first: T }).first = this._results[0]; + } + + /** + * Triggers a change event by emitting on the `changes` {@link EventEmitter}. + */ + notifyOnChanges(): void { + (this.changes as EventEmitter).emit(this); + } + + /** internal */ + setDirty() { + (this as { dirty: boolean }).dirty = true; + } + + /** internal */ + destroy(): void { + (this.changes as EventEmitter).complete(); + (this.changes as EventEmitter).unsubscribe(); + } + + // The implementation of `Symbol.iterator` should be declared here, but this would cause + // tree-shaking issues with `QueryList. So instead, it's added in the constructor (see comments + // there) and this declaration is left here to ensure that TypeScript considers QueryList to + // implement the Iterable interface. This is required for template type-checking of NgFor loops + // over QueryLists to work correctly, since QueryList must be assignable to NgIterable. + [Symbol.iterator]!: () => Iterator; +} diff --git a/packages/devui-vue/devui/dragdrop-new/src/touch-support/dragdrop-touch.ts b/packages/devui-vue/devui/dragdrop-new/src/touch-support/dragdrop-touch.ts new file mode 100644 index 0000000000..ac88e9a12c --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/touch-support/dragdrop-touch.ts @@ -0,0 +1,576 @@ +/** + * 2020.03.23-Modified from https://github.com/Bernardo-Castilho/dragdroptouch, license: MIT,reason:Converting .js file to .ts file + */ +export class DragDropTouch { + static readonly THRESHOLD = 5; // pixels to move before drag starts + static readonly OPACITY = 0.5; // drag image opacity + static readonly DBLCLICK = 500; // max ms between clicks in a double click + static readonly DRAG_OVER_TIME = 300; // interval ms when drag over + static readonly CTX_MENU = 900; // ms to hold before raising 'contextmenu' event + static readonly IS_PRESS_HOLD_MODE = true; // decides of press & hold mode presence + static readonly PRESS_HOLD_AWAIT = 400; // ms to wait before press & hold is detected + static readonly PRESS_HOLD_MARGIN = 25; // pixels that finger might shiver while pressing + static readonly PRESS_HOLD_THRESHOLD = 0; // pixels to move before drag starts + static readonly DRAG_HANDLE_ATTR = 'data-drag-handle-selector'; + + static readonly rmvAttrs = 'id,class,style,draggable'.split(','); + static readonly kbdProps = 'altKey,ctrlKey,metaKey,shiftKey'.split(','); + static readonly ptProps = 'pageX,pageY,clientX,clientY,screenX,screenY'.split(','); + private static instance: DragDropTouch | null = null; + + dataTransfer: DataTransfer; + lastClick = 0; + lastTouch: TouchEvent | null; + // touched element + lastTarget: HTMLElement | null; + // touched draggable element + dragSource: HTMLElement | null; + ptDown: { x: number; y: number } | null; + isDragEnabled: boolean; + isDropZone: boolean; + pressHoldInterval; + img; + imgCustom; + imgOffset; + // for continual drag over event even touch point stop at a certain point for a while. + dragoverTimer; + // for bind touch move and touch end event to touch target incase virtual scroll cause + // document no longer get capture/ bubble of touchmove event from dom removed from document tree + touchTarget?: EventTarget; + touchmoveListener: EventListener; + touchendListener: EventListener; + listenerOpt: boolean | EventListenerOptions; + + constructor() { + // enforce singleton pattern + if (DragDropTouch.instance) { + throw new Error('DragDropTouch instance already created.'); + } + // detect passive event support + // https://github.com/Modernizr/Modernizr/issues/1894 + let supportsPassive = false; + if (typeof document !== 'undefined') { + document.addEventListener('test', () => {}, { + get passive() { + supportsPassive = true; + return true; + }, + }); + // listen to touch events + if (DragDropTouch.isTouchDevice()) { + // 能响应触摸事件 + const d = document; + const ts = this.touchstart; + const tmod = this.touchmoveOnDocument; + const teod = this.touchendOnDocument; + const opt = supportsPassive ? { passive: false, capture: false } : false; + const optPassive = supportsPassive ? { passive: true } : false; + d.addEventListener('touchstart', ts, opt); + d.addEventListener('touchmove', tmod, optPassive); + d.addEventListener('touchend', teod); + d.addEventListener('touchcancel', teod); + this.touchmoveListener = this.touchmove as EventListener; + this.touchendListener = this.touchend as EventListener; + this.listenerOpt = opt; + } + } + } + /** + * Gets a reference to the @see:DragDropTouch singleton. + */ + static getInstance() { + if (!DragDropTouch.instance) { + DragDropTouch.instance = new DragDropTouch(); + } + return DragDropTouch.instance; + } + static isTouchDevice() { + if (typeof window === 'undefined' || typeof document === 'undefined') { + return false; + } + const d: Document = document; + const w: Window = window; + let bool; + if ( + 'ontouchstart' in d || // normal mobile device + 'ontouchstart' in w || + navigator.maxTouchPoints > 0 || + navigator['msMaxTouchPoints'] > 0 || + (window['DocumentTouch'] && document instanceof window['DocumentTouch']) + ) { + bool = true; + } else { + const fakeBody = document.createElement('fakebody'); + fakeBody.innerHTML += ` + `; + document.documentElement.appendChild(fakeBody); + const touchTestNode = document.createElement('div'); + touchTestNode.id = 'touch_test'; + fakeBody.appendChild(touchTestNode); + bool = touchTestNode.offsetTop === 42; + fakeBody.parentElement?.removeChild(fakeBody); + } + return bool; + } + // ** event listener binding + bindTouchmoveTouchend(e: TouchEvent) { + this.touchTarget = e.target!; + (e.target as Node).addEventListener('touchmove', this.touchmoveListener, this.listenerOpt); + (e.target as Node).addEventListener('touchend', this.touchendListener); + (e.target as Node).addEventListener('touchcancel', this.touchendListener); + } + removeTouchmoveTouchend() { + if (this.touchTarget) { + this.touchTarget.removeEventListener('touchmove', this.touchmoveListener); + this.touchTarget.removeEventListener('touchend', this.touchendListener); + this.touchTarget.removeEventListener('touchcancel', this.touchendListener); + this.touchTarget = undefined; + } + } + // ** event handlers + touchstart = (e: TouchEvent) => { + if (this.shouldHandle(e)) { + // raise double-click and prevent zooming + if (Date.now() - this.lastClick < DragDropTouch.DBLCLICK) { + if (this.dispatchEvent(e, 'dblclick', e.target)) { + e.preventDefault(); + this.reset(); + return; + } + } + // clear all variables + this.reset(); + // get nearest draggable element + const src = this.closestDraggable(e.target); + if (src) { + this.dragSource = src; + this.ptDown = this.getPoint(e); + this.lastTouch = e; + if (DragDropTouch.IS_PRESS_HOLD_MODE) { + this.pressHoldInterval = setTimeout(() => { + this.bindTouchmoveTouchend(e); + this.isDragEnabled = true; + this.touchmove(e); + }, DragDropTouch.PRESS_HOLD_AWAIT); + } else { + e.preventDefault(); + this.bindTouchmoveTouchend(e); + } + } + } + }; + touchmoveOnDocument = (e) => { + if (this.shouldCancelPressHoldMove(e)) { + this.reset(); + return; + } + }; + touchmove = (e: TouchEvent) => { + if (this.shouldCancelPressHoldMove(e)) { + this.reset(); + return; + } + if (this.shouldHandleMove(e) || this.shouldHandlePressHoldMove(e)) { + const target = this.getTarget(e); + // start dragging + if (this.dragSource && !this.img && this.shouldStartDragging(e)) { + this.dispatchEvent(e, 'dragstart', this.dragSource); + this.createImage(e); + } + // continue dragging + if (this.img) { + this.clearDragoverInterval(); + this.lastTouch = e; + e.preventDefault(); // prevent scrolling + if (target !== this.lastTarget) { + // according to drag drop implementation of the browser, dragenterB is supposed to fired before dragleaveA + this.dispatchEvent(e, 'dragenter', target); + this.dispatchEvent(this.lastTouch, 'dragleave', this.lastTarget); + this.lastTarget = target; + } + this.moveImage(e); + this.isDropZone = this.dispatchEvent(e, 'dragover', target); + // should continue dispatch dragover event when touch position stay still + this.setDragoverInterval(e); + } + } + }; + touchendOnDocument = (e) => { + if (this.shouldHandle(e)) { + if (!this.img) { + this.dragSource = null; + this.lastClick = Date.now(); + } + // finish dragging + this.destroyImage(); + if (this.dragSource) { + this.reset(); + } + } + }; + touchend = (e) => { + if (this.shouldHandle(e)) { + // user clicked the element but didn't drag, so clear the source and simulate a click + if (!this.img) { + this.dragSource = null; + // browser will dispatch click event after trigger touchend, since touchstart didn't preventDefault + this.lastClick = Date.now(); + } + // finish dragging + this.destroyImage(); + if (this.dragSource) { + if (e.type.indexOf('cancel') < 0 && this.isDropZone) { + this.dispatchEvent(this.lastTouch, 'drop', this.lastTarget); + } + this.dispatchEvent(this.lastTouch, 'dragend', this.dragSource); + this.reset(); + } + } + }; + // ** utilities + // ignore events that have been handled or that involve more than one touch + shouldHandle(e) { + return e && !e.defaultPrevented && e.touches && e.touches.length < 2; + } + // use regular condition outside of press & hold mode + shouldHandleMove(e) { + return !DragDropTouch.IS_PRESS_HOLD_MODE && this.shouldHandle(e); + } + // allow to handle moves that involve many touches for press & hold + shouldHandlePressHoldMove(e) { + return DragDropTouch.IS_PRESS_HOLD_MODE && this.isDragEnabled && e && e.touches && e.touches.length; + } + // reset data if user drags without pressing & holding + shouldCancelPressHoldMove(e) { + return DragDropTouch.IS_PRESS_HOLD_MODE && !this.isDragEnabled && this.getDelta(e) > DragDropTouch.PRESS_HOLD_MARGIN; + } + // start dragging when mouseover element matches drag handler selector and specified delta is detected + shouldStartDragging(e) { + const dragHandleSelector = this.getDragHandle(); + // start dragging when mouseover element matches drag handler selector + if (dragHandleSelector && !this.matchSelector(e.target, dragHandleSelector)) { + return false; + } + // start dragging when specified delta is detected + const delta = this.getDelta(e); + return delta > DragDropTouch.THRESHOLD || (DragDropTouch.IS_PRESS_HOLD_MODE && delta >= DragDropTouch.PRESS_HOLD_THRESHOLD); + } + // find drag handler selector for dragstart only with partial element + getDragHandle() { + if (this.dragSource) { + return this.dragSource.getAttribute(DragDropTouch.DRAG_HANDLE_ATTR) || ''; + } + return ''; + } + // test if element matches selector + matchSelector(element, selector) { + if (selector) { + const proto = Element.prototype; + const func = + proto['matches'] || + proto['matchesSelector'] || + proto['mozMatchesSelector'] || + proto['msMatchesSelector'] || + proto['oMatchesSelector'] || + proto['webkitMatchesSelector'] || + function (s) { + const matches = (this.document || this.ownerDocument).querySelectorAll(s); + let i = matches.length; + while (--i >= 0 && matches.item(i) !== this) { + // do nothing + } + return i > -1; + }; + return func.call(element, selector); + } + return true; + } + // clear all members + reset() { + this.removeTouchmoveTouchend(); + this.destroyImage(); + this.dragSource = null; + this.lastTouch = null; + this.lastTarget = null; + this.ptDown = null; + this.isDragEnabled = false; + this.isDropZone = false; + this.dataTransfer = new DragDropTouch.DataTransfer(); + clearInterval(this.pressHoldInterval); + this.clearDragoverInterval(); + } + // get point for a touch event + getPoint(e, page?) { + if (e && e.touches) { + e = e.touches[0]; + } + return { x: page ? e.pageX : e.clientX, y: page ? e.pageY : e.clientY }; + } + // get distance between the current touch event and the first one + getDelta(e) { + if (DragDropTouch.IS_PRESS_HOLD_MODE && !this.ptDown) { + return 0; + } + const p = this.getPoint(e); + return Math.abs(p.x - this.ptDown!.x) + Math.abs(p.y - this.ptDown!.y); + } + // get the element at a given touch event + getTarget(e: TouchEvent) { + const pt = this.getPoint(e); + let el = document.elementFromPoint(pt.x, pt.y); + while (el && getComputedStyle(el).pointerEvents === 'none') { + el = el.parentElement; + } + return el; + } + // create drag image from source element + createImage(e) { + // just in case... + if (this.img) { + this.destroyImage(); + } + // create drag image from custom element or drag source + const src = this.imgCustom || this.dragSource; + this.img = src.cloneNode(true); + this.copyStyle(src, this.img); + this.img.style.top = this.img.style.left = '-9999px'; + // if creating from drag source, apply offset and opacity + if (!this.imgCustom) { + const rc = src.getBoundingClientRect(); + const pt = this.getPoint(e); + this.imgOffset = { x: pt.x - rc.left, y: pt.y - rc.top }; + this.img.style.opacity = DragDropTouch.OPACITY.toString(); + } + // add image to document + this.moveImage(e); + document.body.appendChild(this.img); + } + // dispose of drag image element + destroyImage() { + if (this.img && this.img.parentElement) { + this.img.parentElement.removeChild(this.img); + } + this.img = null; + this.imgCustom = null; + } + // move the drag image element + moveImage(e) { + requestAnimationFrame(() => { + if (this.img) { + const pt = this.getPoint(e, true); + const s = this.img.style; + s.position = 'absolute'; + s.pointerEvents = 'none'; + s.zIndex = '999999'; + s.left = Math.round(pt.x - this.imgOffset.x) + 'px'; + s.top = Math.round(pt.y - this.imgOffset.y) + 'px'; + } + }); + } + // copy properties from an object to another + copyProps(dst, src, props) { + for (let i = 0; i < props.length; i++) { + const p = props[i]; + dst[p] = src[p]; + } + } + // copy styles/attributes from drag source to drag image element + copyStyle(src, dst) { + // remove potentially troublesome attributes + DragDropTouch.rmvAttrs.forEach(function (att) { + dst.removeAttribute(att); + }); + // copy canvas content + if (src instanceof HTMLCanvasElement) { + const canSrc = src; + const canDst = dst; + canDst.width = canSrc.width; + canDst.height = canSrc.height; + canDst.getContext('2d').drawImage(canSrc, 0, 0); + } + // copy canvas content for nested canvas element + const srcCanvases = src.querySelectorAll('canvas'); + if (srcCanvases.length > 0) { + const dstCanvases = dst.querySelectorAll('canvas'); + for (let i = 0; i < dstCanvases.length; i++) { + const cSrc = srcCanvases[i]; + const cDst = dstCanvases[i]; + cDst.getContext('2d').drawImage(cSrc, 0, 0); + } + } + // copy style (without transitions) + const cs = getComputedStyle(src); + for (let i = 0; i < cs.length; i++) { + const key = cs[i]; + if (key.indexOf('transition') < 0) { + dst.style[key] = cs[key]; + } + } + dst.style.pointerEvents = 'none'; + // and repeat for all children + for (let i = 0; i < src.children.length; i++) { + this.copyStyle(src.children[i], dst.children[i]); + } + } + // synthesize and dispatch an event + // returns true if the event has been handled (e.preventDefault == true) + dispatchEvent(e, type, target) { + if (e && target) { + const evt = document.createEvent('Event'); + const t = e.touches ? e.touches[0] : e; + evt.initEvent(type, true, true); + const obj = { + button: 0, + which: 0, + buttons: 1, + dataTransfer: this.dataTransfer, + }; + this.copyProps(evt, e, DragDropTouch.kbdProps); + this.copyProps(evt, t, DragDropTouch.ptProps); + this.copyProps(evt, { fromTouch: true }, ['fromTouch']); // mark as from touch event + this.copyProps(evt, obj, Object.keys(obj)); + + target.dispatchEvent(evt); + return evt.defaultPrevented; + } + return false; + } + // gets an element's closest draggable ancestor + closestDraggable(e) { + for (; e; e = e.parentElement) { + if (e.hasAttribute('draggable') && e.draggable) { + return e; + } + } + return null; + } + // repeat dispatch dragover event when touch point stay still + setDragoverInterval(e) { + this.dragoverTimer = setInterval(() => { + const target = this.getTarget(e); + if (target !== this.lastTarget) { + this.dispatchEvent(e, 'dragenter', target); + this.dispatchEvent(e, 'dragleave', this.lastTarget); + this.lastTarget = target; + } + this.isDropZone = this.dispatchEvent(e, 'dragover', target); + }, DragDropTouch.DRAG_OVER_TIME); + } + clearDragoverInterval() { + if (this.dragoverTimer) { + clearInterval(this.dragoverTimer); + this.dragoverTimer = undefined; + } + } +} +/* eslint-disable-next-line @typescript-eslint/no-namespace */ +export namespace DragDropTouch { + /** + * Object used to hold the data that is being dragged during drag and drop operations. + * + * It may hold one or more data items of different types. For more information about + * drag and drop operations and data transfer objects, see + * HTML Drag and Drop API. + * + * This object is created automatically by the @see:DragDropTouch singleton and is + * accessible through the @see:dataTransfer property of all drag events. + */ + export class DataTransfer implements DataTransfer { + files; + items; + private _data; + /** + * Gets or sets the type of drag-and-drop operation currently selected. + * The value must be 'none', 'copy', 'link', or 'move'. + */ + private _dropEffect; + get dropEffect() { + return this._dropEffect; + } + set dropEffect(value) { + this._dropEffect = value; + } + /** + * Gets or sets the types of operations that are possible. + * Must be one of 'none', 'copy', 'copyLink', 'copyMove', 'link', + * 'linkMove', 'move', 'all' or 'uninitialized'. + */ + private _effectAllowed; + get effectAllowed() { + return this._effectAllowed; + } + set effectAllowed(value) { + this._effectAllowed = value; + } + /** + * Gets an array of strings giving the formats that were set in the @see:dragstart event. + */ + private _types; + get types() { + return Object.keys(this._data); + } + + constructor() { + this._dropEffect = 'move'; + this._effectAllowed = 'all'; + this._data = {}; + } + /** + * Removes the data associated with a given type. + * + * The type argument is optional. If the type is empty or not specified, the data + * associated with all types is removed. If data for the specified type does not exist, + * or the data transfer contains no data, this method will have no effect. + * + * @param type Type of data to remove. + */ + clearData(type) { + if (type !== null) { + delete this._data[type]; + } else { + this._data = null; + } + } + /** + * Retrieves the data for a given type, or an empty string if data for that type does + * not exist or the data transfer contains no data. + * + * @param type Type of data to retrieve. + */ + getData(type) { + return this._data[type] || ''; + } + + /** + * Set the data for a given type. + * + * For a list of recommended drag types, please see + * https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Recommended_Drag_Types. + * + * @param type Type of data to add. + * @param value Data to add. + */ + setData(type, value) { + this._data[type] = value; + } + /** + * Set the image to be used for dragging if a custom one is desired. + * + * @param img An image element to use as the drag feedback image. + * @param offsetX The horizontal offset within the image. + * @param offsetY The vertical offset within the image. + */ + setDragImage(img, offsetX, offsetY) { + const ddt = DragDropTouch.getInstance(); + ddt.imgCustom = img; + ddt.imgOffset = { x: offsetX, y: offsetY }; + } + } +} diff --git a/packages/devui-vue/devui/dragdrop-new/src/utils.ts b/packages/devui-vue/devui/dragdrop-new/src/utils.ts new file mode 100644 index 0000000000..328d7c85de --- /dev/null +++ b/packages/devui-vue/devui/dragdrop-new/src/utils.ts @@ -0,0 +1,168 @@ +interface ElementRef { + nativeElement?: any; +} + +export class Utils { + /** + * Polyfill for element.matches. + * See: https://developer.mozilla.org/en/docs/Web/API/Element/matches#Polyfill + * element + */ + public static matches(element: any, selectorName: string): boolean { + const proto: any = Element.prototype; + + const func = + proto['matches'] || + proto.matchesSelector || + proto.mozMatchesSelector || + proto.msMatchesSelector || + proto.oMatchesSelector || + proto.webkitMatchesSelector || + function (s: string) { + const matches = (this.document || this.ownerDocument).querySelectorAll(s); + let i = matches.length; + while (--i >= 0 && matches.item(i) !== this) { + // do nothing + } + return i > -1; + }; + + return func.call(element, selectorName); + } + + /** + * Applies the specified css class on nativeElement + * elementRef + * className + */ + public static addClass(elementRef: ElementRef | any, className: string) { + if (className === undefined) { + return; + } + const e = this.getElementWithValidClassList(elementRef); + + if (e) { + e.classList.add(className); + } + } + + /** + * Removes the specified class from nativeElement + * elementRef + * className + */ + public static removeClass(elementRef: ElementRef | any, className: string) { + if (className === undefined) { + return; + } + const e = this.getElementWithValidClassList(elementRef); + + if (e) { + e.classList.remove(className); + } + } + + /** + * Gets element with valid classList + * + * elementRef + * @returns ElementRef | null + */ + private static getElementWithValidClassList(elementRef: ElementRef) { + const e = elementRef.nativeElement ? elementRef.nativeElement : elementRef; + + if (e.classList !== undefined && e.classList !== null) { + return e; + } + + return null; + } + + public static slice(args: T[], slice?: number, sliceEnd?: number): T[] { + const ret: T[] = []; + let len = args.length; + + if (len === 0) { + return ret; + } + + const start = (slice as number) < 0 ? Math.max(0, slice! + len) : slice || 0; + + if (sliceEnd !== undefined) { + len = sliceEnd < 0 ? sliceEnd + len : sliceEnd; + } + + while (len-- > start) { + ret[len - start] = args[len]; + } + return ret; + } + + // 动态添加styles + public static addElStyles(el: any, styles: any) { + if (styles instanceof Object) { + for (const s in styles) { + if (Object.prototype.hasOwnProperty.call(styles, s)) { + if (Array.isArray(styles[s])) { + // 用于支持兼容渐退 + styles[s].forEach((val: string) => { + el.style[s] = val; + }); + } else { + el.style[s] = styles[s]; + } + } + } + } + } + public static dispatchEventToUnderElement(event: DragEvent, target?: HTMLElement, eventType?: string) { + const up = target || event.target; + up.style.display = 'none'; + const { x, y } = { x: event.clientX, y: event.clientY }; + const under = document.elementFromPoint(x, y); + up.style.display = ''; + if (!under) { + return event; + } + const ev = document.createEvent('DragEvent'); + ev.initMouseEvent( + eventType || event.type, + true, + true, + window, + 0, + event.screenX, + event.screenY, + event.clientX, + event.clientY, + event.ctrlKey, + event.altKey, + event.shiftKey, + event.metaKey, + event.button, + event.relatedTarget + ); + if (ev.dataTransfer !== null) { + ev.dataTransfer.setData('text', ''); + ev.dataTransfer.effectAllowed = event.dataTransfer!.effectAllowed; + } + setTimeout(() => { + under.dispatchEvent(ev); + }, 0); + return event; + } +} + +import { getCurrentInstance, InjectionKey } from 'vue'; + +export function injectFromContext(token: InjectionKey | string, context: any): T | undefined { + return context[token as symbol | string]; +} + +export function getContext(): any { + return (getCurrentInstance() as unknown as { provides: any }).provides; +} + +export function provideToContext(token: InjectionKey | string, value: T, context: any) { + context[token as symbol | string] = value; +} diff --git a/packages/devui-vue/docs/components/dragdrop-new/index.md b/packages/devui-vue/docs/components/dragdrop-new/index.md new file mode 100644 index 0000000000..51df5b34e4 --- /dev/null +++ b/packages/devui-vue/docs/components/dragdrop-new/index.md @@ -0,0 +1,3036 @@ +# DragDrop 2.0 拖拽 + +#### 何时使用 + +拖拽组件 + +### 基本用法 + +:::demo 从一个container拖动到另外一个container,并支持排序。 + +```vue + + + + +``` + +::: + +### 多层树状拖拽 + +:::demo 排序允许拖拽到元素上,支持层级嵌套。 + +```vue + + + +``` + +::: + +### 拖拽实体元素跟随 + +:::demo 允许拖拽时候非半透明元素跟随。也可以使用appendToBody:当拖拽离开后源位置的父对象会被销毁的话,需要把克隆体附着到body上防止被销毁。 默认会通过复制样式保证克隆到body的实体的样式是正确的,但部分深度依赖DOM节点位置的样式和属性可能会失败,需要手动调整部分样式。 + +```vue + + + +``` + +::: + +### 越边交换 + +:::demo 设置switchWhileCrossEdge允许越过边缘的时候交换。注意:不可与dropOnItem一起用,dropOnItem为true的时候无效。 + +```vue + + + +``` + +::: + +### 外部放置位置:就近,前面,后面 + +:::demo 使用defaultDropPostion配置排序器之外的区域拖拽元素放下的时候默认加到列表的前面或者后面,默认为就近('closest')。 + +```vue + + + +``` + +::: + +### 拖拽滚动容器增强 + +:::demo 搭配使用dDropScrollEnhanced指令允许拖拽到边缘的时候加速滚动条向两边滚动。 + +```vue + + + +``` + +::: + +### 源占位符 + +:::demo 使用originPlaceholder显示源位置的占位符,示例为消失动画。 + +```vue + + + +``` + +::: + +### 拖拽预览 + +:::demo 允许拖拽的时候展示自定义拖拽元素样式 + +```vue + + + +``` + +::: + +### 批量拖拽 + +:::demo 使用batchDrag指令标记可以批量拖拽,请用ctrl按键和鼠标选中多个并进行拖拽 + +```vue + + + +``` + +::: + +### 二维拖拽和组合拖拽预览 + +:::demo 使用dDragDropSyncBox指令、dDragSync指令、dDropSync指令协同拖拽,实现二维拖拽;使用dDragPreview配置拖拽预览,使用d-drag-preview-clone-dom-ref 完成拖拽节点预览的克隆。 + +```vue + + + +``` + +::: + +#### dDraggable 指令 + +##### dDraggable 参数 + +| 参数 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :---------------------------: | :--------------------------------------------------------------------------------------------------------------- | :------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------ | +| dragData | `any` | -- | 可选,转递给 `DropEvent`事件的数据. | [基本用法](#基本用法) | +| dragScope | `string \| Array` | 'default' | 可选,限制 drop 的位置,必须匹配对应的 `dropScope` | [基本用法](#基本用法) | +| dragOverClass | `string` | -- | 可选,拖动时被拖动元素的 css | [拖拽实体元素跟随](#拖拽实体元素跟随) | +| dragHandle | `string` | -- | 可选,可拖动拖动内容的 css 选择器,只有匹配 css 选择器的元素才能响应拖动事件, 注意是css选择器,示例:`'.title, .title > *'`,`'#header'`, `'.title *:not(input)'` | | | +| dragHandleClass | `string` | 'drag-handle' | 可选, 会给可拖动内容的应用的 css 选择器命中的元素添加的 css 类名, 第一个匹配 css 选择器的会被加上该 css 类 | [基本用法](#基本用法) | +| disabled | `boolean` | false | 可选,控制当前元素是否可拖动 false 为可以,true 为不可以 | [基本用法](#基本用法) | +| enableDragFollow | `boolean` | false | 可选,是否启用实体元素跟随(可以添加更多特效,如阴影等) | [拖拽实体元素跟随](#拖拽实体元素跟随) | +| dragFollowOptions | `{appendToBody?: boolean}` | -- | 可选,用于控制实体拖拽的一些配置 | [拖拽实体元素跟随](#拖拽实体元素跟随) | +| dragFollowOptions.appendToBody | `boolean` | false | 可选,用于控制实体拖拽的克隆元素插入的位置。默认 false 会插入到源元素父元素所有子的最后,设置为 true 会附着到。见说明 1 | [拖拽实体元素跟随](#拖拽实体元素跟随) | +| originPlaceholder | `{show?: boolean; tag?: string; style?: {cssProperties: string]: string}; text?: string; removeDelay?: number;}` | -- | 可选,设置源占位符号,用于被拖拽元素原始位置占位 | [源占位符](#源占位符) | +| originPlaceholder.show | `boolean` | true | 可选,是否显示,默认 originPlaceholder 有 Input 则显示,特殊情况可以关闭 | +| originPlaceholder.tag | `string` | 'div' | 可选,使用 tag 名,默认 originPlaceholder 使用'div',特殊情况可以置换 | +| originPlaceholder.style | `Object` | -- | 可选,传 style 对象,key 为样式属性,value 为样式值 | [源占位符](#源占位符) | +| originPlaceholder.text | `string` | -- | 可选,placeholder 内的文字 | [源占位符](#源占位符) | +| originPlaceholder.removeDelay | `number` | -- | 可选,用于希望源占位符在拖拽之后的延时里再删除,方便做动画,单位为 ms 毫秒 | [源占位符](#源占位符) | + +说明 1:dragFollowOptions 的 appendToBody 的使用场景:当拖拽离开后源位置的父对象会被销毁的话,需要把克隆体附着到 body 上防止被销毁。默认会通过复制样式保证克隆到 body 的实体的样式是正确的,但部分深度依赖 DOM 节点位置的样式和属性可能会失败,需要手动调整部分样式。 + +##### dDraggable 事件 + +| 事件 | 类型 | 描述 | 跳转 Demo | +| :------------- | :------------------------ | :------------------------ | :--------------------------- | +| dragStartEvent | `EventEmitter` | 开始拖动的 DragStart 事件 | [基本用法](#基本用法) | +| dragEndEvent | `EventEmitter` | 拖动结束的 DragEnd 事件 | [基本用法](#基本用法) | +| dropEndEvent | `EventEmitter` | 放置结束的 Drop 事件 | [基本用法](#基本用法) | + +Drag DOM Events 详情: [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent) + +#### dDraggableBatchDrag 附加指令 + +使用方法 dDraggableBatchDrag + +##### dDraggableBatchDrag 属性 + +| 名字 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :---------------------------------- | :------------------------ | :----------------- | :--------------------------------------------------------------------------------------------- | :----------------------------------- | +| batchDragGroup \| batchDrag | `string` | 'default' | 可选,批量拖拽分组组名,不同组名 | +| batchDragActive | `boolean` | false | 可选,是否把元素加入到批量拖拽组. 见说明 1。 | [批量拖拽](#批量拖拽) | +| batchDragLastOneAutoActiveEventKeys | `Array` | ['ctrlKey'] | 可选,通过过拖拽可以激活批量选中的拖拽事件判断。见说明 2。 | +| batchDragStyle | `Array` | ['badge', 'stack'] | 可选,批量拖拽的效果,badge 代表右上角有统计数字,stack 代表有堆叠效果,数组里有该字符串则有效 | [批量拖拽](#批量拖拽) | + +说明 1: `batchDragActive`为`true`的时候会把元素加入组里,加入顺序为变为 true 的顺序,先加入的在数组前面。第一个元素会确认批量的组名,如果后加入的组名和先加入的组名不一致,则后者无法加入。 +说明 2: `batchDragLastOneAutoActiveEventKeys`的默认值为['ctrlKey'], 即可以通过按住 ctrl 键拖动最后一个元素, 该元素自动加入批量拖拽的组,判断条件是 dragStart 事件里的 ctrlKey 事件为 true。目前仅支持判断 true/false。该参数为数组,可以判断任意一个属性值为 true 则生效,可用于不同操作系统的按键申明。 + +##### dDraggableBatchDrag事件 + +| 名字 | 类型 | 描述 | 跳转 Demo | +| :------------------- | :--------------------------------------- | :------------------------------------------------- | :----------------------------------- | +| batchDragActiveEvent | `EventEmitter<{el: Element, data: any}>` | 通过拖拽把元素加入了批量拖拽组,通知外部选中该元素 | [批量拖拽](#批量拖拽) | + +#### dDroppable 指令 + +##### dDroppable 参数 + +| 参数 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :-------------------------- | :--------------------------------------------- | :------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | +| dropScope | `string \| Array` | 'default' | 可选,限制 drop 的区域,对应 dragScope | [基本用法](#基本用法) | +| dragOverClass | `string` | -- | 可选,dragover 时 drop 元素上应用的 css | +| placeholderStyle | `Object` | {backgroundColor: '#6A98E3', opacity: '.4'} | 可选,允许 sort 时,用于占位显示 | [源占位符](#源占位符) | +| placeholderText | `string` | '' | 可选,允许 sort 时,用于占位显示内部的文字 | +| allowDropOnItem | `boolean` | false | 可选,允许 sort 时,用于允许拖动到元素上,方便树形结构的拖动可以成为元素的子节点 | [多层树状拖拽](#多层树状拖拽) | +| dragOverItemClass | `string` | -- | 可选,`allowDropOnItem`为`true`时,才有效,用于允许拖动到元素上后,被命中的元素增加样式 | [多层树状拖拽](#多层树状拖拽) | +| nestingTargetRect | `{height?: number, width?: number}` | -- | 可选,用于修正有内嵌列表后,父项高度被撑大,此处 height,width 为父项自己的高度(用于纵向拖动),宽度(用于横向拖动) | [多层树状拖拽](#多层树状拖拽) | +| defaultDropPosition | `'closest' \| 'before' \| 'after'` | 'closest' | 可选,设置拖拽到可放置区域但不在列表区域的放置位置,`'closest'` 为就近放下, `'before'`为加到列表头部, `'after'`为加到列表尾部 | [外部放置位置](#外部放置位置:就近,前面,后面) | +| dropSortCountSelector | `string` | -- | 可选,带有 sortable 的容器的情况下排序,计数的内容的选择器名称,可以用于过滤掉不应该被计数的元素 | +| dropSortVirtualScrollOption | `{totalLength?: number; startIndex?: number;}` | -- | 可选,用于虚拟滚动列表中返回正确的 dropIndex 需要接收 totalLength 为列表的真实总长度, startIndex 为当前排序区域显示的第一个 dom 的在列表内的 index 值 | +| switchWhileCrossEdge | `boolean` | false | 可选,是否启用越过立即交换位置的算法, 不能与allowDropOnItem一起用,allowDropOnItem为true时,此规则无效 | +| placeholderTag | `string` | 'div' | 可选,占位显示的元素标签 | + +##### dDroppable 事件 + +| 事件 | 类型 | 描述 | 跳转 Demo | +| :------------- | :------------------------------------------ | :------------------------------------------------------------------------------ | :--------------------------- | +| dragEnterEvent | `EventEmitter` | drag 元素进入的 dragenter 事件 | [基本用法](#基本用法) | +| dragOverEvent | `EventEmitter` | drag 元素在 drop 区域上的 dragover 事件 | [基本用法](#基本用法) | +| dragLeaveEvent | `EventEmitter` | drag 元素离开的 dragleave 事件 | [基本用法](#基本用法) | +| dropEvent | `EventEmitter<`[`DropEvent`](#dropevent)`>` | 放置一个元素, 接收的事件,其中 nativeEvent 表示原生的 drop 事件,其他见定义注释 | [基本用法](#基本用法) | + +##### DropEvent + +```typescript +type DropEvent = { + nativeEvent: any; // 原生的drop事件 + dragData: any; // drag元素的dragData数据 + dropSubject: Subject; //drop事件的Subject + dropIndex?: number; // drop的位置在列表的index + dragFromIndex?: number; // drag元素在原来的列表的index,注意使用虚拟滚动数据无效 + dropOnItem?: boolean; // 是否drop到了元素的上面,搭配allowDropOnItem使用 +} +``` + +#### dSortable 指令 + +指定需要参与排序的 Dom 父容器(因为 drop 只是限定可拖拽区域,具体渲染由使用者控制) + +##### dSortable 参数 + +| 名字 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :------------- | :----------- | :----- | :------------------------------ | :-------- | +| dSortDirection | `'v' \| 'h'` | 'v' | 'v'垂直排序,'h'水平排序 | +| dSortableZMode | `boolean` | false | 是否是 z 模式折回排序,见说明 1 | + +说明 1: z 自行排序最后是以大方向为准的,如果从左到右排遇到行末换行,需要使用的垂直排序+z 模式,因为最后数据是从上到下的只是局部的数据是从左到右。 + +##### dDropScrollEnhanced 参数 + +| 名字 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :----------------- | :---------------------------------------------------------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- | +| direction | [`DropScrollDirection`](#dropscrolldirection)即`'v'\|'h'` | 'v' | 滚动方向,垂直滚动`'v'`, 水平滚动 `'h'` | [拖拽滚动容器增强](#拖拽滚动容器增强) | +| responseEdgeWidth | `string \| ((total: number) => string)` | '100px' | 响应自动滚动边缘宽度, 函数的情况传入的为列表容器同个方向相对宽度 | [拖拽滚动容器增强](#拖拽滚动容器增强) | +| speedFn | [`DropScrollSpeedFunction`](#dropscrolldirection) | 内置函数 | 速率函数,见备注 | +| minSpeed | `DropScrollSpeed`即`number` | 50 | 响应最小速度 ,函数计算小于这个速度的时候,以最小速度为准 | +| maxSpeed | `DropScrollSpeed`即`number` | 1000 | 响应最大速度 ,函数计算大于这个速度的时候,以最大速度为准 | +| viewOffset | {forward?: [`DropScrollAreaOffset`](#dropscrollareaoffset); backward?: `DropScrollAreaOffset`;} | -- | 设置拖拽区域的偏移,用于某些位置修正 | +| dropScrollScope | `string\| Array` | -- | 允许触发滚动 scope,不配置为默认接收所有 scope,配置情况下,draggable 的`dragScope`和`dropScrollScope`匹配得上才能触发滚动 | [拖拽滚动容器增强](#拖拽滚动容器增强) | +| backSpaceDroppable | `boolean` | true | 是否允许在滚动面板上同时触发放置到滚动面板的下边的具体可以放置元素,默认为 true,设置为 false 则不能边滚动边放置 | + +备注: speedFn 默认函数为`(x: number) => Math.ceil((1 - x) * 18) * 100`,传入数字`x`是 鼠标位置距离边缘的距离占全响应宽度的百分比, +最终速度将会是 speedFn(x),但不会小于最小速度`minSpeed`或者大于最大速度`maxSpeed`。 + +相关类型定义: + +###### DropScrollDirection + +```typescript +export type DropScrollDirection = 'h' | 'v'; +``` + +###### DropScrollSpeed + +```typescript +export type DropScrollEdgeDistancePercent = number; // unit: 1 +export type DropScrollSpeed = number; // Unit: px/s +export type DropScrollSpeedFunction = (x: DropScrollEdgeDistancePercent) => DropScrollSpeed; +``` + +###### DropScrollAreaOffset + +```typescript +export type DropScrollAreaOffset = { + left?: number; + right?: number; + top?: number; + bottom?: number; + widthOffset?: number; + heightOffset?: number; +}; + +export enum DropScrollOrientation { + forward, // Forward, right/bottom + backward, // Backward, left/up +} +export type DropScrollTriggerEdge = 'left' | 'right' | 'top' | 'bottom'; +``` + +`DropScrollAreaOffset` 仅重要和次要定位边有效, forward 代表后右或者往下滚动,backward 表示往左或者往上滚动 + +| direction | `v` 上下滚动 | `h` 左右滚动 | +| :------------------ | :--------------- | :------------- | +| forward 往下或往右 | `left` ,`bottom` | `top` ,`right` | +| backward 往左或网上 | `left`,`top` | `top`,`left` | + +##### dDropScrollEnhancedSide 附属指令 + +如果需要同时两个方向都有滚动条,则需要使用 dDropScrollEnhanced 的同时使用 dDropScrollEnhancedSide,参数列表同 dDropScrollEnhanced 指令,唯一不同是 direction,如果为`'v'`则 side 附属指令的实际方向为`'h'`。 + +| 名字 | 类型 | 默认值 | 描述 | +| :----------------- | :--------------------------------------------------------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------- | +| direction | `DropScrollSpeed`即`'v'\|'h'` | 'v' | 滚动方向,垂直滚动`'v'`, 水平滚动 `'h'` | +| responseEdgeWidth | `string \| ((total: number) => string)` | '100px' | 响应自动滚动边缘宽度, 函数的情况传入的为列表容器同个方向相对宽度 | +| speedFn | `DropScrollSpeedFunction` | 内置函数 | 速率函数,见备注 | +| minSpeed | `DropScrollSpeed`即`number` | 50 | 响应最小速度 ,函数计算小于这个速度的时候,以最小速度为准 | +| maxSpeed | `DropScrollSpeed`即`number` | 1000 | 响应最大速度 ,函数计算大于这个速度的时候,以最大速度为准 | +| viewOffset | {forward?: `DropScrollAreaOffset`; backward?: `DropScrollAreaOffset`;} | -- | 设置拖拽区域的偏移,用于某些位置修正 | +| dropScrollScope | `string\| Array` | -- | 允许触发滚动 scope,不配置为默认接收所有 scope,配置情况下,draggable 的`dragScope`和`dropScrollScope`匹配得上才能触发滚动 | +| backSpaceDroppable | `boolean` | true | 是否允许在滚动面板上同时触发放置到滚动面板的下边的具体可以放置元素,默认为 true,设置为 false 则不能边滚动边放置 | + +#### 使用 `dDraggable` & `dDroppable` 指令 + +```html +
    +
  • Coffee
  • +
  • Tea
  • +
  • Milk
  • +
+``` + +```html +
+

Drop items here

+
+``` + +#### CSS + +`dDraggable` & `dDroppable` 指令都有`[dragOverClass]`作为输入. + 提供 drag 和 drop 时的 hover 样式,注意是`字符串` + +```html +
+

Drop items here

+
+``` + +#### 限制 Drop 区域 + +用[dragScope]和[dropScope]限制拖动区域,可以是字符串或数组,只有 drag 和 drop 的区域对应上才能放进去 + +```html +
    +
  • Coffee
  • +
  • Tea
  • +
  • Biryani
  • +
  • Kebab
  • + ... +
+``` + +```html +
+

只有 Drinks 可以放在这个container里

+
+ +
+

Meal 和 Drinks 可以放在这个container里

+
+``` + +#### 传递数据 + +`dDraggable`可以用`dragData`向`dDroppable`传递数据 +`dDroppable`用`@dropEvent`事件接收数据 + +```html +
    +
  • {{item.name}}
  • +
+ +
+
Drop Items here
+
+
  • {{item.name}}
  • +
    +
    +``` + +```js +setup() { + const items = [ + { name: 'Apple', type: 'fruit' }, + { name: 'Carrot', type: 'vegetable' }, + { name: 'Orange', type: 'fruit' }, + ]; + const droppedItems = []; + + onItemDrop(e) { + // Get the dropped data here + droppedItems.push(e.dragData); + } +} +``` + +###### Drag Handle + +Drag 句柄可以指定实际响应 draggable 事件的元素,而不是 draggable 本身 +这个参数必须是一个字符串,实际上是一个 css 选择器 + +```html +
  • + 只有.drag-handle可以响应拖动事件来拖起li +
    +
  • +``` + +###### 异步 DropEnd,通知 Drag 元素 + +`dDraggable`有一个`dropEndEvent`事件,此事件非浏览器默认事件而是自定义事件,非组件自动触发触发方式是在`dDroppable`的`dropEvent`事件的参数中有一个 dropSubject,当需要触发 drag 元素上的 dropEndEvent 事件的时候调用 dropSubject.next(params) 一般是在接口返回之后 例如: + +```html +
      +
    • {{item.name}}
    • +
    + +
    +
    Drop Items here
    +
    +
  • {{item.name}}
  • +
    +
    +``` + +```js +setup() { + onItemDrop(e) { + ajax.onSuccess(() => { + e.dropSubject.next(params); //此时才触发dragComponent的dropEnd 并且params对应onDropEnd的$event; + }); + } + onDropEnd(event, i) {} +} +``` + +#### 协同拖拽, 用于二维拖拽,跨纬度拖拽场景 + +##### 协同拖 dDragSync + +用于 dDraggle 对象和同时会被拖走的对象。 + +###### dDragSync 参数 + +| 参数 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :-------- | :------- | :----- | :--------------------------------------------------------------- | :-------------------------------------------------- | +| dDragSync | `string` | '' | 必选,拖同步的组名,为空或者空字符串的时候无效,不与其他内容同步 | [二维拖拽和组合拖拽预览](#二维拖拽和组合拖拽预览) | + +##### 协同放 dDropSortSync + +用于 dDroppable 对象和与 droppable 内 sortable 结构相同的 sortable 区域, 注意 dDroppable 对象里是与 dDroppable 对象同个对象上注册 dDropSortSync,其他不带 dDroppable 的与放置在排序区域。 + +###### dDropSortSync 参数 + +| 参数 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :----------------- | :---------- | :----- | :--------------------------------------------------------------- | :-------------------------------------------------- | +| dDropSortSync | `string` | '' | 必选,放同步的组名,为空或者空字符串的时候无效,不与其他内容同步 | [二维拖拽和组合拖拽预览](#二维拖拽和组合拖拽预览) | +| dDropSyncDirection | `'v'\| 'h'` | 'v' | 可选,与 dSortable 的方向正交 | + +##### 协同监听盒子 dDragDropSyncBox + +用于统计 dDragSync 和 dDropSortSync 的公共父祖先。 +无参数,放置在公共统计区域则可。 + +#### 拖拽预览, 用于需要替换拖拽预览的场景 + +##### 拖拽预览 dDragPreview + +需要和 dDraggable 搭配使用, 用于拖起的时候拖动对象的模板 + +###### dDragPreview 参数 + +| 参数 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :---------------------------------- | :------------------------------ | :----- | :--------------------------------------------------------------------------------- | :-------------------------------------------------- | +| dDragPreview | `TemplateRef` | -- | 必选,预览的模板引用 | [二维拖拽和组合拖拽预览](#二维拖拽和组合拖拽预览) | +| dragPreviewData | `any` | -- | 可选,自定义数据,将由模板变量获得 | +| dragPreviewOptions | `{ skipBatchPreview : boolean}` | -- | 可选,预览选项 | +| dragPreviewOptions.skipBatchPreview | `boolean` | false | 可选,预览选项, 是否跳过批量预览的样式处理。建议自行处理批量拖拽预览模板的可以跳过 | + +###### dDragPreview 模板可用变量 + +| 变量 | 类型 | 变量含义说明 | +| :-----------------: | :------------------: | :-----------------------------------------------------------------------------------------: | +| data | `any` | 从拖拽预览传入的 dragPreviewData 数据 | +| draggedEl | `HTMLElement` | 被拖拽的 DOM 元素 | +| dragData | `any` | 被拖拽元素携带的 dragData 数据 | +| batchDragData | `Array` | 被批量拖拽的对象的 dragData 数据的数组, 含被拖拽元素的 dragData, 并且 dragData 处于第一位 | +| dragSyncDOMElements | `Array` | 被协同拖拽的 DOM 元素, 不包括 draggedEl 指向的 DOM 元素 | + +##### 拖拽预览辅助克隆节点 组件`` + +可以从节点的引用中恢复 DOM 的克隆对象作为预览 + +| 参数 | 类型 | 默认值 | 描述 | 跳转 Demo | +| :-------- | :------------ | :----- | :----------------------------------------- | :-------- | +| domRef | `HTMLElement` | -- | 必选,否则无意义,克隆节点的 DOM 引用 | [二维拖拽和组合拖拽预览](#二维拖拽和组合拖拽预览)| +| copyStyle | `boolean` | true | 可选,是否克隆节点的时候对节点依次克隆样式 |[二维拖拽和组合拖拽预览](#二维拖拽和组合拖拽预览)| diff --git a/packages/devui-vue/package.json b/packages/devui-vue/package.json index f588bb4631..582e89fd70 100644 --- a/packages/devui-vue/package.json +++ b/packages/devui-vue/package.json @@ -1,6 +1,6 @@ { "name": "vue-devui", - "version": "1.6.3-select.0", + "version": "1.6.4", "license": "MIT", "description": "DevUI components based on Vite and Vue3", "keywords": [ @@ -42,7 +42,7 @@ "devui-cli": "devui" }, "peerDependencies": { - "vue": "^3.2" + "vue": "^3.3" }, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", @@ -71,8 +71,9 @@ "mermaid": "9.1.1", "mitt": "^3.0.0", "monaco-editor": "0.34.0", + "rxjs": "^7.8.1", + "vue": "^3.3.4", "uuid": "^9.0.1", - "vue": "^3.2.37", "vue-router": "^4.0.3", "xss": "^1.0.14" }, @@ -125,4 +126,4 @@ "vitepress-theme-demoblock": "1.3.2", "vue-tsc": "0.38.8" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84aedc4140..7b6e84f1f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -165,6 +165,9 @@ importers: '@floating-ui/dom': specifier: ^0.4.4 version: 0.4.4 + '@iktakahiro/markdown-it-katex': + specifier: ^4.0.1 + version: 4.0.1 '@types/codemirror': specifier: 0.0.97 version: 0.0.97 @@ -176,7 +179,7 @@ importers: version: 3.2.33 '@vueuse/core': specifier: 8.9.4 - version: 8.9.4(vue@3.2.37) + version: 8.9.4(vue@3.3.4) async-validator: specifier: ^4.0.7 version: 4.0.7 @@ -205,8 +208,11 @@ importers: specifier: ^10.0.0 version: 10.0.0 highlight.js: - specifier: 10.7.3 - version: 10.7.3 + specifier: ^11.6.0 + version: 11.6.0 + katex: + specifier: ^0.12.0 + version: 0.12.0 lodash: specifier: ^4.17.21 version: 4.17.21 @@ -216,6 +222,9 @@ importers: markdown-it: specifier: 12.2.0 version: 12.2.0 + markdown-it-plantuml: + specifier: ^1.4.1 + version: 1.4.1 mermaid: specifier: 9.1.1 version: 9.1.1 @@ -225,12 +234,18 @@ importers: monaco-editor: specifier: 0.34.0 version: 0.34.0 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + uuid: + specifier: ^9.0.1 + version: 9.0.1 vue: - specifier: ^3.2.37 - version: 3.2.37 + specifier: ^3.3.4 + version: 3.3.4 vue-router: specifier: ^4.0.3 - version: 4.0.12(vue@3.2.37) + version: 4.0.12(vue@3.3.4) xss: specifier: ^1.0.14 version: 1.0.14 @@ -297,7 +312,7 @@ importers: version: 3.2.31 '@vue/test-utils': specifier: ^2.0.0-rc.9 - version: 2.0.0-rc.17(vue@3.2.37) + version: 2.0.0-rc.17(vue@3.3.4) '@vuedx/typecheck': specifier: ^0.4.1 version: 0.4.1 @@ -374,84 +389,6 @@ importers: specifier: 0.38.8 version: 0.38.8(typescript@4.5.5) - packages/devui-vue/build: - dependencies: - '@devui-design/icons': - specifier: ^1.3.0 - version: 1.3.0 - '@floating-ui/dom': - specifier: ^0.4.4 - version: 0.4.4 - '@types/codemirror': - specifier: 0.0.97 - version: 0.0.97 - '@types/lodash-es': - specifier: ^4.17.4 - version: 4.17.6 - '@vue/shared': - specifier: ^3.2.33 - version: 3.2.33 - '@vueuse/core': - specifier: 8.9.4 - version: 8.9.4(vue@3.2.37) - async-validator: - specifier: ^4.0.7 - version: 4.0.7 - clipboard: - specifier: ^2.0.11 - version: 2.0.11 - clipboard-copy: - specifier: ^4.0.1 - version: 4.0.1 - codemirror: - specifier: 5.63.3 - version: 5.63.3 - dayjs: - specifier: ^1.11.3 - version: 1.11.3 - devui-theme: - specifier: ^0.0.1 - version: link:../../devui-theme - diff2html: - specifier: ^3.4.35 - version: 3.4.35 - echarts: - specifier: 5.3.3 - version: 5.3.3 - fs-extra: - specifier: ^10.0.0 - version: 10.0.0 - highlight.js: - specifier: 10.7.3 - version: 10.7.3 - lodash: - specifier: ^4.17.21 - version: 4.17.21 - lodash-es: - specifier: ^4.17.20 - version: 4.17.21 - markdown-it: - specifier: 12.2.0 - version: 12.2.0 - mermaid: - specifier: 9.1.1 - version: 9.1.1 - mitt: - specifier: ^3.0.0 - version: 3.0.0 - monaco-editor: - specifier: 0.34.0 - version: 0.34.0 - vue: - specifier: ^3.2 - version: 3.2.37 - vue-router: - specifier: ^4.0.3 - version: 4.0.12(vue@3.2.37) - xss: - specifier: ^1.0.14 - version: 1.0.14 - packages/devui-vue/build/action-timeline: {} packages/devui-vue/build/alert: {} @@ -482,6 +419,8 @@ importers: packages/devui-vue/build/date-picker-pro: {} + packages/devui-vue/build/dragdrop: {} + packages/devui-vue/build/drawer: {} packages/devui-vue/build/dropdown: {} @@ -494,7 +433,7 @@ importers: packages/devui-vue/build/form: {} - packages/devui-vue/build/fullscreen: {} + packages/devui-vue/build/git-graph: {} packages/devui-vue/build/grid: {} @@ -755,7 +694,7 @@ packages: resolution: {integrity: sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@babel/helper-builder-binary-assignment-operator-visitor@7.16.7: @@ -763,7 +702,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/helper-explode-assignable-expression': 7.16.7 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@babel/helper-compilation-targets@7.16.7(@babel/core@7.17.5): @@ -837,7 +776,7 @@ packages: resolution: {integrity: sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@babel/helper-function-name@7.16.7: @@ -853,7 +792,7 @@ packages: resolution: {integrity: sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@babel/helper-hoist-variables@7.22.5: @@ -866,7 +805,7 @@ packages: resolution: {integrity: sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@babel/helper-module-imports@7.16.7: @@ -884,10 +823,10 @@ packages: '@babel/helper-module-imports': 7.16.7 '@babel/helper-simple-access': 7.16.7 '@babel/helper-split-export-declaration': 7.16.7 - '@babel/helper-validator-identifier': 7.16.7 + '@babel/helper-validator-identifier': 7.22.5 '@babel/template': 7.16.7 '@babel/traverse': 7.17.3 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color dev: true @@ -896,7 +835,7 @@ packages: resolution: {integrity: sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@babel/helper-plugin-utils@7.16.7: @@ -910,7 +849,7 @@ packages: dependencies: '@babel/helper-annotate-as-pure': 7.16.7 '@babel/helper-wrap-function': 7.16.8 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color dev: true @@ -923,7 +862,7 @@ packages: '@babel/helper-member-expression-to-functions': 7.16.7 '@babel/helper-optimise-call-expression': 7.16.7 '@babel/traverse': 7.17.3 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color dev: true @@ -932,14 +871,14 @@ packages: resolution: {integrity: sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@babel/helper-skip-transparent-expression-wrappers@7.16.0: resolution: {integrity: sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@babel/helper-split-export-declaration@7.16.7: @@ -973,7 +912,7 @@ packages: '@babel/helper-function-name': 7.16.7 '@babel/template': 7.16.7 '@babel/traverse': 7.17.3 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color dev: true @@ -984,7 +923,7 @@ packages: dependencies: '@babel/template': 7.16.7 '@babel/traverse': 7.17.3 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color dev: true @@ -993,7 +932,7 @@ packages: resolution: {integrity: sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-validator-identifier': 7.16.7 + '@babel/helper-validator-identifier': 7.22.5 chalk: 2.4.2 js-tokens: 4.0.0 dev: true @@ -1003,7 +942,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@babel/parser@7.17.3: @@ -1013,6 +952,13 @@ packages: dependencies: '@babel/types': 7.17.0 + /@babel/parser@7.24.1: + resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.22.5 + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.16.7(@babel/core@7.17.5): resolution: {integrity: sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==} engines: {node: '>=6.9.0'} @@ -1895,7 +1841,7 @@ packages: '@babel/helper-function-name': 7.16.7 '@babel/helper-split-export-declaration': 7.16.7 '@babel/parser': 7.17.3 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 debug: 4.3.3(supports-color@8.1.1) globals: 11.12.0 lodash: 4.17.21 @@ -1924,7 +1870,7 @@ packages: /@babel/types@7.12.1: resolution: {integrity: sha512-BzSY3NJBKM4kyatSOWh3D/JJ2O3CVzBybHWxtgxnggaxEuaSTTDqeiSb/xk9lrkw2Tbqyivw5ZU4rT+EfznQsA==} dependencies: - '@babel/helper-validator-identifier': 7.16.7 + '@babel/helper-validator-identifier': 7.22.5 lodash: 4.17.21 to-fast-properties: 2.0.0 dev: true @@ -2239,6 +2185,12 @@ packages: engines: {node: '>=6.9.0'} dev: true + /@iktakahiro/markdown-it-katex@4.0.1: + resolution: {integrity: sha512-kGFooO7fIOgY34PSG8ZNVsUlKhhNoqhzW2kq94TNGa8COzh73PO4KsEoPOsQVG1mEAe8tg7GqG0FoVao0aMHaw==} + dependencies: + katex: 0.12.0 + dev: false + /@intlify/core-base@9.1.9: resolution: {integrity: sha512-x5T0p/Ja0S8hs5xs+ImKyYckVkL4CzcEXykVYYV6rcbXxJTe2o58IquSqX9bdncVKbRZP7GlBU1EcRaQEEJ+vw==} engines: {node: '>= 10'} @@ -2529,6 +2481,9 @@ packages: resolution: {integrity: sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==} dev: true + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + /@jridgewell/trace-mapping@0.3.4: resolution: {integrity: sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==} dependencies: @@ -2643,20 +2598,20 @@ packages: /@types/babel__generator@7.6.4: resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@types/babel__template@7.4.1: resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==} dependencies: '@babel/parser': 7.17.3 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@types/babel__traverse@7.14.2: resolution: {integrity: sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /@types/braces@3.0.1: @@ -3102,6 +3057,15 @@ packages: '@vue/shared': 3.2.37 estree-walker: 2.0.2 source-map: 0.6.1 + dev: true + + /@vue/compiler-core@3.3.4: + resolution: {integrity: sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==} + dependencies: + '@babel/parser': 7.24.1 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + source-map-js: 1.0.2 /@vue/compiler-dom@3.2.31: resolution: {integrity: sha512-60zIlFfzIDf3u91cqfqy9KhCKIJgPeqxgveH2L+87RcGU/alT6BRrk5JtUso0OibH3O7NXuNOQ0cDc9beT0wrg==} @@ -3114,6 +3078,13 @@ packages: dependencies: '@vue/compiler-core': 3.2.37 '@vue/shared': 3.2.37 + dev: true + + /@vue/compiler-dom@3.3.4: + resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==} + dependencies: + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 /@vue/compiler-sfc@3.2.31: resolution: {integrity: sha512-748adc9msSPGzXgibHiO6T7RWgfnDcVQD+VVwYgSsyyY8Ans64tALHZANrKtOzvkwznV/F4H7OAod/jIlp/dkQ==} @@ -3142,6 +3113,21 @@ packages: magic-string: 0.25.7 postcss: 8.4.6 source-map: 0.6.1 + dev: true + + /@vue/compiler-sfc@3.3.4: + resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==} + dependencies: + '@babel/parser': 7.24.1 + '@vue/compiler-core': 3.3.4 + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-ssr': 3.3.4 + '@vue/reactivity-transform': 3.3.4 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + magic-string: 0.30.8 + postcss: 8.4.6 + source-map-js: 1.0.2 /@vue/compiler-ssr@3.2.31: resolution: {integrity: sha512-mjN0rqig+A8TVDnsGPYJM5dpbjlXeHUm2oZHZwGyMYiGT/F4fhJf/cXy8QpjnLQK4Y9Et4GWzHn9PS8AHUnSkw==} @@ -3154,6 +3140,13 @@ packages: dependencies: '@vue/compiler-dom': 3.2.37 '@vue/shared': 3.2.37 + dev: true + + /@vue/compiler-ssr@3.3.4: + resolution: {integrity: sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==} + dependencies: + '@vue/compiler-dom': 3.3.4 + '@vue/shared': 3.3.4 /@vue/devtools-api@6.0.12: resolution: {integrity: sha512-iO/4FIezHKXhiDBdKySCvJVh8/mZPxHpiQrTy+PXVqJZgpTPTdHy4q8GXulaY+UKEagdkBb0onxNQZ0LNiqVhw==} @@ -3176,6 +3169,16 @@ packages: '@vue/shared': 3.2.37 estree-walker: 2.0.2 magic-string: 0.25.7 + dev: true + + /@vue/reactivity-transform@3.3.4: + resolution: {integrity: sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==} + dependencies: + '@babel/parser': 7.24.1 + '@vue/compiler-core': 3.3.4 + '@vue/shared': 3.3.4 + estree-walker: 2.0.2 + magic-string: 0.30.8 /@vue/reactivity@3.2.31: resolution: {integrity: sha512-HVr0l211gbhpEKYr2hYe7hRsV91uIVGFYNHj73njbARVGHQvIojkImKMaZNDdoDZOIkMsBc9a1sMqR+WZwfSCw==} @@ -3186,6 +3189,12 @@ packages: resolution: {integrity: sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A==} dependencies: '@vue/shared': 3.2.37 + dev: true + + /@vue/reactivity@3.3.4: + resolution: {integrity: sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==} + dependencies: + '@vue/shared': 3.3.4 /@vue/runtime-core@3.2.31: resolution: {integrity: sha512-Kcog5XmSY7VHFEMuk4+Gap8gUssYMZ2+w+cmGI6OpZWYOEIcbE0TPzzPHi+8XTzAgx1w/ZxDFcXhZeXN5eKWsA==} @@ -3193,11 +3202,11 @@ packages: '@vue/reactivity': 3.2.31 '@vue/shared': 3.2.31 - /@vue/runtime-core@3.2.37: - resolution: {integrity: sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ==} + /@vue/runtime-core@3.3.4: + resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} dependencies: - '@vue/reactivity': 3.2.37 - '@vue/shared': 3.2.37 + '@vue/reactivity': 3.3.4 + '@vue/shared': 3.3.4 /@vue/runtime-dom@3.2.31: resolution: {integrity: sha512-N+o0sICVLScUjfLG7u9u5XCjvmsexAiPt17GNnaWHJUfsKed5e85/A3SWgKxzlxx2SW/Hw7RQxzxbXez9PtY3g==} @@ -3206,12 +3215,12 @@ packages: '@vue/shared': 3.2.31 csstype: 2.6.19 - /@vue/runtime-dom@3.2.37: - resolution: {integrity: sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw==} + /@vue/runtime-dom@3.3.4: + resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} dependencies: - '@vue/runtime-core': 3.2.37 - '@vue/shared': 3.2.37 - csstype: 2.6.19 + '@vue/runtime-core': 3.3.4 + '@vue/shared': 3.3.4 + csstype: 3.1.3 /@vue/server-renderer@3.2.31(vue@3.2.31): resolution: {integrity: sha512-8CN3Zj2HyR2LQQBHZ61HexF5NReqngLT3oahyiVRfSSvak+oAvVmu8iNLSu6XR77Ili2AOpnAt1y8ywjjqtmkg==} @@ -3222,14 +3231,24 @@ packages: '@vue/shared': 3.2.31 vue: 3.2.31 - /@vue/server-renderer@3.2.37(vue@3.2.37): + /@vue/server-renderer@3.2.37(vue@3.3.4): resolution: {integrity: sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA==} peerDependencies: vue: 3.2.37 dependencies: '@vue/compiler-ssr': 3.2.37 '@vue/shared': 3.2.37 - vue: 3.2.37 + vue: 3.3.4 + dev: true + + /@vue/server-renderer@3.3.4(vue@3.3.4): + resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} + peerDependencies: + vue: 3.3.4 + dependencies: + '@vue/compiler-ssr': 3.3.4 + '@vue/shared': 3.3.4 + vue: 3.3.4 /@vue/shared@3.2.31: resolution: {integrity: sha512-ymN2pj6zEjiKJZbrf98UM2pfDd6F2H7ksKw7NDt/ZZ1fh5Ei39X5tABugtT03ZRlWd9imccoK0hE8hpjpU7irQ==} @@ -3240,13 +3259,17 @@ packages: /@vue/shared@3.2.37: resolution: {integrity: sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==} + dev: true - /@vue/test-utils@2.0.0-rc.17(vue@3.2.37): + /@vue/shared@3.3.4: + resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==} + + /@vue/test-utils@2.0.0-rc.17(vue@3.3.4): resolution: {integrity: sha512-7LHZKsFRV/HqDoMVY+cJamFzgHgsrmQFalROHC5FMWrzPzd+utG5e11krj1tVsnxYufGA2ABShX4nlcHXED+zQ==} peerDependencies: vue: ^3.0.1 dependencies: - vue: 3.2.37 + vue: 3.3.4 dev: true /@vuedx/analyze@0.4.1: @@ -3345,7 +3368,7 @@ packages: - supports-color dev: true - /@vueuse/core@8.9.4(vue@3.2.37): + /@vueuse/core@8.9.4(vue@3.3.4): resolution: {integrity: sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==} peerDependencies: '@vue/composition-api': ^1.1.0 @@ -3358,16 +3381,16 @@ packages: dependencies: '@types/web-bluetooth': 0.0.14 '@vueuse/metadata': 8.9.4 - '@vueuse/shared': 8.9.4(vue@3.2.37) - vue: 3.2.37 - vue-demi: 0.12.1(vue@3.2.37) + '@vueuse/shared': 8.9.4(vue@3.3.4) + vue: 3.3.4 + vue-demi: 0.12.1(vue@3.3.4) dev: false /@vueuse/metadata@8.9.4: resolution: {integrity: sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==} dev: false - /@vueuse/shared@8.9.4(vue@3.2.37): + /@vueuse/shared@8.9.4(vue@3.3.4): resolution: {integrity: sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==} peerDependencies: '@vue/composition-api': ^1.1.0 @@ -3378,8 +3401,8 @@ packages: vue: optional: true dependencies: - vue: 3.2.37 - vue-demi: 0.12.1(vue@3.2.37) + vue: 3.3.4 + vue-demi: 0.12.1(vue@3.3.4) dev: false /JSONStream@1.3.5: @@ -3692,7 +3715,7 @@ packages: engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} dependencies: '@babel/template': 7.16.7 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 '@types/babel__core': 7.1.18 '@types/babel__traverse': 7.14.2 dev: true @@ -3768,7 +3791,7 @@ packages: resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} engines: {node: '>= 10.0.0'} dependencies: - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /bail@1.0.5: @@ -4184,7 +4207,7 @@ packages: resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} dependencies: '@babel/parser': 7.17.3 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 dev: true /conventional-changelog-angular@5.0.13: @@ -4463,6 +4486,9 @@ packages: /csstype@2.6.19: resolution: {integrity: sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==} + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + /d3-array@1.2.4: resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} dev: false @@ -6382,13 +6408,13 @@ packages: /highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + dev: true /highlight.js@11.6.0: resolution: {integrity: sha512-ig1eqDzJaB0pqEvlPVIpSSyMaO92bH1N2rJpLMN/nX396wTpDA4Eq0uK+7I/2XG17pFaaKE0kjV/XPeGt7Evjw==} engines: {node: '>=12.0.0'} requiresBuild: true dev: false - optional: true /hogan.js@3.0.2: resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} @@ -6588,7 +6614,7 @@ packages: mute-stream: 0.0.8 ora: 5.4.1 run-async: 2.4.1 - rxjs: 7.5.4 + rxjs: 7.8.1 string-width: 4.2.3 strip-ansi: 6.0.1 through: 2.3.8 @@ -7273,7 +7299,7 @@ packages: '@babel/generator': 7.17.3 '@babel/plugin-syntax-typescript': 7.16.7(@babel/core@7.17.5) '@babel/traverse': 7.17.3 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 '@jest/transform': 27.5.1 '@jest/types': 27.5.1 '@types/babel__traverse': 7.14.2 @@ -7506,6 +7532,13 @@ packages: promise: 7.3.1 dev: true + /katex@0.12.0: + resolution: {integrity: sha512-y+8btoc/CK70XqcHqjxiGWBOeIL8upbS0peTPXTvgrh21n1RiWWcIpSWM+4uXq+IAgNh9YYQWdc7LVDPDAEEAg==} + hasBin: true + dependencies: + commander: 2.20.3 + dev: false + /khroma@2.0.0: resolution: {integrity: sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g==} dev: false @@ -7593,7 +7626,7 @@ packages: log-update: 4.0.0 p-map: 4.0.0 rfdc: 1.3.0 - rxjs: 7.5.4 + rxjs: 7.8.1 through: 2.3.8 wrap-ansi: 7.0.0 dev: true @@ -7700,6 +7733,12 @@ packages: dependencies: sourcemap-codec: 1.4.8 + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + /make-dir@3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -7739,6 +7778,10 @@ packages: resolution: {integrity: sha512-39j7/9vP/CPCKbEI44oV8yoPJTpvfeReTn/COgRhSpNrjWF3PfP/JUxxB0hxV6ynOY8KH8Y8aX9NMDdo6z+6YQ==} dev: true + /markdown-it-plantuml@1.4.1: + resolution: {integrity: sha512-13KgnZaGYTHBp4iUmGofzZSBz+Zj6cyqfR0SXUIc9wgWTto5Xhn7NjaXYxY0z7uBeTUMlc9LMQq5uP4OM5xCHg==} + dev: false + /markdown-it-table-of-contents@0.5.2: resolution: {integrity: sha512-6o+rxSwzXmXCUn1n8QGTSpgbcnHBG6lUU8x7A5Cssuq5vbfzTfitfGPvQ5PZkp+gP1NGS/DR2rkYqJPn0rbZ1A==} engines: {node: '>6.4.0'} @@ -9030,6 +9073,12 @@ packages: resolution: {integrity: sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ==} dependencies: tslib: 2.3.1 + dev: false + + /rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + dependencies: + tslib: 2.3.1 /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -9901,6 +9950,11 @@ packages: hasBin: true dev: true + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /v8-compile-cache@2.3.0: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true @@ -10016,7 +10070,7 @@ packages: '@types/markdown-it': 12.2.3 '@vitejs/plugin-vue': 1.10.2(vite@2.8.4) '@vue/compiler-sfc': 3.2.37 - '@vue/server-renderer': 3.2.37(vue@3.2.37) + '@vue/server-renderer': 3.2.37(vue@3.3.4) chalk: 4.1.2 compression: 1.7.4 debug: 4.3.3(supports-color@8.1.1) @@ -10037,7 +10091,7 @@ packages: prismjs: 1.29.0 sirv: 1.0.19 vite: 2.8.4(sass@1.49.8) - vue: 3.2.37 + vue: 3.3.4 transitivePeerDependencies: - less - react @@ -10057,7 +10111,7 @@ packages: '@vitejs/plugin-vue': 1.10.2(vite@2.8.4) prismjs: 1.29.0 vite: 2.8.4(sass@1.49.8) - vue: 3.2.37 + vue: 3.3.4 transitivePeerDependencies: - less - react @@ -10174,7 +10228,7 @@ packages: '@volar/transforms': 0.29.8 '@volar/vue-code-gen': 0.29.8 '@vscode/emmet-helper': 2.8.4 - '@vue/reactivity': 3.2.31 + '@vue/reactivity': 3.2.37 '@vue/shared': 3.2.37 request-light: 0.5.7 upath: 2.0.1 @@ -10192,7 +10246,7 @@ packages: deprecated: This package has been renamed to @vscode/web-custom-data, please update to the new name dev: true - /vue-demi@0.12.1(vue@3.2.37): + /vue-demi@0.12.1(vue@3.3.4): resolution: {integrity: sha512-QL3ny+wX8c6Xm1/EZylbgzdoDolye+VpCXRhI2hug9dJTP3OUJ3lmiKN3CsVV3mOJKwFi0nsstbgob0vG7aoIw==} engines: {node: '>=12'} hasBin: true @@ -10204,7 +10258,7 @@ packages: '@vue/composition-api': optional: true dependencies: - vue: 3.2.37 + vue: 3.3.4 dev: false /vue-eslint-parser@7.11.0(eslint@7.32.0): @@ -10225,13 +10279,13 @@ packages: - supports-color dev: true - /vue-router@4.0.12(vue@3.2.37): + /vue-router@4.0.12(vue@3.3.4): resolution: {integrity: sha512-CPXvfqe+mZLB1kBWssssTiWg4EQERyqJZes7USiqfW9B5N2x+nHlnsM1D3b5CaJ6qgCvMmYJnz+G0iWjNCvXrg==} peerDependencies: vue: ^3.0.0 dependencies: '@vue/devtools-api': 6.0.12 - vue: 3.2.37 + vue: 3.3.4 dev: false /vue-tsc@0.29.8(typescript@4.5.5): @@ -10264,14 +10318,14 @@ packages: '@vue/server-renderer': 3.2.31(vue@3.2.31) '@vue/shared': 3.2.31 - /vue@3.2.37: - resolution: {integrity: sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ==} + /vue@3.3.4: + resolution: {integrity: sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==} dependencies: - '@vue/compiler-dom': 3.2.37 - '@vue/compiler-sfc': 3.2.37 - '@vue/runtime-dom': 3.2.37 - '@vue/server-renderer': 3.2.37(vue@3.2.37) - '@vue/shared': 3.2.37 + '@vue/compiler-dom': 3.3.4 + '@vue/compiler-sfc': 3.3.4 + '@vue/runtime-dom': 3.3.4 + '@vue/server-renderer': 3.3.4(vue@3.3.4) + '@vue/shared': 3.3.4 /w3c-hr-time@1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} @@ -10372,7 +10426,7 @@ packages: engines: {node: '>= 10.0.0'} dependencies: '@babel/parser': 7.17.3 - '@babel/types': 7.17.0 + '@babel/types': 7.22.5 assert-never: 1.2.1 babel-walk: 3.0.0-canary-5 dev: true