49c33b4eea
Since the pan tool is only available for the presenter, it has to be checked whether it actually exists before attempting to modify its class list.
1193 lines
40 KiB
JavaScript
1193 lines
40 KiB
JavaScript
import * as React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { TldrawApp, Tldraw } from '@tldraw/tldraw';
|
|
import SlideCalcUtil, { HUNDRED_PERCENT, MAX_PERCENT } from '/imports/utils/slideCalcUtils';
|
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
import { Utils } from '@tldraw/core';
|
|
import Cursors from './cursors/container';
|
|
import Settings from '/imports/ui/services/settings';
|
|
import logger from '/imports/startup/client/logger';
|
|
import KEY_CODES from '/imports/utils/keyCodes';
|
|
import {
|
|
presentationMenuHeight,
|
|
styleMenuOffset,
|
|
styleMenuOffsetSmall
|
|
} from '/imports/ui/stylesheets/styled-components/general';
|
|
import Styled from './styles';
|
|
import PanToolInjector from './pan-tool-injector/component';
|
|
import {
|
|
findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, usePrevious,
|
|
} from './utils';
|
|
import { isEqual } from 'radash';
|
|
|
|
const SMALL_HEIGHT = 435;
|
|
const SMALLEST_DOCK_HEIGHT = 475;
|
|
const SMALL_WIDTH = 800;
|
|
const SMALLEST_DOCK_WIDTH = 710;
|
|
const TOOLBAR_SMALL = 28;
|
|
const TOOLBAR_LARGE = 32;
|
|
const MOUNTED_RESIZE_DELAY = 1500;
|
|
|
|
export default function Whiteboard(props) {
|
|
const {
|
|
isPresenter,
|
|
removeShapes,
|
|
initDefaultPages,
|
|
persistShape,
|
|
shapes,
|
|
assets,
|
|
currentUser,
|
|
curPres,
|
|
whiteboardId,
|
|
podId,
|
|
zoomSlide,
|
|
skipToSlide,
|
|
slidePosition,
|
|
curPageId,
|
|
presentationWidth,
|
|
presentationHeight,
|
|
isViewersCursorLocked,
|
|
zoomChanger,
|
|
isMultiUserActive,
|
|
isRTL,
|
|
fitToWidth,
|
|
zoomValue,
|
|
intl,
|
|
svgUri,
|
|
maxStickyNoteLength,
|
|
fontFamily,
|
|
hasShapeAccess,
|
|
presentationAreaHeight,
|
|
presentationAreaWidth,
|
|
maxNumberOfAnnotations,
|
|
notifyShapeNumberExceeded,
|
|
darkTheme,
|
|
isPanning: shortcutPanning,
|
|
setTldrawIsMounting,
|
|
width,
|
|
height,
|
|
hasMultiUserAccess,
|
|
tldrawAPI,
|
|
setTldrawAPI,
|
|
whiteboardToolbarAutoHide,
|
|
toggleToolsAnimations,
|
|
isIphone,
|
|
sidebarNavigationWidth,
|
|
animations,
|
|
isToolbarVisible,
|
|
isModerator,
|
|
fullscreenRef,
|
|
fullscreenElementId,
|
|
layoutContextDispatch,
|
|
} = props;
|
|
const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1);
|
|
const rDocument = React.useRef({
|
|
name: 'test',
|
|
version: TldrawApp.version,
|
|
id: whiteboardId,
|
|
pages,
|
|
pageStates,
|
|
bindings: {},
|
|
assets: {},
|
|
});
|
|
const [history, setHistory] = React.useState(null);
|
|
const [zoom, setZoom] = React.useState(HUNDRED_PERCENT);
|
|
const [tldrawZoom, setTldrawZoom] = React.useState(1);
|
|
const zoomValueRef = React.useRef(zoomValue);
|
|
const [isMounting, setIsMounting] = React.useState(true);
|
|
const prevShapes = usePrevious(shapes);
|
|
const prevSlidePosition = usePrevious(slidePosition);
|
|
const prevFitToWidth = usePrevious(fitToWidth);
|
|
const prevSvgUri = usePrevious(svgUri);
|
|
const prevPageId = usePrevious(curPageId);
|
|
const language = mapLanguage(Settings?.application?.locale?.toLowerCase() || 'en');
|
|
const [currentTool, setCurrentTool] = React.useState(null);
|
|
const [currentStyle, setCurrentStyle] = React.useState({});
|
|
const [isMoving, setIsMoving] = React.useState(false);
|
|
const [isPanning, setIsPanning] = React.useState(shortcutPanning);
|
|
const [panSelected, setPanSelected] = React.useState(isPanning);
|
|
const isMountedRef = React.useRef(true);
|
|
const [isToolLocked, setIsToolLocked] = React.useState(tldrawAPI?.appState?.isToolLocked);
|
|
const [bgShape, setBgShape] = React.useState(null);
|
|
|
|
// eslint-disable-next-line arrow-body-style
|
|
React.useEffect(() => {
|
|
return () => {
|
|
isMountedRef.current = false;
|
|
};
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
zoomValueRef.current = zoomValue;
|
|
}, [zoomValue]);
|
|
|
|
const setSafeTLDrawAPI = (api) => {
|
|
if (isMountedRef.current) {
|
|
setTldrawAPI(api);
|
|
}
|
|
};
|
|
|
|
const toggleOffCheck = (evt) => {
|
|
const clickedElement = evt.target;
|
|
const panBtnClicked = clickedElement?.getAttribute('data-test') === 'panButton'
|
|
|| clickedElement?.parentElement?.getAttribute('data-test') === 'panButton';
|
|
|
|
setIsToolLocked(false);
|
|
|
|
const panButton = document.querySelector('[data-test="panButton"]');
|
|
if (panBtnClicked) {
|
|
const dataZoom = panButton.getAttribute('data-zoom');
|
|
if ((dataZoom <= HUNDRED_PERCENT && !fitToWidth)) {
|
|
return;
|
|
}
|
|
panButton.classList.add('select');
|
|
panButton.classList.remove('selectOverride');
|
|
} else {
|
|
setIsPanning(false);
|
|
setPanSelected(false);
|
|
if (panButton) {
|
|
// only presenter has the pan button
|
|
panButton.classList.add('selectOverride');
|
|
panButton.classList.remove('select');
|
|
}
|
|
}
|
|
};
|
|
|
|
const setDockPosition = (setSetting) => {
|
|
if (hasWBAccess || isPresenter) {
|
|
if (((height < SMALLEST_DOCK_HEIGHT) || (width < SMALLEST_DOCK_WIDTH))) {
|
|
setSetting('dockPosition', 'bottom');
|
|
} else {
|
|
setSetting('dockPosition', isRTL ? 'left' : 'right');
|
|
}
|
|
}
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
const toolbar = document.getElementById('TD-PrimaryTools');
|
|
const handleClick = (evt) => {
|
|
toggleOffCheck(evt);
|
|
};
|
|
const handleDBClick = (evt) => {
|
|
evt.preventDefault();
|
|
evt.stopPropagation();
|
|
setIsToolLocked(true);
|
|
tldrawAPI?.patchState(
|
|
{
|
|
appState: {
|
|
isToolLocked: true,
|
|
},
|
|
},
|
|
);
|
|
};
|
|
toolbar?.addEventListener('click', handleClick);
|
|
toolbar?.addEventListener('dblclick', handleDBClick);
|
|
|
|
return () => {
|
|
toolbar?.removeEventListener('click', handleClick);
|
|
toolbar?.removeEventListener('dblclick', handleDBClick);
|
|
};
|
|
}, [tldrawAPI, isToolLocked]);
|
|
|
|
React.useEffect(() => {
|
|
if (whiteboardToolbarAutoHide) {
|
|
toggleToolsAnimations('fade-in', 'fade-out', animations ? '3s' : '0s');
|
|
} else {
|
|
toggleToolsAnimations('fade-out', 'fade-in', animations ? '.3s' : '0s');
|
|
}
|
|
}, [whiteboardToolbarAutoHide]);
|
|
|
|
const calculateZoom = (localWidth, localHeight) => {
|
|
const calcedZoom = fitToWidth ? (presentationWidth / localWidth) : Math.min(
|
|
(presentationWidth) / localWidth,
|
|
(presentationHeight) / localHeight,
|
|
);
|
|
|
|
return (calcedZoom === 0 || calcedZoom === Infinity) ? HUNDRED_PERCENT : calcedZoom;
|
|
};
|
|
|
|
React.useEffect(() => {
|
|
setTldrawIsMounting(true);
|
|
}, []);
|
|
|
|
const checkClientBounds = (e) => {
|
|
if (
|
|
e.clientX > document.documentElement.clientWidth
|
|
|| e.clientX < 0
|
|
|| e.clientY > document.documentElement.clientHeight
|
|
|| e.clientY < 0
|
|
) {
|
|
if (tldrawAPI?.session) {
|
|
tldrawAPI?.completeSession?.();
|
|
}
|
|
}
|
|
};
|
|
|
|
const checkVisibility = () => {
|
|
if (document.visibilityState === 'hidden' && tldrawAPI?.session) {
|
|
tldrawAPI?.completeSession?.();
|
|
}
|
|
};
|
|
|
|
const handleWheelEvent = (event) => {
|
|
if ((zoomValueRef.current >= MAX_PERCENT && event.deltaY < 0)
|
|
|| (zoomValueRef.current <= HUNDRED_PERCENT && event.deltaY > 0))
|
|
{
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
return window.dispatchEvent(new Event('resize'));
|
|
}
|
|
|
|
if (!event.ctrlKey) {
|
|
// Prevent the event from reaching the tldraw library
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
const newEvent = new WheelEvent('wheel', {
|
|
deltaX: event.deltaX,
|
|
deltaY: event.deltaY,
|
|
deltaZ: event.deltaZ,
|
|
ctrlKey: true,
|
|
clientX: event.clientX,
|
|
clientY: event.clientY,
|
|
});
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
canvas.dispatchEvent(newEvent);
|
|
}
|
|
}
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
document.addEventListener('mouseup', checkClientBounds);
|
|
document.addEventListener('visibilitychange', checkVisibility);
|
|
|
|
return () => {
|
|
document.removeEventListener('mouseup', checkClientBounds);
|
|
document.removeEventListener('visibilitychange', checkVisibility);
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
canvas.removeEventListener('wheel', handleWheelEvent);
|
|
}
|
|
};
|
|
}, [tldrawAPI]);
|
|
|
|
/* needed to prevent an issue with presentation images not loading correctly in Firefox
|
|
more info: https://github.com/bigbluebutton/bigbluebutton/issues/17969#issuecomment-1561758200 */
|
|
React.useEffect(() => {
|
|
if (bgShape && bgShape.parentElement && bgShape.parentElement.clientWidth > 0) {
|
|
bgShape.parentElement.style.width = `${bgShape.parentElement.clientWidth + .1}px`;
|
|
}
|
|
}, [bgShape]);
|
|
|
|
const doc = React.useMemo(() => {
|
|
const currentDoc = rDocument.current;
|
|
|
|
// update document if the number of pages has changed
|
|
if (currentDoc.id !== whiteboardId && currentDoc?.pages.length !== curPres?.pages.length) {
|
|
const currentPageShapes = currentDoc?.pages[curPageId]?.shapes;
|
|
currentDoc.id = whiteboardId;
|
|
currentDoc.pages = pages;
|
|
currentDoc.pages[curPageId].shapes = currentPageShapes;
|
|
currentDoc.pageStates = pageStates;
|
|
}
|
|
|
|
const next = { ...currentDoc };
|
|
|
|
let changed = false;
|
|
|
|
if (next.pageStates[curPageId] && !isEqual(prevShapes, shapes)) {
|
|
const editingShape = tldrawAPI?.getShape(tldrawAPI?.getPageState()?.editingId);
|
|
|
|
if (editingShape) {
|
|
shapes[editingShape?.id] = editingShape;
|
|
}
|
|
|
|
const removed = prevShapes && findRemoved(Object.keys(prevShapes), Object.keys((shapes)));
|
|
if (removed && removed.length > 0) {
|
|
const patchedShapes = Object.fromEntries(removed.map((id) => [id, undefined]));
|
|
|
|
try {
|
|
tldrawAPI?.patchState(
|
|
{
|
|
document: {
|
|
pageStates: {
|
|
[curPageId]: {
|
|
selectedIds:
|
|
tldrawAPI?.selectedIds?.filter((id) => !removed.includes(id)) || [],
|
|
},
|
|
},
|
|
pages: {
|
|
[curPageId]: {
|
|
shapes: patchedShapes,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
);
|
|
} catch (error) {
|
|
logger.error({
|
|
logCode: 'whiteboard_shapes_remove_error',
|
|
extraInfo: { error },
|
|
}, 'Whiteboard catch error on removing shapes');
|
|
}
|
|
}
|
|
|
|
next.pages[curPageId].shapes = filterInvalidShapes(shapes, curPageId, tldrawAPI);
|
|
changed = true;
|
|
}
|
|
|
|
if (curPageId && (!next.assets[`slide-background-asset-${curPageId}`] || (svgUri && !isEqual(prevSvgUri, svgUri)))) {
|
|
next.assets[`slide-background-asset-${curPageId}`] = assets[`slide-background-asset-${curPageId}`];
|
|
tldrawAPI?.patchState(
|
|
{
|
|
document: {
|
|
assets,
|
|
},
|
|
},
|
|
);
|
|
changed = true;
|
|
}
|
|
|
|
if (changed && tldrawAPI) {
|
|
// merge patch manually (this improves performance and reduce side effects on fast updates)
|
|
const patch = {
|
|
document: {
|
|
pages: {
|
|
[curPageId]: { shapes: filterInvalidShapes(shapes, curPageId, tldrawAPI) },
|
|
},
|
|
},
|
|
};
|
|
const prevState = tldrawAPI._state;
|
|
const nextState = Utils.deepMerge(tldrawAPI._state, patch);
|
|
if (nextState.document.pages[curPageId].shapes) {
|
|
filterInvalidShapes(nextState.document.pages[curPageId].shapes, curPageId, tldrawAPI);
|
|
}
|
|
const final = tldrawAPI.cleanup(nextState, prevState, patch, '');
|
|
tldrawAPI._state = final;
|
|
|
|
try {
|
|
tldrawAPI?.forceUpdate();
|
|
} catch (error) {
|
|
logger.error({
|
|
logCode: 'whiteboard_shapes_update_error',
|
|
extraInfo: { error },
|
|
}, 'Whiteboard catch error on updating shapes');
|
|
}
|
|
}
|
|
|
|
// move poll result text to bottom right
|
|
if (next.pages[curPageId] && slidePosition) {
|
|
const pollResults = Object.entries(next.pages[curPageId].shapes)
|
|
.filter(([, shape]) => shape.name?.includes('poll-result'));
|
|
pollResults.forEach(([id, shape]) => {
|
|
if (isEqual(shape.point, [0, 0])) {
|
|
try {
|
|
const shapeBounds = tldrawAPI?.getShapeBounds(id);
|
|
if (shapeBounds) {
|
|
const editedShape = shape;
|
|
editedShape.point = [
|
|
slidePosition.width - shapeBounds.width,
|
|
slidePosition.height - shapeBounds.height,
|
|
];
|
|
editedShape.size = [shapeBounds.width, shapeBounds.height];
|
|
if (isPresenter) persistShape(editedShape, whiteboardId, isModerator);
|
|
}
|
|
} catch (error) {
|
|
logger.error({
|
|
logCode: 'whiteboard_poll_results_error',
|
|
extraInfo: { error },
|
|
}, 'Whiteboard catch error on moving unpublished poll results');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return currentDoc;
|
|
}, [shapes, tldrawAPI, curPageId, slidePosition]);
|
|
|
|
// when presentationSizes change, update tldraw camera
|
|
React.useEffect(() => {
|
|
if (curPageId && slidePosition && tldrawAPI
|
|
&& presentationWidth > 0 && presentationHeight > 0
|
|
) {
|
|
if (prevFitToWidth !== null && fitToWidth !== prevFitToWidth) {
|
|
const newZoom = calculateZoom(slidePosition.width, slidePosition.height);
|
|
tldrawAPI?.setCamera([0, 0], newZoom);
|
|
const viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(
|
|
tldrawAPI?.viewport.height, slidePosition.height,
|
|
);
|
|
setZoom(HUNDRED_PERCENT);
|
|
zoomChanger(HUNDRED_PERCENT);
|
|
zoomSlide(parseInt(curPageId, 10), podId, HUNDRED_PERCENT, viewedRegionH, 0, 0);
|
|
} else {
|
|
const currentAspectRatio = Math.round((presentationWidth / presentationHeight) * 100) / 100;
|
|
const previousAspectRatio = Math.round(
|
|
(slidePosition.viewBoxWidth / slidePosition.viewBoxHeight) * 100,
|
|
) / 100;
|
|
if (fitToWidth && currentAspectRatio !== previousAspectRatio) {
|
|
// we need this to ensure tldraw updates the viewport size after re-mounting
|
|
setTimeout(() => {
|
|
const newZoom = calculateZoom(slidePosition.viewBoxWidth, slidePosition.viewBoxHeight);
|
|
tldrawAPI.setCamera([slidePosition.x, slidePosition.y], newZoom, 'zoomed');
|
|
}, 50);
|
|
} else {
|
|
const newZoom = calculateZoom(slidePosition.viewBoxWidth, slidePosition.viewBoxHeight);
|
|
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newZoom);
|
|
}
|
|
}
|
|
}
|
|
}, [presentationWidth, presentationHeight, curPageId, document?.documentElement?.dir]);
|
|
|
|
React.useEffect(() => {
|
|
if (presentationWidth > 0 && presentationHeight > 0 && slidePosition) {
|
|
const cameraZoom = tldrawAPI?.getPageState()?.camera?.zoom;
|
|
const newzoom = calculateZoom(slidePosition.viewBoxWidth, slidePosition.viewBoxHeight);
|
|
if (cameraZoom && cameraZoom === 1) {
|
|
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom);
|
|
} else if (isMounting) {
|
|
setIsMounting(false);
|
|
setTldrawIsMounting(false);
|
|
const currentAspectRatio = Math.round((presentationWidth / presentationHeight) * 100) / 100;
|
|
const previousAspectRatio = Math.round(
|
|
(slidePosition.viewBoxWidth / slidePosition.viewBoxHeight) * 100,
|
|
) / 100;
|
|
// case where the presenter had fit-to-width enabled and he reloads the page
|
|
if (!fitToWidth && currentAspectRatio !== previousAspectRatio) {
|
|
// wee need this to ensure tldraw updates the viewport size after re-mounting
|
|
setTimeout(() => {
|
|
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom, 'zoomed');
|
|
}, 50);
|
|
} else {
|
|
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newzoom);
|
|
}
|
|
}
|
|
}
|
|
}, [tldrawAPI?.getPageState()?.camera, presentationWidth, presentationHeight]);
|
|
|
|
// change tldraw camera when slidePosition changes
|
|
React.useEffect(() => {
|
|
if (tldrawAPI && !isPresenter && curPageId && slidePosition) {
|
|
const newZoom = calculateZoom(slidePosition.viewBoxWidth, slidePosition.viewBoxHeight);
|
|
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newZoom, 'zoomed');
|
|
}
|
|
|
|
const camera = tldrawAPI?.getPageState()?.camera;
|
|
if (isPresenter && slidePosition && camera) {
|
|
const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height);
|
|
const zoomCamera = (zoomFitSlide * zoomValue) / HUNDRED_PERCENT;
|
|
let zoomToolbar = Math.round(
|
|
((HUNDRED_PERCENT * camera.zoom) / zoomFitSlide) * 100,
|
|
) / 100;
|
|
if ((zoom !== zoomToolbar) && (curPageId && curPageId !== prevPageId)) {
|
|
setZoom(zoomToolbar);
|
|
zoomChanger(zoomToolbar);
|
|
}
|
|
}
|
|
}, [curPageId, slidePosition]);
|
|
|
|
// update zoom according to toolbar
|
|
React.useEffect(() => {
|
|
if (tldrawAPI && isPresenter && curPageId && slidePosition && zoom !== zoomValue) {
|
|
const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height);
|
|
const zoomCamera = (zoomFitSlide * zoomValue) / HUNDRED_PERCENT;
|
|
setTimeout(() => {
|
|
tldrawAPI?.zoomTo(zoomCamera);
|
|
}, 50);
|
|
}
|
|
}, [zoomValue]);
|
|
|
|
// update zoom when presenter changes if the aspectRatio has changed
|
|
React.useEffect(() => {
|
|
if (tldrawAPI && isPresenter && curPageId && slidePosition && !isMounting) {
|
|
const currentAspectRatio = Math.round((presentationWidth / presentationHeight) * 100) / 100;
|
|
const previousAspectRatio = Math.round(
|
|
(slidePosition.viewBoxWidth / slidePosition.viewBoxHeight) * 100,
|
|
) / 100;
|
|
if (previousAspectRatio !== currentAspectRatio) {
|
|
if (fitToWidth) {
|
|
const newZoom = calculateZoom(slidePosition.width, slidePosition.height);
|
|
tldrawAPI?.setCamera([0, 0], newZoom);
|
|
const viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(
|
|
tldrawAPI?.viewport.height, slidePosition.height,
|
|
);
|
|
zoomSlide(parseInt(curPageId, 10), podId, HUNDRED_PERCENT, viewedRegionH, 0, 0);
|
|
setZoom(HUNDRED_PERCENT);
|
|
zoomChanger(HUNDRED_PERCENT);
|
|
} else if (!isMounting) {
|
|
let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(
|
|
tldrawAPI?.viewport.width, slidePosition.width,
|
|
);
|
|
let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(
|
|
tldrawAPI?.viewport.height, slidePosition.height,
|
|
);
|
|
const camera = tldrawAPI?.getPageState()?.camera;
|
|
const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height);
|
|
if (!fitToWidth && camera.zoom === zoomFitSlide) {
|
|
viewedRegionW = HUNDRED_PERCENT;
|
|
viewedRegionH = HUNDRED_PERCENT;
|
|
}
|
|
zoomSlide(
|
|
parseInt(curPageId, 10),
|
|
podId,
|
|
viewedRegionW,
|
|
viewedRegionH,
|
|
camera.point[0],
|
|
camera.point[1],
|
|
);
|
|
const zoomToolbar = Math.round(
|
|
((HUNDRED_PERCENT * camera.zoom) / zoomFitSlide) * 100,
|
|
) / 100;
|
|
if (zoom !== zoomToolbar) {
|
|
setZoom(zoomToolbar);
|
|
zoomChanger(zoomToolbar);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, [isPresenter]);
|
|
|
|
const hasWBAccess = hasMultiUserAccess(whiteboardId, currentUser.userId);
|
|
|
|
React.useEffect(() => {
|
|
if (tldrawAPI) {
|
|
tldrawAPI.isForcePanning = isPanning;
|
|
}
|
|
}, [isPanning]);
|
|
|
|
React.useEffect(() => {
|
|
tldrawAPI && setDockPosition(tldrawAPI?.setSetting);
|
|
}, [height, width]);
|
|
|
|
React.useEffect(() => {
|
|
tldrawAPI?.setSetting('language', language);
|
|
}, [language]);
|
|
|
|
// Reset zoom to default when current presentation changes.
|
|
React.useEffect(() => {
|
|
if (isPresenter && slidePosition && tldrawAPI) {
|
|
tldrawAPI?.zoomTo(0);
|
|
setHistory(null);
|
|
tldrawAPI?.resetHistory();
|
|
}
|
|
}, [curPres?.id]);
|
|
|
|
React.useEffect(() => {
|
|
const currentZoom = tldrawAPI?.getPageState()?.camera?.zoom;
|
|
if (currentZoom !== tldrawZoom) {
|
|
setTldrawZoom(currentZoom);
|
|
}
|
|
setBgShape(null);
|
|
}, [presentationAreaHeight, presentationAreaWidth]);
|
|
|
|
const fullscreenToggleHandler = () => {
|
|
const {
|
|
isFullscreen,
|
|
fullscreenAction,
|
|
handleToggleFullScreen,
|
|
} = props;
|
|
|
|
handleToggleFullScreen(fullscreenRef);
|
|
const newElement = isFullscreen ? '' : fullscreenElementId;
|
|
|
|
layoutContextDispatch({
|
|
type: fullscreenAction,
|
|
value: {
|
|
element: newElement,
|
|
group: '',
|
|
},
|
|
});
|
|
};
|
|
|
|
const nextSlideHandler = (event) => {
|
|
const { nextSlide, numberOfSlides } = props;
|
|
|
|
if (event) event.currentTarget.blur();
|
|
nextSlide(+curPageId, numberOfSlides, podId);
|
|
};
|
|
|
|
const previousSlideHandler = (event) => {
|
|
const { previousSlide } = props;
|
|
|
|
if (event) event.currentTarget.blur();
|
|
previousSlide(+curPageId, podId);
|
|
};
|
|
|
|
const handleOnKeyDown = (event) => {
|
|
const { which, ctrlKey } = event;
|
|
|
|
switch (which) {
|
|
case KEY_CODES.ARROW_LEFT:
|
|
case KEY_CODES.PAGE_UP:
|
|
previousSlideHandler();
|
|
break;
|
|
case KEY_CODES.ARROW_RIGHT:
|
|
case KEY_CODES.PAGE_DOWN:
|
|
nextSlideHandler();
|
|
break;
|
|
case KEY_CODES.ENTER:
|
|
fullscreenToggleHandler();
|
|
break;
|
|
case KEY_CODES.A:
|
|
if (ctrlKey) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
tldrawAPI?.selectAll();
|
|
}
|
|
break;
|
|
case KEY_CODES.ARROW_DOWN:
|
|
case KEY_CODES.ARROW_UP:
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
break;
|
|
default:
|
|
}
|
|
};
|
|
|
|
const onMount = (app) => {
|
|
setDockPosition(app?.setSetting);
|
|
const menu = document.getElementById('TD-Styles')?.parentElement;
|
|
const canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
canvas.addEventListener('wheel', handleWheelEvent, { capture: true });
|
|
}
|
|
|
|
if (menu) {
|
|
menu.style.position = 'relative';
|
|
menu.style.height = presentationMenuHeight;
|
|
menu.setAttribute('id', 'TD-Styles-Parent');
|
|
|
|
[...menu.children]
|
|
.sort((a, b) => (a?.id > b?.id ? -1 : 1))
|
|
.forEach((n) => menu.appendChild(n));
|
|
}
|
|
|
|
app.setSetting('language', language);
|
|
app?.setSetting('isDarkMode', false);
|
|
|
|
const textAlign = isRTL ? 'end' : 'start';
|
|
|
|
app?.patchState(
|
|
{
|
|
appState: {
|
|
currentStyle: {
|
|
textAlign: currentStyle?.textAlign || textAlign,
|
|
font: currentStyle?.font || fontFamily,
|
|
},
|
|
},
|
|
},
|
|
);
|
|
|
|
setSafeTLDrawAPI(app);
|
|
// disable for non presenter that doesn't have multi user access
|
|
if (!hasWBAccess && !isPresenter) {
|
|
const newApp = app;
|
|
newApp.onPan = () => { };
|
|
newApp.setSelectedIds = () => { };
|
|
newApp.setHoveredId = () => { };
|
|
}
|
|
|
|
if (history) {
|
|
app.replaceHistory(history);
|
|
}
|
|
|
|
if (curPageId) {
|
|
app.patchState(
|
|
{
|
|
appState: {
|
|
currentPageId: curPageId,
|
|
},
|
|
},
|
|
);
|
|
setIsMounting(true);
|
|
}
|
|
|
|
// needed to ensure the correct calculations for cursors on mount.
|
|
setTimeout(() => {
|
|
window.dispatchEvent(new Event('resize'));
|
|
}, MOUNTED_RESIZE_DELAY);
|
|
};
|
|
|
|
const onPatch = (e, t, reason) => {
|
|
if (!e?.pageState || !reason) return;
|
|
|
|
if (((isPanning || panSelected) && (reason === 'selected' || reason === 'set_hovered_id'))) {
|
|
e.patchState(
|
|
{
|
|
document: {
|
|
pageStates: {
|
|
[e.getPage()?.id]: {
|
|
selectedIds: [],
|
|
hoveredId: null,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
|
|
// don't allow select others shapes for editing if don't have permission
|
|
if (reason && reason.includes('set_editing_id')) {
|
|
if (!hasShapeAccess(e.pageState.editingId)) {
|
|
e.pageState.editingId = null;
|
|
}
|
|
}
|
|
// don't allow hover others shapes for editing if don't have permission
|
|
if (reason && reason.includes('set_hovered_id')) {
|
|
if (!hasShapeAccess(e.pageState.hoveredId)) {
|
|
e.pageState.hoveredId = null;
|
|
}
|
|
}
|
|
// don't allow select others shapes if don't have permission
|
|
if (reason && reason.includes('selected')) {
|
|
const validIds = [];
|
|
e.pageState.selectedIds.forEach((id) => hasShapeAccess(id) && validIds.push(id));
|
|
e.pageState.selectedIds = validIds;
|
|
e.patchState(
|
|
{
|
|
document: {
|
|
pageStates: {
|
|
[e.getPage()?.id]: {
|
|
selectedIds: validIds,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
);
|
|
}
|
|
// don't allow selecting others shapes with ctrl (brush)
|
|
if (e?.session?.type === 'brush' && e?.session?.status === 'brushing') {
|
|
const validIds = [];
|
|
e.pageState.selectedIds.forEach((id) => hasShapeAccess(id) && validIds.push(id));
|
|
e.pageState.selectedIds = validIds;
|
|
if (!validIds.find((id) => id === e.pageState.hoveredId)) {
|
|
e.pageState.hoveredId = undefined;
|
|
}
|
|
}
|
|
|
|
// change cursor when moving shapes
|
|
if (e?.session?.type === 'translate' && e?.session?.status === 'translating') {
|
|
if (!isMoving) setIsMoving(true);
|
|
if (reason === 'set_status:idle') setIsMoving(false);
|
|
}
|
|
|
|
if (reason && isPresenter && slidePosition && (reason.includes('zoomed') || reason.includes('panned'))) {
|
|
const camera = tldrawAPI?.getPageState()?.camera;
|
|
|
|
// limit bounds
|
|
if (tldrawAPI?.viewport.maxX > slidePosition.width) {
|
|
camera.point[0] += (tldrawAPI?.viewport.maxX - slidePosition.width);
|
|
}
|
|
if (tldrawAPI?.viewport.maxY > slidePosition.height) {
|
|
camera.point[1] += (tldrawAPI?.viewport.maxY - slidePosition.height);
|
|
}
|
|
if (camera.point[0] > 0 || tldrawAPI?.viewport.minX < 0) {
|
|
camera.point[0] = 0;
|
|
}
|
|
if (camera.point[1] > 0 || tldrawAPI?.viewport.minY < 0) {
|
|
camera.point[1] = 0;
|
|
}
|
|
|
|
const zoomFitSlide = calculateZoom(slidePosition.width, slidePosition.height);
|
|
if (camera.zoom < zoomFitSlide) {
|
|
camera.zoom = zoomFitSlide;
|
|
}
|
|
|
|
const zoomToolbar = Math.round(((HUNDRED_PERCENT * camera.zoom) / zoomFitSlide) * 100) / 100;
|
|
if (zoom !== zoomToolbar) {
|
|
setZoom(zoomToolbar);
|
|
if (isPresenter) zoomChanger(zoomToolbar);
|
|
}
|
|
|
|
let viewedRegionW = SlideCalcUtil.calcViewedRegionWidth(
|
|
tldrawAPI?.viewport.width, slidePosition.width,
|
|
);
|
|
let viewedRegionH = SlideCalcUtil.calcViewedRegionHeight(
|
|
tldrawAPI?.viewport.height, slidePosition.height,
|
|
);
|
|
|
|
if (!fitToWidth && camera.zoom === zoomFitSlide) {
|
|
viewedRegionW = HUNDRED_PERCENT;
|
|
viewedRegionH = HUNDRED_PERCENT;
|
|
camera.point = [0,0];
|
|
}
|
|
|
|
zoomSlide(
|
|
parseInt(curPageId, 10),
|
|
podId,
|
|
viewedRegionW,
|
|
viewedRegionH,
|
|
camera.point[0],
|
|
camera.point[1],
|
|
);
|
|
}
|
|
// don't allow non-presenters to pan&zoom
|
|
if (slidePosition && reason && !isPresenter && (reason.includes('zoomed') || reason.includes('panned'))) {
|
|
const newZoom = calculateZoom(slidePosition.viewBoxWidth, slidePosition.viewBoxHeight);
|
|
tldrawAPI?.setCamera([slidePosition.x, slidePosition.y], newZoom);
|
|
}
|
|
// disable select for non presenter that doesn't have multi user access
|
|
if (!hasWBAccess && !isPresenter) {
|
|
if (e?.getPageState()?.brush || e?.selectedIds?.length !== 0) {
|
|
e.patchState(
|
|
{
|
|
document: {
|
|
pageStates: {
|
|
[e?.currentPageId]: {
|
|
selectedIds: [],
|
|
brush: null,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
if (reason && reason === 'patched_shapes' && e?.session?.type === 'edit') {
|
|
const patchedShape = e?.getShape(e?.getPageState()?.editingId);
|
|
|
|
if (e?.session?.initialShape?.type === 'sticky' && patchedShape?.text?.length > maxStickyNoteLength) {
|
|
patchedShape.text = patchedShape.text.substring(0, maxStickyNoteLength);
|
|
}
|
|
|
|
if (e?.session?.initialShape?.type === 'text' && !shapes[patchedShape.id]) {
|
|
// check for maxShapes
|
|
const currentShapes = e?.document?.pages[e?.currentPageId]?.shapes;
|
|
const shapeNumberExceeded = Object.keys(currentShapes).length - 1 > maxNumberOfAnnotations;
|
|
if (shapeNumberExceeded) {
|
|
notifyShapeNumberExceeded(intl, maxNumberOfAnnotations);
|
|
e?.cancelSession?.();
|
|
} else {
|
|
patchedShape.userId = currentUser?.userId;
|
|
persistShape(patchedShape, whiteboardId, isModerator);
|
|
}
|
|
} else {
|
|
const diff = {
|
|
id: patchedShape.id,
|
|
point: patchedShape.point,
|
|
text: patchedShape.text,
|
|
};
|
|
persistShape(diff, whiteboardId, isModerator);
|
|
}
|
|
}
|
|
|
|
if (reason && reason.includes('selected_tool')) {
|
|
const tool = reason.split(':')[1];
|
|
setCurrentTool(tool);
|
|
setPanSelected(false);
|
|
setIsPanning(false);
|
|
}
|
|
|
|
if (reason && reason.includes('ui:toggled_is_loading')) {
|
|
e?.patchState(
|
|
{
|
|
appState: {
|
|
currentStyle,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
e?.patchState(
|
|
{
|
|
appState: {
|
|
isToolLocked,
|
|
},
|
|
},
|
|
);
|
|
|
|
if ((panSelected || isPanning)) {
|
|
e.isForcePanning = isPanning;
|
|
}
|
|
};
|
|
|
|
const onUndo = (app) => {
|
|
if (app.currentPageId !== curPageId) {
|
|
if (isPresenter) {
|
|
// change slide for others
|
|
skipToSlide(Number.parseInt(app.currentPageId, 10), podId);
|
|
} else {
|
|
// ignore, stay on same page
|
|
app.changePage(curPageId);
|
|
}
|
|
return;
|
|
}
|
|
const lastCommand = app.stack[app.pointer + 1];
|
|
const changedShapes = lastCommand?.before?.document?.pages[app.currentPageId]?.shapes;
|
|
if (changedShapes) {
|
|
sendShapeChanges(
|
|
app, changedShapes, shapes, prevShapes, hasShapeAccess,
|
|
whiteboardId, currentUser, intl, true,
|
|
);
|
|
}
|
|
};
|
|
|
|
const onRedo = (app) => {
|
|
if (app.currentPageId !== curPageId) {
|
|
if (isPresenter) {
|
|
// change slide for others
|
|
skipToSlide(Number.parseInt(app.currentPageId, 10), podId);
|
|
} else {
|
|
// ignore, stay on same page
|
|
app.changePage(curPageId);
|
|
}
|
|
return;
|
|
}
|
|
const lastCommand = app.stack[app.pointer];
|
|
const changedShapes = lastCommand?.after?.document?.pages[app.currentPageId]?.shapes;
|
|
if (changedShapes) {
|
|
sendShapeChanges(
|
|
app, changedShapes, shapes, prevShapes, hasShapeAccess, whiteboardId, currentUser, intl,
|
|
);
|
|
}
|
|
};
|
|
|
|
const onCommand = (app, command) => {
|
|
const isFirstCommand = command.id === "change_page" && command.before?.appState.currentPageId === "0";
|
|
if (!isFirstCommand){
|
|
setHistory(app.history);
|
|
}
|
|
|
|
if (whiteboardToolbarAutoHide && command && command.id === "change_page") {
|
|
toggleToolsAnimations('fade-in', 'fade-out', '0s');
|
|
}
|
|
|
|
if (command?.id?.includes('style')) {
|
|
setCurrentStyle({ ...currentStyle, ...command?.after?.appState?.currentStyle });
|
|
}
|
|
|
|
const changedShapes = command.after?.document?.pages[app.currentPageId]?.shapes;
|
|
if (!isMounting && app.currentPageId !== curPageId) {
|
|
// can happen then the "move to page action" is called, or using undo after changing a page
|
|
const currentPage = curPres.pages.find(
|
|
(page) => page.num === Number.parseInt(app.currentPageId, 10),
|
|
);
|
|
if (!currentPage) return;
|
|
const newWhiteboardId = currentPage.id;
|
|
// remove from previous page and persist on new
|
|
if (changedShapes) {
|
|
removeShapes(Object.keys(changedShapes), whiteboardId);
|
|
Object.entries(changedShapes)
|
|
.forEach(([id, shape]) => {
|
|
const shapeBounds = app.getShapeBounds(id);
|
|
const editedShape = shape;
|
|
editedShape.size = [shapeBounds.width, shapeBounds.height];
|
|
persistShape(editedShape, newWhiteboardId, isModerator);
|
|
});
|
|
}
|
|
if (isPresenter) {
|
|
// change slide for others
|
|
skipToSlide(Number.parseInt(app.currentPageId, 10), podId);
|
|
} else {
|
|
// ignore, stay on same page
|
|
app.changePage(curPageId);
|
|
}
|
|
} else if (changedShapes) {
|
|
sendShapeChanges(
|
|
app, changedShapes, shapes, prevShapes, hasShapeAccess, whiteboardId, currentUser, intl,
|
|
);
|
|
}
|
|
};
|
|
|
|
const webcams = document.getElementById('cameraDock');
|
|
const dockPos = webcams?.getAttribute('data-position');
|
|
const backgroundShape = document.getElementById('slide-background-shape_image');
|
|
|
|
if (currentTool && !isPanning && !tldrawAPI?.isForcePanning) tldrawAPI?.selectTool(currentTool);
|
|
|
|
if (backgroundShape && backgroundShape.src // if there is a background image
|
|
&& backgroundShape.complete // and it's fully downloaded
|
|
&& backgroundShape.src !== bgShape?.src // and if it's a different image
|
|
&& backgroundShape.parentElement?.clientWidth > 0 // and if the whiteboard area is visible
|
|
) {
|
|
setBgShape(backgroundShape);
|
|
}
|
|
const editableWB = (
|
|
<Styled.EditableWBWrapper onKeyDown={handleOnKeyDown}>
|
|
<Tldraw
|
|
key={`wb-${isRTL}-${dockPos}-${presentationAreaHeight}-${presentationAreaWidth}-${sidebarNavigationWidth}`}
|
|
document={doc}
|
|
// disable the ability to drag and drop files onto the whiteboard
|
|
// until we handle saving of assets in akka.
|
|
disableAssets
|
|
// Disable automatic focus. Users were losing focus on shared notes
|
|
// and chat on presentation mount.
|
|
autofocus={false}
|
|
onMount={onMount}
|
|
showPages={false}
|
|
showZoom={false}
|
|
showUI={curPres ? (isPresenter || hasWBAccess) : true}
|
|
showMenu={!curPres}
|
|
showMultiplayerMenu={false}
|
|
readOnly={false}
|
|
onPatch={onPatch}
|
|
onUndo={onUndo}
|
|
onRedo={onRedo}
|
|
onCommand={onCommand}
|
|
/>
|
|
</Styled.EditableWBWrapper>
|
|
);
|
|
|
|
const readOnlyWB = (
|
|
<Tldraw
|
|
key="wb-readOnly"
|
|
document={doc}
|
|
onMount={onMount}
|
|
// disable the ability to drag and drop files onto the whiteboard
|
|
// until we handle saving of assets in akka.
|
|
disableAssets
|
|
// Disable automatic focus. Users were losing focus on shared notes
|
|
// and chat on presentation mount.
|
|
autofocus={false}
|
|
showPages={false}
|
|
showZoom={false}
|
|
showUI={false}
|
|
showMenu={false}
|
|
showMultiplayerMenu={false}
|
|
readOnly
|
|
onPatch={onPatch}
|
|
/>
|
|
);
|
|
|
|
const size = ((height < SMALL_HEIGHT) || (width < SMALL_WIDTH))
|
|
? TOOLBAR_SMALL : TOOLBAR_LARGE;
|
|
|
|
const menuOffsetValues = {
|
|
true: {
|
|
true: `${styleMenuOffsetSmall}`,
|
|
false: `${styleMenuOffset}`,
|
|
},
|
|
false: {
|
|
true: `-${styleMenuOffsetSmall}`,
|
|
false: `-${styleMenuOffset}`,
|
|
},
|
|
};
|
|
|
|
const menuOffset = menuOffsetValues[isRTL][isIphone];
|
|
|
|
return (
|
|
<div key={`animations=-${animations}`}>
|
|
<Cursors
|
|
tldrawAPI={tldrawAPI}
|
|
currentUser={currentUser}
|
|
hasMultiUserAccess={hasMultiUserAccess}
|
|
whiteboardId={whiteboardId}
|
|
isViewersCursorLocked={isViewersCursorLocked}
|
|
isMultiUserActive={isMultiUserActive}
|
|
isPanning={isPanning || panSelected}
|
|
isMoving={isMoving}
|
|
currentTool={currentTool}
|
|
whiteboardToolbarAutoHide={whiteboardToolbarAutoHide}
|
|
toggleToolsAnimations={toggleToolsAnimations}
|
|
>
|
|
{(hasWBAccess || isPresenter) ? editableWB : readOnlyWB}
|
|
<Styled.TldrawGlobalStyle
|
|
hideContextMenu={!hasWBAccess && !isPresenter}
|
|
{...{
|
|
hasWBAccess,
|
|
isPresenter,
|
|
size,
|
|
darkTheme,
|
|
menuOffset,
|
|
panSelected,
|
|
isToolbarVisible,
|
|
}}
|
|
/>
|
|
</Cursors>
|
|
{isPresenter && (
|
|
<PanToolInjector
|
|
{...{
|
|
tldrawAPI,
|
|
fitToWidth,
|
|
isPanning,
|
|
setIsPanning,
|
|
zoomValue,
|
|
panSelected,
|
|
setPanSelected,
|
|
currentTool,
|
|
}}
|
|
formatMessage={intl?.formatMessage}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
Whiteboard.propTypes = {
|
|
isPresenter: PropTypes.bool.isRequired,
|
|
isIphone: PropTypes.bool.isRequired,
|
|
removeShapes: PropTypes.func.isRequired,
|
|
initDefaultPages: PropTypes.func.isRequired,
|
|
persistShape: PropTypes.func.isRequired,
|
|
notifyNotAllowedChange: PropTypes.func.isRequired,
|
|
shapes: PropTypes.objectOf(PropTypes.shape).isRequired,
|
|
assets: PropTypes.objectOf(PropTypes.shape).isRequired,
|
|
currentUser: PropTypes.shape({
|
|
userId: PropTypes.string.isRequired,
|
|
}).isRequired,
|
|
curPres: PropTypes.shape({
|
|
pages: PropTypes.arrayOf(PropTypes.shape({})),
|
|
id: PropTypes.string.isRequired,
|
|
}),
|
|
whiteboardId: PropTypes.string,
|
|
podId: PropTypes.string.isRequired,
|
|
zoomSlide: PropTypes.func.isRequired,
|
|
skipToSlide: PropTypes.func.isRequired,
|
|
slidePosition: PropTypes.shape({
|
|
x: PropTypes.number.isRequired,
|
|
y: PropTypes.number.isRequired,
|
|
height: PropTypes.number.isRequired,
|
|
width: PropTypes.number.isRequired,
|
|
viewBoxWidth: PropTypes.number.isRequired,
|
|
viewBoxHeight: PropTypes.number.isRequired,
|
|
}),
|
|
curPageId: PropTypes.string.isRequired,
|
|
presentationWidth: PropTypes.number.isRequired,
|
|
presentationHeight: PropTypes.number.isRequired,
|
|
isViewersCursorLocked: PropTypes.bool.isRequired,
|
|
zoomChanger: PropTypes.func.isRequired,
|
|
isMultiUserActive: PropTypes.func.isRequired,
|
|
isRTL: PropTypes.bool.isRequired,
|
|
fitToWidth: PropTypes.bool.isRequired,
|
|
zoomValue: PropTypes.number.isRequired,
|
|
intl: PropTypes.shape({
|
|
formatMessage: PropTypes.func.isRequired,
|
|
}).isRequired,
|
|
svgUri: PropTypes.string,
|
|
maxStickyNoteLength: PropTypes.number.isRequired,
|
|
fontFamily: PropTypes.string.isRequired,
|
|
hasShapeAccess: PropTypes.func.isRequired,
|
|
presentationAreaHeight: PropTypes.number.isRequired,
|
|
presentationAreaWidth: PropTypes.number.isRequired,
|
|
maxNumberOfAnnotations: PropTypes.number.isRequired,
|
|
notifyShapeNumberExceeded: PropTypes.func.isRequired,
|
|
darkTheme: PropTypes.bool.isRequired,
|
|
isPanning: PropTypes.bool.isRequired,
|
|
setTldrawIsMounting: PropTypes.func.isRequired,
|
|
width: PropTypes.number.isRequired,
|
|
height: PropTypes.number.isRequired,
|
|
hasMultiUserAccess: PropTypes.func.isRequired,
|
|
fullscreenElementId: PropTypes.string.isRequired,
|
|
isFullscreen: PropTypes.bool.isRequired,
|
|
layoutContextDispatch: PropTypes.func.isRequired,
|
|
fullscreenAction: PropTypes.string.isRequired,
|
|
fullscreenRef: PropTypes.instanceOf(Element),
|
|
handleToggleFullScreen: PropTypes.func.isRequired,
|
|
nextSlide: PropTypes.func.isRequired,
|
|
numberOfSlides: PropTypes.number.isRequired,
|
|
previousSlide: PropTypes.func.isRequired,
|
|
sidebarNavigationWidth: PropTypes.number,
|
|
};
|
|
|
|
Whiteboard.defaultProps = {
|
|
curPres: undefined,
|
|
fullscreenRef: undefined,
|
|
slidePosition: undefined,
|
|
svgUri: undefined,
|
|
whiteboardId: undefined,
|
|
sidebarNavigationWidth: 0,
|
|
};
|