diff --git a/packages/blocks/src/note-block/note-block.ts b/packages/blocks/src/note-block/note-block.ts index c54e73f6b811..81935a4f08a7 100644 --- a/packages/blocks/src/note-block/note-block.ts +++ b/packages/blocks/src/note-block/note-block.ts @@ -10,7 +10,10 @@ import { AFFINE_DRAG_HANDLE_WIDGET, AffineDragHandleWidget, } from '../page-block/widgets/drag-handle/drag-handle.js'; -import { captureEventTarget } from '../page-block/widgets/drag-handle/utils.js'; +import { + captureEventTarget, + getDuplicateBlocks, +} from '../page-block/widgets/drag-handle/utils.js'; import { KeymapController } from './keymap-controller.js'; import { type NoteBlockModel, NoteBlockSchema } from './note-model.js'; @@ -31,11 +34,15 @@ export class NoteBlockComponent extends BlockElement { flavour: NoteBlockSchema.model.flavour, edgeless: true, onDragStart: ({ state, startDragging, anchorBlockPath }) => { - if (!anchorBlockPath) return false; + if (!anchorBlockPath) { + return false; + } const element = captureEventTarget(state.raw.target); const insideDragHandle = !!element?.closest(AFFINE_DRAG_HANDLE_WIDGET); - if (!insideDragHandle) return false; + if (!insideDragHandle) { + return false; + } const anchorComponent = this.std.view.viewFromPath( 'block', @@ -44,32 +51,37 @@ export class NoteBlockComponent extends BlockElement { if ( !anchorComponent || !matchFlavours(anchorComponent.model, [NoteBlockSchema.model.flavour]) - ) + ) { return false; - + } const noteComponent = anchorComponent as NoteBlockComponent; const notePortal = noteComponent.closest('.edgeless-block-portal-note'); assertExists(notePortal); + const dragPreviewEl = notePortal.cloneNode() as HTMLElement; dragPreviewEl.style.transform = ''; dragPreviewEl.style.left = '0'; dragPreviewEl.style.top = '0'; + const noteBackground = notePortal.querySelector('.note-background'); assertExists(noteBackground); + const noteBackgroundClone = noteBackground.cloneNode(); dragPreviewEl.appendChild(noteBackgroundClone); + const container = document.createElement('div'); container.style.width = '100%'; container.style.height = '100%'; container.style.overflow = 'hidden'; dragPreviewEl.appendChild(container); + render(noteComponent.host.renderModel(noteComponent.model), container); startDragging([noteComponent], state, dragPreviewEl); return true; }, - onDragEnd: ({ draggingElements, dropBlockId, dropType }) => { + onDragEnd: ({ draggingElements, dropBlockId, dropType, state }) => { if ( draggingElements.length !== 1 || !matchFlavours(draggingElements[0].model, [ @@ -78,20 +90,37 @@ export class NoteBlockComponent extends BlockElement { ) { return false; } - if (dropType === 'in') return true; + + if (dropType === 'in') { + return true; + } const noteBlock = draggingElements[0].model as NoteBlockModel; const targetBlock = this.page.getBlockById(dropBlockId); const parentBlock = this.page.getParent(dropBlockId); - if (!targetBlock || !parentBlock) return true; + if (!targetBlock || !parentBlock) { + return true; + } - this.page.moveBlocks( - noteBlock.children, - parentBlock, - targetBlock, - dropType === 'before' - ); - this.page.deleteBlock(noteBlock); + const altKey = state.raw.altKey; + if (altKey) { + const duplicateBlocks = getDuplicateBlocks(noteBlock.children); + + const parentIndex = + parentBlock.children.indexOf(targetBlock) + + (dropType === 'after' ? 1 : 0); + + this.page.addBlocks(duplicateBlocks, parentBlock, parentIndex); + } else { + this.page.moveBlocks( + noteBlock.children, + parentBlock, + targetBlock, + dropType === 'before' + ); + + this.page.deleteBlock(noteBlock); + } return true; }, diff --git a/packages/blocks/src/page-block/widgets/drag-handle/drag-handle.ts b/packages/blocks/src/page-block/widgets/drag-handle/drag-handle.ts index c2ed09657f79..cb5fcde4851e 100644 --- a/packages/blocks/src/page-block/widgets/drag-handle/drag-handle.ts +++ b/packages/blocks/src/page-block/widgets/drag-handle/drag-handle.ts @@ -64,6 +64,7 @@ import { getClosestNoteBlock, getDragHandleContainerHeight, getDragHandleLeftPadding, + getDuplicateBlocks, getNoteId, includeTextSelection, insideDatabaseTable, @@ -366,12 +367,15 @@ export class AffineDragHandleWidget extends WidgetElement< const offset = this._calculatePreviewOffset(blockElements, state); const posX = state.raw.x - offset.x; const posY = state.raw.y - offset.y; + const altKey = state.raw.altKey; dragPreview = new DragPreview(offset); dragPreview.style.width = `${width / this.scale}px`; dragPreview.style.transform = `translate(${posX}px, ${posY}px) scale(${ this.scale * this.noteScale })`; + + dragPreview.style.opacity = altKey ? '1' : '0.5'; dragPreview.appendChild(fragment); } this.pageBlockElement.appendChild(dragPreview); @@ -405,6 +409,9 @@ export class AffineDragHandleWidget extends WidgetElement< this.dragPreview.style.transform = `translate(${posX}px, ${posY}px) scale(${ this.scale * this.noteScale })`; + + const altKey = state.raw.altKey; + this.dragPreview.style.opacity = altKey ? '1' : '0.5'; }; private _updateDragPreviewOnViewportUpdate = () => { @@ -434,11 +441,16 @@ export class AffineDragHandleWidget extends WidgetElement< dragPreviewEl?: HTMLElement, dragPreviewOffset?: Point ) => { - if (!blockElements.length) return; + if (!blockElements.length) { + return; + } this.draggingElements = blockElements; - if (this.dragPreview) this._removeDragPreview(); + if (this.dragPreview) { + this._removeDragPreview(); + } + this.dragPreview = this._createDragPreview( blockElements, state, @@ -462,10 +474,13 @@ export class AffineDragHandleWidget extends WidgetElement< this._anchorBlockId = ''; this._anchorBlockPath = null; - if (this._dragHandleContainer) + if (this._dragHandleContainer) { this._dragHandleContainer.style.display = 'none'; + } - if (force) this._reset(); + if (force) { + this._reset(); + } }; private _reset = () => { @@ -1143,7 +1158,14 @@ export class AffineDragHandleWidget extends WidgetElement< }, }); - this.page.moveBlocks(selectedBlocks, newNoteBlock); + const altKey = state.raw.altKey; + if (altKey) { + const duplicateBlocks = getDuplicateBlocks(selectedBlocks); + + this.page.addBlocks(duplicateBlocks, newNoteBlock); + } else { + this.page.moveBlocks(selectedBlocks, newNoteBlock); + } return true; } @@ -1154,13 +1176,16 @@ export class AffineDragHandleWidget extends WidgetElement< this.selectedBlocks.map(selection => selection.blockId), targetBlockId ) - ) + ) { return false; + } const selectedBlocks = getBlockElementsExcludeSubtrees(draggingElements) .map(element => getModelByBlockComponent(element)) .filter((x): x is BlockModel => !!x); - if (!selectedBlocks.length) return false; + if (!selectedBlocks.length) { + return false; + } const targetBlock = this.page.getBlockById(targetBlockId); assertExists(targetBlock); @@ -1172,15 +1197,32 @@ export class AffineDragHandleWidget extends WidgetElement< : this.page.getParent(targetBlockId); assertExists(parent); + const altKey = state.raw.altKey; + if (shouldInsertIn) { - this.page.moveBlocks(selectedBlocks, targetBlock); + if (altKey) { + const duplicateBlocks = getDuplicateBlocks(selectedBlocks); + + this.page.addBlocks(duplicateBlocks, targetBlock); + } else { + this.page.moveBlocks(selectedBlocks, targetBlock); + } } else { - this.page.moveBlocks( - selectedBlocks, - parent, - targetBlock, - dropType === 'before' - ); + if (altKey) { + const duplicateBlocks = getDuplicateBlocks(selectedBlocks); + + const parentIndex = + parent.children.indexOf(targetBlock) + (dropType === 'after' ? 1 : 0); + + this.page.addBlocks(duplicateBlocks, parent, parentIndex); + } else { + this.page.moveBlocks( + selectedBlocks, + parent, + targetBlock, + dropType === 'before' + ); + } } // TODO: need a better way to update selection @@ -1216,7 +1258,9 @@ export class AffineDragHandleWidget extends WidgetElement< const state = ctx.get('pointerState'); // If not click left button to start dragging, should do nothing const { button } = state.raw; - if (button !== 0) return false; + if (button !== 0) { + return false; + } // call default drag start handler if no option return true for (const option of this.optionRunner.options) { @@ -1298,7 +1342,11 @@ export class AffineDragHandleWidget extends WidgetElement< // call default drag end handler if no option return true this._onDragEnd(state); - if (isInsideEdgelessEditor(this.host)) this._checkTopLevelBlockSelection(); + + if (isInsideEdgelessEditor(this.host)) { + this._checkTopLevelBlockSelection(); + } + return true; }; @@ -1322,6 +1370,20 @@ export class AffineDragHandleWidget extends WidgetElement< if (outOfPageViewPort && !inDragHandle && !inPage) this._hide(); }; + private _keyboardHandler: UIEventHandler = ctx => { + if (!this.dragging || !this.dragPreview) { + return; + } + + const state = ctx.get('defaultState'); + const event = state.event as KeyboardEvent; + event.preventDefault(); + event.stopPropagation(); + + const altKey = event.key === 'Alt' && event.altKey; + this.dragPreview.style.opacity = altKey ? '1' : '0.5'; + }; + private _onDragHandlePointerEnter = () => { const container = this._dragHandleContainer; const grabber = this._dragHandleGrabber; @@ -1529,6 +1591,8 @@ export class AffineDragHandleWidget extends WidgetElement< this.handleEvent('dragEnd', this._dragEndHandler); this.handleEvent('pointerOut', this._pointerOutHandler); this.handleEvent('beforeInput', () => this._hide()); + this.handleEvent('keyDown', this._keyboardHandler, { global: true }); + this.handleEvent('keyUp', this._keyboardHandler, { global: true }); } override disconnectedCallback() { diff --git a/packages/blocks/src/page-block/widgets/drag-handle/utils.ts b/packages/blocks/src/page-block/widgets/drag-handle/utils.ts index 9c058b7b5952..b0173823ab13 100644 --- a/packages/blocks/src/page-block/widgets/drag-handle/utils.ts +++ b/packages/blocks/src/page-block/widgets/drag-handle/utils.ts @@ -346,13 +346,21 @@ export function updateDragHandleClassName(blockElements: BlockElement[] = []) { blockElements.forEach(blockElement => blockElement.classList.add(className)); } -function getBlockProps(model: BlockModel) { +function getBlockProps(model: BlockModel): { [index: string]: unknown } { const keys = model.keys as (keyof typeof model)[]; const values = keys.map(key => model[key]); const blockProps = Object.fromEntries(keys.map((key, i) => [key, values[i]])); return blockProps; } +export function getDuplicateBlocks(blocks: BlockModel[]) { + const duplicateBlocks = blocks.map(block => ({ + flavour: block.flavour, + blockProps: getBlockProps(block), + })); + return duplicateBlocks; +} + export function convertDragPreviewDocToEdgeless({ blockComponent, dragPreview, @@ -360,6 +368,7 @@ export function convertDragPreviewDocToEdgeless({ width, height, noteScale, + state, }: OnDragEndProps & { blockComponent: BlockElement; cssSelector: string; @@ -369,7 +378,9 @@ export function convertDragPreviewDocToEdgeless({ const edgelessPage = blockComponent.closest( 'affine-edgeless-page' ) as EdgelessPageBlockComponent; - if (!edgelessPage) return false; + if (!edgelessPage) { + return false; + } const previewEl = dragPreview.querySelector(cssSelector); assertExists(previewEl); @@ -397,7 +408,12 @@ export function convertDragPreviewDocToEdgeless({ }, edgelessPage.surfaceBlockModel ); - blockComponent.page.deleteBlock(blockModel); + + const altKey = state.raw.altKey; + if (!altKey) { + blockComponent.page.deleteBlock(blockModel); + } + return true; } @@ -405,6 +421,7 @@ export function convertDragPreviewEdgelessToDoc({ blockComponent, dropBlockId, dropType, + state, }: OnDragEndProps & { blockComponent: BlockElement; }): boolean { @@ -419,12 +436,18 @@ export function convertDragPreviewEdgelessToDoc({ assertExists(parentBlock); const parentIndex = shouldInsertIn ? 0 - : parentBlock.children.indexOf(targetBlock); + : parentBlock.children.indexOf(targetBlock) + + (dropType === 'after' ? 1 : 0); const blockModel = blockComponent.model; const { width, height, xywh, rotate, zIndex, ...blockProps } = getBlockProps(blockModel); page.addBlock(blockModel.flavour, blockProps, parentBlock, parentIndex); - page.deleteBlock(blockModel); + + const altKey = state.raw.altKey; + if (!altKey) { + page.deleteBlock(blockModel); + } + return true; }