Cleanup page state commands (#1800)

This PR cleans up some APIs around the editor's current page state:

- `setEditingShapeId` -> `setEditingShape`
- `setHoveredShapeId` -> `setHoveredShape`
- `setCroppingShapeId` -> `setCroppingShape`
- `setFocusedGroupId` -> `setFocusedGroup`
- `setErasingShapeIds` -> `setErasingShapes`
- `setHintingShapeIds` -> `setHintingShapes`

It also adds some additional computed getters, e.g.
`Editor.croppingShape`.

It also adds some errors around `setCroppingShape`.

### Change Type

- [x] `major` — Breaking change

### Test Plan

- [x] Unit Tests
This commit is contained in:
Steve Ruiz 2023-08-06 13:05:35 +01:00 committed by GitHub
parent eabb0d52f8
commit 13ef8be58d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 252 additions and 125 deletions

View file

@ -627,9 +627,11 @@ export class Editor extends EventEmitter<TLEventMap> {
duplicateShapes(shapes: TLShape[], offset?: VecLike): this;
// (undocumented)
duplicateShapes(ids: TLShapeId[], offset?: VecLike): this;
get editingShape(): TLShape | undefined;
get editingShapeId(): null | TLShapeId;
readonly environment: EnvironmentManager;
get erasingShapeIds(): TLShapeId[];
get erasingShapes(): NonNullable<TLShape | undefined>[];
// @internal (undocumented)
externalAssetContentHandlers: {
[K in TLExternalAssetContent_2['type']]: {
@ -655,6 +657,7 @@ export class Editor extends EventEmitter<TLEventMap> {
flipShapes(shapes: TLShape[], operation: 'horizontal' | 'vertical'): this;
// (undocumented)
flipShapes(ids: TLShapeId[], operation: 'horizontal' | 'vertical'): this;
get focusedGroup(): TLShape | undefined;
get focusedGroupId(): TLPageId | TLShapeId;
getAncestorPageId(shape?: TLShape): TLPageId | undefined;
// (undocumented)
@ -773,8 +776,9 @@ export class Editor extends EventEmitter<TLEventMap> {
// (undocumented)
hasAncestor(shapeId: TLShapeId | undefined, ancestorId: TLShapeId): boolean;
get hintingShapeIds(): TLShapeId[];
get hintingShapes(): NonNullable<TLShape | undefined>[];
readonly history: HistoryManager<this>;
get hoveredShape(): TLUnknownShape | undefined;
get hoveredShape(): TLShape | undefined;
get hoveredShapeId(): null | TLShapeId;
inputs: {
originPagePoint: Vec2d;
@ -843,7 +847,7 @@ export class Editor extends EventEmitter<TLEventMap> {
};
pan(offset: VecLike, animation?: TLAnimationOptions): this;
panZoomIntoView(ids: TLShapeId[], animation?: TLAnimationOptions): this;
popFocusLayer(): this;
popFocusedGroupId(): this;
putContentOntoCurrentPage(content: TLContent, options?: {
point?: VecLike;
select?: boolean;
@ -907,17 +911,29 @@ export class Editor extends EventEmitter<TLEventMap> {
// (undocumented)
sendToBack(ids: TLShapeId[]): this;
setCamera(point: VecLike, animation?: TLAnimationOptions): this;
setCroppingShapeId(id: null | TLShapeId): this;
setCroppingShape(shape: null | TLShape): this;
// (undocumented)
setCroppingShape(id: null | TLShapeId): this;
setCurrentPage(page: TLPage, historyOptions?: TLCommandHistoryOptions): this;
// (undocumented)
setCurrentPage(pageId: TLPageId, historyOptions?: TLCommandHistoryOptions): this;
setCurrentTool(id: string, info?: {}): this;
setCursor: (cursor: Partial<TLCursor>) => this;
setEditingShapeId(id: null | TLShapeId): this;
setErasingShapeIds(ids: TLShapeId[]): this;
setFocusedGroupId(next: null | TLShapeId): this;
setHintingIds(ids: TLShapeId[]): this;
setHoveredShapeId(id: null | TLShapeId): this;
setEditingShape(shape: null | TLShape): this;
// (undocumented)
setEditingShape(id: null | TLShapeId): this;
setErasingShapes(shapes: TLShape[]): this;
// (undocumented)
setErasingShapes(ids: TLShapeId[]): this;
setFocusedGroup(shape: null | TLGroupShape): this;
// (undocumented)
setFocusedGroup(id: null | TLShapeId): this;
setHintingShapes(shapes: TLShape[]): this;
// (undocumented)
setHintingShapes(ids: TLShapeId[]): this;
setHoveredShape(shape: null | TLShape): this;
// (undocumented)
setHoveredShape(id: null | TLShapeId): this;
setOpacity(opacity: number, historyOptions?: TLCommandHistoryOptions): this;
setSelectedShapeIds(ids: TLShapeId[], historyOptions?: TLCommandHistoryOptions): this;
setStyle<T>(style: StyleProp<T>, value: T, historyOptions?: TLCommandHistoryOptions): this;

View file

@ -1677,26 +1677,54 @@ export class Editor extends EventEmitter<TLEventMap> {
return boxFromRotatedVertices
}
// Focus Layer Id
// Focus Group
/**
* The shape id of the current focus layer. Null when the focus layer id is the current page.
* The current focused group id.
*
* @public
*/
get focusedGroupId(): TLShapeId | TLPageId {
@computed get focusedGroupId(): TLShapeId | TLPageId {
return this.currentPageState.focusedGroupId ?? this.currentPageId
}
/**
* Set the current focus layer id.
*
* @param next - The shape id (or page id) to set as the focus layer id.
* The current focused group.
*
* @public
*/
setFocusedGroupId(next: TLShapeId | null): this {
this._setFocusedGroupId(next)
@computed get focusedGroup(): TLShape | undefined {
const { focusedGroupId } = this
return focusedGroupId ? this.getShape(focusedGroupId) : undefined
}
/**
* Set the current focused group shape.
*
* @param shape - The group shape id (or group shape's id) to set as the focused group shape.
*
* @public
*/
setFocusedGroup(shape: TLGroupShape | null): this
setFocusedGroup(id: TLShapeId | null): this
setFocusedGroup(arg: TLShapeId | TLGroupShape | null): this {
const id = typeof arg === 'string' ? arg : arg?.id ?? null
if (id !== null) {
const shape = typeof arg === 'string' ? this.getShape(arg) : arg
if (!shape) {
throw Error(`Editor.setFocusedGroup: Shape with id ${id} does not exist`)
}
if (!this.isShapeOfType<TLGroupShape>(shape, 'group')) {
throw Error(
`Editor.setFocusedGroup: Cannot set focused group to shape of type ${shape.type}`
)
}
}
if (id === this.focusedGroupId) return this
this._setFocusedGroupId(id)
return this
}
@ -1704,11 +1732,8 @@ export class Editor extends EventEmitter<TLEventMap> {
private _setFocusedGroupId = this.history.createCommand(
'setFocusedGroupId',
(next: TLShapeId | null) => {
// When we first click an empty canvas we don't want this to show up in the undo stack
if (!next && !this.canUndo) {
return
}
const prev = this.currentPageState.focusedGroupId
if (prev === next) return
return {
data: {
prev,
@ -1732,25 +1757,24 @@ export class Editor extends EventEmitter<TLEventMap> {
)
/**
* Exit the current focus layer, moving up to the next group if there is one.
* Exit the current focused group, moving up to the next parent group if there is one.
*
* @public
*/
popFocusLayer(): this {
const current = this.currentPageState.focusedGroupId
const focusedShape = current && this.getShape(current)
popFocusedGroupId(): this {
const { focusedGroup } = this
if (focusedShape) {
if (focusedGroup) {
// If we have a focused layer, look for an ancestor of the focused shape that is a group
const match = this.findShapeAncestor(focusedShape, (shape) =>
const match = this.findShapeAncestor(focusedGroup, (shape) =>
this.isShapeOfType<TLGroupShape>(shape, 'group')
)
// If we have an ancestor that can become a focused layer, set it as the focused layer
this.setFocusedGroupId(match?.id ?? null)
this.select(focusedShape.id)
this.setFocusedGroup(match?.id ?? null)
this.select(focusedGroup.id)
} else {
// If there's no focused shape, then clear the focus layer and clear selection
this.setFocusedGroupId(null)
// If there's no parent focused group, then clear the focus layer and clear selection
this.setFocusedGroup(null)
this.selectNone()
}
@ -1762,18 +1786,37 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
get editingShapeId() {
@computed get editingShapeId() {
return this.currentPageState.editingShapeId
}
/**
* Set the current editing shape id.
*
* @param id - The shape id to set as editing.
* The current editing shape.
*
* @public
*/
setEditingShapeId(id: TLShapeId | null): this {
@computed get editingShape(): TLShape | undefined {
const { editingShapeId } = this
return editingShapeId ? this.getShape(editingShapeId) : undefined
}
/**
* Set the current editing shape.
*
* @example
* ```ts
* editor.setEditingShape(myShape)
* editor.setEditingShape(myShape.id)
* ```
*
* @param shapes - The shape (or shape id) to set as editing.
*
* @public
*/
setEditingShape(shape: TLShape | null): this
setEditingShape(id: TLShapeId | null): this
setEditingShape(arg: TLShapeId | TLShape | null): this {
const id = typeof arg === 'string' ? arg : arg?.id ?? null
if (!id) {
this._setInstancePageState({ editingShapeId: null })
} else {
@ -1788,7 +1831,7 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
// Hovered Id
// Hovered
/**
* The current hovered shape id.
@ -1801,28 +1844,38 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/**
* Set the editor's current hovered shape id.
*
* @param id - The shape id to set as hovered.
* The current hovered shape.
*
* @public
*/
setHoveredShapeId(id: TLShapeId | null): this {
if (id === this.currentPageState.hoveredShapeId) return this
@computed get hoveredShape(): TLShape | undefined {
const { hoveredShapeId } = this
return hoveredShapeId ? this.getShape(hoveredShapeId) : undefined
}
/**
* Set the editor's current hovered shape.
*
* @example
* ```ts
* editor.setHoveredShape(myShape)
* editor.setHoveredShape(myShape.id)
* ```
*
* @param shapes - The shape (or shape id) to set as hovered.
*
* @public
*/
setHoveredShape(shape: TLShape | null): this
setHoveredShape(id: TLShapeId | null): this
setHoveredShape(arg: TLShapeId | TLShape | null): this {
const id = typeof arg === 'string' ? arg : arg?.id ?? null
if (id === this.hoveredShapeId) return this
this.updateCurrentPageState({ hoveredShapeId: id }, { ephemeral: true })
return this
}
/**
* The editor's current hovered shape.
*
* @public
*/
@computed get hoveredShape() {
return this.hoveredShapeId ? this.getShape(this.hoveredShapeId) : undefined
}
// Hinting ids
// Hinting
/**
* The editor's current hinting shape ids.
@ -1834,18 +1887,42 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/**
* Set the editor's current hinting shape ids.
*
* @param ids - The shape ids to set as hinting.
* The editor's current hinting shapes.
*
* @public
*/
setHintingIds(ids: TLShapeId[]): this {
@computed get hintingShapes() {
const { hintingShapeIds } = this
return compact(hintingShapeIds.map((id) => this.getShape(id)))
}
/**
* Set the editor's current hinting shapes.
*
* @example
* ```ts
* editor.setHintingShapes([myShape])
* editor.setHintingShapes([myShape.id])
* ```
*
* @param shapes - The shapes (or shape ids) to set as hinting.
*
* @public
*/
setHintingShapes(shapes: TLShape[]): this
setHintingShapes(ids: TLShapeId[]): this
setHintingShapes(arg: TLShapeId[] | TLShape[]): this {
const ids =
typeof arg[0] === 'string'
? (arg as TLShapeId[])
: (arg as TLShape[]).map((shape) => shape.id)
// always ephemeral
this.updateCurrentPageState({ hintingShapeIds: dedupe(ids) }, { ephemeral: true })
return this
}
// Erasing
/**
* The editor's current erasing ids.
*
@ -1856,13 +1933,35 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/**
* Set the editor's current erasing shape ids.
*
* @param ids - The shape ids to set as erasing.
* The editor's current hinting shapes.
*
* @public
*/
setErasingShapeIds(ids: TLShapeId[]): this {
@computed get erasingShapes() {
const { erasingShapeIds } = this
return compact(erasingShapeIds.map((id) => this.getShape(id)))
}
/**
* Set the editor's current erasing shapes.
*
* @example
* ```ts
* editor.setErasingShapes([myShape])
* editor.setErasingShapes([myShape.id])
* ```
*
* @param shapes - The shapes (or shape ids) to set as hinting.
*
* @public
*/
setErasingShapes(shapes: TLShape[]): this
setErasingShapes(ids: TLShapeId[]): this
setErasingShapes(arg: TLShapeId[] | TLShape[]): this {
const ids =
typeof arg[0] === 'string'
? (arg as TLShapeId[])
: (arg as TLShape[]).map((shape) => shape.id)
ids.sort() // sort the incoming ids
const { erasingShapeIds } = this
if (ids.length === erasingShapeIds.length) {
@ -1883,6 +1982,8 @@ export class Editor extends EventEmitter<TLEventMap> {
return this
}
// Cropping
/**
* The current cropping shape's id.
*
@ -1893,13 +1994,23 @@ export class Editor extends EventEmitter<TLEventMap> {
}
/**
* Set the current cropping shape id.
* Set the current cropping shape.
*
* @param id - The shape id to set as cropping.
* @example
* ```ts
* editor.setCroppingShape(myShape)
* editor.setCroppingShape(myShape.id)
* ```
*
*
* @param shape - The shape (or shape id) to set as cropping.
*
* @public
*/
setCroppingShapeId(id: TLShapeId | null): this {
setCroppingShape(shape: TLShape | null): this
setCroppingShape(id: TLShapeId | null): this
setCroppingShape(arg: TLShapeId | TLShape | null): this {
const id = typeof arg === 'string' ? arg : arg?.id ?? null
if (id !== this.croppingShapeId) {
if (!id) {
this.updateCurrentPageState({ croppingShapeId: null })
@ -5252,7 +5363,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// Put the shape content onto the new page; parents and indices will
// be taken care of by the putContent method; make sure to pop any focus
// layers so that the content will be put onto the page.
this.setFocusedGroupId(null)
this.setFocusedGroup(null)
this.selectNone()
this.putContentOntoCurrentPage(content, {
select: true,

View file

@ -101,13 +101,13 @@ export class GroupShapeUtil extends ShapeUtil<TLGroupShape> {
const children = this.editor.getSortedChildIdsForParent(group.id)
if (children.length === 0) {
if (this.editor.currentPageState.focusedGroupId === group.id) {
this.editor.popFocusLayer()
this.editor.popFocusedGroupId()
}
this.editor.deleteShapes([group.id])
return
} else if (children.length === 1) {
if (this.editor.currentPageState.focusedGroupId === group.id) {
this.editor.popFocusLayer()
this.editor.popFocusedGroupId()
}
this.editor.reparentShapes(children, group.parentId)
this.editor.deleteShapes([group.id])