Skip to content

Commit

Permalink
DOM: merge place caret at edge functions
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix committed May 19, 2021
1 parent d8d2a39 commit b5b89be
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 145 deletions.
1 change: 0 additions & 1 deletion packages/dom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,6 @@ _Parameters_
- _container_ `HTMLElement`: Focusable element.
- _isReverse_ `boolean`: True for bottom, false for top.
- _rect_ `[DOMRect]`: The rectangle to position the caret with.
- _mayUseScroll_ `[boolean]`: True to allow scrolling, false to disallow.
<a name="remove" href="#remove">#</a> **remove**
Expand Down
95 changes: 95 additions & 0 deletions packages/dom/src/dom/place-caret-at-edge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Internal dependencies
*/
import hiddenCaretRangeFromPoint from './hidden-caret-range-from-point';
import { assertIsDefined } from '../utils/assert-is-defined';
import isInputOrTextArea from './is-input-or-text-area';
import isRTL from './is-rtl';

/**
* Gets the range to place.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse True for end, false for start.
* @param {number|undefined} x X coordinate to vertically position.
*
* @return {Range|null} The range to place.
*/
function getRange( container, isReverse, x ) {
const { ownerDocument } = container;
// In the case of RTL scripts, the horizontal edge is at the opposite side.
const isReverseDir = isRTL( container ) ? ! isReverse : isReverse;
const containerRect = container.getBoundingClientRect();
// When placing at the end (isReverse), find the closest range to the bottom
// right corner. When placing at the start, to the top left corner.
if ( x === undefined ) {
x = isReverse ? containerRect.right - 1 : containerRect.left + 1;
}
const y = isReverseDir ? containerRect.bottom - 1 : containerRect.top + 1;
return hiddenCaretRangeFromPoint( ownerDocument, x, y, container );
}

/**
* Places the caret at start or end of a given element.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse True for end, false for start.
* @param {number|undefined} x X coordinate to vertically position.
*/
export default function placeCaretAtEdge( container, isReverse, x ) {
if ( ! container ) {
return;
}

container.focus();

if ( isInputOrTextArea( container ) ) {
// The element may not support selection setting.
if ( typeof container.selectionStart !== 'number' ) {
return;
}

if ( isReverse ) {
container.selectionStart = container.value.length;
container.selectionEnd = container.value.length;
} else {
container.selectionStart = 0;
container.selectionEnd = 0;
}

return;
}

if ( ! container.isContentEditable ) {
return;
}

let range = getRange( container, isReverse, x );

// If no range range can be created or it is outside the container, the
// element may be out of view.
if (
! range ||
! range.startContainer ||
! container.contains( range.startContainer )
) {
container.scrollIntoView( isReverse );
range = range = getRange( container, isReverse, x );

if (
! range ||
! range.startContainer ||
! container.contains( range.startContainer )
) {
return;
}
}

const { ownerDocument } = container;
const { defaultView } = ownerDocument;
assertIsDefined( defaultView, 'defaultView' );
const selection = defaultView.getSelection();
assertIsDefined( selection, 'selection' );
selection.removeAllRanges();
selection.addRange( range );
}
85 changes: 2 additions & 83 deletions packages/dom/src/dom/place-caret-at-horizontal-edge.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,7 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';

/**
* Internal dependencies
*/
import hiddenCaretRangeFromPoint from './hidden-caret-range-from-point';
import isInputOrTextArea from './is-input-or-text-area';
import isRTL from './is-rtl';

/**
* Gets the range to place.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse True for end, false for start.
*
* @return {Range|null} The range to place.
*/
function getRange( container, isReverse ) {
const { ownerDocument } = container;
// In the case of RTL scripts, the horizontal edge is at the opposite side.
const isReverseDir = isRTL( container ) ? ! isReverse : isReverse;
const containerRect = container.getBoundingClientRect();
// When placing at the end (isReverse), find the closest range to the bottom
// right corner. When placing at the start, to the top left corner.
const x = isReverse ? containerRect.right - 1 : containerRect.left + 1;
const y = isReverseDir ? containerRect.bottom - 1 : containerRect.top + 1;
return hiddenCaretRangeFromPoint( ownerDocument, x, y, container );
}
import placeCaretAtEdge from './place-caret-at-edge';

/**
* Places the caret at start or end of a given element.
Expand All @@ -37,59 +10,5 @@ function getRange( container, isReverse ) {
* @param {boolean} isReverse True for end, false for start.
*/
export default function placeCaretAtHorizontalEdge( container, isReverse ) {
if ( ! container ) {
return;
}

container.focus();

if ( isInputOrTextArea( container ) ) {
// The element may not support selection setting.
if ( typeof container.selectionStart !== 'number' ) {
return;
}

if ( isReverse ) {
container.selectionStart = container.value.length;
container.selectionEnd = container.value.length;
} else {
container.selectionStart = 0;
container.selectionEnd = 0;
}

return;
}

if ( ! container.isContentEditable ) {
return;
}

let range = getRange( container, isReverse );

// If no range range can be created or it is outside the container, the
// element may be out of view.
if (
! range ||
! range.startContainer ||
! container.contains( range.startContainer )
) {
container.scrollIntoView( isReverse );
range = getRange( container, isReverse );

if (
! range ||
! range.startContainer ||
! container.contains( range.startContainer )
) {
return;
}
}

const { ownerDocument } = container;
const { defaultView } = ownerDocument;
assertIsDefined( defaultView, 'defaultView' );
const selection = defaultView.getSelection();
assertIsDefined( selection, 'selection' );
selection.removeAllRanges();
selection.addRange( range );
return placeCaretAtEdge( container, isReverse, undefined );
}
64 changes: 3 additions & 61 deletions packages/dom/src/dom/place-caret-at-vertical-edge.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,15 @@
/**
* Internal dependencies
*/
import placeCaretAtHorizontalEdge from './place-caret-at-horizontal-edge';
import hiddenCaretRangeFromPoint from './hidden-caret-range-from-point';
import { assertIsDefined } from '../utils/assert-is-defined';
import placeCaretAtEdge from './place-caret-at-edge';

/**
* Places the caret at the top or bottom of a given element.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse True for bottom, false for top.
* @param {DOMRect} [rect] The rectangle to position the caret with.
* @param {boolean} [mayUseScroll=true] True to allow scrolling, false to disallow.
*/
export default function placeCaretAtVerticalEdge(
container,
isReverse,
rect,
mayUseScroll = true
) {
if ( ! container ) {
return;
}

if ( ! rect || ! container.isContentEditable ) {
placeCaretAtHorizontalEdge( container, isReverse );
return;
}

container.focus();

// Offset by a buffer half the height of the caret rect. This is needed
// because caretRangeFromPoint may default to the end of the selection if
// offset is too close to the edge. It's unclear how to precisely calculate
// this threshold; it may be the padded area of some combination of line
// height, caret height, and font size. The buffer offset is effectively
// equivalent to a point at half the height of a line of text.
const buffer = rect.height / 2;
const editableRect = container.getBoundingClientRect();
const x = rect.left;
const y = isReverse
? editableRect.bottom - buffer
: editableRect.top + buffer;

const { ownerDocument } = container;
const { defaultView } = ownerDocument;
const range = hiddenCaretRangeFromPoint( ownerDocument, x, y, container );

if ( ! range || ! container.contains( range.startContainer ) ) {
if (
mayUseScroll &&
( ! range ||
! range.startContainer ||
! range.startContainer.contains( container ) )
) {
// Might be out of view.
// Easier than attempting to calculate manually.
container.scrollIntoView( isReverse );
placeCaretAtVerticalEdge( container, isReverse, rect, false );
return;
}

placeCaretAtHorizontalEdge( container, isReverse );
return;
}

assertIsDefined( defaultView, 'defaultView' );
const selection = defaultView.getSelection();
assertIsDefined( selection, 'selection' );
selection.removeAllRanges();
selection.addRange( range );
export default function placeCaretAtVerticalEdge( container, isReverse, rect ) {
return placeCaretAtEdge( container, isReverse, rect?.left );
}

0 comments on commit b5b89be

Please sign in to comment.