mirror of
https://github.com/codex-team/editor.js
synced 2026-03-16 23:55:49 +01:00
nice composable for cis
This commit is contained in:
parent
473301528b
commit
8f09692e07
2 changed files with 99 additions and 76 deletions
|
|
@ -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();
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue