From 8f09692e07158b42f900f0364f79089866fed034 Mon Sep 17 00:00:00 2001 From: Peter Savchenko Date: Sun, 21 Apr 2024 00:16:46 +0300 Subject: [PATCH] nice composable for cis --- src/components/modules/crossInputSelection.ts | 135 +++++++++--------- src/components/utils/cbs.ts | 40 ++++-- 2 files changed, 99 insertions(+), 76 deletions(-) diff --git a/src/components/modules/crossInputSelection.ts b/src/components/modules/crossInputSelection.ts index 76e4aada..4b770c59 100644 --- a/src/components/modules/crossInputSelection.ts +++ b/src/components/modules/crossInputSelection.ts @@ -28,6 +28,8 @@ import { isPrintableKey } from '../utils'; * * What is done: * - Backspace handling + * - Manual tripple click selection of the whole input + * - Enter handling */ @@ -61,6 +63,43 @@ export default class CrossInputSelection extends Module { }, { capture: true, }); + + this.listeners.on(this.Editor.UI.nodes.redactor, 'click', (event: MouseEvent) => { // @todo implement generics in Listeners + /** + * Handle tripple click on a block. + * 4 or more clicks behaves the same + */ + if (event.detail > 3) { + this.handleTripleClick(event); + } + }); + } + + /** + * When user makes a tripple click to select the whole block, range.endContainer will be a next block container. + * To workaround this browser behavior, we should manually select the whole input + * @param event + */ + private handleTripleClick(event: MouseEvent): void { + const currentClickedElement = event.target as HTMLElement; + + /** + * @todo support native inputs + */ + const currentInput = currentClickedElement.closest('[contenteditable=true]'); + + if (!currentInput) { + return; + } + + const selection = window.getSelection(); + const range = selection?.getRangeAt(0); + + if (!range) { + return; + } + + range.selectNodeContents(currentInput); } /** @@ -173,81 +212,45 @@ export default class CrossInputSelection extends Module { } private handleEnter(): void { - console.log('handle enter (wip)'); - const api = this.Editor.API.methods; - const { - blocks: intersectedBlocks, - inputs: intersectedInputs, - range, - firstInput, - lastInput, - middleInputs, - isCrossBlockSelection, - } = useCrossInputSelection(api, { - onSingleFullySelectedInput: ({ input, block }) => { - console.log('OPAAA'); - api.blocks.delete(block.id); + useCrossInputSelection(api, { + onSingleFullySelectedInput: () => { + console.warn('Enter / onSingleFullySelectedInput. @todo: clear input and add the new block below'); }, - onSinglePartiallySelectedInput: ({ input, block }) => { - console.log('default behavior'); + onSinglePartiallySelectedInput: () => { + console.warn('Enter / onSinglePartiallySelectedInput: @todo: clear selection and split block'); }, - }); + onCrossInputSelection({ range, firstInput, lastInput, middleInputs, blocks, inputs }){ + console.info('Enter / onCrossInputSelection: remove selected content and set caret to the start of the next block'); + /** + * Now we need: + * 1. Get first input and remove selected content starting from the beginning of the selection to the end of the input + * 2. Get last input and remove selected content starting from the beginning of the input to the end of the selection + * 3. Get all inputs between first and last and remove them (and blocks if they are empty after removing inputs) + * 4. Set caret to the start of the last input + */ + removeRangePartFromInput(range, firstInput.input, { fromRangeStartToInputEnd: true }); + removeRangePartFromInput(range, lastInput.input, { fromInputStartToRangeEnd: true }); - if (!intersectedBlocks.length && !intersectedInputs.length || !range) { - return; - } + const removedInputs: BlockInput[] = []; + middleInputs.forEach(({ input }: BlockInputIntersected) => { + removedInputs.push(input); + input.remove(); + }); - // if (intersectedInputs.length === 1) { - // const { input, block } = intersectedInputs[0]; - // const isWholeInputSelected = range.toString() === input.textContent; + /** + * Remove blocks if they are empty + */ + blocks.forEach((block: BlockAPI) => { + if (block.inputs.every(input => removedInputs.includes(input))) { + api.blocks.delete(block.id); + } + }); - // if (isWholeInputSelected) { - // console.log('OPA'); - - - // } else { - // console.log('default behavior'); - - // return; - // } - // } - - if (!isCrossBlockSelection) { - /** @todo Split block */ - return; - } - - /** - * Now we need: - * 1. Get first input and remove selected content starting from the beginning of the selection to the end of the input - * 2. Get last input and remove selected content starting from the beginning of the input to the end of the selection - * 3. Get all inputs between first and last and remove them (and blocks if they are empty after removing inputs) - * 4. Set caret to the start of the last input - */ - removeRangePartFromInput(range, firstInput!.input, { fromRangeStartToInputEnd: true }); - removeRangePartFromInput(range, lastInput!.input, { fromInputStartToRangeEnd: true }); - - - const removedInputs: BlockInput[] = []; - middleInputs.forEach(({ input }: BlockInputIntersected) => { - removedInputs.push(input); - input.remove(); - }); - - /** - * Remove blocks if they are empty - */ - intersectedBlocks.forEach((block: BlockAPI) => { - if (block.inputs.every(input => removedInputs.includes(input))) { - api.blocks.delete(block.id); + window.getSelection()?.collapseToEnd(); } }); - - window.getSelection()?.collapseToEnd(); - - } /** diff --git a/src/components/utils/cbs.ts b/src/components/utils/cbs.ts index 248f0f8b..8033460b 100644 --- a/src/components/utils/cbs.ts +++ b/src/components/utils/cbs.ts @@ -22,7 +22,7 @@ export type BlockInputIntersected = { block: BlockAPI, }; -export interface CrossInputSelection { +export interface MaybeCrossInputSelection { isCrossBlockSelection: boolean; isCrossInputSelection: boolean; blocks: BlockAPI[]; @@ -33,6 +33,14 @@ export interface CrossInputSelection { middleInputs: BlockInputIntersected[]; } +export type CrossInputSelection = MaybeCrossInputSelection & { + isCrossInputSelection: true; + firstInput: BlockInputIntersected; + lastInput: BlockInputIntersected; + middleInputs: BlockInputIntersected[]; + range: Range; +} + /** * Return a Block API * @@ -214,8 +222,9 @@ export function removeRangePartFromInput(range: Range, input: HTMLElement, optio } interface CBSOptions { - onSingleFullySelectedInput: (input: BlockInputIntersected) => void; - onSinglePartiallySelectedInput: (input: BlockInputIntersected) => void; + onSingleFullySelectedInput?: (input: BlockInputIntersected) => void; + onSinglePartiallySelectedInput?: (input: BlockInputIntersected) => void; + onCrossInputSelection?: (selection: CrossInputSelection) => void; } /** @@ -223,14 +232,15 @@ interface CBSOptions { * * @param api - Editor API */ -export function useCrossInputSelection(api: API, options?: CBSOptions): CrossInputSelection { +export function useCrossInputSelection(api: API, options?: CBSOptions): MaybeCrossInputSelection { const selection = window.getSelection(); /** * @todo handle native inputs */ - if (selection === null || !selection.rangeCount || selection.isCollapsed) { + if (selection === null || !selection.rangeCount) { + console.log('No selection'); return { blocks: [], inputs: [], @@ -249,7 +259,6 @@ export function useCrossInputSelection(api: API, options?: CBSOptions): CrossInp const intersectedInputs = findIntersectedInputs(intersectedBlocks, range); const isCrossBlockSelection = intersectedBlocks.length > 1; - const isCrossInputSelection = intersectedInputs.length > 1; const firstInput = intersectedInputs[0] ?? null; const lastInput = intersectedInputs[intersectedInputs.length - 1] ?? null; @@ -259,16 +268,27 @@ export function useCrossInputSelection(api: API, options?: CBSOptions): CrossInp const { input, block } = firstInput; const isWholeInputSelected = range.toString() === input.textContent; - if (isWholeInputSelected) {; - options?.onSingleFullySelectedInput(firstInput); + if (isWholeInputSelected) { + options?.onSingleFullySelectedInput?.(firstInput); } else { - options?.onSinglePartiallySelectedInput(firstInput); + options?.onSinglePartiallySelectedInput?.(firstInput); } + } else { + options?.onCrossInputSelection?.({ + isCrossBlockSelection, + isCrossInputSelection: true, + blocks: intersectedBlocks, + inputs: intersectedInputs, + range, + firstInput, + lastInput, + middleInputs, + }); } return { isCrossBlockSelection, - isCrossInputSelection, + isCrossInputSelection: intersectedInputs.length > 1, blocks: intersectedBlocks, inputs: intersectedInputs, range,