Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable selecting image when the only element in the range is an Image #2554

Merged
merged 13 commits into from
Apr 5, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ export function addRangeToSelection(doc: Document, range: Range, isReverted: boo
const selection = doc.defaultView?.getSelection();

if (selection) {
const currentRange = selection.rangeCount > 0 && selection.getRangeAt(0);
if (
currentRange &&
currentRange.startContainer == range.startContainer &&
currentRange.endContainer == range.endContainer &&
currentRange.startOffset == range.startOffset &&
currentRange.endOffset == range.endOffset
) {
return;
}
selection.removeAllRanges();

if (!isReverted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import type {

const DOM_SELECTION_CSS_KEY = '_DOMSelection';
const HIDE_CURSOR_CSS_KEY = '_DOMSelectionHideCursor';
const HIDE_SELECTION_CSS_KEY = '_DOMSelectionHideSelection';
const IMAGE_ID = 'image';
const TABLE_ID = 'table';
const DEFAULT_SELECTION_BORDER_COLOR = '#DB626C';
const TABLE_CSS_RULE = 'background-color:#C6C6C6!important;';
const CARET_CSS_RULE = 'caret-color: transparent';
const TRANSPARENT_SELECTION_CSS_RULE = 'background-color: transparent !important';
const SELECTION_SELECTOR = '*::selection';

/**
* @internal
Expand All @@ -31,6 +34,7 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC
core.selection.skipReselectOnFocus = true;
core.api.setEditorStyle(core, DOM_SELECTION_CSS_KEY, null /*cssRule*/);
core.api.setEditorStyle(core, HIDE_CURSOR_CSS_KEY, null /*cssRule*/);
core.api.setEditorStyle(core, HIDE_SELECTION_CSS_KEY, null /*cssRule*/);

try {
switch (selection?.type) {
Expand All @@ -46,9 +50,14 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC
}!important;`,
[`#${ensureUniqueId(image, IMAGE_ID)}`]
);
core.api.setEditorStyle(core, HIDE_CURSOR_CSS_KEY, CARET_CSS_RULE);
core.api.setEditorStyle(
core,
HIDE_SELECTION_CSS_KEY,
TRANSPARENT_SELECTION_CSS_RULE,
[SELECTION_SELECTOR]
);

setRangeSelection(doc, image);
setRangeSelection(doc, image, false /* collapse */);
break;
case 'table':
const { table, firstColumn, firstRow, lastColumn, lastRow } = selection;
Expand Down Expand Up @@ -105,7 +114,11 @@ export const setDOMSelection: SetDOMSelection = (core, selection, skipSelectionC
const nodeToSelect = firstCell.cell?.firstElementChild || firstCell.cell;

if (nodeToSelect) {
setRangeSelection(doc, (nodeToSelect as HTMLElement) || undefined);
setRangeSelection(
doc,
(nodeToSelect as HTMLElement) || undefined,
true /* collapse */
);
}

break;
Expand Down Expand Up @@ -197,13 +210,24 @@ function handleTableSelected(
return selectors;
}

function setRangeSelection(doc: Document, element: HTMLElement | undefined) {
function setRangeSelection(doc: Document, element: HTMLElement | undefined, collapse: boolean) {
if (element && doc.contains(element)) {
const range = doc.createRange();
let isReverted: boolean | undefined = undefined;

range.selectNode(element);
range.collapse();
if (collapse) {
range.collapse();
} else {
const selection = doc.defaultView?.getSelection();
const range = selection && selection.rangeCount > 0 && selection.getRangeAt(0);
if (selection && range) {
isReverted =
selection.focusNode != range.endContainer ||
selection.focusOffset != range.endOffset;
}
}

addRangeToSelection(doc, range);
addRangeToSelection(doc, range, isReverted);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { findCoordinate } from './findCoordinate';
import { findTableCellElement } from '../../coreApi/setDOMSelection/findTableCellElement';
import { isSingleImageInSelection } from './isSingleImageInSelection';
import { normalizePos } from './normalizePos';
import {
isCharacterValue,
Expand All @@ -21,6 +22,7 @@ import type {
ParsedTable,
TableSelectionInfo,
TableCellCoordinate,
RangeSelection,
} from 'roosterjs-content-model-types';

const MouseLeftButton = 0;
Expand Down Expand Up @@ -126,8 +128,7 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
this.getContainedTargetImage(rawEvent, selection)) &&
image.isContentEditable
) {
this.selectImage(image);

this.selectImageWithRange(image, rawEvent);
return;
} else if (selection?.type == 'image' && selection.image !== rawEvent.target) {
this.selectBeforeOrAfterElement(editor, selection.image);
Expand Down Expand Up @@ -232,6 +233,25 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
}
};

private selectImageWithRange(image: HTMLImageElement, event: Event) {
const range = image.ownerDocument.createRange();
range.selectNode(image);

const domSelection = this.editor?.getDOMSelection();
if (domSelection?.type == 'image' && image == domSelection.image) {
event.preventDefault();
} else {
this.setDOMSelection(
{
type: 'range',
isReverted: false,
range,
},
null
);
}
}

private onMouseUp(event: MouseUpEvent) {
let image: HTMLImageElement | null;

Expand All @@ -243,7 +263,7 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
MouseRightButton /* it's not possible to drag using right click */ ||
event.isClicking)
) {
this.selectImage(image);
this.selectImageWithRange(image, event.rawEvent);
}

this.detachMouseEvent();
Expand Down Expand Up @@ -442,16 +462,6 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
}
}

private selectImage(image: HTMLImageElement) {
this.setDOMSelection(
{
type: 'image',
image: image,
},
null /*tableSelection*/
);
}

private selectBeforeOrAfterElement(editor: IEditor, element: HTMLElement, after?: boolean) {
const doc = editor.getDocument();
const parent = element.parentNode;
Expand Down Expand Up @@ -525,22 +535,27 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {

//If am image selection changed to a wider range due a keyboard event, we should update the selection
const selection = this.editor.getDocument().getSelection();
if (
newSelection?.type == 'image' &&
selection &&
selection.containsNode(newSelection.image, false /*partiallyContained*/)
) {
this.editor.setDOMSelection({
type: 'range',
range: selection.getRangeAt(0),
isReverted: false,
});

if (newSelection?.type == 'image' && selection) {
if (selection && !isSingleImageInSelection(selection)) {
const range = selection.getRangeAt(0);
this.editor.setDOMSelection({
type: 'range',
range,
isReverted:
selection.focusNode != range.endContainer ||
selection.focusOffset != range.endOffset,
});
}
}

// Safari has problem to handle onBlur event. When blur, we cannot get the original selection from editor.
// So we always save a selection whenever editor has focus. Then after blur, we can still use this cached selection.
if (newSelection?.type == 'range' && this.isSafari) {
this.state.selection = newSelection;
if (newSelection?.type == 'range') {
if (this.isSafari) {
this.state.selection = newSelection;
}
this.trySelectSingleImage(newSelection);
}
}
};
Expand Down Expand Up @@ -611,6 +626,21 @@ class SelectionPlugin implements PluginWithState<SelectionPluginState> {
this.state.mouseDisposer = undefined;
}
}

private trySelectSingleImage(selection: RangeSelection) {
if (!selection.range.collapsed) {
const image = isSingleImageInSelection(selection.range);
if (image) {
this.setDOMSelection(
{
type: 'image',
image: image,
},
null /*tableSelection*/
);
}
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { isElementOfType, isNodeOfType } from 'roosterjs-content-model-dom';

/**
* @internal
*/
export function isSingleImageInSelection(selection: Selection | Range): HTMLImageElement | null {
const { startNode, endNode, startOffset, endOffset } = getProps(selection);

const max = Math.max(startOffset, endOffset);
const min = Math.min(startOffset, endOffset);

if (startNode && endNode && startNode == endNode && max - min == 1) {
const node = startNode?.childNodes.item(min);
if (isNodeOfType(node, 'ELEMENT_NODE') && isElementOfType(node, 'img')) {
return node;
}
}
return null;
}
function getProps(
selection: Selection | Range
): { startNode: Node | null; endNode: Node | null; startOffset: number; endOffset: number } {
if (isSelection(selection)) {
return {
startNode: selection.anchorNode,
endNode: selection.focusNode,
startOffset: selection.anchorOffset,
endOffset: selection.focusOffset,
};
} else {
return {
startNode: selection.startContainer,
endNode: selection.endContainer,
startOffset: selection.startOffset,
endOffset: selection.endOffset,
};
}
}

function isSelection(selection: Selection | Range): selection is Selection {
return !!(selection as Selection).getRangeAt;
}
Loading
Loading