@instructure/canvas-rce
Version:
A component wrapping Canvas's usage of Tinymce
203 lines (199 loc) • 7.36 kB
JavaScript
/*
* Copyright (C) 2022 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { useEffect, useState, useRef, useCallback } from 'react';
import { debounce } from '@instructure/debounce';
import { KEY_EVENT_DELAY, KEY_EVENT_ACCELERATION } from './constants';
import { actions } from './reducers/imageCropper';
const EVENT_EXCEPTION_ELEMENT_IDS = ['imageCropperHeader', 'imageCropperFooter', 'imageCropperControls'];
const TOUCH_EVENTS = ['ontouchmove', 'ontouchend', 'ontouchcancel'];
const MOUSE_EVENTS = ['onmousemove', 'onmouseup', 'onmouseout'];
function useKeysEvents(tempTranslateXRef, tempTranslateYRef, tempTranslateX, tempTranslateY, setTempTranslateX, setTempTranslateY, isMoving, setIsMoving, dispatch) {
// Refs that manage the keydown acceleration
const direction = useRef(0);
const initialTime = useRef(null);
const onKeyDown = event => {
// 37 = Left, 38 = Up, 39 = Right, 40 = Down
const {
keyCode
} = event;
if (![37, 38, 39, 40].includes(keyCode)) {
return;
}
event.preventDefault();
let elapsedTime;
if (keyCode !== direction.current) {
elapsedTime = 0;
initialTime.current = new Date();
direction.current = keyCode;
dispatch({
type: actions.UPDATE_SETTINGS,
payload: {
direction: keyCode
}
});
} else {
const currentTime = new Date();
elapsedTime = (currentTime - initialTime.current) / 1000;
}
const translationDiff = Math.floor(KEY_EVENT_ACCELERATION * elapsedTime ** 2) || 1;
if ([37, 39].includes(keyCode)) {
const sign = keyCode === 37 ? -1 : 1;
const newTranslateX = tempTranslateXRef.current + sign * translationDiff;
setTempTranslateX(newTranslateX);
}
if ([38, 40].includes(keyCode)) {
const sign = keyCode === 38 ? -1 : 1;
const newTranslateY = tempTranslateYRef.current + sign * translationDiff;
setTempTranslateY(newTranslateY);
}
};
const stopMovement = useCallback(debounce(() => {
direction.current = 0;
initialTime.current = null;
setIsMoving(false);
}, KEY_EVENT_DELAY, {
trailing: true
}), []);
useEffect(() => {
const onKeyDownWrapper = event => {
// If the active element is in the modal header, footer or controls.
if (EVENT_EXCEPTION_ELEMENT_IDS.some(id => document.getElementById(id)?.contains(document.activeElement))) {
return;
}
onKeyDown(event);
};
// Adds the event listener when component did mount
document.addEventListener('keydown', onKeyDownWrapper);
return () => {
// Removes the event listener when component will unmount
document.removeEventListener('keydown', onKeyDownWrapper);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isMoving && direction.current !== 0) {
stopMovement();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tempTranslateX, tempTranslateY]);
}
function useMouseAndTouchEvents(tempTranslateX, tempTranslateY, setTempTranslateX, setTempTranslateY, setIsMoving) {
const initialPageX = useRef(0);
const initialPageY = useRef(0);
const isDragging = useRef(false);
const imgElement = useRef(null);
const onStopMove = isMouseEvent => () => {
isDragging.current = false;
initialPageX.current = 0;
initialPageY.current = 0;
if (imgElement.current) {
const [move, end, cancel] = isMouseEvent ? MOUSE_EVENTS : TOUCH_EVENTS;
imgElement.current[move] = null;
imgElement.current[end] = null;
imgElement.current[cancel] = null;
imgElement.current = null;
}
setIsMoving(false);
};
const onMove = isMouseEvent => e => {
if (!isDragging.current) {
return onStopMove();
}
const {
clientX,
clientY
} = isMouseEvent ? e : e.touches[0];
setTempTranslateX(tempTranslateX + clientX - initialPageX.current);
setTempTranslateY(tempTranslateY + clientY - initialPageY.current);
};
const onStartMove = (e, isMouseEvent) => {
isDragging.current = true;
const {
target
} = e;
const {
clientX,
clientY
} = isMouseEvent ? e : e.touches[0];
initialPageX.current = clientX;
initialPageY.current = clientY;
const [move, end, cancel] = isMouseEvent ? MOUSE_EVENTS : TOUCH_EVENTS;
target[move] = onMove(isMouseEvent);
target[end] = onStopMove(isMouseEvent);
// Should stop the movement when touch/mouse leaves the preview
target[cancel] = onStopMove(isMouseEvent);
imgElement.current = target;
setIsMoving(true);
};
return [e => onStartMove(e, true), e => onStartMove(e, false)];
}
export function useKeyMouseTouchEvents(translateX, translateY, dispatch) {
const [tempTranslateX, _setTempTranslateX] = useState(translateX);
const [tempTranslateY, _setTempTranslateY] = useState(translateY);
const [isMoving, setIsMoving] = useState(false);
// These are used to get the current values when the callback is called from outside.
const tempTranslateXRef = useRef(tempTranslateX);
const tempTranslateYRef = useRef(tempTranslateY);
const setTempTranslateX = data => {
setIsMoving(true);
tempTranslateXRef.current = data;
_setTempTranslateX(data);
};
const setTempTranslateY = data => {
setIsMoving(true);
tempTranslateYRef.current = data;
_setTempTranslateY(data);
};
useKeysEvents(tempTranslateXRef, tempTranslateYRef, tempTranslateX, tempTranslateY, setTempTranslateX, setTempTranslateY, isMoving, setIsMoving, dispatch);
const [onMouseDown, onTouchStart] = useMouseAndTouchEvents(tempTranslateX, tempTranslateY, setTempTranslateX, setTempTranslateY, setIsMoving);
// Updates the reducer state when user stops moving.
useEffect(() => {
if (isMoving) {
return;
}
if (tempTranslateX !== translateX) {
dispatch({
type: actions.SET_TRANSLATE_X,
payload: tempTranslateX
});
}
if (tempTranslateY !== translateY) {
dispatch({
type: actions.SET_TRANSLATE_Y,
payload: tempTranslateY
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMoving]);
// Updates the component state when props changed.
useEffect(() => {
if (isMoving) {
return;
}
if (translateX !== tempTranslateX) {
tempTranslateXRef.current = translateX;
_setTempTranslateX(translateX);
}
if (translateY !== tempTranslateY) {
tempTranslateYRef.current = translateY;
_setTempTranslateY(translateY);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [translateX, translateY]);
return [tempTranslateX, tempTranslateY, onMouseDown, onTouchStart];
}