@gechiui/block-editor
Version:
252 lines (203 loc) • 8.09 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
exports.useTypewriter = useTypewriter;
var _element = require("@gechiui/element");
var _compose = require("@gechiui/compose");
var _dom = require("@gechiui/dom");
var _data = require("@gechiui/data");
var _keycodes = require("@gechiui/keycodes");
var _store = require("../../store");
/**
* GeChiUI dependencies
*/
/**
* Internal dependencies
*/
const isIE = window.navigator.userAgent.indexOf('Trident') !== -1;
const arrowKeyCodes = new Set([_keycodes.UP, _keycodes.DOWN, _keycodes.LEFT, _keycodes.RIGHT]);
const initialTriggerPercentage = 0.75;
function useTypewriter() {
const hasSelectedBlock = (0, _data.useSelect)(select => select(_store.store).hasSelectedBlock(), []);
return (0, _compose.useRefEffect)(node => {
if (!hasSelectedBlock) {
return;
}
const {
ownerDocument
} = node;
const {
defaultView
} = ownerDocument;
let scrollResizeRafId;
let onKeyDownRafId;
let caretRect;
function onScrollResize() {
if (scrollResizeRafId) {
return;
}
scrollResizeRafId = defaultView.requestAnimationFrame(() => {
computeCaretRectangle();
scrollResizeRafId = null;
});
}
function onKeyDown(event) {
// Ensure the any remaining request is cancelled.
if (onKeyDownRafId) {
defaultView.cancelAnimationFrame(onKeyDownRafId);
} // Use an animation frame for a smooth result.
onKeyDownRafId = defaultView.requestAnimationFrame(() => {
maintainCaretPosition(event);
onKeyDownRafId = null;
});
}
/**
* Maintains the scroll position after a selection change caused by a
* keyboard event.
*
* @param {KeyboardEvent} event Keyboard event.
*/
function maintainCaretPosition(_ref) {
let {
keyCode
} = _ref;
if (!isSelectionEligibleForScroll()) {
return;
}
const currentCaretRect = (0, _dom.computeCaretRect)(defaultView);
if (!currentCaretRect) {
return;
} // If for some reason there is no position set to be scrolled to, let
// this be the position to be scrolled to in the future.
if (!caretRect) {
caretRect = currentCaretRect;
return;
} // Even though enabling the typewriter effect for arrow keys results in
// a pleasant experience, it may not be the case for everyone, so, for
// now, let's disable it.
if (arrowKeyCodes.has(keyCode)) {
// Reset the caret position to maintain.
caretRect = currentCaretRect;
return;
}
const diff = currentCaretRect.top - caretRect.top;
if (diff === 0) {
return;
}
const scrollContainer = (0, _dom.getScrollContainer)(node); // The page must be scrollable.
if (!scrollContainer) {
return;
}
const windowScroll = scrollContainer === ownerDocument.body;
const scrollY = windowScroll ? defaultView.scrollY : scrollContainer.scrollTop;
const scrollContainerY = windowScroll ? 0 : scrollContainer.getBoundingClientRect().top;
const relativeScrollPosition = windowScroll ? caretRect.top / defaultView.innerHeight : (caretRect.top - scrollContainerY) / (defaultView.innerHeight - scrollContainerY); // If the scroll position is at the start, the active editable element
// is the last one, and the caret is positioned within the initial
// trigger percentage of the page, do not scroll the page.
// The typewriter effect should not kick in until an empty page has been
// filled with the initial trigger percentage or the user scrolls
// intentionally down.
if (scrollY === 0 && relativeScrollPosition < initialTriggerPercentage && isLastEditableNode()) {
// Reset the caret position to maintain.
caretRect = currentCaretRect;
return;
}
const scrollContainerHeight = windowScroll ? defaultView.innerHeight : scrollContainer.clientHeight; // Abort if the target scroll position would scroll the caret out of
// view.
if ( // The caret is under the lower fold.
caretRect.top + caretRect.height > scrollContainerY + scrollContainerHeight || // The caret is above the upper fold.
caretRect.top < scrollContainerY) {
// Reset the caret position to maintain.
caretRect = currentCaretRect;
return;
}
if (windowScroll) {
defaultView.scrollBy(0, diff);
} else {
scrollContainer.scrollTop += diff;
}
}
/**
* Adds a `selectionchange` listener to reset the scroll position to be
* maintained.
*/
function addSelectionChangeListener() {
ownerDocument.addEventListener('selectionchange', computeCaretRectOnSelectionChange);
}
/**
* Resets the scroll position to be maintained during a `selectionchange`
* event. Also removes the listener, so it acts as a one-time listener.
*/
function computeCaretRectOnSelectionChange() {
ownerDocument.removeEventListener('selectionchange', computeCaretRectOnSelectionChange);
computeCaretRectangle();
}
/**
* Resets the scroll position to be maintained.
*/
function computeCaretRectangle() {
if (isSelectionEligibleForScroll()) {
caretRect = (0, _dom.computeCaretRect)(defaultView);
}
}
/**
* Checks if the current situation is elegible for scroll:
* - There should be one and only one block selected.
* - The component must contain the selection.
* - The active element must be contenteditable.
*/
function isSelectionEligibleForScroll() {
return node.contains(ownerDocument.activeElement) && ownerDocument.activeElement.isContentEditable;
}
function isLastEditableNode() {
const editableNodes = node.querySelectorAll('[contenteditable="true"]');
const lastEditableNode = editableNodes[editableNodes.length - 1];
return lastEditableNode === ownerDocument.activeElement;
} // When the user scrolls or resizes, the scroll position should be
// reset.
defaultView.addEventListener('scroll', onScrollResize, true);
defaultView.addEventListener('resize', onScrollResize, true);
node.addEventListener('keydown', onKeyDown);
node.addEventListener('keyup', maintainCaretPosition);
node.addEventListener('mousedown', addSelectionChangeListener);
node.addEventListener('touchstart', addSelectionChangeListener);
return () => {
defaultView.removeEventListener('scroll', onScrollResize, true);
defaultView.removeEventListener('resize', onScrollResize, true);
node.removeEventListener('keydown', onKeyDown);
node.removeEventListener('keyup', maintainCaretPosition);
node.removeEventListener('mousedown', addSelectionChangeListener);
node.removeEventListener('touchstart', addSelectionChangeListener);
ownerDocument.removeEventListener('selectionchange', computeCaretRectOnSelectionChange);
defaultView.cancelAnimationFrame(scrollResizeRafId);
defaultView.cancelAnimationFrame(onKeyDownRafId);
};
}, [hasSelectedBlock]);
}
function Typewriter(_ref2) {
let {
children
} = _ref2;
return (0, _element.createElement)("div", {
ref: useTypewriter(),
className: "block-editor__typewriter"
}, children);
}
/**
* The exported component. The implementation of Typewriter faced technical
* challenges in Internet Explorer, and is simply skipped, rendering the given
* props children instead.
*
* @type {GCComponent}
*/
const TypewriterOrIEBypass = isIE ? props => props.children : Typewriter;
/**
* Ensures that the text selection keeps the same vertical distance from the
* viewport during keyboard events within this component. The vertical distance
* can vary. It is the last clicked or scrolled to position.
*/
var _default = TypewriterOrIEBypass;
exports.default = _default;
//# sourceMappingURL=index.js.map