bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/whiteboard/utils.js
2023-04-05 09:44:47 -03:00

279 lines
8.2 KiB
JavaScript

import React from 'react';
import { isEqual } from 'radash';
import {
persistShape,
removeShapes,
notifyNotAllowedChange,
notifyShapeNumberExceeded,
} from './service';
const WHITEBOARD_CONFIG = Meteor.settings.public.whiteboard;
const usePrevious = (value) => {
const ref = React.useRef();
React.useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
};
const findRemoved = (A, B) => A.filter((a) => !B.includes(a));
const filterInvalidShapes = (shapes, curPageId, tldrawAPI) => {
const retShapes = shapes;
const keys = Object.keys(shapes);
const removedChildren = [];
const removedParents = [];
keys.forEach((shape) => {
if (shapes[shape].parentId !== curPageId) {
if (!keys.includes(shapes[shape].parentId)) {
delete retShapes[shape];
}
} else if (shapes[shape].type === 'group') {
const groupChildren = shapes[shape].children;
groupChildren.forEach((child) => {
if (!keys.includes(child)) {
removedChildren.push(child);
}
});
retShapes[shape].children = groupChildren.filter((child) => !removedChildren.includes(child));
if (shapes[shape].children.length < 2) {
removedParents.push(shape);
delete retShapes[shape];
}
}
});
// remove orphaned children
Object.keys(shapes).forEach((shape) => {
if (shapes[shape] && shapes[shape].parentId !== curPageId) {
if (removedParents.includes(shapes[shape].parentId)) {
delete retShapes[shape];
}
}
// remove orphaned bindings
if (shapes[shape] && shapes[shape].type === 'arrow'
&& (shapes[shape].handles.start.bindingId || shapes[shape].handles.end.bindingId)) {
const startBinding = shapes[shape].handles.start.bindingId;
const endBinding = shapes[shape].handles.end.bindingId;
const startBindingData = tldrawAPI?.getBinding(startBinding);
const endBindingData = tldrawAPI?.getBinding(endBinding);
if (startBinding && (!startBindingData && (
removedParents.includes(startBindingData?.fromId)
|| removedParents.includes(startBindingData?.toId)
|| !keys.includes(startBindingData?.fromId)
|| !keys.includes(startBindingData?.toId)
))) {
delete retShapes[shape].handles.start.bindingId;
}
if (endBinding && (!endBindingData && (
removedParents.includes(endBindingData?.fromId)
|| removedParents.includes(endBindingData?.toId)
|| !keys.includes(endBindingData?.fromId)
|| !keys.includes(endBindingData?.toId)
))) {
delete retShapes[shape].handles.end.bindingId;
}
}
});
return retShapes;
};
const isValidShapeType = (shape) => {
const invalidTypes = ['image', 'video'];
return !invalidTypes.includes(shape?.type);
};
const sendShapeChanges = (
app,
changedShapes,
shapes,
prevShapes,
hasShapeAccess,
whiteboardId,
currentUser,
intl,
redo = false,
) => {
const invalidChange = Object.keys(changedShapes)
.find((id) => !hasShapeAccess(id));
const invalidShapeType = Object.keys(changedShapes)
.find((id) => !isValidShapeType(changedShapes[id]));
const currentShapes = app?.document?.pages[app?.currentPageId]?.shapes;
const { maxNumberOfAnnotations } = WHITEBOARD_CONFIG;
// -1 for background shape
const shapeNumberExceeded = Object.keys(currentShapes).length - 1 > maxNumberOfAnnotations;
const isInserting = Object.keys(changedShapes)
.filter(
(shape) => typeof changedShapes[shape] === 'object'
&& changedShapes[shape].type
&& !prevShapes[shape],
).length !== 0;
if (invalidChange || invalidShapeType || (shapeNumberExceeded && isInserting)) {
if (shapeNumberExceeded) {
notifyShapeNumberExceeded(intl, maxNumberOfAnnotations);
} else {
notifyNotAllowedChange(intl);
}
const modApp = app;
// undo last command without persisting to not generate the onUndo/onRedo callback
if (!redo) {
const command = app.stack[app.pointer];
modApp.pointer -= 1;
app.applyPatch(command.before, 'undo');
return;
// eslint-disable-next-line no-else-return
} else {
modApp.pointer += 1;
const command = app.stack[app.pointer];
app.applyPatch(command.after, 'redo');
return;
}
}
const deletedShapes = [];
Object.entries(changedShapes)
.forEach(([id, shape]) => {
if (!shape) deletedShapes.push(id);
else {
// checks to find any bindings assosiated with the changed shapes.
// If any, they may need to be updated as well.
const pageBindings = app.page.bindings;
if (pageBindings) {
Object.entries(pageBindings).forEach(([, b]) => {
if (b.toId.includes(id)) {
const boundShape = app.getShape(b.fromId);
if (shapes[b.fromId] && !isEqual(boundShape, shapes[b.fromId])) {
const shapeBounds = app.getShapeBounds(b.fromId);
boundShape.size = [shapeBounds.width, shapeBounds.height];
persistShape(boundShape, whiteboardId);
}
}
});
}
let modShape = shape;
if (!shape.id) {
// check it already exists (otherwise we need the full shape)
if (!shapes[id]) {
modShape = app.getShape(id);
}
modShape.id = id;
}
const shapeBounds = app.getShapeBounds(id);
const size = [shapeBounds.width, shapeBounds.height];
if (!shapes[id] || (shapes[id] && !isEqual(shapes[id].size, size))) {
modShape.size = size;
}
if (!shapes[id] || (shapes[id] && !shapes[id].userId)) {
modShape.userId = currentUser?.userId;
}
persistShape(modShape, whiteboardId);
}
});
// order the ids of shapes being deleted to prevent crash
// when removing a group shape before its children
const orderedDeletedShapes = [];
deletedShapes.forEach((eid) => {
if (shapes[eid]?.type !== 'group') {
orderedDeletedShapes.unshift(eid);
} else {
orderedDeletedShapes.push(eid);
}
});
if (orderedDeletedShapes.length > 0) {
removeShapes(orderedDeletedShapes, whiteboardId);
}
};
// map different localeCodes from bbb to tldraw
const mapLanguage = (language) => {
// bbb has xx-xx but in tldraw it's only xx
if (['es', 'fa', 'it', 'pl', 'sv', 'uk'].some((lang) => language.startsWith(lang))) {
return language.substring(0, 2);
}
// exceptions
switch (language) {
case 'nb-no':
return 'no';
case 'zh-cn':
return 'zh-ch';
default:
return language;
}
};
/* getFontStyle adapted from tldraw source code
https://github.com/tldraw/tldraw/blob/55a8831a6b036faae0dfd77d6733a8f585f5ae23/packages/tldraw/src/state/shapes/shared/shape-styles.ts#L123 */
const getFontStyle = (style) => {
const fontSizes = {
small: 28,
medium: 48,
large: 96,
auto: 'auto',
};
const fontFaces = {
script: '"Caveat Brush"',
sans: '"Source Sans Pro"',
serif: '"Crimson Pro"',
mono: '"Source Code Pro"',
}
const fontSize = fontSizes[style.size];
const fontFace = fontFaces[style.font];
const { scale = 1 } = style;
return `${fontSize * scale}px/1 ${fontFace}`;
}
/* getMeasurementDiv and getTextSize adapted from tldraw source code
https://github.com/tldraw/tldraw/blob/55a8831a6b036faae0dfd77d6733a8f585f5ae23/packages/tldraw/src/state/shapes/shared/getTextSize.ts */
const getMeasurementDiv = (font) => {
// A div used for measurement
const pre = document.getElementById('text-measure');
pre.style.font = font;
return pre;
}
const getTextSize = (text, style, padding) => {
const font = getFontStyle(style);
if (!text) {
return [16, 32];
}
const melm = getMeasurementDiv(font);
melm.textContent = text;
if (!melm) {
// We're in SSR
return [10, 10];
}
// In tests, offsetWidth and offsetHeight will be 0
const width = melm.offsetWidth || 1;
const height = melm.offsetHeight || 1;
return [width + padding, height + padding];
};
const Utils = {
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize,
};
export default Utils;
export {
usePrevious, findRemoved, filterInvalidShapes, mapLanguage, sendShapeChanges, getTextSize,
};