/*
* This file is part of TREB.
*
* TREB is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with TREB. If not, see .
*
* Copyright 2022-2026 trebco, llc.
* info@treb.app
*
*/
import type { ICellAddress, AnnotationLayout, IRectangle, CellStyle } from 'treb-base-types';
import { Rectangle } from 'treb-base-types';
/**
* new annotation class. annotations are arbitrary content
* inserted into the sheet, using a floating div element. the
* class is serialized with the sheet, so the caller can recreate
* the content if desired.
*
* because there's an element of layout involved, callers should
* interact with annotations through the grid class rather than the
* sheet.
*
* we are redesigning layout so that instead of a rectangle, in
* coordinate space, annotations use extents and offsets in cell space.
* so layout should now have a TL cell and a BR cell plus offsets for
* each. Offset is implemented as a % of the given cell, so offsets are
* inverted in the TL/BR cells.
*
* UPDATE: actually while the inverted BR offset makes intuitive sense,
* it doesn't make technical sense -- easier to always calcluate offsets
* in the same direction. so offsets are always positive.
*
* we'll leave the old extent in there (for now, at least) to prevent
* any unintended consequences.
*
* UPDATE: adding a view interface for view-specific data. this is prep
* for supporting annotations in split views; we have to change how we
* manage nodes and callbacks.
*
*/
/**
* key is used to match annotations for freeze panes. we're repurposing
* it as a general (ephemeral, per-session) id
*/
let key_generator = 100;
/**
* moving view-specific data into a separate interface to support split.
* nothing in view is serialized.
*/
export interface ViewData {
/** flag indicating we have inflated this. not serialized */
inflated?: boolean;
/** if function exists, will be called when the annotation is resized */
resize_callback?: () => void;
/** if function exists, will be called when the annotation needs to update */
update_callback?: () => void;
/** layout node */
node?: HTMLDivElement;
/** content node */
content_node?: HTMLDivElement;
/** view-specific dirty flag */
dirty?: boolean;
}
export interface ImageSize {
width: number;
height: number;
}
export interface ImageAnnotationData {
src: string;
/**
* @privateRemarks
* why is this a string?
*/
scale?: string;
original_size?: ImageSize;
}
export type AnnotationType = 'treb-chart'|'image'|'textbox'|'external';
/**
* splitting persisted data from the annotation class. that class might
* disappear in the future in favor of just a type. this interface should
* fully match the old Partial we used before. note that we
* used to define values for all members, but they may now be undefined
* because the Annotation class as a Partial instance of this data.
*
* conceptually annotation was originally intended to support types other
* than our own charts and images, but no one ever used it. so we could
* lock down the `type` field if we wanted to. or perhaps have an `external`
* type with opaque data. TODO.
*
*/
export interface AnnotationDataBase {
/** the new layout, persisted and takes preference over the old one */
layout?: AnnotationLayout;
/**
* adding cell style as a convenient store for font stack; atm we are
* ignoring everything but the font_face attribute
*/
style?: CellStyle;
/**
* the old layout used rectangles, and we need to keep support for
* that. this is not the layout rectangle. this rectangle is just
* for serialization/deserialization. the actual rectangle is maintained
* in the Annotation class.
*/
rect?: Partial;
/** annotation can be resized. this is advisory, for UI */
resizable: boolean;
/** annotation can be moved. this is advisory, for UI */
movable: boolean;
/** annotation can be removed/deleted. this is advisory, for UI */
removable: boolean;
/** annotation can be selected. this is advisory, for UI */
selectable: boolean;
/** move when resizing/inserting rows/columns */
move_with_cells: boolean;
/** resize when resizing/inserting rows/columns */
resize_with_cells: boolean;
/**
* optional formula. the formula will be updated on structure events
* (insert/delete row/column).
*/
formula: string;
/**
* extent, useful for exporting. we could probably serialize this,
* just be sure to clear it when layout changes so it will be
* recalculated.
*
* the idea is to know the bottom/right row/column of the annotation,
* so when we preserve/restore the sheet we don't trim those rows/columns.
* they don't need any data, but it just looks bad. we can do this
* dynamically but since it won't change all that often, we might
* as well precalculate.
*/
extent: ICellAddress;
}
const default_annotation_data: Partial = {
move_with_cells: true,
resize_with_cells: true,
movable: true,
resizable: true,
removable: true,
selectable: true,
};
export interface AnnotationImageData extends AnnotationDataBase {
type: 'image';
data: ImageAnnotationData;
}
export interface AnnotationChartData extends AnnotationDataBase {
type: 'treb-chart';
}
export interface AnnotationTextBoxData extends AnnotationDataBase {
type: 'textbox';
/**
* @internalRemarks
* what's with this weird structure? did we inherit it? can we clean it up?
*/
data: {
style?: CellStyle;
paragraphs: {
style?: CellStyle,
content: {
text: string,
style?: CellStyle ,
}[],
}[];
};
}
export interface AnnotationExternalData extends AnnotationDataBase {
type: 'external';
data: Record;
}
export type AnnotationData = AnnotationChartData | AnnotationImageData | AnnotationExternalData | AnnotationTextBoxData;
/**
* why is this a class? it doesn't do anything.
* FIXME: -> interface
*/
export class Annotation {
public data: Partial = {
...default_annotation_data
};
/**
* the key field is used to identify and coordinate annotations when we
* have freeze panes. when an annotation crosses a freeze pane, we need
* two copies of the rendered node so that we can scroll. we use the key
* to match the frozen/unfrozen instances.
*/
public get key(): number { return this.key_; }
/** coordinates, in sheet space */
public rect?: Rectangle;
// public get rect(): Rectangle|undefined { return this.rect_; }
/** display coordinates, possibly scaled. not persisted. */
public scaled_rect?: Rectangle;
/** also opaque data, but not serialized. */
public temp: unknown = {};
public view: ViewData[] = [];
/**
* advisory, meaning we probably need an update if there's an opportunity.
* only advisory and not persisted.
*/
public dirty?: boolean;
private key_ = (key_generator++);
/**
* constructor takes persisted data
*/
constructor(opts: Partial = {}) {
this.data = {
...default_annotation_data,
...JSON.parse(JSON.stringify(opts))
}; // why clone?
if (opts.rect) {
this.rect = Rectangle.Create(opts.rect);
}
}
/**
* serialization just returns persisted data, plus we update the
* rectangle.
*
* anyone serializing annotations should just be fetching the data
* object, but we're leaving this in place because we can't trace
* it back using tooling. that's a real drawback of toJSON, we
* should stop using it.
*
* although as long as we need to support `rect` here, it's not bad
* that we do it this way. perhaps change the function name, and
* call it directly?
*
*/
public toJSON(): Partial {
return {
...this.data, rect: this.rect };
}
}