this.domElement = elt}
- onDragOver={this.handleDragOver}
- onDragLeave={this.handleDragLeave}
- onDrop={this.handleDrop} />,
- this.renderRotateHandle(),
- this.renderTitleArea(),
- this.renderInvalidTableDataAlert()
- ]);
+ return (
+ <>
+ {this.renderCommentEditor()}
+ {this.renderLineEditor()}
+ {this.renderPolygonLabelDialog()}
+ {this.renderSegmentLabelDialog()}
+ {this.renderPointLabelDialog()}
+
this.domElement = elt}
+ onMouseMove={this.handlePointerMove}
+ onMouseLeave={this.handlePointerLeave}
+ onDragOver={this.handleDragOver}
+ onDragLeave={this.handleDragLeave}
+ onDrop={this.handleDrop} />,
+ {this.renderRotateHandle()}
+ {this.renderTitleArea()}
+ {this.renderInvalidTableDataAlert()}
+ >);
}
private renderCommentEditor() {
@@ -498,15 +589,22 @@ export class GeometryContentComponent extends BaseComponent
{
}
}
- private renderSettingsEditor() {
- const { board, axisSettingsOpen } = this.state;
- if (board && axisSettingsOpen) {
+ private renderPointLabelDialog() {
+ const content = this.getContent();
+ const { board, showPointLabelDialog } = this.state;
+ if (board && showPointLabelDialog) {
+ const point = content.getOneSelectedPoint(board);
+ if (!point) return;
+ const handleClose = () => this.setState({ showPointLabelDialog: false });
+ const handleAccept = (p: JXG.Point, labelOption: ELabelOption, name: string, angleLabel: boolean) => {
+ this.handleSetPointLabelOptions(p, labelOption, name, angleLabel);
+ };
return (
-
);
}
@@ -521,14 +619,13 @@ export class GeometryContentComponent extends BaseComponent {
const polygon = segment && getAssociatedPolygon(segment);
if (!polygon || !segment || (points.length !== 2)) return;
const handleClose = () => this.setState({ showSegmentLabelDialog: false });
- const handleAccept = (poly: JXG.Polygon, pts: [JXG.Point, JXG.Point], labelOption: ESegmentLabelOption) =>
+ const handleAccept = (poly: JXG.Polygon, pts: [JXG.Point, JXG.Point], labelOption: ELabelOption, name: string) =>
{
- this.handleLabelSegment(poly, pts, labelOption);
+ this.handleLabelSegment(poly, pts, labelOption, name);
handleClose();
};
return (
{
}
}
+ private renderPolygonLabelDialog() {
+ const content = this.getContent();
+ const { board, showPolygonLabelDialog } = this.state;
+ if (board && showPolygonLabelDialog) {
+ const polygon = content.getOneSelectedPolygon(board);
+ if (!polygon) return;
+ const handleClose = () => this.setState({ showPolygonLabelDialog: false });
+ const handleAccept = (poly: JXG.Polygon, labelOption: ELabelOption, name: string) => {
+ this.handleLabelPolygon(poly, labelOption, name);
+ handleClose();
+ };
+ return (
+
+ );
+ }
+ }
+
private renderRotateHandle() {
const { board, disableRotate } = this.state;
const selectedPolygon = board && !disableRotate && !this.props.readOnly
@@ -568,7 +687,6 @@ export class GeometryContentComponent extends BaseComponent {
return (
{this.renderTitle()}
- {this.renderTileLinkButton()}
);
}
@@ -582,13 +700,6 @@ export class GeometryContentComponent extends BaseComponent {
);
}
- private renderTileLinkButton() {
- const { isLinkButtonEnabled, onLinkTileButtonClick } = this.props;
- return (!this.state.isEditingTitle && !this.props.readOnly &&
-
- );
- }
-
private renderInvalidTableDataAlert() {
const { showInvalidTableDataAlert } = this.state;
if (!showInvalidTableDataAlert) return;
@@ -612,14 +723,15 @@ export class GeometryContentComponent extends BaseComponent {
private async initializeBoard(): Promise {
return new Promise((resolve, reject) => {
isGeometryContentReady(this.getContent()).then(() => {
- const board = this.getContent().initializeBoard(this.elementId, this.handleCreateElements);
+ const board = this.getContent()
+ .initializeBoard(this.elementId, this.handleCreateElements,
+ (b: JXG.Board) => this.syncLinkedGeometry(b));
if (board) {
this.handleCreateBoard(board);
const { url, filename } = this.getContent().bgImage || {};
if (url) {
this.updateImageUrl(url, filename);
}
- this.syncLinkedGeometry(board);
this.setState({ board });
resolve(board);
}
@@ -695,50 +807,102 @@ export class GeometryContentComponent extends BaseComponent {
syncLinkedGeometry(_board?: JXG.Board) {
const board = _board || this.state.board;
if (!board) return;
+ const content = this.getContent();
+
+ // Make sure each linked dataset's attributes have colors assigned.
+ this.applyChange(() => {
+ content.linkedDataSets.forEach(link => {
+ link.dataSet.attributes.forEach(attr => {
+ content.assignColorSchemeForAttributeId(attr.id);
+ });
+ });
+ });
+
+ this.updateSharedPoints(board);
+ }
+
+ /**
+ * Update/add/remove linked points to matched what is in shared data sets.
+ *
+ * @param board
+ */
+ updateSharedPoints(board: JXG.Board) {
+ this.applyChange(() => {
+ let pointsAdded = false;
+ const content = this.getContent();
+ const data = content.getLinkedPointsData();
+ const remainingIds = getAllLinkedPoints(board);
+ for (const [link, points] of data.entries()) {
+ // Loop through points, adding new ones and updating any that need to be moved.
+ for (let i=0; i 0){
+ applyChange(board, { operation: "delete", target: "linkedPoint", targetID: remainingIds });
+ }
- // identify objects that exist in the model but not in JSXGraph
- const modelObjectsToConvert: GeometryObjectModelType[] = [];
- this.getContent().objects.forEach(obj => {
- if (!board.objects[obj.id]) {
- modelObjectsToConvert.push(obj);
+ if (pointsAdded) {
+ this.scaleToFit();
}
});
+ }
- if (modelObjectsToConvert.length > 0) {
- const changesToApply = convertModelObjectsToChanges(modelObjectsToConvert);
- applyChanges(board, changesToApply);
- }
+ private handleZoomIn = () => {
+ const { board } = this.state;
+ const content = this.getContent();
+ if (!board || !content) return;
+ content.zoomBoard(board, zoomFactor);
+ logGeometryEvent(content, "update", "board", undefined, { userAction: "zoom in" });
+ };
+ private handleZoomOut = () => {
+ const { board } = this.state;
+ const content = this.getContent();
+ if (!board || !content) return;
+ content.zoomBoard(board, 1/zoomFactor);
+ logGeometryEvent(content, "update", "board", undefined, { userAction: "zoom out" });
+ };
+
+ private handleScaleToFit = () => {
+ const content = this.getContent();
+ logGeometryEvent(content, "update", "board", undefined, { userAction: "fit all" });
+ this.scaleToFit();
+ };
+
+ private scaleToFit = () => {
+ const { board } = this.state;
+ if (!board || this.props.readOnly) return;
const extents = this.getBoardPointsExtents(board);
this.rescaleBoardAndAxes(extents);
- }
-
- // remove/recreate all linked points
- // Shared points are deleted, and in the process, so are the polygons that depend on them
- // This is built into JSXGraph's Board#removeObject function, which descends through and deletes all children:
- // https://github.com/jsxgraph/jsxgraph/blob/60a2504ed66b8c6fea30ef67a801e86877fb2e9f/src/base/board.js#L4775
- // Ids persist in their recreation because they are ultimately derived from canonical values
- // NOTE: A more tailored response would match up the existing points with the data set and only
- // change the affected points, which would eliminate some visual flashing that occurs when
- // unchanged points are re-created and would allow derived polygons to be preserved rather than created anew.
- recreateSharedPoints(board: JXG.Board){
- const ids = getAllLinkedPoints(board);
- if (ids.length > 0){
- applyChange(board, { operation: "delete", target: "linkedPoint", targetID: ids });
- }
- const data = this.getContent().getLinkedPointsData();
- for (const [link,points] of data.entries()) {
- const pts = applyChange(board, {
- operation: "create",
- target: "linkedPoint",
- parents: points.coords,
- properties: points.properties,
- links: { tileIds: [link]} });
- castArray(pts || []).forEach(pt => !isBoard(pt) && this.handleCreateElements(pt));
- }
- }
+ };
private handleArrowKeys = (e: React.KeyboardEvent, keys: string) => {
const { board } = this.state;
@@ -760,33 +924,58 @@ export class GeometryContentComponent extends BaseComponent {
return hasSelectedPoints;
};
- private handleToggleVertexAngle = () => {
- const { board } = this.state;
- const selectedObjects = board && this.getContent().selectedObjects(board);
- const selectedPoints = selectedObjects?.filter(isPoint);
- const selectedPoint = selectedPoints?.[0];
- if (board && selectedPoint) {
- const vertexAngle = getVertexAngle(selectedPoint);
- if (!vertexAngle) {
- const anglePts = getPointsForVertexAngle(selectedPoint);
- if (anglePts) {
- const anglePtIds = anglePts.map(pt => pt.id);
- this.applyChange(() => {
- const angle = this.getContent().addVertexAngle(board, anglePtIds);
- if (angle) {
- this.handleCreateVertexAngle(angle);
- }
- });
- }
+ private handleLabelDialog = (selectedPoint: JXG.Point|undefined, selectedSegment: JXG.Line|undefined,
+ selectedPolygon: JXG.Polygon|undefined) => {
+ // If there are just two points in a polygon, we want to label the segment not the polygon.
+ if (selectedSegment) {
+ this.setState({ showSegmentLabelDialog: true });
+ } else if (selectedPolygon) {
+ this.setState({ showPolygonLabelDialog: true });
+ } else {
+ this.setState({ showPointLabelDialog: true });
+ }
+ };
+
+ private handleSetPointLabelOptions =
+ (point: JXG.Point, labelOption: ELabelOption, name: string, angleLabel: boolean) => {
+ point._set("clientLabelOption", labelOption);
+ point._set("clientName", name);
+ setPropertiesForLabelOption(point);
+ this.applyChange(() => {
+ this.getContent().setPointLabelProps(point.id, name, labelOption);
+ const vertexAngle = getVertexAngle(point);
+ if (vertexAngle && !angleLabel) {
+ this.handleUnlabelVertexAngle(vertexAngle);
}
- else {
- this.applyChange(() => {
- this.getContent().removeObjects(board, vertexAngle.id);
- });
+ if (!vertexAngle && angleLabel) {
+ this.handleLabelVertexAngle(point);
}
+ });
+ logGeometryEvent(this.getContent(), "update", "point", point.id, { text: name, labelOption });
+ };
+
+ private handleLabelVertexAngle = (point: JXG.Point) => {
+ const { board } = this.state;
+ const anglePts = getPointsForVertexAngle(point);
+ if (board && anglePts) {
+ const anglePtIds = anglePts.map(pt => pt.id);
+ this.applyChange(() => {
+ const angle = this.getContent().addVertexAngle(board, anglePtIds);
+ if (angle) {
+ this.handleCreateVertexAngle(angle);
+ }
+ });
}
};
+ private handleUnlabelVertexAngle = (vertexAngle: JXG.Angle) => {
+ const { board } = this.state;
+ if (!board || !vertexAngle) return;
+ this.applyChange(() => {
+ this.getContent().removeObjects(board, vertexAngle.id);
+ });
+ };
+
private handleCreateMovableLine = () => {
const { board } = this.state;
const content = this.getContent();
@@ -806,21 +995,6 @@ export class GeometryContentComponent extends BaseComponent {
this.setState({ selectedLine: undefined });
};
- private closeSettings = () => {
- this.setState({ axisSettingsOpen: false });
- };
-
- private handleCreateLineLabel = () => {
- const { board } = this.state;
- const content = this.getContent();
- if (board) {
- const segment = content.getOneSelectedSegment(board);
- if (segment) {
- this.setState({ showSegmentLabelDialog: true });
- }
- }
- };
-
// Currently, we don't allow commenting of polygon edges because the commenting feature
// requires that objects have persistent/unique IDs, but polygon edges don't have such
// IDs because their IDs are generated by JSXGraph.
@@ -849,14 +1023,16 @@ export class GeometryContentComponent extends BaseComponent {
};
private handleLabelSegment =
- (polygon: JXG.Polygon, points: [JXG.Point, JXG.Point], labelOption: ESegmentLabelOption) => {
+ (polygon: JXG.Polygon, points: [JXG.Point, JXG.Point], labelOption: ELabelOption, name: string) => {
this.applyChange(() => {
- this.getContent().updatePolygonSegmentLabel(this.state.board, polygon, points, labelOption);
+ this.getContent().updatePolygonSegmentLabel(this.state.board, polygon, points, labelOption, name);
});
};
- private handleOpenAxisSettings = () => {
- this.setState({ axisSettingsOpen: true });
+ private handleLabelPolygon = (polygon: JXG.Polygon, labelOption: ELabelOption, name: string) => {
+ this.applyChange(() => {
+ this.getContent().updatePolygonLabel(this.state.board, polygon, labelOption, name);
+ });
};
private handleUpdateComment = (text: string, commentId?: string) => {
@@ -879,11 +1055,6 @@ export class GeometryContentComponent extends BaseComponent {
this.setState({ selectedLine: undefined });
};
- private handleUpdateSettings = (params: IAxesParams) => {
- this.rescaleBoardAndAxes(params);
- this.setState({ axisSettingsOpen: false });
- };
-
private handleRotatePolygon = (polygon: JXG.Polygon, vertexCoords: JXG.Coords[], isComplete: boolean) => {
const { board } = this.state;
if (!board) return;
@@ -909,7 +1080,9 @@ export class GeometryContentComponent extends BaseComponent {
vertexCoords
.map(coords => ({ snapToGrid: false,
position: coords.usrCoords.slice(1) }))
- .slice(0, vertexCount));
+ .slice(0, vertexCount),
+ undefined,
+ "rotate");
});
}
};
@@ -933,7 +1106,7 @@ export class GeometryContentComponent extends BaseComponent {
// hash the copied objects to create a pasteId tied to the content
const excludeKeys = (key: string) => ["id", "anchors", "points"].includes(key);
const hash = objectHash(copiedObjects.map(obj => getSnapshot(obj)), { excludeKeys });
- this.pasteObjects({ pasteId: hash, isSameTile: true, objects: copiedObjects });
+ this.pasteObjects({ pasteId: hash, isSameTile: true, objects: copiedObjects }, "duplicate");
}
};
@@ -1011,11 +1184,11 @@ export class GeometryContentComponent extends BaseComponent {
const objects = clipboard.getTileContent(content.type);
const pasteId = clipboard.getTileContentId(content.type) || objectHash(objects);
const isSameTile = clipboard.isSourceTile(content.type, content.metadata.id);
- this.pasteObjects({ pasteId, isSameTile, objects });
+ this.pasteObjects({ pasteId, isSameTile, objects }, "paste");
};
// paste specified object content
- private pasteObjects = (pasteContent: IPasteContent) => {
+ private pasteObjects = (pasteContent: IPasteContent, userAction: string) => {
const content = this.getContent();
const { readOnly } = this.props;
const { board } = this.state;
@@ -1058,6 +1231,10 @@ export class GeometryContentComponent extends BaseComponent {
content.deselectAll(board);
content.selectObjects(board, newPointIds);
}
+
+ // Log both the old and new IDs
+ const targetIds = [ ...Object.keys(idMap), ...Object.values(idMap)];
+ logGeometryEvent(content, "paste", "object", targetIds, { userAction });
}
return true;
}
@@ -1224,15 +1401,23 @@ export class GeometryContentComponent extends BaseComponent {
this.isSqrDistanceWithinThreshold(9, c1.coords, c2.coords));
}
+ /**
+ * Adjust display parameters depending on whether user is currently dragging a point.
+ * @param value {boolean}
+ */
+ private setDragging(value: boolean) {
+ this.state.board?.infobox.setAttribute({ opacity: value ? 1 : .75 });
+ }
+
private moveSelectedPoints(dx: number, dy: number) {
this.beginDragSelectedPoints();
- if (this.endDragSelectedPoints(undefined, undefined, [0, dx, dy])) {
+ if (this.endDragSelectedPoints(undefined, undefined, [0, dx, dy], "keyboard")) {
const { board } = this.state;
const content = this.getContent();
if (board) {
Object.keys(this.dragPts || {})
.forEach(id => {
- const elt = board.objects[id];
+ const elt = getBoardObject(board, id);
if (elt && content.isSelected(id)) {
board.updateInfobox(elt);
}
@@ -1246,6 +1431,7 @@ export class GeometryContentComponent extends BaseComponent {
private beginDragSelectedPoints(evt?: any, dragTarget?: JXG.GeometryElement) {
const { board } = this.state;
const content = this.getContent();
+ this.setDragging(true);
if (board && !hasSelectionModifier(evt || {})) {
content.metadata.selection.forEach((isSelected: boolean, id: string) => {
const obj = board.objects[id];
@@ -1278,18 +1464,20 @@ export class GeometryContentComponent extends BaseComponent {
}
});
- const affectedObjects = _keys(this.dragPts).map(id => board.objects[id]);
+ const affectedObjects = _keys(this.dragPts).map(id => getBoardObject(board, id)).filter(notEmpty);
updateVertexAnglesFromObjects(affectedObjects);
}
- private endDragSelectedPoints(evt: any, dragTarget: JXG.GeometryElement | undefined, usrDiff: number[]) {
+ private endDragSelectedPoints(evt: any, dragTarget: JXG.GeometryElement | undefined,
+ usrDiff: number[], userAction: string) {
const { board } = this.state;
const content = this.getContent();
if (!board || !content) return false;
+ this.setDragging(false);
let didDragPoints = false;
each(this.dragPts, (entry, id) => {
- const obj = board.objects[id];
+ const obj = getBoardObject(board, id);
if (obj) {
obj.setAttribute({ snapToGrid: !!entry.snapToGrid });
}
@@ -1314,7 +1502,7 @@ export class GeometryContentComponent extends BaseComponent {
}
});
- this.applyChange(() => content.updateObjects(board, ids, props));
+ this.applyChange(() => content.updateObjects(board, ids, props, undefined, userAction));
}
this.dragPts = {};
@@ -1363,10 +1551,12 @@ export class GeometryContentComponent extends BaseComponent {
.filter(obj => obj && (obj.elType !== "image"));
if (!elements.length && !hasSelectionModifier(evt) && geometryContent.hasSelection()) {
geometryContent.deselectAll(board);
- return;
}
- if (readOnly) return;
+ // Consider whether we should create a point or not.
+ if (readOnly || this.context.mode === "select" || hasSelectionModifier(evt)) {
+ return;
+ }
// extended clicks don't create new points
const clickTimeThreshold = 500;
@@ -1380,36 +1570,42 @@ export class GeometryContentComponent extends BaseComponent {
return;
}
- for (const elt of board.objectsList) {
- if (shouldInterceptPointCreation(elt) && elt.hasPoint(coords.scrCoords[1], coords.scrCoords[2])) {
- return;
- }
- }
-
- // clicks that affect selection don't create new points
- if (this.lastSelectDown &&
- (evt.timeStamp - this.lastSelectDown.timeStamp < clickTimeThreshold)) {
+ // Certain objects can block point creation
+ if (findBoardObject(board, elt => {
+ const shouldIntercept = (this.context.mode === "polygon")
+ ? shouldInterceptVertexCreation(elt)
+ : shouldInterceptPointCreation(elt);
+ return (shouldIntercept && elt.hasPoint(coords.scrCoords[1], coords.scrCoords[2]));
+ })) {
return;
}
- // clicks on board background create new points
- if (!hasSelectionModifier(evt)) {
- const props = { snapToGrid: true, snapSizeX: kSnapUnit, snapSizeY: kSnapUnit };
- this.applyChange(() => {
- const point = geometryContent.addPoint(board, [x, y], props);
- if (point) {
- this.handleCreatePoint(point);
- }
- });
- }
+ // other clicks on board background create new points, perhaps even starting a polygon.
+ this.applyChange(() => {
+ const createPoly = this.context.mode === "polygon";
+ const { point, polygon } = geometryContent.realizePhantomPoint(board, [x, y], createPoly);
+ if (point) {
+ this.handleCreatePoint(point);
+ }
+ if (polygon) {
+ this.handleCreatePolygon(polygon);
+ }
+ });
};
+ // Don't create new points on top of an existing point, line, etc.
const shouldInterceptPointCreation = (elt: JXG.GeometryElement) => {
- return isPolygon(elt)
- || isVisiblePoint(elt)
+ return isRealVisiblePoint(elt)
|| isVisibleEdge(elt)
|| isVisibleMovableLine(elt)
- || isAxisLabel(elt)
+ || isComment(elt)
+ || isMovableLineLabel(elt);
+ };
+
+ // When creating a polygon, don't put points on top of points or labels.
+ // But, you can put a point on a line or inside another polygon.
+ const shouldInterceptVertexCreation = (elt: JXG.GeometryElement) => {
+ return isRealVisiblePoint(elt)
|| isComment(elt)
|| isMovableLineLabel(elt);
};
@@ -1423,16 +1619,6 @@ export class GeometryContentComponent extends BaseComponent {
}
});
- // synchronize selection changes
- this.disposers.push(observe(content.metadata.selection, (change: any) => {
- const { board: _board } = this.state;
- if (_board) {
- // this may be a shared selection change; get all points associated with it
- const objs = getPointsByCaseId(_board, change.name);
- objs.forEach(obj => setElementColor(_board, obj.id, change.newValue.value));
- }
- }));
-
if (this.props.onSetBoard) {
this.props.onSetBoard(board);
}
@@ -1442,81 +1628,91 @@ export class GeometryContentComponent extends BaseComponent {
};
private handleCreateAxis = (axis: JXG.Line) => {
- const handlePointerDown = (evt: any) => {
- const { readOnly, scale } = this.props;
- const { board } = this.state;
- // Axis labels get the event preferentially even though we think of other potentially
- // overlapping objects (like movable line labels) as being on top. Therefore, we only
- // open the axis settings dialog if we consider the axis label to be the preferred
- // clickable object at the position of the event.
- if (board && !readOnly && (axis.label === getClickableObjectUnderMouse(board, evt, false, scale))) {
- this.handleOpenAxisSettings();
- }
- };
-
- axis.label && axis.label.on("down", handlePointerDown);
+ // nothing needed, but keep this method for consistency
};
private handleCreatePoint = (point: JXG.Point) => {
- const handlePointerDown = (evt: any) => {
+ const handlePointerDown = (evt: Event) => {
+ const { board, mode } = this.context;
const geometryContent = this.props.model.content as GeometryContentModelType;
- const { board } = this.state;
if (!board) return;
const id = point.id;
const coords = copyCoords(point.coords);
- const tableId = point.getAttribute("linkedTableId");
- const columnId = point.getAttribute("linkedColId");
const isPointDraggable = !this.props.readOnly && !point.getAttribute("fixed");
- if (isFreePoint(point) && this.isDoubleClick(this.lastPointDown, { evt, coords })) {
- if (board) {
- this.applyChange(() => {
- const polygon = geometryContent.createPolygonFromFreePoints(board, tableId, columnId);
- if (polygon) {
- this.handleCreatePolygon(polygon);
- this.props.onContentChange();
+
+ // Polygon mode interactions with existing points
+ if (mode === "polygon") {
+ this.applyChange(() => {
+ if (geometryContent.phantomPoint && geometryContent.activePolygonId) {
+ const poly = getPolygon(board, geometryContent.activePolygonId);
+ const vertex = poly && poly.vertices.find(p => p.id === id);
+ if (vertex) {
+ // user clicked on a vertex that is in the current polygon - close the polygon.
+ const polygon = geometryContent.closeActivePolygon(board, vertex);
+ if (polygon) {
+ this.handleCreatePolygon(polygon);
+ }
+ } else {
+ // use clicked a vertex that is not part of the current polygon - adopt it.
+ geometryContent.addPointToActivePolygon(board, point.id);
}
- });
- this.lastPointDown = undefined;
- }
+ } else {
+ // No active polygon. Activate one for the point clicked.
+ const polys = Object.values(point.childElements).filter(child => isPolygon(child));
+ if (polys.length > 0 && isPolygon(polys[0])) {
+ // The point clicked is in one or more polygons.
+ // Activate the first polygon returned.
+ const poly = polys[0];
+ const polygon = geometryContent.makePolygonActive(board, poly.id, point.id);
+ if (polygon) {
+ this.handleCreatePolygon(polygon);
+ }
+ } else {
+ // Point clicked is not part of a polygon. Create one.
+ const polygon = geometryContent.createPolygonIncludingPoint(board, point.id);
+ if (polygon) {
+ this.handleCreatePolygon(polygon);
+ }
+ }
+ }
+ });
+ return;
}
- else {
- this.dragPts = isPointDraggable ? { [id]: { initial: coords } } : {};
- this.lastPointDown = { evt, coords };
- // click on selected element - deselect if appropriate modifier key is down
- if (geometryContent.isSelected(id)) {
- if (hasSelectionModifier(evt)) {
- geometryContent.deselectElement(board, id);
- }
+ this.dragPts = isPointDraggable ? { [id]: { initial: coords } } : {};
+ this.lastPointDown = { evt, coords };
- if (isMovableLineControlPoint(point)) {
- // When a control point is clicked, deselect the rest of the line so the line slope can be changed
- const line = find(point.descendants, isMovableLine);
- if (line) {
- geometryContent.deselectElement(undefined, line.id);
- each(line.ancestors, (parentPoint, parentId) => {
- if (parentId !== point.id) {
- geometryContent.deselectElement(undefined, parentId);
- }
- });
- }
- }
+ // click on selected element - deselect if appropriate modifier key is down
+ if (geometryContent.isSelected(id)) {
+ if (evt instanceof MouseEvent && hasSelectionModifier(evt)) {
+ geometryContent.deselectElement(board, id);
}
- // click on unselected element
- else {
- // deselect other elements unless appropriate modifier key is down
- if (!hasSelectionModifier(evt)) {
- geometryContent.deselectAll(board);
+
+ if (isMovableLineControlPoint(point)) {
+ // When a control point is clicked, deselect the rest of the line so the line slope can be changed
+ const line = find(point.descendants, isMovableLine);
+ if (line) {
+ geometryContent.deselectElement(undefined, line.id);
+ each(line.ancestors, (parentPoint, parentId) => {
+ if (parentId !== point.id) {
+ geometryContent.deselectElement(undefined, parentId);
+ }
+ });
}
- geometryContent.selectElement(board, id);
}
-
- if (isPointDraggable) {
- this.beginDragSelectedPoints(evt, point);
+ }
+ // click on unselected element
+ else {
+ // deselect other elements unless appropriate modifier key is down
+ if (evt instanceof MouseEvent && !hasSelectionModifier(evt)) {
+ geometryContent.deselectAll(board);
}
+ geometryContent.selectElement(board, id);
+ }
- this.lastSelectDown = evt;
+ if (isPointDraggable) {
+ this.beginDragSelectedPoints(evt, point);
}
};
@@ -1546,7 +1742,7 @@ export class GeometryContentComponent extends BaseComponent {
dragEntry.final = copyCoords(point.coords);
const usrDiff = JXG.Math.Statistics.subtract(dragEntry.final.usrCoords,
dragEntry.initial.usrCoords) as number[];
- this.endDragSelectedPoints(evt, point, usrDiff);
+ this.endDragSelectedPoints(evt, point, usrDiff, "drag point");
}
delete this.dragPts[id];
@@ -1560,7 +1756,7 @@ export class GeometryContentComponent extends BaseComponent {
private handleCreateLine = (line: JXG.Line) => {
function getVertices() {
- return filter(line.ancestors, isPoint);
+ return [line.point1, line.point2];
}
const isInVertex = (evt: any) => {
@@ -1575,6 +1771,7 @@ export class GeometryContentComponent extends BaseComponent {
const { readOnly, scale } = this.props;
const { board } = this.state;
if (!board || (line !== getClickableObjectUnderMouse(board, evt, !readOnly, scale))) return;
+ if (isInVertex(evt)) return;
const content = this.getContent();
const vertices = getVertices();
@@ -1622,7 +1819,7 @@ export class GeometryContentComponent extends BaseComponent {
if (dragEntry && dragEntry.initial) {
const usrDiff = JXG.Math.Statistics.subtract(vertex.coords.usrCoords,
dragEntry.initial.usrCoords) as number[];
- this.endDragSelectedPoints(evt, line, usrDiff);
+ this.endDragSelectedPoints(evt, line, usrDiff, "drag segment");
}
}
this.isVertexDrag = false;
@@ -1667,22 +1864,14 @@ export class GeometryContentComponent extends BaseComponent {
const geometryContent = this.props.model.content as GeometryContentModelType;
const inVertex = isInVertex(evt);
const allVerticesSelected = areAllVerticesSelected();
- let selectPolygon = false;
if (!inVertex && !allVerticesSelected) {
// deselect other elements unless appropriate modifier key is down
- if (board && !hasSelectionModifier(evt)) {
+ if (!hasSelectionModifier(evt)) {
geometryContent.deselectAll(board);
}
- selectPolygon = true;
- this.lastSelectDown = evt;
- }
- if (selectPolygon) {
- geometryContent.selectElement(board, polygon.id);
- each(polygon.ancestors, point => {
- if (board && isPoint(point) && !inVertex) {
- geometryContent.selectElement(board, point.id);
- }
- });
+ const ids = Object.values(polygon.ancestors).filter(obj => isPoint(obj)).map(obj => obj.id);
+ ids.push(polygon.id);
+ geometryContent.selectObjects(board, ids);
}
if (!readOnly) {
@@ -1716,7 +1905,7 @@ export class GeometryContentComponent extends BaseComponent {
if (dragEntry && dragEntry.initial) {
const usrDiff = JXG.Math.Statistics.subtract(vertex.coords.usrCoords,
dragEntry.initial.usrCoords) as number[];
- this.endDragSelectedPoints(evt, polygon, usrDiff);
+ this.endDragSelectedPoints(evt, polygon, usrDiff, "drag polygon");
}
}
this.isVertexDrag = false;
diff --git a/src/components/tiles/geometry/geometry-shared.tsx b/src/components/tiles/geometry/geometry-shared.tsx
index e999ba4faf..90a5828ed9 100644
--- a/src/components/tiles/geometry/geometry-shared.tsx
+++ b/src/components/tiles/geometry/geometry-shared.tsx
@@ -4,11 +4,14 @@ import { HotKeyHandler } from "../../../utilities/hot-keys";
export interface IToolbarActionHandlers {
handleDuplicate: () => void;
handleDelete: () => void;
- handleToggleVertexAngle: () => void;
+ handleLabelDialog: (selectedPoint: JXG.Point|undefined, selectedSegment: JXG.Line|undefined,
+ selectedPolygon: JXG.Polygon|undefined ) => void;
handleCreateMovableLine: () => void;
- handleCreateLineLabel: () => void;
handleCreateComment: () => void;
handleUploadImageFile: (file: File) => void;
+ handleZoomIn: () => void;
+ handleZoomOut: () => void;
+ handleFitAll: () => void;
}
export interface IActionHandlers extends IToolbarActionHandlers {
handleArrows: HotKeyHandler;
diff --git a/src/components/tiles/geometry/geometry-tile-context.ts b/src/components/tiles/geometry/geometry-tile-context.ts
new file mode 100644
index 0000000000..f2c8329573
--- /dev/null
+++ b/src/components/tiles/geometry/geometry-tile-context.ts
@@ -0,0 +1,24 @@
+import { createContext, useContext } from "react";
+import { GeometryContentModelType } from "../../../models/tiles/geometry/geometry-content";
+import { IActionHandlers } from "./geometry-shared";
+import { GeometryTileMode, GeometryTileModes } from "./geometry-types";
+
+export interface IGeometryTileContext {
+ mode: GeometryTileMode;
+ setMode: (mode: GeometryTileMode) => void;
+ content: GeometryContentModelType|undefined;
+ board: JXG.Board|undefined;
+ handlers: IActionHandlers|undefined;
+}
+
+const defaultValue = {
+ mode: GeometryTileModes[0],
+ setMode: (mode: GeometryTileMode) => { },
+ content: undefined,
+ board: undefined,
+ handlers: undefined
+};
+
+export const GeometryTileContext = createContext(defaultValue);
+
+export const useGeometryTileContext = () => useContext(GeometryTileContext);
diff --git a/src/components/tiles/geometry/geometry-tile.sass b/src/components/tiles/geometry/geometry-tile.sass
deleted file mode 100644
index c41fd57d0c..0000000000
--- a/src/components/tiles/geometry/geometry-tile.sass
+++ /dev/null
@@ -1,48 +0,0 @@
-@import ../../vars
-
-$toolbar-width: 44px
-
-.geometry-tool
- position: relative
- width: 100%
- height: 100%
- min-height: 52px
-
- .geometry-wrapper
- position: absolute
- height: 100%
- left: 0
- right: 0
-
- &.read-only
- left: 0
-
- .geometry-size-me
- height: 100%
-
- .geometry-content
- height: 100%
- outline: none
-
- .comment
- min-width: 30px
- max-width: 250px
- background-color: #009CDC
- border: 1px black solid
- border-radius: 5px
- padding: 3px
- cursor: pointer
-
- &.selected
- background-color: red
-
-.rotate-polygon-icon
- background-image: url("../../../assets/rotate.png")
- position: absolute
- width: 16px
- height: 16px
- z-index: 10
- display: none
-
- &.enabled
- display: block
diff --git a/src/components/tiles/geometry/geometry-tile.scss b/src/components/tiles/geometry/geometry-tile.scss
new file mode 100644
index 0000000000..18f4b6219e
--- /dev/null
+++ b/src/components/tiles/geometry/geometry-tile.scss
@@ -0,0 +1,80 @@
+@import "../../vars";
+
+$toolbar-width: 44px;
+
+.geometry-tool {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ min-height: 52px;
+
+ .geometry-wrapper {
+ position: absolute;
+ height: 100%;
+ left: 0;
+ right: 0;
+
+ &.read-only {
+ left: 0;
+ }
+ .geometry-size-me {
+ height: 100%;
+ overflow: hidden; // for older browsers
+ overflow: clip;
+
+ .geometry-content {
+ height: 100%;
+ outline: none;
+
+ .comment {
+ min-width: 30px;
+ max-width: 250px;
+ background-color: #009CDC;
+ border: 1px black solid;
+ border-radius: 5px;
+ padding: 3px;
+ cursor: pointer;
+
+ &.selected {
+ background-color: red;
+ }
+ }
+
+ svg {
+
+ ellipse {
+ paint-order: stroke fill;
+ }
+
+ // JSXGraph doesn't allow us to set a class attribute
+ // so we use stroke opacity .99 to signal highlighting via drop-shadow.
+ line[stroke-opacity="0.99"] {
+ -webkit-filter: drop-shadow(0 0 6px #0081ff);
+ filter: drop-shadow(0 0 6px #0081ff);
+ }
+
+ .tool-tile.selected:not(.readonly) & {
+ ellipse, line, polygon {
+ cursor: move;
+ }
+ }
+
+ }
+
+ }
+ }
+ }
+}
+
+.rotate-polygon-icon {
+ background-image: url("../../../assets/rotate-selection.svg");
+ position: absolute;
+ width: 30px;
+ height: 30px;
+ z-index: 10;
+ display: none;
+
+ &.enabled {
+ display: block;
+ }
+}
diff --git a/src/components/tiles/geometry/geometry-tile.tsx b/src/components/tiles/geometry/geometry-tile.tsx
index c049ee4e4d..85a5f2e7fc 100644
--- a/src/components/tiles/geometry/geometry-tile.tsx
+++ b/src/components/tiles/geometry/geometry-tile.tsx
@@ -1,27 +1,30 @@
import React, { useCallback, useRef, useState } from "react";
import { GeometryContentWrapper } from "./geometry-content-wrapper";
import { IGeometryProps, IActionHandlers } from "./geometry-shared";
-import { GeometryToolbar } from "./geometry-toolbar";
import { GeometryContentModelType } from "../../../models/tiles/geometry/geometry-content";
import { useTileSelectionPointerEvents } from "./use-tile-selection-pointer-events";
import { useUIStore } from "../../../hooks/use-stores";
import { useCurrent } from "../../../hooks/use-current";
import { useForceUpdate } from "../hooks/use-force-update";
-import { useToolbarTileApi } from "../hooks/use-toolbar-tile-api";
-import { useProviderTileLinking } from "../../../hooks/use-provider-tile-linking";
import { HotKeys } from "../../../utilities/hot-keys";
+import { TileToolbar } from "../../toolbar/tile-toolbar";
+import { IGeometryTileContext, GeometryTileContext } from "./geometry-tile-context";
+import { GeometryTileMode } from "./geometry-types";
-import "./geometry-tile.sass";
+import "./geometry-toolbar-registration";
+
+import "./geometry-tile.scss";
const _GeometryToolComponent: React.FC = ({
model, readOnly, ...others
}) => {
- const { documentContent, tileElt, scale, onRegisterTileApi, onUnregisterTileApi } = others;
+ const { tileElt } = others;
const modelRef = useCurrent(model);
const domElement = useRef(null);
const content = model.content as GeometryContentModelType;
const [board, setBoard] = useState();
const [actionHandlers, setActionHandlers] = useState();
+ const [mode, setMode] = useState("select");
const hotKeys = useRef(new HotKeys());
const forceUpdate = useForceUpdate();
@@ -40,35 +43,41 @@ const _GeometryToolComponent: React.FC = ({
setActionHandlers(handlers);
};
+ const context: IGeometryTileContext = {
+ mode,
+ setMode,
+ content,
+ board,
+ handlers: actionHandlers
+ };
+
const ui = useUIStore();
const [handlePointerDown, handlePointerUp] = useTileSelectionPointerEvents(
useCallback(() => ui.isSelectedTile(modelRef.current), [modelRef, ui]),
useCallback((append: boolean) => ui.setSelectedTile(modelRef.current, { append }), [modelRef, ui]),
domElement
);
- const enabled = !readOnly && !!board && !!actionHandlers;
- const toolbarProps = useToolbarTileApi({ id: model.id, enabled, onRegisterTileApi, onUnregisterTileApi });
- const { isLinkEnabled, showLinkTileDialog }
- = useProviderTileLinking({ model, readOnly, sharedModelTypes: [ "SharedDataSet" ] });
+
// We must listen for pointer events because we want to get the events before
// JSXGraph, which appears to listen to pointer events on browsers that support them.
// We must listen for mouse events because some browsers (notably Safari) don't
// support pointer events.
return (
- hotKeys.current.dispatch(e)} >
-
-
-
-
+
+ hotKeys.current.dispatch(e)} >
+
+
+
+
);
};
+
const GeometryToolComponent = React.memo(_GeometryToolComponent);
export default GeometryToolComponent;
diff --git a/src/components/tiles/geometry/geometry-tool-buttons.tsx b/src/components/tiles/geometry/geometry-tool-buttons.tsx
deleted file mode 100644
index e6993d1bdd..0000000000
--- a/src/components/tiles/geometry/geometry-tool-buttons.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import classNames from "classnames";
-import React from "react";
-import { Tooltip } from "react-tippy";
-// geometry icons
-import AngleLabelSvg from "../../../clue/assets/icons/geometry/angle-label.svg";
-import CopyPolygonSvg from "../../../clue/assets/icons/geometry/copy-polygon.svg";
-import LineLabelSvg from "../../../clue/assets/icons/geometry/line-label.svg";
-import MovableLineSvg from "../../../clue/assets/icons/geometry/movable-line.svg";
-// generic icons
-import CommentSvg from "../../../assets/icons/comment/comment.svg";
-import DeleteSvg from "../../../assets/icons/delete/delete-selection-icon.svg";
-import { useTooltipOptions } from "../../../hooks/use-tooltip-options";
-
-type SvgComponent = React.FC>;
-
-export interface IClientToolButtonProps {
- disabled?: boolean;
- selected?: boolean;
- onClick?: () => void;
-}
-interface IGeometryToolButtonProps extends IClientToolButtonProps {
- SvgComponent: SvgComponent;
- className: string;
-}
-export const GeometryToolButton: React.FC = ({
- SvgComponent, className, disabled, selected, onClick
-}) => {
- const classes = classNames("button", className, { enabled: !disabled, disabled, selected });
- return (
-
-
-
- );
-};
-
-/*
- * Geometry buttons
- */
-const kTooltipYDistance = 2;
-export const AngleLabelButton: React.FC = (props) => {
- const tooltipOptions = useTooltipOptions({ distance: kTooltipYDistance });
- return (
-
-
-
- );
-};
-
-export const DuplicateButton: React.FC = (props) => {
- const tooltipOptions = useTooltipOptions({ distance: kTooltipYDistance });
- return (
-
-
-
- );
-};
-
-export const LineLabelButton: React.FC = (props) => {
- const tooltipOptions = useTooltipOptions({ distance: kTooltipYDistance });
- return (
-
-
-
- );
-};
-
-export const MovableLineButton: React.FC = (props) => {
- const tooltipOptions = useTooltipOptions({ distance: kTooltipYDistance });
- return (
-
-
-
- );
-};
-
-/*
- * Generic buttons
- */
-export const CommentButton: React.FC = (props) => {
- const tooltipOptions = useTooltipOptions({ distance: kTooltipYDistance });
- return (
-
-
-
- );
-};
-
-export const DeleteButton: React.FC = (props) => {
- const tooltipOptions = useTooltipOptions({ distance: kTooltipYDistance });
- return (
-
-
-
- );
-};
diff --git a/src/components/tiles/geometry/geometry-toolbar-registration.tsx b/src/components/tiles/geometry/geometry-toolbar-registration.tsx
new file mode 100644
index 0000000000..7634c4d910
--- /dev/null
+++ b/src/components/tiles/geometry/geometry-toolbar-registration.tsx
@@ -0,0 +1,303 @@
+import React, { FunctionComponent, SVGProps } from "react";
+import { observer } from "mobx-react";
+import { IToolbarButtonComponentProps, registerTileToolbarButtons } from "../../toolbar/toolbar-button-manager";
+import { TileToolbarButton } from "../../toolbar/tile-toolbar-button";
+import { useGeometryTileContext } from "./geometry-tile-context";
+import { UploadButton } from "../../toolbar/upload-button";
+import { useProviderTileLinking } from "../../../hooks/use-provider-tile-linking";
+import { useReadOnlyContext } from "../../document/read-only-context";
+import { useTileModelContext } from "../hooks/use-tile-model-context";
+import { GeometryTileMode } from "./geometry-types";
+
+import AddImageSvg from "../../../clue/assets/icons/geometry/add-image-icon.svg";
+import CommentSvg from "../../../assets/icons/comment/comment.svg";
+import DeleteSvg from "../../../assets/icons/delete/delete-selection-icon.svg";
+import LabelSvg from "../../../clue/assets/icons/shapes-label-value-icon.svg";
+import MovableLineSvg from "../../../clue/assets/icons/geometry/movable-line.svg";
+import PointSvg from "../../../clue/assets/icons/geometry/point-icon.svg";
+import PolygonSvg from "../../../clue/assets/icons/geometry/polygon-icon.svg";
+import SelectSvg from "../../../clue/assets/icons/select-tool.svg";
+import ShapesDuplicateSvg from "../../../clue/assets/icons/geometry/shapes-duplicate-icon.svg";
+import AddDataSvg from "../../../assets/icons/add-data-graph-icon.svg";
+import ZoomInSvg from "../../../clue/assets/icons/zoom-in-icon.svg";
+import ZoomOutSvg from "../../../clue/assets/icons/zoom-out-icon.svg";
+import FitAllSvg from "../../../clue/assets/icons/fit-view-icon.svg";
+
+function ModeButton({name, title, targetMode, Icon}:
+ { name: string, title: string, targetMode: GeometryTileMode, Icon: FunctionComponent> }) {
+ const { board, content, mode, setMode } = useGeometryTileContext();
+
+ function onClick() {
+ if (mode !== targetMode) {
+ setMode(targetMode);
+ if (board) {
+ content?.clearPhantomPoint(board);
+ content?.clearActivePolygon(board);
+ }
+ }
+ }
+
+ return (
+
+
+
+ );
+}
+
+const SelectButton = observer(function SelectButton({name}: IToolbarButtonComponentProps) {
+ return();
+});
+
+const PointButton = observer(function PointButton({name}: IToolbarButtonComponentProps) {
+ return();
+});
+
+const PolygonButton = observer(function PolygonButton({name}: IToolbarButtonComponentProps) {
+ return();
+});
+
+const DuplicateButton = observer(function DuplicateButton({name}: IToolbarButtonComponentProps) {
+ const { content, board, handlers } = useGeometryTileContext();
+ const disableDuplicate = !content || !board || !content.hasDeletableSelection(board);
+ return (
+ handlers?.handleDuplicate()}
+ >
+
+
+ );
+
+});
+
+const LabelButton = observer(function LabelButton({name}: IToolbarButtonComponentProps) {
+ const { content, board, handlers } = useGeometryTileContext();
+ const selectedPoint = board && content?.getOneSelectedPoint(board);
+ const selectedSegment = board && content?.getOneSelectedSegment(board);
+ const selectedPolygon = board && content?.getOneSelectedPolygon(board);
+
+ const pointHasLabel = selectedPoint && selectedPoint.hasLabel;
+ const segmentHasLabel = selectedSegment && selectedSegment.hasLabel;
+ const polygonHasLabel = selectedPolygon && selectedPolygon.hasLabel;
+
+ function handleClick() {
+ handlers?.handleLabelDialog(selectedPoint, selectedSegment, selectedPolygon);
+ }
+
+ return (
+
+
+
+ );
+
+});
+
+const MovableLineButton = observer(function MovableLineButton({name}: IToolbarButtonComponentProps) {
+ const { handlers } = useGeometryTileContext();
+ return (
+ handlers?.handleCreateMovableLine()}
+ >
+
+
+ );
+});
+
+const CommentButton = observer(function CommentButton({name}: IToolbarButtonComponentProps) {
+ const { content, board, handlers } = useGeometryTileContext();
+ const disableComment = board && !content?.getCommentAnchor(board) && !content?.getOneSelectedComment(board);
+
+ return (
+ handlers?.handleCreateComment()}
+ >
+
+
+ );
+});
+
+const DeleteButton = observer(function DeleteButton({name}: IToolbarButtonComponentProps) {
+ const { content, board, handlers } = useGeometryTileContext();
+ const disableDelete = !board || !content?.hasDeletableSelection(board);
+
+ return (
+ handlers?.handleDelete()}
+ >
+
+
+ );
+});
+
+const ImageUploadButton = observer(function ImageUploadButton({name}: IToolbarButtonComponentProps) {
+ const { handlers } = useGeometryTileContext();
+
+ const onUploadImageFile = (x: File) => {
+ handlers?.handleUploadImageFile(x);
+ };
+
+ return (
+
+
+
+ );
+});
+
+const AddDataButton = observer (function AddDataButton({name}: IToolbarButtonComponentProps) {
+ const readOnly = useReadOnlyContext();
+ const { tile } = useTileModelContext();
+ const { isLinkEnabled, showLinkTileDialog }
+ = useProviderTileLinking({ model: tile!, readOnly, sharedModelTypes: [ "SharedDataSet" ] });
+ return (
+
+
+
+ );
+});
+
+function ZoomInButton({name}: IToolbarButtonComponentProps) {
+ const readOnly = useReadOnlyContext();
+ const { handlers } = useGeometryTileContext();
+
+ function handleClick() {
+ if (readOnly) return;
+ handlers?.handleZoomIn();
+ }
+
+ return (
+
+
+
+ );
+}
+
+function ZoomOutButton({name}: IToolbarButtonComponentProps) {
+ const readOnly = useReadOnlyContext();
+ const { handlers } = useGeometryTileContext();
+
+ function handleClick() {
+ if (readOnly) return;
+ handlers?.handleZoomOut();
+ }
+
+ return (
+
+
+
+ );
+}
+
+function FitAllButton({name}: IToolbarButtonComponentProps) {
+ const readOnly = useReadOnlyContext();
+ const { handlers } = useGeometryTileContext();
+
+ function handleClick() {
+ if (readOnly) return;
+ handlers?.handleFitAll();
+ }
+
+ return (
+
+
+
+ );
+}
+
+registerTileToolbarButtons("geometry",
+ [
+ { name: "select",
+ component: SelectButton
+ },
+ {
+ name: "point",
+ component: PointButton
+ },
+ {
+ name: "polygon",
+ component: PolygonButton
+ },
+ {
+ name: "duplicate",
+ component: DuplicateButton
+ },
+ {
+ name: "label",
+ component: LabelButton
+ },
+ {
+ name: "movable-line",
+ component: MovableLineButton
+ },
+ {
+ name: "comment",
+ component: CommentButton
+ },
+ {
+ name: "upload",
+ component: ImageUploadButton
+ },
+ {
+ name: "add-data",
+ component: AddDataButton
+ },
+ {
+ name: "delete",
+ component: DeleteButton
+ },
+ {
+ name: "zoom-in",
+ component: ZoomInButton
+ },
+ {
+ name: "zoom-out",
+ component: ZoomOutButton
+ },
+ {
+ name: "fit-all",
+ component: FitAllButton
+ }
+ ]
+);
diff --git a/src/components/tiles/geometry/geometry-toolbar.sass b/src/components/tiles/geometry/geometry-toolbar.sass
deleted file mode 100644
index b97a44b261..0000000000
--- a/src/components/tiles/geometry/geometry-toolbar.sass
+++ /dev/null
@@ -1,69 +0,0 @@
-@import ../../vars
-
-.geometry-toolbar
- position: absolute
- height: $toolbar-button-height + 4px
- border: $toolbar-border
- border-radius: 0 0 $toolbar-border-radius $toolbar-border-radius
- background-color: $workspace-teal-light-9
- overflow: hidden
- z-index: $toolbar-z-index
-
- &.disabled
- display: none
-
- .toolbar-buttons
- height: $toolbar-button-height
- display: flex
- flex-direction: row
- justify-content: center
- align-items: center
-
- .button
- box-sizing: content-box
- width: $toolbar-button-width
- height: $toolbar-button-height
- margin: 0 2px
- background-color: $workspace-teal-light-9
- display: flex
- justify-content: center
- align-items: center
- &:active
- background-color: $workspace-teal-light-4
- cursor: pointer
- &.selected
- background-color: $workspace-teal-light-4
- &:hover
- background-color: $workspace-teal-light-6
- cursor: pointer
- &.disabled
- opacity: .25
- pointer-events: none
-
- svg
- height: 34px
- &.movable-line svg
- height: 30px
-
- // image upload button
- .toolbar-button
- position: relative
- width: $toolbar-button-width
- height: $toolbar-button-height
- background-color: $workspace-teal-light-9
-
- &:hover
- background-color: $workspace-teal-light-6
-
- &:active
- background-color: $workspace-teal-light-4
-
- svg path
- fill: $workspace-teal-dark-1
-
- input
- position: absolute
- left: 0
- top: 0
- width: $toolbar-button-width
- height: $toolbar-button-height
diff --git a/src/components/tiles/geometry/geometry-toolbar.test.tsx b/src/components/tiles/geometry/geometry-toolbar.test.tsx
deleted file mode 100644
index d4b4d3b80d..0000000000
--- a/src/components/tiles/geometry/geometry-toolbar.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from "react";
-import { render, screen } from "@testing-library/react";
-import { GeometryToolbar } from "./geometry-toolbar";
-import { GeometryContentModel, GeometryMetadataModel } from "../../../models/tiles/geometry/geometry-content";
-
-describe("GeometryToolbar", () => {
- const content = GeometryContentModel.create();
- const metadata = GeometryMetadataModel.create({ id: "test-metadata" });
- content.doPostCreate!(metadata);
-
- it("renders successfully", () => {
- render();
- const documentContent = screen.getByTestId("document-content");
-
- render(
- true}
- onRegisterTileApi={() => null} onUnregisterTileApi={() => null} />
- );
- expect(screen.getByTestId("geometry-toolbar")).toBeInTheDocument();
- });
-});
diff --git a/src/components/tiles/geometry/geometry-toolbar.tsx b/src/components/tiles/geometry/geometry-toolbar.tsx
deleted file mode 100644
index 5a9adfdb1d..0000000000
--- a/src/components/tiles/geometry/geometry-toolbar.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import classNames from "classnames";
-import { observer } from "mobx-react";
-import React from "react";
-import ReactDOM from "react-dom";
-import { GeometryContentModelType } from "../../../models/tiles/geometry/geometry-content";
-import { isPoint } from "../../../models/tiles/geometry/jxg-types";
-import { canSupportVertexAngle, getVertexAngle } from "../../../models/tiles/geometry/jxg-vertex-angle";
-import { IFloatingToolbarProps, useFloatingToolbarLocation } from "../hooks/use-floating-toolbar-location";
-import { IToolbarActionHandlers } from "./geometry-shared";
-import {
- AngleLabelButton, CommentButton, DeleteButton, DuplicateButton, LineLabelButton, MovableLineButton
-} from "./geometry-tool-buttons";
-import { ImageUploadButton } from "../image/image-toolbar";
-
-import "./geometry-toolbar.sass";
-
-interface IProps extends IFloatingToolbarProps {
- board?: JXG.Board;
- content: GeometryContentModelType;
- handlers?: IToolbarActionHandlers;
-}
-
-export const GeometryToolbar: React.FC = observer(({
- documentContent, tileElt, board, content, handlers, onIsEnabled, ...others
-}) => {
- const {
- handleCreateComment, handleCreateMovableLine, handleDelete, handleDuplicate,
- handleToggleVertexAngle, handleCreateLineLabel, handleUploadImageFile
- } = handlers || {};
- const enabled = onIsEnabled();
- const location = useFloatingToolbarLocation({
- documentContent,
- tileElt,
- toolbarHeight: 38,
- toolbarTopOffset: 2,
- enabled,
- ...others
- });
- // reference the entire selection map so selection changes trigger observable render
- content.metadata.selection.toJSON();
- const selectedObjects = board && content.selectedObjects(board);
- const selectedPoints = selectedObjects?.filter(isPoint);
- const selectedPoint = selectedPoints?.length === 1 ? selectedPoints[0] : undefined;
- const disableVertexAngle = !(selectedPoint && canSupportVertexAngle(selectedPoint));
- const disableLineLabel = board && !content.getOneSelectedSegment(board);
- const hasVertexAngle = !!selectedPoint && !!getVertexAngle(selectedPoint);
- const disableDelete = board && !content.getDeletableSelectedIds(board).length;
- const disableDuplicate = board && (!content.getOneSelectedPoint(board) &&
- !content.getOneSelectedPolygon(board));
- const disableComment = board && !content.getCommentAnchor(board) &&
- !content.getOneSelectedComment(board);
- return documentContent
- ? ReactDOM.createPortal(
- e.stopPropagation()}>
-
-
, documentContent)
- : null;
-});
diff --git a/src/components/tiles/geometry/geometry-types.ts b/src/components/tiles/geometry/geometry-types.ts
new file mode 100644
index 0000000000..4af11bdd3c
--- /dev/null
+++ b/src/components/tiles/geometry/geometry-types.ts
@@ -0,0 +1,2 @@
+export const GeometryTileModes = ["select", "points", "polygon"] as const;
+export type GeometryTileMode = typeof GeometryTileModes[number];
diff --git a/src/components/tiles/geometry/label-dialog.scss b/src/components/tiles/geometry/label-dialog.scss
new file mode 100644
index 0000000000..4956edeac8
--- /dev/null
+++ b/src/components/tiles/geometry/label-dialog.scss
@@ -0,0 +1,21 @@
+fieldset.radio-button-set {
+ border: none;
+ padding: 6px 0;
+}
+
+.radio-button-container {
+ display: flex;
+ align-items: center;
+}
+
+input.radio-button {
+ margin-right: .5em;
+}
+
+input.name-input {
+ margin-left: 8px;
+}
+
+input.checkbox {
+ margin-right: .5em;
+}
diff --git a/src/components/tiles/geometry/label-point-dialog.tsx b/src/components/tiles/geometry/label-point-dialog.tsx
new file mode 100644
index 0000000000..d73fb4bb99
--- /dev/null
+++ b/src/components/tiles/geometry/label-point-dialog.tsx
@@ -0,0 +1,31 @@
+import React, { useEffect } from "react";
+import { ELabelOption } from "../../../models/tiles/geometry/jxg-changes";
+import { useLabelPointDialog } from "./use-label-point-dialog";
+
+interface IProps {
+ board: JXG.Board;
+ point: JXG.Point;
+ onAccept: (point: JXG.Point, labelOption: ELabelOption, name: string, hasAngle: boolean) => void;
+ onClose: () => void;
+}
+
+// Component wrapper for useLabelPointDialog() for use by class components.
+const LabelPointDialog: React.FC = ({
+ board, point, onAccept, onClose
+}: IProps) => {
+
+ const [showDialog, hideDialog] = useLabelPointDialog({
+ board,
+ point,
+ onAccept,
+ onClose
+ });
+
+ useEffect(() => {
+ showDialog();
+ return () => hideDialog();
+ }, [hideDialog, showDialog]);
+
+ return null;
+};
+export default LabelPointDialog;
diff --git a/src/components/tiles/geometry/label-polygon-dialog.tsx b/src/components/tiles/geometry/label-polygon-dialog.tsx
new file mode 100644
index 0000000000..4a5bd1e89c
--- /dev/null
+++ b/src/components/tiles/geometry/label-polygon-dialog.tsx
@@ -0,0 +1,31 @@
+import React, { useEffect } from "react";
+import { ELabelOption } from "../../../models/tiles/geometry/jxg-changes";
+import { useLabelPolygonDialog } from "./use-label-polygon-dialog";
+
+interface IProps {
+ board: JXG.Board;
+ polygon: JXG.Polygon;
+ onAccept: (polygon: JXG.Polygon, labelOption: ELabelOption, name: string) => void;
+ onClose: () => void;
+}
+
+// Component wrapper for useLabelPolygonDialog() for use by class components.
+const LabelPolygonDialog: React.FC = ({
+ board, polygon, onAccept, onClose
+}: IProps) => {
+
+ const [showDialog, hideDialog] = useLabelPolygonDialog({
+ board,
+ polygon,
+ onAccept,
+ onClose
+ });
+
+ useEffect(() => {
+ showDialog();
+ return () => hideDialog();
+ }, [hideDialog, showDialog]);
+
+ return null;
+};
+export default LabelPolygonDialog;
diff --git a/src/components/tiles/geometry/label-radio-button.tsx b/src/components/tiles/geometry/label-radio-button.tsx
new file mode 100644
index 0000000000..de25538d21
--- /dev/null
+++ b/src/components/tiles/geometry/label-radio-button.tsx
@@ -0,0 +1,32 @@
+import React, { PropsWithChildren } from "react";
+
+interface LabelRadioButtonProps {
+ display: string;
+ label: string;
+ checkedLabel: string;
+ setLabelOption: React.Dispatch>;
+}
+export const LabelRadioButton = function (
+ {display, label, checkedLabel, setLabelOption, children}: PropsWithChildren) {
+ return (
+
+ {
+ if (e.target.checked) {
+ setLabelOption(e.target.value);
+ }
+ }}
+ />
+
+ {children}
+
+ );
+};
diff --git a/src/components/tiles/geometry/label-segment-dialog.scss b/src/components/tiles/geometry/label-segment-dialog.scss
deleted file mode 100644
index cf59fa68f1..0000000000
--- a/src/components/tiles/geometry/label-segment-dialog.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-
-.radio-button-set {
- border: 0px;
- width: 250px;
- padding: 0px;
-}
-
-.radio-button-container {
- display: flex;
- align-items: center;
-}
-
-.radio-button {
- margin-right: .5em;
-}
diff --git a/src/components/tiles/geometry/label-segment-dialog.tsx b/src/components/tiles/geometry/label-segment-dialog.tsx
index 6c8ad46756..4054fc6856 100644
--- a/src/components/tiles/geometry/label-segment-dialog.tsx
+++ b/src/components/tiles/geometry/label-segment-dialog.tsx
@@ -1,16 +1,16 @@
import React, { useEffect } from "react";
-import { ESegmentLabelOption } from "../../../models/tiles/geometry/jxg-changes";
+import { ELabelOption } from "../../../models/tiles/geometry/jxg-changes";
import { useLabelSegmentDialog } from "./use-label-segment-dialog";
interface IProps {
board: JXG.Board;
polygon: JXG.Polygon;
points: [JXG.Point, JXG.Point];
- onAccept: (polygon: JXG.Polygon, points: [JXG.Point, JXG.Point], labelOption: ESegmentLabelOption) => void;
+ onAccept: (polygon: JXG.Polygon, points: [JXG.Point, JXG.Point], labelOption: ELabelOption, name: string) => void;
onClose: () => void;
}
-// Component wrapper for useAxisSettingsDialog() for use by class components.
+// Component wrapper for useLabelSegmentDialog() for use by class components.
const LabelSegmentDialog: React.FC = ({
board, polygon, points, onAccept, onClose
}: IProps) => {
diff --git a/src/components/tiles/geometry/link-table-button.scss b/src/components/tiles/geometry/link-table-button.scss
deleted file mode 100644
index f01e82c716..0000000000
--- a/src/components/tiles/geometry/link-table-button.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-@import "../../vars.sass";
-
-.link-table-button {
- position: relative;
- margin-left: 10px;
- width: 32px;
- height: 26px;
- border-radius: 5px;
- border: solid 1.5px $charcoal-light-1;
- background-color: white;
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 10;
-
- &.disabled {
- svg {
- opacity: 35%;
- }
- border-color: $charcoal-light-4;
- }
-
- &:hover:not(.disabled) {
- background-color: $workspace-teal-light-4;
- }
-
- &:active:not(.disabled) {
- background-color: $workspace-teal-light-2;
- }
-}
diff --git a/src/components/tiles/geometry/link-table-button.true.test.tsx b/src/components/tiles/geometry/link-table-button.true.test.tsx
deleted file mode 100644
index c5711bb085..0000000000
--- a/src/components/tiles/geometry/link-table-button.true.test.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from "react";
-import { render, screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { LinkTableButton } from "./link-table-button";
-import { act } from "react-dom/test-utils";
-
-// mocking is module-level, so we have separate modules to mock the different return values
-const useFeatureFlag = jest.fn().mockReturnValue(true);
-jest.mock("../../../hooks/use-stores", () => ({
- useFeatureFlag: (...args: any) => useFeatureFlag(...args)
-}));
-
-describe("LinkTableButton with linking enabled", () => {
-
- const onClick = jest.fn();
-
- beforeEach(() => {
- onClick.mockReset();
- });
-
- it("renders when disabled", () => {
- const { unmount } = render();
- expect(screen.getByTestId("table-link-button")).toBeInTheDocument();
- act(() => {
- userEvent.click(screen.getByTestId("table-link-button"));
- unmount();
- });
- expect(onClick).not.toHaveBeenCalled();
- });
-
- it("renders when enabled", () => {
- const { unmount } = render();
- expect(screen.getByTestId("table-link-button")).toBeInTheDocument();
- act(() => {
- userEvent.click(screen.getByTestId("table-link-button"));
- unmount();
- });
- expect(onClick).toHaveBeenCalledTimes(1);
- });
-
- it("renders when enabled without onClick", () => {
- const { unmount } = render();
- expect(screen.getByTestId("table-link-button")).toBeInTheDocument();
- act(() => {
- userEvent.click(screen.getByTestId("table-link-button"));
- unmount();
- });
- expect(onClick).not.toHaveBeenCalled();
- });
-});
diff --git a/src/components/tiles/geometry/link-table-button.tsx b/src/components/tiles/geometry/link-table-button.tsx
deleted file mode 100644
index 5a8c078c5d..0000000000
--- a/src/components/tiles/geometry/link-table-button.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import classNames from "classnames";
-import React from "react";
-import LinkTableIcon from "../../../clue/assets/icons/geometry/link-table-icon.svg";
-
-import "./link-table-button.scss";
-
-//TODO: dataflow-program-link-table-button.tsx is very similar
-//consider refactoring -> https://www.pivotaltracker.com/n/projects/2441242/stories/184992684
-
-interface IProps {
- isEnabled?: boolean;
- onClick?: () => void;
-}
-export const LinkTableButton: React.FC = ({ isEnabled, onClick }) => {
- const classes = classNames("link-table-button", { disabled: !isEnabled });
- const handleClick = (e: React.MouseEvent) => {
- isEnabled && onClick?.();
- e.stopPropagation();
- };
- return (
-
-
-
- );
-};
diff --git a/src/components/tiles/geometry/link-table-dialog.scss b/src/components/tiles/geometry/link-table-dialog.scss
deleted file mode 100644
index 028f97b698..0000000000
--- a/src/components/tiles/geometry/link-table-dialog.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-@import "../../../components/vars.sass";
-
-.custom-modal.link-table {
- width: 420px;
- height: 200px;
- outline: none;
-
- .modal-content {
- justify-content: flex-start;
-
- .prompt {
- margin-top: 15px;
- }
- }
-
- select {
- margin: 15px 20px;
- font-style: italic;
- }
-
- .modal-button.disabled {
- opacity: 35%;
- }
-};
diff --git a/src/components/tiles/geometry/rotate-polygon-icon.tsx b/src/components/tiles/geometry/rotate-polygon-icon.tsx
index 744300c3d2..9193f2ba70 100644
--- a/src/components/tiles/geometry/rotate-polygon-icon.tsx
+++ b/src/components/tiles/geometry/rotate-polygon-icon.tsx
@@ -104,8 +104,8 @@ export class RotatePolygonIcon extends React.Component {
if (!board || !polygon) return;
const polygonBounds = polygon.bounds();
- const centerCoords = [(polygonBounds[0] + polygonBounds[2]) / 2,
- (polygonBounds[1] + polygonBounds[3]) / 2];
+ const centerCoords: [number, number] = [(polygonBounds[0] + polygonBounds[2]) / 2,
+ (polygonBounds[1] + polygonBounds[3]) / 2];
this.polygonCenter = new JXG.Coords(JXG.COORDS_BY_USER, centerCoords, board);
this.initialIconAnchor = this.state.iconAnchor
? copyCoords(this.state.iconAnchor)
diff --git a/src/components/tiles/geometry/use-axis-settings-dialog.tsx b/src/components/tiles/geometry/use-axis-settings-dialog.tsx
deleted file mode 100644
index 28943bb8f2..0000000000
--- a/src/components/tiles/geometry/use-axis-settings-dialog.tsx
+++ /dev/null
@@ -1,242 +0,0 @@
-import React, { useState } from "react";
-import GeometryToolIcon from "../../../clue/assets/icons/geometry-tool.svg";
-import { useCustomModal } from "../../../hooks/use-custom-modal";
-import { IAxesParams } from "../../../models/tiles/geometry/geometry-content";
-import { getAxisAnnotations, getBaseAxisLabels, guessUserDesiredBoundingBox
- } from "../../../models/tiles/geometry/jxg-board";
-import "./axis-settings-dialog.scss";
-import "./dialog.scss";
-
-const kBoundsMaxChars = 6;
-const kNameMaxChars = 20;
-const kLabelMaxChars = 40;
-
-// The complete dialog content
-interface IContentProps {
- xName: string;
- setXName: React.Dispatch>;
- yName: string;
- setYName: React.Dispatch>;
- xLabel: string;
- setXLabel: React.Dispatch>;
- yLabel: string;
- setYLabel: React.Dispatch>;
- xMin: string;
- setXMin: React.Dispatch>;
- yMin: string;
- setYMin: React.Dispatch>;
- xMax: string;
- setXMax: React.Dispatch>;
- yMax: string;
- setYMax: React.Dispatch>;
- errorMessage: string;
-}
-const Content: React.FC = ({
- xName, setXName, yName, setYName,
- xLabel, setXLabel, yLabel, setYLabel,
- xMin, setXMin, yMin, setYMin,
- xMax, setXMax, yMax, setYMax,
- errorMessage
- })=> {
- return (
- <>
-
-
-
- {errorMessage}
-
- >
- );
-};
-
-// Options for a single axis (included twice in the content)
-interface axisViewProps {
- title: string;
- axisName: string; // Can't use just 'name' because it's a built-in javascript property
- setName: React.Dispatch>;
- label: string;
- setLabel: React.Dispatch>;
- min: string;
- setMin: React.Dispatch>;
- max: string;
- setMax: React.Dispatch>;
-}
-const AxisView: React.FC = ({
- title,
- axisName, setName,
- label, setLabel,
- min, setMin,
- max, setMax
-}: axisViewProps) => {
- const labelId = `${title}-label-input-id`;
- return (
-
-
{title}
-
-
- setLabel(e.target.value)}
- dir="auto"
- />
-
-
-
- );
-};
-
-// A single axis option
-interface axisOptionProps {
- axis: string;
- optionLabel: string;
- defaultValue: string;
- setValue: React.Dispatch>;
- maxChars: number;
-}
-const AxisOption: React.FC = ({
- axis, optionLabel, defaultValue, setValue, maxChars
-}: axisOptionProps) => {
- const id = `${axis}-${optionLabel}-input-id`;
- return (
-
-
- setValue(e.target.value)}
- dir="auto"
- />
-
- );
-};
-
-interface IProps {
- board: JXG.Board;
- onAccept: (params: IAxesParams) => void;
- onClose: () => void;
-}
-export const useAxisSettingsDialog = ({ board, onAccept, onClose }: IProps) => {
- const [hName, vName] = getBaseAxisLabels(board);
- const [xName, setXName] = useState(hName);
- const [yName, setYName] = useState(vName);
-
- const [hLabel, vLabel] = getAxisAnnotations(board);
- const [xLabel, setXLabel] = useState(hLabel);
- const [yLabel, setYLabel] = useState(vLabel);
-
- const bBox = guessUserDesiredBoundingBox(board);
- const [xMin, setXMin] = useState(JXG.toFixed(Math.min(0, bBox[0]), 1));
- const [yMax, setYMax] = useState(JXG.toFixed(Math.max(0, bBox[1]), 1));
- const [xMax, setXMax] = useState(JXG.toFixed(Math.max(0, bBox[2]), 1));
- const [yMin, setYMin] = useState(JXG.toFixed(Math.min(0, bBox[3]), 1));
-
- const fXMin = parseFloat(xMin);
- const fXMax = parseFloat(xMax);
- const fYMin = parseFloat(yMin);
- const fYMax = parseFloat(yMax);
- const errorMessage =
- !isFinite(fXMin) || !isFinite(fXMax) || !isFinite(fYMin) || !isFinite(fYMax)
- ? "Please enter valid numbers for axis minimum and maximum values"
- : fXMin > 0 || fYMin > 0
- ? "Axis minimum values must be less than or equal to 0."
- : fXMax < 0 || fYMax < 0
- ? "Axis maximum values must be greater than or equal to 0."
- : fXMin >= fXMax || fYMin >= fYMax
- ? "Axis minimum values must be less than axis maximum values"
- : "";
-
- const handleClick = () => {
- if (errorMessage.length === 0) {
- onAccept({
- xName,
- yName,
- xAnnotation: xLabel,
- yAnnotation: yLabel,
- xMax: fXMax,
- yMax: fYMax,
- xMin: fXMin,
- yMin: fYMin
- });
- } else {
- onClose();
- }
- };
-
- const [showModal, hideModal] = useCustomModal({
- Icon: GeometryToolIcon,
- title: "Axis Settings",
- Content,
- contentProps: {
- xName, setXName, yName, setYName,
- xLabel, setXLabel, yLabel, setYLabel,
- xMin, setXMin, yMin, setYMin,
- xMax, setXMax, yMax, setYMax,
- errorMessage
- },
- buttons: [
- { label: "Cancel" },
- { label: "OK",
- isDefault: true,
- isDisabled: errorMessage.length > 0,
- onClick: handleClick
- }
- ],
- onClose
- }, [xName, yName, xLabel, yLabel, xMin, yMin, xMax, yMax, errorMessage]);
-
- return [showModal, hideModal];
-};
diff --git a/src/components/tiles/geometry/use-label-point-dialog.tsx b/src/components/tiles/geometry/use-label-point-dialog.tsx
new file mode 100644
index 0000000000..f44ea2cfbd
--- /dev/null
+++ b/src/components/tiles/geometry/use-label-point-dialog.tsx
@@ -0,0 +1,105 @@
+import React, { useState } from "react";
+import { ELabelOption } from "../../../models/tiles/geometry/jxg-changes";
+import { useCustomModal } from "../../../hooks/use-custom-modal";
+import { canSupportVertexAngle, getVertexAngle } from "../../../models/tiles/geometry/jxg-vertex-angle";
+import { LabelRadioButton } from "./label-radio-button";
+
+import LabelSvg from "../../../clue/assets/icons/shapes-label-value-icon.svg";
+
+import "./label-dialog.scss";
+
+interface IContentProps {
+ labelOption: string;
+ setLabelOption: React.Dispatch>;
+ pointName?: string;
+ onNameChange: React.Dispatch>;
+ supportsAngle: boolean;
+ hasAngle: boolean;
+ setHasAngle: React.Dispatch>;
+}
+const Content = function (
+ { labelOption, setLabelOption, pointName, onNameChange, supportsAngle, hasAngle, setHasAngle }: IContentProps) {
+ return (
+
+ );
+};
+
+interface IProps {
+ board: JXG.Board;
+ point: JXG.Point;
+ onAccept: (point: JXG.Point, labelOption: ELabelOption, name: string, hasAngle: boolean) => void;
+ onClose: () => void;
+}
+
+export const useLabelPointDialog = ({ board, point, onAccept, onClose }: IProps) => {
+ const [initialLabelOption] = useState(point.getAttribute("clientLabelOption") || ELabelOption.kNone);
+ const [initialPointName] = useState(point.getAttribute("clientName") || "");
+ const [labelOption, setLabelOption] = useState(initialLabelOption);
+ const [pointName, setPointName] = useState(initialPointName);
+ const supportsAngle = canSupportVertexAngle(point);
+ const [initialHasAngle] = useState(!!getVertexAngle(point));
+ const [hasAngle, setHasAngle] = useState(initialHasAngle);
+
+ const handleSubmit = () => {
+ if (initialLabelOption !== labelOption || initialPointName !== pointName || initialHasAngle !== hasAngle) {
+ onAccept(point, labelOption, pointName, hasAngle);
+ } else {
+ onClose();
+ }
+ };
+
+ const [showModal, hideModal] = useCustomModal({
+ Icon: LabelSvg,
+ title: "Point Label/Value",
+ Content,
+ contentProps:
+ { labelOption, setLabelOption, pointName, onNameChange: setPointName, supportsAngle, hasAngle, setHasAngle },
+ buttons: [
+ { label: "Cancel" },
+ { label: "OK",
+ isDefault: true,
+ isDisabled: false,
+ onClick: handleSubmit
+ }
+ ],
+ onClose
+ }, [labelOption, pointName, hasAngle]);
+
+ return [showModal, hideModal];
+};
diff --git a/src/components/tiles/geometry/use-label-polygon-dialog.tsx b/src/components/tiles/geometry/use-label-polygon-dialog.tsx
new file mode 100644
index 0000000000..543c642a76
--- /dev/null
+++ b/src/components/tiles/geometry/use-label-polygon-dialog.tsx
@@ -0,0 +1,90 @@
+import React, { useState } from "react";
+import { ELabelOption } from "../../../models/tiles/geometry/jxg-changes";
+import { useCustomModal } from "../../../hooks/use-custom-modal";
+import { LabelRadioButton } from "./label-radio-button";
+import { pointName } from "../../../models/tiles/geometry/jxg-point";
+
+import LabelSvg from "../../../clue/assets/icons/shapes-label-value-icon.svg";
+
+import "./label-dialog.scss";
+
+interface IContentProps {
+ labelOption: string;
+ setLabelOption: React.Dispatch>;
+ name?: string;
+ setName: React.Dispatch>;
+}
+const Content: React.FC = (
+ { labelOption, setLabelOption, name, setName }) => {
+ return (
+
+ );
+};
+
+function constructName(polygon: JXG.Polygon) {
+ return polygon.vertices.slice(0, -1)
+ .reduce((name: string, point) => { return name + pointName(point); }, "");
+}
+
+interface IProps {
+ board: JXG.Board;
+ polygon: JXG.Polygon;
+ onAccept: (polygon: JXG.Polygon, labelOption: ELabelOption, name: string) => void;
+ onClose: () => void;
+}
+export const useLabelPolygonDialog = ({ board, polygon, onAccept, onClose }: IProps) => {
+ const [initialLabelOption] = useState(polygon?.getAttribute("clientLabelOption") || "none");
+ const [labelOption, setLabelOption] = useState(initialLabelOption);
+ const [initialName] = useState(polygon?.getAttribute("clientName"));
+ const [name, setName] = useState(initialName || constructName(polygon));
+
+ const handleSubmit = () => {
+ if (polygon && (initialLabelOption !== labelOption || initialName !== name)) {
+ onAccept(polygon, labelOption, name);
+ } else {
+ onClose();
+ }
+ };
+
+ const [showModal, hideModal] = useCustomModal({
+ Icon: LabelSvg,
+ title: "Polygon Label/Value",
+ Content,
+ contentProps: { labelOption, setLabelOption, name, setName },
+ buttons: [
+ { label: "Cancel" },
+ { label: "OK",
+ isDefault: true,
+ isDisabled: false,
+ onClick: handleSubmit
+ }
+ ],
+ onClose
+ }, [labelOption, name]);
+
+ return [showModal, hideModal];
+};
diff --git a/src/components/tiles/geometry/use-label-segment-dialog.tsx b/src/components/tiles/geometry/use-label-segment-dialog.tsx
index ae92f1c6ca..3cf4ebdd6e 100644
--- a/src/components/tiles/geometry/use-label-segment-dialog.tsx
+++ b/src/components/tiles/geometry/use-label-segment-dialog.tsx
@@ -1,61 +1,44 @@
import React, { useState, useMemo } from "react";
-import LineLabelSvg from "../../../clue/assets/icons/geometry/line-label.svg";
-import { ESegmentLabelOption } from "../../../models/tiles/geometry/jxg-changes";
+import { ELabelOption } from "../../../models/tiles/geometry/jxg-changes";
import { getPolygonEdge } from "../../../models/tiles/geometry/jxg-polygon";
import { useCustomModal } from "../../../hooks/use-custom-modal";
-import "./label-segment-dialog.scss";
+import { LabelRadioButton } from "./label-radio-button";
+import { pointName } from "../../../models/tiles/geometry/jxg-point";
-interface LabelRadioButtonProps {
- display: string;
- label: string;
- checkedLabel: string;
- setLabelOption: React.Dispatch>;
-}
-const LabelRadioButton: React.FC = ({display, label, checkedLabel, setLabelOption}) => {
- return (
-
- {
- if (e.target.checked) {
- setLabelOption(e.target.value);
- }
- }}
- />
-
-
- );
-};
+import LabelSvg from "../../../clue/assets/icons/shapes-label-value-icon.svg";
+
+import "./label-dialog.scss";
interface IContentProps {
labelOption: string;
setLabelOption: React.Dispatch>;
+ name?: string;
+ setName: React.Dispatch>;
}
-const Content: React.FC = ({ labelOption, setLabelOption }) => {
+const Content: React.FC = (
+ { labelOption, setLabelOption, name, setName }) => {
return (