diff options
| author | mehekj <mehek.jethani@gmail.com> | 2023-04-10 16:08:58 -0400 |
|---|---|---|
| committer | mehekj <mehek.jethani@gmail.com> | 2023-04-10 16:08:58 -0400 |
| commit | bff54ce7283f7a50a8c4184ea0543b7a2d338e25 (patch) | |
| tree | 26af049cb8549aa75cc376524e123b78adf5a94f /src/client/views/nodes | |
| parent | 7934c38ed641f4a10cd008fe415a50aef1240e10 (diff) | |
| parent | 3cb7f85b23eb0ae3a432bbe15b8a2cda37290ce2 (diff) | |
Merge branch 'master' into schema-mehek
Diffstat (limited to 'src/client/views/nodes')
30 files changed, 974 insertions, 718 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 384975b45..a6acf882c 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -7,7 +7,7 @@ import { Doc, DocListCast } from '../../../fields/Doc'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, DateCast, NumCast } from '../../../fields/Types'; import { AudioField, nullAudio } from '../../../fields/URLField'; -import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { Networking } from '../../Network'; import { DragManager } from '../../util/DragManager'; @@ -19,7 +19,6 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import './AudioBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; -import { SelectionManager } from '../../util/SelectionManager'; import { PinProps, PresBox } from './trails'; /** @@ -655,7 +654,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return ( <CollectionStackedTimeline ref={action((r: any) => (this._stackedTimeline = r))} - {...OmitKeys(this.props, ['CollectionFreeFormDocumentView']).omit} + {...this.props} + CollectionFreeFormDocumentView={undefined} fieldKey={this.annotationKey} dictationKey={this.fieldKey + '-dictation'} mediaPath={this.path} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index ecffe6c4f..ace388c57 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -3,7 +3,7 @@ import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; +import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; @@ -122,7 +122,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl ref={r => { //whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true }); }} - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit} + {...this.props} + NativeWidth={returnZero} + NativeHeight={returnZero} isContentActive={returnFalse} isDocumentActive={returnFalse} styleProvider={this.docStyleProvider} diff --git a/src/client/views/nodes/DataViz.tsx b/src/client/views/nodes/DataViz.tsx deleted file mode 100644 index df4c8f937..000000000 --- a/src/client/views/nodes/DataViz.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { ViewBoxBaseComponent } from '../DocComponent'; -import './DataViz.scss'; -import { FieldView, FieldViewProps } from './FieldView'; - -@observer -export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(DataVizBox, fieldKey); - } - - render() { - return ( - <div> - <div>Hi</div> - </div> - ); - } -} diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index e69de29bb..cd500e9ae 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -0,0 +1,4 @@ +.dataviz { + overflow: auto; + height: 100%; +} diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 592723ee9..eb25d3264 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,90 +1,151 @@ -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { StrCast } from "../../../../fields/Types"; -import { ViewBoxBaseComponent } from "../../DocComponent"; -import { FieldViewProps, FieldView } from "../FieldView"; -import "./DataVizBox.scss"; -import { HistogramBox } from "./HistogramBox"; -import { TableBox } from "./TableBox"; - -enum DataVizView { - TABLE = "table", - HISTOGRAM= "histogram" -} +import { action, computed, observable, ObservableMap, ObservableSet } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, StrListCast } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; +import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { CsvField } from '../../../../fields/URLField'; +import { Docs } from '../../../documents/Documents'; +import { ViewBoxAnnotatableComponent } from '../../DocComponent'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { PinProps } from '../trails'; +import { LineChart } from './components/LineChart'; +import { TableBox } from './components/TableBox'; +import './DataVizBox.scss'; +export enum DataVizView { + TABLE = 'table', + LINECHART = 'lineChart', +} @observer -export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() { - @observable private pairs: {x: number, y:number}[] = [{x: 1, y:2}]; - - // TODO: nda - make this use enum values instead - // @observable private currView: DataVizView = DataVizView.TABLE; - @computed get currView() { - if (this.rootDoc._dataVizView) { - return StrCast(this.rootDoc._dataVizView); - } else { - return "table"; - } +export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(DataVizBox, fieldKey); } - - constructor(props: any) { - super(props); - if (!this.rootDoc._dataVizView) { - // TODO: nda - this might not always want to default to "table" - this.rootDoc._dataVizView = "table"; - } + // says we have an object and any string + // 2 ways of doing it + // @observable private pairs: { [key: string]: number | string | undefined }[] = []; + // @observable private pairs: { [key: string]: FieldResult }[] = []; + static pairSet = new ObservableMap<string, { [key: string]: string }[]>(); + @computed.struct get pairs() { + return DataVizBox.pairSet.get(StrCast(this.rootDoc.fileUpload)); } + private _chartRenderer: LineChart | undefined; + // // another way would be store a schema that defines the type of data we are expecting from an imported doc + + // method1() { + // this.pairs[0].x = 3; + // } + + // method() { + // // this.pairs[0].x = 3; + // // go through the pairs + // const x = this.pairs[0].x; + // if (typeof x == 'number') { + // let x1 = Number(x); + // // let x1 = NumCast(x); + // } + // } + + // could use field result + // [key: string]: FieldResult; + // instead of numeric x,y in there, + + // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); } + @computed get dataVizView(): DataVizView { + return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView; + } @action - private createPairs() { - const xVals: number[] = [0, 1, 2, 3, 4, 5]; - // const yVals: number[] = [10, 20, 30, 40, 50, 60]; - const yVals: number[] = [1, 2, 3, 4, 5, 6]; - let pairs: { - x: number, - y:number - }[] = []; - if (xVals.length != yVals.length) return pairs; - for (let i = 0; i < xVals.length; i++) { - pairs.push({x: xVals[i], y: yVals[i]}); + restoreView = (data: Doc) => { + const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView); + const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._dataVizAxes = new List<string>(StrListCast(data.presDataVizAxes))); + const func = () => this._chartRenderer?.restoreView(data); + if (changedView || changedAxes) { + setTimeout(func, 100); + return true; } - this.pairs = pairs; - return pairs; + return func() ?? false; + }; + + getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { + const anchor = + this._chartRenderer?.getAnchor(pinProps) ?? + Docs.Create.TextanchorDocument({ + unrendered: true, + // when we clear selection -> we should have it so chartBox getAnchor returns undefined + // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) + /*put in some options*/ + }); + + anchor.presDataVizView = this.dataVizView; + anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined; + + this.addDocument(anchor); + return anchor; + }; + + @computed.struct get axes() { + return StrListCast(this.layoutDoc.dataVizAxes); } + selectAxes = (axes: string[]) => (this.layoutDoc.dataVizAxes = new List<string>(axes)); @computed get selectView() { - switch(this.currView) { - case "table": - return (<TableBox pairs={this.pairs} />) - case "histogram": - return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>) + const width = this.props.PanelWidth() * 0.9; + const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; + const margin = { top: 10, right: 25, bottom: 50, left:25}; + if (!this.pairs) return 'no data'; + // prettier-ignore + switch (this.dataVizView) { + case DataVizView.TABLE: return <TableBox pairs={this.pairs} axes={this.axes} docView={this.props.DocumentView} selectAxes={this.selectAxes}/>; + case DataVizView.LINECHART: return <LineChart ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />; } } - - @computed get pairVals() { - return this.createPairs(); + @computed get dataUrl() { + return Cast(this.dataDoc[this.fieldKey], CsvField); } componentDidMount() { - this.createPairs(); + this.props.setContentView?.(this); + this.fetchData(); + } + + fetchData() { + if (DataVizBox.pairSet.has(StrCast(this.rootDoc.fileUpload))) return; + DataVizBox.pairSet.set(StrCast(this.rootDoc.fileUpload), []); + fetch('/csvData?uri=' + this.dataUrl?.url.href) // + .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(StrCast(this.rootDoc.fileUpload), res)))); } // handle changing the view using a button @action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) { e.preventDefault(); e.stopPropagation(); - this.rootDoc._dataVizView = this.currView == "table" ? "histogram" : "table"; + this.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE; } render() { - return ( - <div className="dataViz"> - <button onClick={(e) => this.changeViewHandler(e)}>Change View</button> + return !this.pairs?.length ? ( + <div>Loading...</div> + ) : ( + <div + className="dataViz" + onWheel={e => e.stopPropagation()} + ref={r => + r?.addEventListener( + 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) + (e: WheelEvent) => { + if (!r.scrollTop && e.deltaY <= 0) e.preventDefault(); + e.stopPropagation(); + }, + { passive: false } + ) + }> + <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</button> {this.selectView} </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DataVizBox/DrawHelper.ts b/src/client/views/nodes/DataVizBox/DrawHelper.ts deleted file mode 100644 index 595cecebf..000000000 --- a/src/client/views/nodes/DataVizBox/DrawHelper.ts +++ /dev/null @@ -1,247 +0,0 @@ -export class PIXIPoint { - public get x() { return this.coords[0]; } - public get y() { return this.coords[1]; } - public set x(value: number) { this.coords[0] = value; } - public set y(value: number) { this.coords[1] = value; } - public coords: number[] = [0, 0]; - constructor(x: number, y: number) { - this.coords[0] = x; - this.coords[1] = y; - } -} - -export class PIXIRectangle { - public x: number; - public y: number; - public width: number; - public height: number; - public get left() { return this.x; } - public get right() { return this.x + this.width; } - public get top() { return this.y; } - public get bottom() { return this.top + this.height; } - public static get EMPTY() { return new PIXIRectangle(0, 0, -1, -1); } - constructor(x: number, y: number, width: number, height: number) { - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } -} - -export class MathUtil { - - public static EPSILON: number = 0.001; - - public static Sign(value: number): number { - return value >= 0 ? 1 : -1; - } - - public static AddPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint { - if (inline) { - p1.x += p2.x; - p1.y += p2.y; - return p1; - } - else { - return new PIXIPoint(p1.x + p2.x, p1.y + p2.y); - } - } - - public static Perp(p1: PIXIPoint): PIXIPoint { - return new PIXIPoint(-p1.y, p1.x); - } - - public static DividePoint(p1: PIXIPoint, by: number, inline: boolean = false): PIXIPoint { - if (inline) { - p1.x /= by; - p1.y /= by; - return p1; - } - else { - return new PIXIPoint(p1.x / by, p1.y / by); - } - } - - public static MultiplyConstant(p1: PIXIPoint, by: number, inline: boolean = false) { - if (inline) { - p1.x *= by; - p1.y *= by; - return p1; - } - else { - return new PIXIPoint(p1.x * by, p1.y * by); - } - } - - public static SubtractPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint { - if (inline) { - p1.x -= p2.x; - p1.y -= p2.y; - return p1; - } - else { - return new PIXIPoint(p1.x - p2.x, p1.y - p2.y); - } - } - - public static Area(rect: PIXIRectangle): number { - return rect.width * rect.height; - } - - public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) { - // Return minimum distance between line segment vw and point p - var l2 = MathUtil.DistSquared(v, w); // i.e. |w-v|^2 - avoid a sqrt - if (l2 === 0.0) return MathUtil.Dist(p, v); // v === w case - // Consider the line extending the segment, parameterized as v + t (w - v). - // We find projection of point p onto the line. - // It falls where t = [(p-v) . (w-v)] / |w-v|^2 - // We clamp t from [0,1] to handle points outside the segment vw. - var dot = MathUtil.Dot( - MathUtil.SubtractPoint(p, v), - MathUtil.SubtractPoint(w, v)) / l2; - var t = Math.max(0, Math.min(1, dot)); - // Projection falls on the segment - var projection = MathUtil.AddPoint(v, - MathUtil.MultiplyConstant( - MathUtil.SubtractPoint(w, v), t)); - return MathUtil.Dist(p, projection); - } - - public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined { - var a1 = pe1.y - ps1.y; - var b1 = ps1.x - pe1.x; - - var a2 = pe2.y - ps2.y; - var b2 = ps2.x - pe2.x; - - var delta = a1 * b2 - a2 * b1; - if (delta === 0) { - return undefined; - } - var c2 = a2 * ps2.x + b2 * ps2.y; - var c1 = a1 * ps1.x + b1 * ps1.y; - var invdelta = 1 / delta; - return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta); - } - - public static PointInPIXIRectangle(p: PIXIPoint, rect: PIXIRectangle): boolean { - if (p.x < rect.left - this.EPSILON) { - return false; - } - if (p.x > rect.right + this.EPSILON) { - return false; - } - if (p.y < rect.top - this.EPSILON) { - return false; - } - if (p.y > rect.bottom + this.EPSILON) { - return false; - } - - return true; - } - - public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> { - var r1 = new PIXIPoint(rect.left, rect.top); - var r2 = new PIXIPoint(rect.right, rect.top); - var r3 = new PIXIPoint(rect.right, rect.bottom); - var r4 = new PIXIPoint(rect.left, rect.bottom); - var ret = new Array<PIXIPoint>(); - var dist = this.Dist(lineFrom, lineTo); - var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2); - if (inter && this.PointInPIXIRectangle(inter, rect) && - this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) { - ret.push(inter); - } - inter = this.LineSegmentIntersection(lineFrom, lineTo, r2, r3); - if (inter && this.PointInPIXIRectangle(inter, rect) && - this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) { - ret.push(inter); - } - inter = this.LineSegmentIntersection(lineFrom, lineTo, r3, r4); - if (inter && this.PointInPIXIRectangle(inter, rect) && - this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) { - ret.push(inter); - } - inter = this.LineSegmentIntersection(lineFrom, lineTo, r4, r1); - if (inter && this.PointInPIXIRectangle(inter, rect) && - this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) { - ret.push(inter); - } - return ret; - } - - public static Intersection(rect1: PIXIRectangle, rect2: PIXIRectangle): PIXIRectangle { - const left = Math.max(rect1.x, rect2.x); - const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width); - const top = Math.max(rect1.y, rect2.y); - const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height); - return new PIXIRectangle(left, top, right - left, bottom - top); - } - - public static Dist(p1: PIXIPoint, p2: PIXIPoint): number { - return Math.sqrt(MathUtil.DistSquared(p1, p2)); - } - - public static Dot(p1: PIXIPoint, p2: PIXIPoint): number { - return p1.x * p2.x + p1.y * p2.y; - } - - public static Normalize(p1: PIXIPoint) { - var d = this.Length(p1); - return new PIXIPoint(p1.x / d, p1.y / d); - } - - public static Length(p1: PIXIPoint): number { - return Math.sqrt(p1.x * p1.x + p1.y * p1.y); - } - - public static DistSquared(p1: PIXIPoint, p2: PIXIPoint): number { - const a = p1.x - p2.x; - const b = p1.y - p2.y; - return (a * a + b * b); - } - - public static RectIntersectsRect(r1: PIXIRectangle, r2: PIXIRectangle): boolean { - return !(r2.x > r1.x + r1.width || - r2.x + r2.width < r1.x || - r2.y > r1.y + r1.height || - r2.y + r2.height < r1.y); - } - - public static ArgMin(temp: number[]): number { - let index = 0; - let value = temp[0]; - for (let i = 1; i < temp.length; i++) { - if (temp[i] < value) { - value = temp[i]; - index = i; - } - } - return index; - } - - public static ArgMax(temp: number[]): number { - let index = 0; - let value = temp[0]; - for (let i = 1; i < temp.length; i++) { - if (temp[i] > value) { - value = temp[i]; - index = i; - } - } - return index; - } - - public static Combinations<T>(chars: T[]) { - let result = new Array<T>(); - let f = (prefix: any, chars: any) => { - for (let i = 0; i < chars.length; i++) { - result.push(prefix.concat(chars[i])); - f(prefix.concat(chars[i]), chars.slice(i + 1)); - } - }; - f([], chars); - return result; - } -}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.scss b/src/client/views/nodes/DataVizBox/HistogramBox.scss deleted file mode 100644 index 5aac9dc77..000000000 --- a/src/client/views/nodes/DataVizBox/HistogramBox.scss +++ /dev/null @@ -1,18 +0,0 @@ -// change the stroke color of line-svg class -.svgLine { - position: absolute; - background: darkGray; - stroke: #000; - stroke-width: 1px; - width:100%; - height:100%; - opacity: 0.4; -} - -.svgContainer { - position: absolute; - top:0; - left:0; - width:100%; - height: 100%; -}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.tsx b/src/client/views/nodes/DataVizBox/HistogramBox.tsx deleted file mode 100644 index 00dc2ef46..000000000 --- a/src/client/views/nodes/DataVizBox/HistogramBox.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc } from "../../../../fields/Doc"; -import { NumCast } from "../../../../fields/Types"; -import "./HistogramBox.scss"; - -interface HistogramBoxProps { - rootDoc: Doc; - pairs: { - x: number, - y: number - }[] -} - - -export class HistogramBox extends React.Component<HistogramBoxProps> { - - private origin = {x: 0.1 * this.width, y: 0.9 * this.height}; - - @computed get width() { - return NumCast(this.props.rootDoc.width); - } - - @computed get height() { - return NumCast(this.props.rootDoc.height); - } - - @computed get x() { - return NumCast(this.props.rootDoc.x); - } - - @computed get y() { - return NumCast(this.props.rootDoc.y); - } - - @computed get generatePoints() { - // evenly distribute points along the x axis - const xVals: number[] = this.props.pairs.map(p => p.x); - const yVals: number[] = this.props.pairs.map(p => p.y); - - const xMin = Math.min(...xVals); - const xMax = Math.max(...xVals); - const yMin = Math.min(...yVals); - const yMax = Math.max(...yVals); - - const xRange = xMax - xMin; - const yRange = yMax - yMin; - - const xScale = this.width / xRange; - const yScale = this.height / yRange; - - const xOffset = (this.x + (0.1 * this.width)) - xMin * xScale; - const yOffset = (this.y + (0.25 * this.height)) - yMin * yScale; - - const points: { - x: number, - y: number - }[] = this.props.pairs.map(p => { - return { - x: (p.x * xScale + xOffset) + this.origin.x, - y: (p.y * yScale + yOffset) - } - }); - - return points; - } - - @computed get generateGraphLine() { - const points = this.generatePoints; - // loop through points and create a line from each point to the next - let lines: { - x1: number, - y1: number, - x2: number, - y2: number - }[] = []; - for (let i = 0; i < points.length - 1; i++) { - lines.push({ - x1: points[i].x, - y1: points[i].y, - x2: points[i + 1].x, - y2: points[i + 1].y - }); - } - // generate array of svg with lines - let svgLines: JSX.Element[] = []; - for (let i = 0; i < lines.length; i++) { - svgLines.push( - <line - className="svgLine" - key={i} - x1={lines[i].x1} - y1={lines[i].y1} - x2={lines[i].x2} - y2={lines[i].y2} - stroke="black" - strokeWidth={2} - /> - ); - } - - let res = []; - for (let i = 0; i < svgLines.length; i++) { - res.push(<svg className="svgContainer">{svgLines[i]}</svg>) - } - return res; - } - - @computed get generateAxes() { - - const xAxis = { - x1: 0.1 * this.width, - x2: 0.9 * this.width, - y1: 0.9 * this.height, - y2: 0.9 * this.height, - }; - - const yAxis = { - x1: 0.1 * this.width, - x2: 0.1 * this.width, - y1: 0.25 * this.height, - y2: 0.9 * this.height, - }; - - - return ( - [ - (<svg className="svgContainer"> - {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={this.width - (0.1 * this.width)} y2={xAxis} /> */} - <line className="svgLine" x1={xAxis.x1} y1={xAxis.y1} x2={xAxis.x2} y2={xAxis.y2}/> - - {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */} - </svg>), - ( - <svg className="svgContainer"> - <line className="svgLine" x1={yAxis.x1} y1={yAxis.y1} x2={yAxis.x2} y2={yAxis.y2} /> - {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */} - </svg>) - ] - ) - } - - - render() { - return ( - <div>histogram box - {/* <svg className="svgContainer"> - {this.generateSVGLine} - </svg> */} - {this.generateAxes[0]} - {this.generateAxes[1]} - {this.generateGraphLine.map(line => line)} - </div> - ) - - } - -}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/TableBox.tsx b/src/client/views/nodes/DataVizBox/TableBox.tsx deleted file mode 100644 index dfa8262d8..000000000 --- a/src/client/views/nodes/DataVizBox/TableBox.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { action, computed, observable } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; - -interface TableBoxProps { - pairs: {x: number, y:number}[] -} - - -export class TableBox extends React.Component<TableBoxProps> { - - - - render() { - return ( - <div className="table-container"> - <table className="table"> - <thead> - <tr className="table-row"> - <th>x</th> - <th>y</th> - </tr> - </thead> - <tbody> - {this.props.pairs.map(p => { - return (<tr className="table-row"> - <td>{p.x}</td> - <td>{p.y}</td> - </tr>) - })} - </tbody> - </table> - </div> - ) - } - -}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss new file mode 100644 index 000000000..f1d09d2e7 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -0,0 +1,40 @@ +.tooltip { + // make the height width bigger + width: fit-content; + height: fit-content; +} + +.hoverHighlight-selected, +.selected { + // change the color of the circle element to be red + fill: transparent; + outline: red solid 2px; + border-radius: 100%; + position: absolute; + transform-box: fill-box; + transform-origin: center; +} +.hoverHighlight { + fill: transparent; + outline: black solid 1px; + border-radius: 100%; +} +.hoverHighlight-selected { + fill: transparent; + scale: 1; + outline: black solid 1px; + border-radius: 100%; +} +.datapoint { + fill: black; +} +.brushed { + // change the color of the circle element to be red + fill: red; +} +.chart-container { + display: flex; + flex-direction: column; + align-items: center; + cursor: default; +} diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx new file mode 100644 index 000000000..777bf2f66 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -0,0 +1,319 @@ +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +// import d3 +import * as d3 from 'd3'; +import { Doc, DocListCast } from '../../../../../fields/Doc'; +import { Id } from '../../../../../fields/FieldSymbols'; +import { List } from '../../../../../fields/List'; +import { listSpec } from '../../../../../fields/Schema'; +import { Cast, DocCast } from '../../../../../fields/Types'; +import { Docs } from '../../../../documents/Documents'; +import { DocumentManager } from '../../../../util/DocumentManager'; +import { LinkManager } from '../../../../util/LinkManager'; +import { PinProps, PresBox } from '../../trails'; +import { DataVizBox } from '../DataVizBox'; +import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; +import './Chart.scss'; + +export interface DataPoint { + x: number; + y: number; +} +interface SelectedDataPoint extends DataPoint { + elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>; +} +export interface LineChartProps { + rootDoc: Doc; + axes: string[]; + pairs: { [key: string]: any }[]; + width: number; + height: number; + dataDoc: Doc; + fieldKey: string; + margin: { + top: number; + right: number; + bottom: number; + left: number; + }; +} + +@observer +export class LineChart extends React.Component<LineChartProps> { + private _disposers: { [key: string]: IReactionDisposer } = {}; + private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); + private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; + @observable _currSelected: SelectedDataPoint | undefined = undefined; + // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates + + @computed get _lineChartData() { + if (this.props.axes.length <= 1) return []; + return this.props.pairs + ?.filter(pair => (!this.incomingLinks.length ? true : Array.from(Object.keys(pair)).some(key => pair[key] && key.startsWith('select')))) + .map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) })) + .sort((a, b) => (a.x < b.x ? -1 : 1)); + } + @computed get incomingLinks() { + return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + .filter(link => link.anchor1 !== this.props.rootDoc) // get links where this chart doc is the target of the link + .map(link => DocCast(link.anchor1)); // then return the source of the link + } + @computed get incomingSelected() { + return this.incomingLinks // all links that are pointing to this node + .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes + .filter(dvb => dvb) + .map(dvb => dvb.pairs?.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor + .lastElement(); + } + @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { + return minMaxRange([this._lineChartData]); + } + componentWillUnmount() { + Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]()); + } + componentDidMount = () => { + this._disposers.chartData = reaction( + () => ({ dataSet: this._lineChartData, w: this.width, h: this.height }), + ({ dataSet, w, h }) => { + if (dataSet) { + this.drawChart([dataSet], this.rangeVals, w, h); + // redraw annotations when the chart data has changed, or the local or inherited selection has changed + this.clearAnnotations(); + this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true); + this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); + } + }, + { fireImmediately: true } + ); + this._disposers.annos = reaction( + () => DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']), + annotations => { + // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way + // could be blue colored to make it look like anchor + // this.drawAnnotations() + // loop through annotations and draw them + annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); + // this.drawAnnotations(annotations.x, annotations.y); + }, + { fireImmediately: true } + ); + this._disposers.highlights = reaction( + () => ({ + selected: this._currSelected, + incomingSelected: this.incomingSelected, + }), + ({ selected, incomingSelected }) => { + // redraw annotations when the chart data has changed, or the local or inherited selection has changed + this.clearAnnotations(); + selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true); + incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); + }, + { fireImmediately: true } + ); + }; + + // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that + + clearAnnotations = () => { + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + element.classList.remove('brushed'); + element.classList.remove('selected'); + } + }; + // gets called whenever the "data-annotations" fields gets updated + drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => { + // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements + // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY + // if it exists, then highlight it + // if it doesn't exist, then remove the highlight + const elements = document.querySelectorAll('.datapoint'); + for (let i = 0; i < elements.length; i++) { + const element = elements[i]; + const x = element.getAttribute('data-x'); + const y = element.getAttribute('data-y'); + if (x === dataX.toString() && y === dataY.toString()) { + element.classList.add(selected ? 'selected' : 'brushed'); + } + // TODO: nda - this remove highlight code should go where we remove the links + // } else { + // } + } + }; + + removeAnnotations(dataX: number, dataY: number) { + // loop through and remove any annotations that no longer exist + } + + @action + restoreView = (data: Doc) => { + const coords = Cast(data.presDataVizSelection, listSpec('number'), null); + if (coords?.length > 1 && (this._currSelected?.x !== coords[0] || this._currSelected?.y !== coords[1])) { + this.setCurrSelected(coords[0], coords[1]); + return true; + } + if (this._currSelected) { + this.setCurrSelected(); + return true; + } + return false; + }; + + // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) + getAnchor = (pinProps?: PinProps) => { + const anchor = Docs.Create.TextanchorDocument({ title: 'line doc selection' + this._currSelected?.x, unrendered: true }); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc); + anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; + return anchor; + }; + + @computed get height() { + return this.props.height - this.props.margin.top - this.props.margin.bottom; + } + + @computed get width() { + return this.props.width - this.props.margin.left - this.props.margin.right; + } + + setupTooltip() { + return d3 + .select(this._lineChartRef.current) + .append('div') + .attr('class', 'tooltip') + .style('opacity', 0) + .style('background', '#fff') + .style('border', '1px solid #ccc') + .style('padding', '5px') + .style('position', 'absolute') + .style('font-size', '12px'); + } + + // TODO: nda - use this everyewhere we update currSelected? + @action + setCurrSelected(x?: number, y?: number) { + // TODO: nda - get rid of svg element in the list? + this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; + this.props.pairs.forEach(pair => pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y && (pair.selected = true)); + this.props.pairs.forEach(pair => (pair.selected = pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y ? true : undefined)); + } + + drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { + if (this._lineChartSvg) { + const circleClass = '.circle-' + idx; + this._lineChartSvg + .selectAll(circleClass) + .data(data) + .join('circle') // enter append + .attr('class', `${circleClass} datapoint`) + .attr('r', '3') // radius + .attr('cx', d => xScale(d.x)) + .attr('cy', d => yScale(d.y)) + .attr('data-x', d => d.x) + .attr('data-y', d => d.y); + } + } + + // TODO: nda - can use d3.create() to create html element instead of appending + drawChart = (dataSet: DataPoint[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => { + // clearing tooltip and the current chart + d3.select(this._lineChartRef.current).select('svg').remove(); + d3.select(this._lineChartRef.current).select('.tooltip').remove(); + + const { xMin, xMax, yMin, yMax } = rangeVals; + if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { + return; + } + + // creating the x and y scales + const xScale = scaleCreatorNumerical(xMin, xMax, 0, width); + const yScale = scaleCreatorNumerical(0, yMax,height, 0); + + // adding svg + const margin = this.props.margin; + const svg = (this._lineChartSvg = d3 + .select(this._lineChartRef.current) + .append('svg') + .attr('width', `${width +margin.left + margin.right}`) + .attr('height', `${height + margin.top + margin.bottom }`) + .append('g') + .attr('transform', `translate(${margin.left}, ${margin.top})`)); + + // create x and y grids + xGrid(svg.append('g'), height, xScale); + yGrid(svg.append('g'), width, yScale); + xAxisCreator(svg.append('g'), height, xScale); + yAxisCreator(svg.append('g'), width, yScale); + + // draw the plot line + const data = dataSet[0]; + const lineGen = createLineGenerator(xScale, yScale); + drawLine(svg.append('path'), data, lineGen); + + // draw the datapoint circle + this.drawDataPoints(data, 0, xScale, yScale); + + const higlightFocusPt = svg.append('g').style('display', 'none'); + higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle'); + const tooltip = this.setupTooltip(); + // add all the tooltipContent to the tooltip + const mousemove = action((e: any) => { + const bisect = d3.bisector((d: DataPoint) => d.x).left; + const xPos = d3.pointer(e)[0]; + const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis + const d0 = data[x0]; + if (!d0) return; + + this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); + }); + + const onPointClick = action((e: any) => { + const bisect = d3.bisector((d: DataPoint) => d.x).left; + const xPos = d3.pointer(e)[0]; + const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis + const d0 = data[x0]; + // find .circle-d1 with data-x = d0.x and data-y = d0.y + const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y); + this.setCurrSelected(d0.x, d0.y); + this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); + }); + + svg.append('rect') + .attr('class', 'overlay') + .attr('width', width) + .attr('height', this.height + margin.top + margin.bottom) + .attr('fill', 'none') + .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`) + .style('opacity', 0) + .on('mouseover', () => higlightFocusPt.style('display', null)) + .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0)) + .on('mousemove', mousemove) + .on('click', onPointClick); + }; + + private updateTooltip( + higlightFocusPt: d3.Selection<SVGGElement, unknown, null, undefined>, + xScale: d3.ScaleLinear<number, number, never>, + d0: DataPoint, + yScale: d3.ScaleLinear<number, number, never>, + tooltip: d3.Selection<HTMLDivElement, unknown, null, undefined> + ) { + higlightFocusPt.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`).attr('class', this._currSelected?.x === d0.x && this._currSelected?.y === d0.y ? 'hoverHighlight-selected' : 'hoverHighlight'); + tooltip.transition().duration(300).style('opacity', 0.9); + // TODO: nda - updating the inner html could be deadly cause injection attacks! + tooltip + .html(() => `<b>(${d0.x},${d0.y})</b>`) // text content for tooltip + .style('pointer-events', 'none') + .style('transform', `translate(${xScale(d0.x) - this.width / 2}px,${yScale(d0.y) - 30}px)`); + } + + render() { + const selectedPt = this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'; + return ( + <div ref={this._lineChartRef} className="chart-container"> + <span> {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Selected: ${selectedPt}`}</span> + </div> + ); + } +} diff --git a/src/client/views/nodes/DataVizBox/TableBox.scss b/src/client/views/nodes/DataVizBox/components/TableBox.scss index 1264d6a46..1264d6a46 100644 --- a/src/client/views/nodes/DataVizBox/TableBox.scss +++ b/src/client/views/nodes/DataVizBox/components/TableBox.scss diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx new file mode 100644 index 000000000..0d69ac890 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -0,0 +1,105 @@ +import { action, computed } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { AnimationSym, Doc } from '../../../../../fields/Doc'; +import { Id } from '../../../../../fields/FieldSymbols'; +import { List } from '../../../../../fields/List'; +import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils'; +import { DragManager } from '../../../../util/DragManager'; +import { DocumentView } from '../../DocumentView'; +import { DataVizView } from '../DataVizBox'; + +interface TableBoxProps { + pairs: { [key: string]: any }[]; + selectAxes: (axes: string[]) => void; + axes: string[]; + docView?: () => DocumentView | undefined; +} + +@observer +export class TableBox extends React.Component<TableBoxProps> { + @computed get columns() { + return this.props.pairs.length ? Array.from(Object.keys(this.props.pairs[0])) : []; + } + render() { + return ( + <div className="table-container"> + <table className="table"> + <thead> + <tr className="table-row"> + {this.columns + .filter(col => !col.startsWith('select')) + .map(col => { + const header = React.createRef<HTMLElement>(); + return ( + <th + ref={header as any} + style={{ + color: this.props.axes.slice().reverse().lastElement() === col ? 'green' : this.props.axes.lastElement() === col ? 'red' : undefined, + fontWeight: this.props.axes.includes(col) ? 'bolder' : 'normal', + }} + onPointerDown={e => { + const downX = e.clientX; + const downY = e.clientY; + setupMoveUpEvents( + {}, + e, + e => { + const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; + const targetCreator = (annotationOn: Doc | undefined) => { + const alias = Doc.MakeAlias(this.props.docView?.()!.rootDoc!); + alias._dataVizView = DataVizView.LINECHART; + alias._dataVizAxes = new List<string>([col, col]); + alias.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; + return alias; + }; + if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { + DragManager.StartAnchorAnnoDrag([header.current!], new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { + dragComplete: e => { + if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { + e.linkDocument.linkDisplay = true; + // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; + // e.annoDragData.linkSourceDoc.followLinkZoom = false; + } + }, + }); + return true; + } + return false; + }, + emptyFunction, + action(e => { + const newAxes = this.props.axes; + if (newAxes.includes(col)) { + newAxes.splice(newAxes.indexOf(col), 1); + } else if (newAxes.length >= 1) { + newAxes[1] = col; + } else { + newAxes[0] = col; + } + this.props.selectAxes(newAxes); + }) + ); + }}> + {col} + </th> + ); + })} + </tr> + </thead> + <tbody> + {this.props.pairs?.map((p, i) => { + return ( + <tr className="table-row" onClick={action(e => (p['select' + this.props.docView?.()?.rootDoc![Id]] = !p['select' + this.props.docView?.()?.rootDoc![Id]]))}> + {this.columns.map(col => ( + <td style={{ fontWeight: p['select' + this.props.docView?.()?.rootDoc![Id]] ? 'bold' : '' }}>{p[col]}</td> + ))} + </tr> + ); + })} + </tbody> + </table> + </div> + ); + } +} diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts new file mode 100644 index 000000000..e1ff6f8eb --- /dev/null +++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts @@ -0,0 +1,67 @@ +import * as d3 from 'd3'; +import { DataPoint } from '../components/LineChart'; + +// TODO: nda - implement function that can handle range for strings + +export const minMaxRange = (dataPts: DataPoint[][]) => { + // find the max and min of all the data points + const yMin = d3.min(dataPts, d => d3.min(d, d => Number(d.y))); + const yMax = d3.max(dataPts, d => d3.max(d, d => Number(d.y))); + + const xMin = d3.min(dataPts, d => d3.min(d, d => Number(d.x))); + const xMax = d3.max(dataPts, d => d3.max(d, d => Number(d.x))); + + return { xMin, xMax, yMin, yMax }; +}; + +export const scaleCreatorCategorical = (labels: string[], range: number[]) => { + const scale = d3.scaleBand().domain(labels).range(range); + + return scale; +}; + +export const scaleCreatorNumerical = (domA: number, domB: number, rangeA: number, rangeB: number) => { + return d3.scaleLinear().domain([domA, domB]).range([rangeA, rangeB]); +}; + +export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) => { + // TODO: nda - look into the different types of curves + return d3 + .line<DataPoint>() + .x(d => xScale(d.x)) + .y(d => yScale(d.y)) + .curve(d3.curveMonotoneX); +}; + +export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => { + console.log('x axis creator being called'); + g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15)); +}; + +export const yAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, marginLeft: number, yScale: d3.ScaleLinear<number, number, never>) => { + g.attr('class', 'y-axis').call(d3.axisLeft(yScale)); +}; + +export const xGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, scale: d3.ScaleLinear<number, number, never>) => { + g.attr('class', 'xGrid') + .attr('transform', `translate(0,${height})`) + .call( + d3 + .axisBottom(scale) + .tickSize(-height) + .tickFormat((a, b) => '') + ); +}; + +export const yGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, width: number, scale: d3.ScaleLinear<number, number, never>) => { + g.attr('class', 'yGrid').call( + d3 + .axisLeft(scale) + .tickSize(-width) + .tickFormat((a, b) => '') + ); +}; + +export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>) => { + p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('class', 'line').attr('d', lineGen); +}; diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index d6679b46d..df3299eef 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -142,7 +142,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } else if (startLink !== endLink) { endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink; startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink; - const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true); + const linkDoc = DocUtils.MakeLink(startLink, endLink, { linkRelationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }, undefined, true); LinkManager.currentLink = linkDoc; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 091a6b050..e24bf35f5 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -12,7 +12,7 @@ import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; -import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../../DocServer'; import { Docs, DocUtils } from '../../documents/Documents'; @@ -108,6 +108,7 @@ export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, p export interface DocComponentView { updateIcon?: () => void; // updates the icon representation of the document getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) + restoreView?: (viewSpec: Doc) => boolean; scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void; getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined @@ -512,7 +513,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.props.isDocumentActive?.() && !this.props.onBrowseClick?.() && !this.Document.ignoreClick && - !e.ctrlKey && e.button === 0 && this.pointerEvents !== 'none' && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) @@ -618,22 +618,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action - drop = async (e: Event, de: DragManager.DropEvent) => { + drop = (e: Event, de: DragManager.DropEvent) => { if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return; if (this.props.Document === Doc.ActiveDashboard) { alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.'); return; } const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; - if (linkdrag) linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); - if (linkdrag?.linkSourceDoc) { - e.stopPropagation(); - if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { - de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); - } - if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) { - const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.props.Document; - de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]); + if (linkdrag) { + linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); + if (linkdrag.linkSourceDoc) { + e.stopPropagation(); + if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { + de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); + } + if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) { + const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc; + de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, undefined, [de.x, de.y - 50]); + } } } }; @@ -644,9 +646,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document && d.linkRelationship === 'portal to:portal from'); if (!portalLink) { DocUtils.MakeLink( - { doc: this.props.Document }, - { doc: Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }) }, - 'portal to:portal from' + this.props.Document, + Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }), + { linkRelationship: 'portal to:portal from' } ); } this.Document.followLinkLocation = OpenWhere.lightbox; @@ -654,6 +656,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps this.Document._isLinkButton = true; }; + importDocument = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.zip'; + input.onchange = _e => { + if (input.files) { + const batch = UndoManager.StartBatch('importing'); + Doc.importDocument(input.files[0]).then(doc => { + if (doc instanceof Doc) { + this.props.addDocTab(doc, OpenWhere.addRight); + batch.end(); + } + }); + } + }; + input.click(); + }; + @action onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { if (e && this.rootDoc._hideContextMenu && Doc.noviceMode) { @@ -705,25 +725,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const appearance = cm.findByDescription('UI Controls...'); const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); - !Doc.noviceMode && - appearanceItems.push({ - description: 'Add a Field', - event: () => { - const alias = Doc.MakeAlias(this.rootDoc); - alias.layout = FormattedTextBox.LayoutString('newfield'); - alias.title = 'newfield'; - alias._height = 35; - alias._width = 100; - alias.syncLayoutFieldWithTitle = true; - alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width); - alias.y = NumCast(this.rootDoc.y); - this.props.addDocument?.(alias); - }, - icon: 'eye', - }); - LinkManager.Links(this.Document).length && - appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' }); - !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); + !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { const existingOnClick = cm.findByDescription('OnClick...'); @@ -791,16 +793,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' }); } - moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); - - (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); } - if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) { + !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); + } + const constantItems: ContextMenuProps[] = []; + + if (!Doc.IsSystem(this.rootDoc)) { + constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); + constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() }); + (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); + if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) - moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); + constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); } - !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' }); + cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' }); } const help = cm.findByDescription('Help...'); const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; @@ -851,7 +858,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps icon: 'book', }); } - cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' }); + if (!help) cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' }); + else cm.moveAfter(help); e?.stopPropagation(); // DocumentViews should stop propagation of this event cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); @@ -863,13 +871,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler); setHeight = (height: number) => (this.layoutDoc._height = height); setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); - isContentActive = (outsideReaction?: boolean) => { + isContentActive = (outsideReaction?: boolean): boolean | undefined => { return this.props.isContentActive() === false ? false : Doc.ActiveTool !== InkTool.None || SnappingManager.GetIsDragging() || this.rootSelected() || - this.props.Document.forceActive || + this.rootDoc.forceActive || this.props.isSelected(outsideReaction) || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive() @@ -1064,7 +1072,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } }; - captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); + captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); @computed get innards() { TraceMobx(); const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1; @@ -1079,7 +1087,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps maxHeight: `max(100%, ${20 * ffscale()}px)`, }}> <FormattedTextBox - {...OmitKeys(this.props, ['children']).omit} + {...this.props} yPadding={10} xPadding={10} fieldKey={this.showCaption} @@ -1090,7 +1098,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dontScale={true} renderDepth={this.props.renderDepth} isContentActive={this.isContentActive} - onClick={this.onClickFunc} /> </div> ); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index e53422ab7..8d3534a5c 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -20,7 +20,7 @@ export interface FieldViewProps extends DocumentViewSharedProps { select: (isCtrlPressed: boolean) => void; isContentActive: (outsideReaction?: boolean) => boolean | undefined; - isDocumentActive?: () => boolean; + isDocumentActive?: () => boolean | undefined; isSelected: (outsideReaction?: boolean) => boolean; setHeight?: (height: number) => void; NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 48e54b722..f38ebba27 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { DashColor, emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; +import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices'; import { Docs, DocUtils } from '../../documents/Documents'; @@ -222,7 +222,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp croppingProto.panYMin = anchy / viewScale; croppingProto.panYMax = anchh / viewScale; if (addCrop) { - DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', ''); + DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' }); cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym](); cropping.y = NumCast(this.rootDoc.y); this.props.addDocTab(cropping, OpenWhere.inParent); @@ -409,8 +409,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ); } - contentFunc = () => [this.content]; - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); @observable _marqueeing: number[] | undefined; @@ -473,7 +471,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }}> <CollectionFreeFormView ref={this._ffref} - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + {...this.props} + setContentView={emptyFunction} + NativeWidth={returnZero} + NativeHeight={returnZero} renderDepth={this.props.renderDepth + 1} fieldKey={this.annotationKey} styleProvider={this.props.styleProvider} @@ -487,11 +488,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp focus={this.focus} getScrollHeight={this.getScrollHeight} NativeDimScaling={returnOne} + isAnyChildContentActive={returnFalse} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocument}> - {this.contentFunc} + {this.content} </CollectionFreeFormView> {this.annotationLayer} {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index ea8f47b39..36be7d257 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -8,7 +8,7 @@ import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; @@ -635,7 +635,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps .map(marker => ( <MapBoxInfoWindow key={marker[Id]} - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + {...this.props} + setContentView={emptyFunction} place={marker} markerMap={this.markerMap} PanelWidth={this.infoWidth} @@ -674,6 +675,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} + usePanelWidth={true} showSidebar={this.SidebarShown} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 00bedafbe..d65ef9c4c 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -4,7 +4,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; -import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView'; @@ -52,7 +52,8 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> <CollectionStackingView ref={r => (this._stack = r)} - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + {...this.props} + setContentView={emptyFunction} Document={this.props.place} DataDoc={undefined} fieldKey="data" diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 5f207cc0d..e38d7e2a8 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -5,16 +5,20 @@ import * as Pdfjs from 'pdfjs-dist'; import 'pdfjs-dist/web/pdf_viewer.css'; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { InkTool } from '../../../fields/InkField'; +import { ComputedField } from '../../../fields/ScriptField'; +import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils'; +import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; -import { DocumentType } from '../../documents/DocumentTypes'; +import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { KeyCodes } from '../../util/KeyCodes'; +import { SelectionManager } from '../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm'; +import { CollectionStackingView } from '../collections/CollectionStackingView'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; @@ -41,7 +45,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _initialScrollTarget: Opt<Doc>; private _pdfViewer: PDFViewer | undefined; private _searchRef = React.createRef<HTMLInputElement>(); - private _selectReactionDisposer: IReactionDisposer | undefined; + private _disposers: { [name: string]: IReactionDisposer } = {}; private _sidebarRef = React.createRef<SidebarAnnos>(); @observable private _searching: boolean = false; @@ -126,7 +130,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps croppingProto['data-nativeWidth'] = anchw; croppingProto['data-nativeHeight'] = anchh; if (addCrop) { - DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', ''); + DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' }); } this.props.bringToFront(cropping); @@ -184,11 +188,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; componentWillUnmount() { - this._selectReactionDisposer?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); } componentDidMount() { this.props.setContentView?.(this); - this._selectReactionDisposer = reaction( + this._disposers.select = reaction( () => this.props.isSelected(), () => { document.removeEventListener('keydown', this.onKeyDown); @@ -196,6 +200,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }, { fireImmediately: true } ); + this._disposers.scroll = reaction( + () => this.rootDoc.scrollTop, + () => { + if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '-panY'])) instanceof ComputedField)) { + this.props.Document[this.SidebarKey + '-panY'] = ComputedField.MakeFunction('this.scrollTop'); + } + this.props.Document[this.SidebarKey + '-viewScale'] = 1; + this.props.Document[this.SidebarKey + '-panX'] = 0; + } + ); } brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view); @@ -303,7 +317,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // adding external documents; to sidebar key // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation") - sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); }; @@ -330,7 +344,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }, (e, movement, isClick) => !isClick && batch.end(), () => { - this.toggleSidebar(); + onButton && this.toggleSidebar(); batch.end(); } ); @@ -426,12 +440,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); }; + @undoBatch + toggleSidebarType = () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); specificContextMenu = (e: React.MouseEvent): void => { - const funcs: ContextMenuProps[] = []; - funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' }); - funcs.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' }); - //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); - ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); + const cm = ContextMenu.Instance; + const options = cm.findByDescription('Options...'); + const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; + optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' }); + !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' }); + //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); + !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' }); + const help = cm.findByDescription('Help...'); + const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; + helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' }); + !help && ContextMenu.Instance.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'asterisk' }); }; @computed get renderTitleBox() { @@ -467,14 +489,87 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); } + public get SidebarKey() { + return this.fieldKey + '-sidebar'; + } + @computed get pdfScale() { + const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); + const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth); + const pdfRatio = pdfNativeWidth / nativeWidth; + return (pdfRatio * this.props.PanelWidth()) / pdfNativeWidth; + } + @computed get sidebarNativeWidth() { + return this.sidebarWidth() / this.pdfScale; + } + @computed get sidebarNativeHeight() { + return this.props.PanelHeight() / this.pdfScale; + } + sidebarNativeWidthFunc = () => this.sidebarNativeWidth; + sidebarNativeHeightFunc = () => this.sidebarNativeHeight; + sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); + sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); + sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate((this.sidebarWidth() - this.props.PanelWidth()) / this.pdfScale, 0); + @computed get sidebarCollection() { + const renderComponent = (tag: string) => { + const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : CollectionStackingView; + return ComponentTag === CollectionStackingView ? ( + <SidebarAnnos + ref={this._sidebarRef} + {...this.props} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + setHeight={emptyFunction} + nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)} + showSidebar={this.SidebarShown} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.removeDocument} + /> + ) : ( + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> + <ComponentTag + {...this.props} + setContentView={emptyFunction} // override setContentView to do nothing + NativeWidth={this.sidebarNativeWidthFunc} + NativeHeight={this.sidebarNativeHeightFunc} + PanelHeight={this.props.PanelHeight} + PanelWidth={this.sidebarWidth} + xPadding={0} + yPadding={0} + viewField={this.SidebarKey} + isAnnotationOverlay={false} + originTopLeft={true} + isAnyChildContentActive={this.isAnyChildContentActive} + select={emptyFunction} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.sidebarRemDocument} + moveDocument={this.sidebarMoveDocument} + addDocument={this.sidebarAddDocument} + CollectionView={undefined} + ScreenToLocalTransform={this.sidebarScreenToLocal} + renderDepth={this.props.renderDepth + 1} + noSidebar={true} + fieldKey={this.SidebarKey} + /> + </div> + ); + }; + return ( + <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}> + {renderComponent(StrCast(this.layoutDoc.sidebarViewType))} + </div> + ); + } isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath())); @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const scale = previewScale * (this.props.NativeDimScaling?.() || 1); - return ( + return !this._pdf ? null : ( <div - className={'pdfBox'} + className="pdfBox" onContextMenu={this.specificContextMenu} style={{ display: this.props.thumbShown?.() ? 'none' : undefined, @@ -496,7 +591,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps addDocTab={this.sidebarAddDocTab} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - pdf={this._pdf!} + pdf={this._pdf} focus={this.focus} url={this.pdfUrl!.url.pathname} isContentActive={this.isPdfContentActive} @@ -510,22 +605,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps crop={this.crop} /> </div> - <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}> - <SidebarAnnos - ref={this._sidebarRef} - {...this.props} - rootDoc={this.rootDoc} - layoutDoc={this.layoutDoc} - dataDoc={this.dataDoc} - setHeight={emptyFunction} - nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)} - showSidebar={this.SidebarShown} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - sidebarAddDocument={this.sidebarAddDocument} - moveDocument={this.moveDocument} - removeDocument={this.removeDocument} - /> - </div> + <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>{this.sidebarCollection}</div> {this.settingsPanel()} </div> ); @@ -535,21 +615,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>(); render() { TraceMobx(); - if (this._pdf) { - if (!this.props.thumbShown?.()) { - return this.renderPdfView; - } - return null; - } + if (this.props.thumbShown?.()) return null; + const pdfView = this.renderPdfView; const href = this.pdfUrl?.url.href; - if (href) { + if (!pdfView && href) { if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href)))); else { if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf)))); } } - return this.renderTitleBox; + return pdfView ?? this.renderTitleBox; } } diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 61e4894f0..db11a7776 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast } from '../../../fields/Types'; import { AudioField, VideoField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, OmitKeys, returnFalse, returnOne } from '../../../Utils'; +import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; @@ -277,7 +277,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState'); this.dataDoc[this.fieldKey + '-dictation'] = dictationText; }; - contentFunc = () => [this.threed, this.content]; videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth(); formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); render() { @@ -287,7 +286,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl <div className="videoBox-viewer"> <div style={{ position: 'relative', height: this.videoPanelHeight() }}> <CollectionFreeFormView - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit} + {...this.props} + setContentView={emptyFunction} + NativeWidth={returnZero} + NativeHeight={returnZero} PanelHeight={this.videoPanelHeight} PanelWidth={this.props.PanelWidth} focus={this.props.focus} @@ -296,6 +298,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl select={emptyFunction} isContentActive={returnFalse} NativeDimScaling={returnOne} + isAnyChildContentActive={returnFalse} whenChildContentsActiveChanged={emptyFunction} removeDocument={returnFalse} moveDocument={returnFalse} @@ -304,17 +307,19 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl ScreenToLocalTransform={this.props.ScreenToLocalTransform} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc}> - {this.contentFunc} + <> + {this.threed} + {this.content} + </> </CollectionFreeFormView> </div> <div style={{ position: 'relative', height: this.formattedPanelHeight() }}> {!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : ( <FormattedTextBox - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit} + {...this.props} Document={this.dataDoc[this.fieldKey + '-dictation']} fieldKey={'text'} PanelHeight={this.formattedPanelHeight} - isAnnotationOverlay={true} select={emptyFunction} isContentActive={emptyFunction} NativeDimScaling={returnOne} @@ -324,7 +329,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl removeDocument={returnFalse} moveDocument={returnFalse} addDocument={returnFalse} - CollectionView={undefined} renderDepth={this.props.renderDepth + 1} ContainingCollectionDoc={this.props.ContainingCollectionDoc}></FormattedTextBox> )} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 0b88f5fe3..e14ad4b05 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -9,7 +9,7 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { AudioField, ImageField, VideoField } from '../../../fields/URLField'; -import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; +import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; @@ -327,7 +327,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp _isLinkButton: true, }); this.props.addDocument?.(b); - DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot'); + DocUtils.MakeLink(b, this.rootDoc, { linkRelationship: 'video snapshot' }); Networking.PostToServer('/youtubeScreenshot', { id: this.youtubeVideoId, timecode: this.layoutDoc._currentTimecode, @@ -376,7 +376,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc)); Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc)); this.props.addDocument?.(imageSnapshot); - const link = DocUtils.MakeLink({ doc: imageSnapshot }, { doc: this.getAnchor(true) }, 'video snapshot'); + const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { linkRelationship: 'video snapshot' }); link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3); setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true)); }; @@ -894,8 +894,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp playing = () => this._playing; - contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content]; - scaling = () => this.props.NativeDimScaling?.() || 1; panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100; @@ -1033,7 +1031,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp croppingProto.panYMin = anchy / viewScale; croppingProto.panYMax = anchh / viewScale; if (addCrop) { - DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', ''); + DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' }); } this.props.bringToFront(cropping); return cropping; @@ -1068,7 +1066,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp left: (this.props.PanelWidth() - this.panelWidth()) / 2, }}> <CollectionFreeFormView - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + {...this.props} + setContentView={emptyFunction} + NativeWidth={returnZero} + NativeHeight={returnZero} renderDepth={this.props.renderDepth + 1} fieldKey={this.annotationKey} CollectionView={undefined} @@ -1076,6 +1077,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp annotationLayerHostsContent={true} PanelWidth={this.panelWidth} PanelHeight={this.panelHeight} + isAnyChildContentActive={returnFalse} ScreenToLocalTransform={this.screenToLocalTransform} docFilters={this.timelineDocFilter} select={emptyFunction} @@ -1084,7 +1086,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocWithTimecode}> - {this.contentFunc} + {this.youtubeVideoId ? this.youtubeContent : this.content} </CollectionFreeFormView> </div> {this.annotationLayer} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index d57518a8d..66d0fd385 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema'; import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; +import { emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; @@ -305,7 +305,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight)); if (scrollTo !== undefined) { if (this._initialScroll === undefined) { - this.goTo(scrollTo, options.zoomTime ?? 500, options.easeFunc); + const focusTime = options.zoomTime ?? 500; + this.goTo(scrollTo, focusTime, options.easeFunc); + return focusTime; } else { this._initialScroll = scrollTo; } @@ -904,9 +906,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const scale = previewScale * (this.props.NativeDimScaling?.() || 1); - const renderAnnotations = (docFilters?: () => string[]) => ( + const renderAnnotations = (docFilters: () => string[]) => ( <CollectionFreeFormView - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + {...this.props} + setContentView={emptyFunction} + NativeWidth={returnZero} + NativeHeight={returnZero} renderDepth={this.props.renderDepth + 1} isAnnotationOverlay={true} fieldKey={this.annotationKey} @@ -921,6 +926,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps dropAction={'alias'} docFilters={docFilters} select={emptyFunction} + isAnyChildContentActive={returnFalse} bringToFront={emptyFunction} styleProvider={this.childStyleProvider} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 28e6eaf1c..8eacfbc51 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -161,7 +161,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() { </div> ); return ( - <div className={`menuButton ${this.type} ${numBtnType}`} onPointerDown={e => e.stopPropagation()} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> + <div + className={`menuButton ${this.type} ${numBtnType}`} + onPointerDown={e => e.stopPropagation()} + onClick={action(() => { + this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; + Doc.UnBrushAllDocs(); + })}> {checkResult} {label} {this.rootDoc.dropDownOpen ? dropdown : null} @@ -198,7 +204,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { e.stopPropagation(); e.preventDefault(); }} - onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> + onClick={action(() => { + this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; + Doc.UnBrushAllDocs(); + })}> <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} /> </div> <div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}> @@ -215,6 +224,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { onClick={e => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; + Doc.UnBrushAllDocs(); }} /> </div> @@ -237,7 +247,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { <div className={`menuButton ${this.type} ${active}`} style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} - onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}> + onClick={action(() => { + this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; + Doc.UnBrushAllDocs(); + })}> {this.Icon(color)} {!this.label || !FontIconBox.GetShowLabels() ? null : ( <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> @@ -316,7 +329,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() { <div className={`menuButton ${this.type} ${active}`} style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : 'flex' }} - onClick={dropdown ? () => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen) : undefined}> + onClick={ + dropdown + ? () => { + this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; + Doc.UnBrushAllDocs(); + } + : undefined + }> {dropdown ? null : <FontAwesomeIcon style={{ marginLeft: 5 }} className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />} <div className="menuButton-dropdown-header">{text && text[0].toUpperCase() + text.slice(1)}</div> {label} @@ -335,6 +355,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { onClick={e => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; + Doc.UnBrushAllDocs(); }} /> </div> @@ -379,6 +400,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} onClick={action(e => { this.colorPickerClosed = !this.colorPickerClosed; + setTimeout(() => Doc.UnBrushAllDocs()); e.stopPropagation(); })} onPointerDown={e => e.stopPropagation()}> @@ -397,6 +419,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { e.preventDefault(); e.stopPropagation(); this.colorPickerClosed = true; + Doc.UnBrushAllDocs(); })} /> </div> @@ -587,32 +610,43 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); }); -ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snap lines' | 'clusters' | 'viewAll', checkResult?: boolean) { +ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snap lines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); // prettier-ignore - const map: Map<'grid' | 'snap lines' | 'clusters' | 'viewAll', { checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ + const map: Map<'grid' | 'snap lines' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ ['grid', { + undo: false, checkResult: (doc:Doc) => doc._backgroundGridShow, setDoc: (doc:Doc) => doc._backgroundGridShow = !doc._backgroundGridShow, }], ['snap lines', { + undo: false, checkResult: (doc:Doc) => doc.showSnapLines, setDoc: (doc:Doc) => doc._showSnapLines = !doc._showSnapLines, }], ['viewAll', { + undo: false, checkResult: (doc:Doc) => doc._fitContentsToBox, setDoc: (doc:Doc) => doc._fitContentsToBox = !doc._fitContentsToBox, }], ['clusters', { + undo: false, checkResult: (doc:Doc) => doc._useClusters, setDoc: (doc:Doc) => doc._useClusters = !doc._useClusters, }], + ['arrange', { + undo: true, + checkResult: (doc:Doc) => doc._autoArrange, + setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange, + }], ]); if (checkResult) { return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent'; } + const batch = map.get(attr)?.undo ? UndoManager.StartBatch('set feature') : { end: () => {} }; SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv)); + setTimeout(() => batch.end(), 100); }); ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: any, checkResult?: boolean) { const editorView = RichTextMenu.Instance?.TextView?.EditorView; @@ -805,7 +839,7 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | ' setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()), }], ['fillColor', { - checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ? Colors.MEDIUM_BLUE : 'transparent'), + checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"), setInk: (doc: Doc) => (doc.fillColor = StrCast(value)), setMode: () => SetActiveFillColor(StrCast(value)), }], diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx index 7f414ddbb..74c3c563c 100644 --- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx +++ b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx @@ -12,7 +12,7 @@ export class ColorDropdown extends Component<IButtonProps> { const active: string = StrCast(this.props.rootDoc.dropDownOpen); const script: string = StrCast(this.props.rootDoc.script); - const scriptCheck: string = script + "(undefined, true)"; + const scriptCheck: string = script + '(undefined, true)'; const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result; const stroke: boolean = false; @@ -24,24 +24,21 @@ export class ColorDropdown extends Component<IButtonProps> { // strokeIcon = (<div style={{ borderRadius: "100%", width: width + '%', height: height + '%', backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />); // } - const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', - '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', - '#FFFFFF', '#f1efeb']; + const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb']; - const colorBox = (func: (color: ColorState) => void) => <SketchPicker - disableAlpha={!stroke} - onChange={func} color={boolResult ? boolResult : "#FFFFFF"} - presetColors={colorOptions} />; - const label = !this.props.label || !FontIconBox.GetShowLabels() ? (null) : - <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: "absolute" }}> - {this.props.label} - </div>; + const colorBox = (func: (color: ColorState) => void) => <SketchPicker disableAlpha={!stroke} onChange={func} color={boolResult ? boolResult : '#FFFFFF'} presetColors={colorOptions} />; + const label = + !this.props.label || !FontIconBox.GetShowLabels() ? null : ( + <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: 'absolute' }}> + {this.props.label} + </div> + ); - const dropdownCaret = <div - className="menuButton-dropDown" - style={{ borderBottomRightRadius: active ? 0 : undefined }}> - <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" /> - </div>; + const dropdownCaret = ( + <div className="menuButton-dropDown" style={{ borderBottomRightRadius: active ? 0 : undefined }}> + <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" /> + </div> + ); const click = (value: ColorState) => { const hex: string = value.hex; @@ -51,26 +48,30 @@ export class ColorDropdown extends Component<IButtonProps> { } }; return ( - <div className={`menuButton ${this.props.type} ${active}`} + <div + className={`menuButton ${this.props.type} ${active}`} style={{ color: this.props.color, borderBottomLeftRadius: active ? 0 : undefined }} - onClick={() => this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen} + onClick={() => (this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen)} onPointerDown={e => e.stopPropagation()}> <FontAwesomeIcon className={`fontIconBox-icon-${this.props.type}`} icon={this.props.icon} color={this.props.color} /> - <div className="colorButton-color" - style={{ backgroundColor: boolResult ? boolResult : "#FFFFFF" }} /> + <div className="colorButton-color" style={{ backgroundColor: boolResult ? boolResult : '#FFFFFF' }} /> {label} {/* {dropdownCaret} */} - {this.props.rootDoc.dropDownOpen ? + {this.props.rootDoc.dropDownOpen ? ( <div> - <div className="menuButton-dropdownBox" - onPointerDown={e => e.stopPropagation()} - onClick={e => e.stopPropagation()}> + <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> {colorBox(click)} </div> - <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.props.rootDoc.dropDownOpen = false; }} /> + <div + className="dropbox-background" + onClick={e => { + e.stopPropagation(); + this.props.rootDoc.dropDownOpen = false; + }} + /> </div> - : null} + ) : null} </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index ee4249b02..361e000f9 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -20,7 +20,7 @@ import { RichTextUtils } from '../../../../fields/RichTextUtils'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -454,7 +454,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps alink = alink ?? (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || - DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!); + DocUtils.MakeLink(this.props.Document, target, { linkRelationship: LinkManager.AutoKeywords })!); newAutoLinks.add(alink); const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); @@ -1273,7 +1273,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ]), ]); - const link = DocUtils.MakeLink({ doc: pdfAnchor }, { doc: this.rootDoc }, 'PDF pasted'); + const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { linkRelationship: 'PDF pasted' }); if (link) { view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste')); } @@ -1739,12 +1739,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } }; - fitContentsToBox = () => this.props.Document._fitContentsToBox; + fitContentsToBox = () => BoolCast(this.props.Document._fitContentsToBox); sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); - // console.log("printting allSideBarDocs"); - // console.log(this.allSidebarDocs); return this.addDocument(doc, sidebarKey); }; sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); @@ -1785,8 +1783,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} style={{ - backgroundColor: backgroundColor, - color: color, + backgroundColor, + color, opacity: annotated ? 1 : undefined, }}> <FontAwesomeIcon icon={'comment-alt'} /> @@ -1800,33 +1798,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps <SidebarAnnos ref={this._sidebarRef} {...this.props} - fieldKey={this.fieldKey} rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - ScreenToLocalTransform={this.sidebarScreenToLocal} + usePanelWidth={true} nativeWidth={NumCast(this.layoutDoc._nativeWidth)} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} showSidebar={this.SidebarShown} - PanelWidth={this.sidebarWidth} - setHeight={this.setSidebarHeight} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} sidebarAddDocument={this.sidebarAddDocument} moveDocument={this.moveDocument} removeDocument={this.removeDocument} + ScreenToLocalTransform={this.sidebarScreenToLocal} + fieldKey={this.fieldKey} + PanelWidth={this.sidebarWidth} + setHeight={this.setSidebarHeight} /> ) : ( <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}> <ComponentTag - {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + {...this.props} + setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} PanelHeight={this.props.PanelHeight} PanelWidth={this.sidebarWidth} xPadding={0} yPadding={0} - scaleField={this.SidebarKey + '-scale'} + viewField={this.SidebarKey} isAnnotationOverlay={false} select={emptyFunction} + isAnyChildContentActive={returnFalse} NativeDimScaling={this.sidebarContentScaling} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.sidebarRemDocument} diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 9e9b61db3..e691869cc 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -259,7 +259,7 @@ export class RichTextRules { this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); } const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500 }, docId); - DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined); + DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { linkRelationship: 'portal to:portal from' }); const fstate = this.TextBox.EditorView?.state; if (fstate && selection) { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 0afb36214..3589a9065 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { AnimationSym, Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -39,6 +39,7 @@ const { Howl } = require('howler'); export interface pinDataTypes { scrollable?: boolean; + dataviz?: number[]; pannable?: boolean; viewType?: boolean; inkable?: boolean; @@ -59,7 +60,6 @@ export interface PinProps { hidePresBox?: boolean; pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected) pinDocLayout?: boolean; // pin layout info (width/height/x/y) - pinDocContent?: boolean; // pin data info (scroll/pan/zoom/text) pinAudioPlay?: boolean; // pin audio annotation pinData?: pinDataTypes; } @@ -492,6 +492,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { changed = true; } } + if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) { + changed = true; + } if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) { if (bestTarget._scrollTop !== activeItem.presViewScroll) { diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 698a2817e..9a74b5dba 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -312,7 +312,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { @action updateCapturedViewContents = (presTargetDoc: Doc, activeItem: Doc) => { const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc; - PresBox.pinDocView(activeItem, { pinDocContent: true, pinData: PresBox.pinDataTypes(target) }, target); + PresBox.pinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target); }; @computed get recordingIsInOverlay() { |
