From 3f69c3e6470bb5f809734e9fb025f8d8a1ed9485 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 6 Apr 2024 23:16:24 +0300 Subject: [PATCH] disable selection of unselectable blocks --- src/components/block/api.ts | 4 + src/components/block/index.ts | 4 + src/components/core.ts | 1 + src/components/modules/redactorKeydown.ts | 149 +++++++++++++++++++++- src/components/modules/ui.ts | 11 +- types/api/block.d.ts | 2 + 6 files changed, 161 insertions(+), 10 deletions(-) diff --git a/src/components/block/api.ts b/src/components/block/api.ts index d532d687..9cbe20c5 100644 --- a/src/components/block/api.ts +++ b/src/components/block/api.ts @@ -91,6 +91,10 @@ function BlockAPI( return block.focusable; }, + get selectable(): boolean { + return block.selectable; + }, + get inputs(): HTMLElement[] { return block.inputs; }, diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 53e14965..9b68c62d 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -398,6 +398,10 @@ export default class Block extends EventsDispatcher { return this.inputs.length !== 0; } + public get selectable(): boolean { + return this.tool.isLineBreaksEnabled !== true; + } + /** * Check block for emptiness * diff --git a/src/components/core.ts b/src/components/core.ts index e491110d..650c0f9c 100644 --- a/src/components/core.ts +++ b/src/components/core.ts @@ -253,6 +253,7 @@ export default class Core { 'RectangleSelection', // 'CrossBlockSelection', 'ReadOnly', + 'RedactorKeydown', // @todo rename ]; await modulesToPrepare.reduce( diff --git a/src/components/modules/redactorKeydown.ts b/src/components/modules/redactorKeydown.ts index bde5544a..017c764f 100644 --- a/src/components/modules/redactorKeydown.ts +++ b/src/components/modules/redactorKeydown.ts @@ -4,6 +4,7 @@ import type { API, BlockAPI } from '../../../types'; import { areBlocksMergeable } from '../utils/blocks'; import Dom from '../dom'; import { SelectionChanged } from '../events'; +import { debounce } from '../utils'; /** @@ -15,6 +16,10 @@ import { SelectionChanged } from '../events'; * 2. Clear selection part from that block * 3. Leave only selection part in the second block * 4. Prevent cross-input selection + * @todo cmd+A should select 1 block, second cmd+A should select all blocks + * @todo handle cross-input selection with keyboard + * @todo when cbs the inline-toolbar should contain intersected lists of tools for selected blocks + * @todo emit "selection changed" event to hide/show the Inline Toolbar */ @@ -126,6 +131,35 @@ function findIntersectedInputs(intersectedBlocks: BlockAPI[], range: Range): Blo }, []); } +/** + * + * @param range + * @param api + */ +function useCrossInputSelection(range: Range, api: API) { + // const selection = window.getSelection(); + + // /** + // * @todo handle native inputs + // */ + + // if (selection === null || !selection.rangeCount) { + // return { + // blocks: [], + // inputs: [], + // } + // } + + // const range = selection.getRangeAt(0); + + const intersectedBlocks = findIntersectedBlocks(range, api); + const intersectedInputs = findIntersectedInputs(intersectedBlocks, range); + + return { + blocks: intersectedBlocks, + inputs: intersectedInputs, + }; +} /** * @@ -136,12 +170,31 @@ export default class RedactorKeydown extends Module { * @param {...any} params */ constructor(...params) { + console.log('RedactorKeydown 1'); super(...params); + console.log('RedactorKeydown 2'); - this.eventsDispatcher.on(SelectionChanged, (data) => { - console.log('SelectionChanged', data); + /** + * Handle selection change to manipulate Inline Toolbar appearance + */ + // const selectionChangeDebounced = debounce(() => { + // this.removeSelectionFromUnselectableBlocks(); + // }, 30); + + // this.eventsDispatcher.on(SelectionChanged, selectionChangeDebounced); + } + + /** + * + */ + public prepare() { + this.listeners.on(this.Editor.UI.nodes.redactor, 'mouseup', (event: KeyboardEvent) => { + console.log('mouseup'); + this.removeSelectionFromUnselectableBlocks(); }); } + + /** * * @param event @@ -175,11 +228,17 @@ export default class RedactorKeydown extends Module { const range = selection.getRangeAt(0); - const intersectedBlocks = findIntersectedBlocks(range, api); - const intersectedInputs = findIntersectedInputs(intersectedBlocks, range); + // const intersectedBlocks = findIntersectedBlocks(range, api); + // const intersectedInputs = findIntersectedInputs(intersectedBlocks, range); + + const { blocks: intersectedBlocks, inputs: intersectedInputs } = useCrossInputSelection(range, api); console.log('intersectedInputs', intersectedInputs); + if (!intersectedInputs.length) { + return; + } + /** * Handle case when user select the whole block. @@ -317,4 +376,86 @@ export default class RedactorKeydown extends Module { // }); }); } + + + /** + * + */ + private removeSelectionFromUnselectableBlocks(): void { + const api = this.Editor.API.methods; + const selection = window.getSelection(); + + /** + * @todo handle native inputs + */ + + if (selection === null || !selection.rangeCount) { + return; + } + + const range = selection.getRangeAt(0); + + const { blocks: intersectedBlocks, inputs: intersectedInputs } = useCrossInputSelection(range, api); + + /** + * If selection is not cross-input, do nothing + */ + if (intersectedBlocks.length < 2) { + return; + } + + + const startingBlockApi = intersectedBlocks[0]; + const endingBlockApi = intersectedBlocks[intersectedBlocks.length - 1]; + + /** + * get rid of this by adding 'merge' api method + */ + const startingBlock = this.Editor.BlockManager.getBlockById(startingBlockApi.id); + const endingBlock = this.Editor.BlockManager.getBlockById(endingBlockApi.id); + + /** + * If selection started in a Block that is not selectable, remove range from this Block to the next selectable Block + */ + if (!startingBlock?.selectable) { + // range.setStart(range.endContainer, range.endOffset); + + const blockIndex = api.blocks.getBlockIndex(startingBlock.id); + + /** + * @todo find next selectable block, not just the next one + */ + const nextBlock = api.blocks.getBlockByIndex(blockIndex + 1); + + if (!nextBlock) { + return; + } + + const nextBlockElement = nextBlock.holder; + + range.setStart(nextBlockElement, 0); + } + + /** + * If selection ended in a Block that is not selectable, remove range from that Block to the previous selectable Block + */ + if (!endingBlock?.selectable) { + // range.setEnd(range.startContainer, range.startOffset); + + const blockIndex = api.blocks.getBlockIndex(endingBlock.id); + + /** + * @todo find previous selectable block, not just the previous one + */ + const previousBlock = api.blocks.getBlockByIndex(blockIndex - 1); + + if (!previousBlock) { + return; + } + + const previousBlockElement = previousBlock.holder; + + range.setEnd(previousBlockElement, previousBlockElement.childNodes.length); + } + } } diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index e9e46e45..b8dde784 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -21,11 +21,10 @@ import { SelectionChanged } from '../events'; /** * CBS + * * @todo disable native ENTER and use a custom one * @todo disable native BACKSPACE/DELETE and use a custom ones * @todo get rid of BlockEvents, use event listener on redactor instead - * - * */ /** @@ -370,11 +369,11 @@ export default class UI extends Module { /** * Handle selection change to manipulate Inline Toolbar appearance */ - const selectionChangeDebounced = _.debounce(() => { - this.selectionChanged(); - }, selectionChangeDebounceTimeout); + // const selectionChangeDebounced = _.debounce(() => { + // this.selectionChanged(); + // }, selectionChangeDebounceTimeout); - this.readOnlyMutableListeners.on(document, 'selectionchange', selectionChangeDebounced, true); + // this.readOnlyMutableListeners.on(document, 'selectionchange', selectionChangeDebounced, true); this.readOnlyMutableListeners.on(window, 'resize', () => { this.resizeDebouncer(); diff --git a/types/api/block.d.ts b/types/api/block.d.ts index 12085684..ec295b60 100644 --- a/types/api/block.d.ts +++ b/types/api/block.d.ts @@ -40,6 +40,8 @@ export interface BlockAPI { */ readonly focusable: boolean; + readonly selectable: boolean; + readonly inputs: HTMLElement[]; /**