diff options
Diffstat (limited to 'src/client/views/collections/CollectionNoteTakingView.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionNoteTakingView.tsx | 702 |
1 files changed, 702 insertions, 0 deletions
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx new file mode 100644 index 000000000..9519e9aaa --- /dev/null +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -0,0 +1,702 @@ +import React = require("react"); +import { CursorProperty } from "csstype"; +import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import { DataSym, Doc, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; +import { Copy, Id, ToScriptString, ToString } from "../../../fields/FieldSymbols"; +import { List } from "../../../fields/List"; +import { listSpec } from "../../../fields/Schema"; +import { SchemaHeaderField } from "../../../fields/SchemaHeaderField"; +import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types"; +import { TraceMobx } from "../../../fields/util"; +import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from "../../../Utils"; +import { Docs, DocUtils } from "../../documents/Documents"; +import { DocumentType } from '../../documents/DocumentTypes'; +import { DragManager, dropActionType } from "../../util/DragManager"; +import { SnappingManager } from "../../util/SnappingManager"; +import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; +import { ContextMenu } from "../ContextMenu"; +import { ContextMenuProps } from "../ContextMenuItem"; +import { LightboxView } from "../LightboxView"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from "../nodes/DocumentView"; +import { StyleProp } from "../StyleProvider"; +import "./CollectionNoteTakingView.scss"; +import CollectionNoteTakingViewDivider from "./CollectionNoteTakingViewDivider"; +import { CollectionNoteTakingViewColumn } from "./CollectionNoteTakingViewColumn"; +import { CollectionSubView } from "./CollectionSubView"; +import { CollectionViewType } from "./CollectionView"; +import { ObjectField } from "../../../fields/ObjectField"; +import { faThumbsDown } from "@fortawesome/free-solid-svg-icons"; +const _global = (window /* browser */ || global /* node */) as any; + +export type collectionNoteTakingViewProps = { + chromeHidden?: boolean; + viewType?: CollectionViewType; + NativeWidth?: () => number; + NativeHeight?: () => number; +}; + +//TODO: somehow need to update the mapping and then have everything else rerender. Maybe with a refresh boolean like +// in Hypermedia? + +@observer +export class CollectionNoteTakingView extends CollectionSubView<Partial<collectionNoteTakingViewProps>>() { + _pivotFieldDisposer?: IReactionDisposer; + _autoHeightDisposer?: IReactionDisposer; + _masonryGridRef: HTMLDivElement | null = null; + _draggerRef = React.createRef<HTMLDivElement>(); + // _docXfs: { height: () => number, width: () => number, noteTakingDocTransform: () => Transform }[] = []; + // @observable _docsByColumnHeader = new Map<string, Doc[]>(); + //TODO: need to make sure that we save the mapping + @observable docsDraggedRowCol: number[] = []; + @observable _cursor: CursorProperty = "grab"; + @observable _scroll = 0; // used to force the document decoration to update when scrolling + @computed get chromeHidden() { return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden); } + @computed get columnHeaders() { return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField), null); } + @computed get pivotField() { return "Col" } + @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => (pair.layout instanceof Doc) && !pair.layout.hidden).map(pair => pair.layout); } + @computed get headerMargin() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); } + @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); } + @computed get yMargin() { return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5); } // 2 * this.gridGap)); } + @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } + @computed get numGroupColumns() { return this.columnHeaders.length; } + @observable columnStartXCoords: number[] = [] + @computed get PanelWidth() {return this.props.PanelWidth()} + @computed get maxColWdith() {return this.props.PanelWidth() - 2 * this.xMargin;} + + // If the user has not yet created any docs (in another view), this will create a single column. Otherwise, + // it will adjust according to the + constructor(props: any) { + super(props); + if (this.columnHeaders === undefined) { + this.layoutDoc._columnHeaders = new List<SchemaHeaderField>([new SchemaHeaderField('New Column')]); + this.columnStartXCoords = [0] + // add all of the docs that have not been added to a column to this new column + } else { + const numHeaders = this.columnHeaders.length + this.resizeColumns(numHeaders) + } + } + + // passed as a prop to the NoteTakingField, which uses this function + // to render the docs you see within an individual column. + children = (docs: Doc[]) => { + TraceMobx(); + return docs.map((d, i) => { + const height = () => this.getDocHeight(d); + const width = () => this.getDocWidth(d); + const style = { width: width(), marginTop: this.gridGap, height: height() }; + return <div className={`collectionNoteTakingView-columnDoc`} key={d[Id]} style={style} > + {this.getDisplayDoc(d, width)} + </div>; + }); + } + + // [CAVEATS] (1) keep track of the offsetting + // (2) documentView gets unmounted as you remove it from the list + get Sections() { + const columnHeaders = this.columnHeaders; + const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); + let docs = this.childDocs + const rowCol = this.docsDraggedRowCol + + // filter out the currently dragged docs from the child docs, since we will insert them later + if (rowCol.length && DragManager.docsBeingDragged.length) { + const docIdsToRemove = new Set() + DragManager.docsBeingDragged.forEach(d => { + docIdsToRemove.add(d[Id]) + }) + docs = docs.filter(d => !docIdsToRemove.has(d[Id])) + } + + // this will sort the docs into the correct columns (minus the ones you're currently dragging) + docs.map(d => { + if (!d[this.pivotField]) { + d[this.pivotField] = columnHeaders.length > 0 ? columnHeaders[0].heading : `New Column` + }; + const sectionValue = d[this.pivotField] as object; + + // look for if header exists already + const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString()); + if (existingHeader) { + sections.get(existingHeader)!.push(d); + } + }); + + // now we add back in the docs that we're dragging + if (rowCol.length && DragManager.docsBeingDragged.length) { + const colHeader = columnHeaders[rowCol[1]] + // TODO: get the actual offset that occurs if the docs were in that column + const offset = 0 + sections.get(colHeader)?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged) + } + return sections; + } + + componentDidMount() { + super.componentDidMount?.(); + // reset section headers when a new filter is inputted + this._pivotFieldDisposer = reaction( + () => this.pivotField, + () => this.layoutDoc._columnHeaders = new List() + ); + + this._autoHeightDisposer = reaction(() => this.layoutDoc._autoHeight, + autoHeight => autoHeight && this.props.setHeight(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), + this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))))); + } + + componentWillUnmount() { + super.componentWillUnmount(); + this._pivotFieldDisposer?.(); + this._autoHeightDisposer?.(); + } + + @action + moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => { + return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false; + } + + createRef = (ele: HTMLDivElement | null) => { + this._masonryGridRef = ele; + this.createDashEventsTarget(ele!); //so the whole grid is the drop target? + } + + @computed get onChildClickHandler() { return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); } + @computed get onChildDoubleClickHandler() { return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } + + addDocTab = (doc: Doc, where: string) => { + if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { + this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]); + return true; + } + return this.props.addDocTab(doc, where); + } + + scrollToBottom = () => { + smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight); + } + + // let's dive in and get the actual document we want to drag/move around + focusDocument = (doc: Doc, options?: DocFocusOptions) => { + Doc.BrushDoc(doc); + + let focusSpeed = 0; + const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName("documentView-node")).find((node: any) => node.id === doc[Id]); + if (found) { + const top = found.getBoundingClientRect().top; + const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); + if (Math.floor(localTop[1]) !== 0) { + smoothScroll(focusSpeed = doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500, this._mainCont!, localTop[1] + this._mainCont!.scrollTop); + } + } + const endFocus = async (moved: boolean) => options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing; + this.props.focus(this.rootDoc, { + willZoom: options?.willZoom, scale: options?.scale, afterFocus: (didFocus: boolean) => + new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)) + }); + } + + styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => { + if (property === StyleProp.Opacity && doc) { + if (this.props.childOpacity) { + return this.props.childOpacity(); + } + if (this.Document._currentFrame !== undefined) { + return CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document._currentFrame))?.opacity; + } + } + return this.props.styleProvider?.(doc, props, property); + } + + isContentActive = () => this.props.isSelected() || this.props.isContentActive(); + + // rules for displaying the documents + getDisplayDoc(doc: Doc, width: () => number) { + const dataDoc = (!doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS) ? undefined : this.props.DataDoc; + const height = () => this.getDocHeight(doc); + let dref: Opt<DocumentView>; + const noteTakingDocTransform = () => this.getDocTransform(doc, dref); + return <DocumentView ref={r => dref = r || undefined} + Document={doc} + DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} + renderDepth={this.props.renderDepth + 1} + PanelWidth={width} + PanelHeight={height} + styleProvider={this.styleProvider} + layerProvider={this.props.layerProvider} + docViewPath={this.props.docViewPath} + fitWidth={this.props.childFitWidth} + isContentActive={emptyFunction} + originalBackgroundColor={StrCast(doc.backgroundColor)} + //TODO: change this from a prop to a parameter passed into a function + isNoteTakingView={true} + isDocumentActive={this.isContentActive} + LayoutTemplate={this.props.childLayoutTemplate} + LayoutTemplateString={this.props.childLayoutString} + freezeDimensions={this.props.childFreezeDimensions} + NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || doc._fitWidth && !Doc.NativeWidth(doc) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || doc._fitWidth && !Doc.NativeHeight(doc) ? height : undefined} + dontCenter={this.props.childIgnoreNativeSize ? "xy" : undefined} + dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} + rootSelected={this.rootSelected} + showTitle={this.props.childShowTitle} + dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} + onClick={this.onChildClickHandler} + onDoubleClick={this.onChildDoubleClickHandler} + ScreenToLocalTransform={noteTakingDocTransform} + focus={this.focusDocument} + docFilters={this.childDocFilters} + hideDecorationTitle={this.props.childHideDecorationTitle?.()} + hideResizeHandles={this.props.childHideResizeHandles?.()} + hideTitle={this.props.childHideTitle?.()} + docRangeFilters={this.childDocRangeFilters} + searchFilterDocs={this.searchFilterDocs} + ContainingCollectionDoc={this.props.CollectionView?.props.Document} + ContainingCollectionView={this.props.CollectionView} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + removeDocument={this.props.removeDocument} + contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)} + whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} + addDocTab={this.addDocTab} + bringToFront={returnFalse} + scriptContext={this.props.scriptContext} + pinToPres={this.props.pinToPres} + />; + } + + // This is used to get the coordinates of a document when we go from a view like freeform to columns + getDocTransform(doc: Doc, dref?: DocumentView) { + const y = this._scroll; // required for document decorations to update when the text box container is scrolled + const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined); + // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off + return new Transform(- translateX + (dref?.centeringX || 0), - translateY + (dref?.centeringY || 0), 1).scale(this.props.ScreenToLocalTransform().Scale); + } + + // how to get the width of a document. Currently returns the width of the column (minus margins) + // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...) + getDocWidth(d: Doc) { + const heading = d[this.pivotField] as object + const castedSectionValue = heading.toString() + const existingHeader = this.columnHeaders.find(sh => sh.heading === (castedSectionValue)); + const colStartXCoords = this.columnStartXCoords + if (!existingHeader) { + return 1000 + } + const index = this.columnHeaders.indexOf(existingHeader) + const endColValue = index == this.columnHeaders.length - 1 ? this.PanelWidth : this.columnStartXCoords[index+1] + const maxWidth = endColValue - colStartXCoords[index] - 3 * this.xMargin + if (d.type === DocumentType.RTF) { + return maxWidth + } + const width = d[WidthSym]() + return width < maxWidth ? width : maxWidth + } + + // how to get the height of a document. Nothing special here. + getDocHeight(d?: Doc) { + if (!d || d.hidden) return 0; + const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); + const childDataDoc = (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc; + const maxHeight = (lim => lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim)(NumCast(this.layoutDoc.childLimitHeight, -1)); + const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0); + const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0); + if (nw && nh) { + // const colWid = this.columnWidth / this.numGroupColumns; + // const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid); + const docWid = this.getDocWidth(d) + return Math.min( + maxHeight, + docWid * nh / nw); + } + const childHeight = NumCast(childLayoutDoc._height); + const panelHeight = (childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin; + return Math.min(childHeight, maxHeight, panelHeight); + } + + // called when a column is either added or deleted. This function creates n evenly spaced columns + resizeColumns = (n: number) => { + const totalWidth = this.PanelWidth + const dividerWidth = 32 + const totaldividerWidth = (n - 1) * dividerWidth + const colWidth = (totalWidth - totaldividerWidth) / n + const newColXCoords: number[] = [] + let colStart = 0 + for (let i = 0; i < n; i++) { + newColXCoords.push(colStart) + colStart += colWidth + dividerWidth + } + this.columnStartXCoords = newColXCoords + } + + // This function is used to preview where a document will drop in a column once a drag is complete. + @action + onPointerOver = (e: React.PointerEvent) => { + if (DragManager.docsBeingDragged.length && this.childDocList) { + // get the current docs for the column based on the mouse's x coordinate + // will use again later, which is why we're saving as local + const xCoord = e.clientX - 2 * this.gridGap + const colDocs = this.getDocsFromXCoord(xCoord) + // get the index for where you need to insert the doc you are currently dragging + const clientY = e.clientY + let dropInd = -1; + // unsure whether we still want this dropAfter field + // let dropAfter = 0; + // manually set to 140, because not sure how to get exact value + let pos0 = 140 + colDocs.forEach((doc, i) => { + const noteTakingDocTransform = () => this.getDocTransform(doc); + let pos1 = noteTakingDocTransform().inverse().transformPoint(0, this.getDocHeight(doc) + 2 * this.gridGap)[1]; + pos1 += pos0 + // updating drop position based on y coordinates + const yCoordInBetween = clientY > pos0 && (clientY < pos1 || i == colDocs.length - 1) + if (yCoordInBetween) { + dropInd = i; + // dropAfter = 0; + if (clientY > (pos0 + pos1) / 2) { + // dropAfter = 1; + } + } + pos0 = pos1 + }) + // we alter the pivot fields of the docs in case they are moved to a new column. + const colIndex = this.getColumnFromXCoord(xCoord) + const colHeader = StrCast(this.columnHeaders[colIndex].heading) + DragManager.docsBeingDragged.forEach(d => d[this.pivotField] = colHeader) + // used to notify sections to re-render + // console.log([dropInd, this.getColumnFromXCoord(xCoord)]) + this.docsDraggedRowCol = [dropInd, this.getColumnFromXCoord(xCoord)] + } + } + + // returns the column index for a given x-coordinate + getColumnFromXCoord = (xCoord: number): number => { + const numColumns = this.columnHeaders.length + const coords = this.columnStartXCoords.slice() + coords.push(this.PanelWidth) + let colIndex = 0 + for (let i = 0; i < numColumns; i++) { + if (xCoord > coords[i] && xCoord < coords[i + 1]) { + colIndex = i + break + } + } + return colIndex + } + + // returns the docs of a column based on the x-coordinate provided. + getDocsFromXCoord = (xCoord: number): Doc[] => { + const colIndex = this.getColumnFromXCoord(xCoord) + const colHeader = StrCast(this.columnHeaders[colIndex].heading) + // const docs = this.childDocList + const docs = this.childDocs + const docsMatchingHeader: Doc[] = [] + if (docs) { + docs.map(d => { + if (d instanceof Promise) return; + const sectionValue = d[this.pivotField] as object; + if (sectionValue.toString() == colHeader) { + docsMatchingHeader.push(d) + } + }) + } + return docsMatchingHeader; + } + + @undoBatch + @action + onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData) { + if (super.onInternalDrop(e, de)) { + DragManager.docsBeingDragged = [] + // this.docsDraggedRowCol = [] + // filter out the currently dragged docs from the child docs, since we will insert them later + const rowCol = this.docsDraggedRowCol + const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= this.childDocs.length); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note). + const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments; + + // const docs = this.childDocs + const docs = this.childDocList + if (docs && newDocs.length) { + // remove the dragged documents from the childDocList + newDocs.filter(d => docs.indexOf(d) !== -1).forEach(d => docs.splice(docs.indexOf(d), 1)) + // if the doc starts a columnm (or the drop index is undefined), we can just push it to the front. Otherwise we need to add it to the column properly + //TODO: you need to update childDocList instead. It seems that childDocs is a copy of the actual array we want to modify + if (rowCol[0] <= 0) { + docs.splice(0, 0, ...newDocs) + } else { + const colDocs = this.getDocsFromXCoord(de.x) + const previousDoc = colDocs[rowCol[0] - 1] + const previousDocIndex = docs.indexOf(previousDoc) + console.log(`docs: ${previousDocIndex}`) + docs.splice(previousDocIndex + 1, 0, ...newDocs) + } + } + } + } // it seems like we're creating a link here. Weird. I didn't know that you could establish links by dragging + else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { + const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, _fitWidth: true, title: "dropped annotation" }); + this.props.addDocument?.(source); + de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, "doc annotation", ""); // TODODO this is where in text links get passed + e.stopPropagation(); + } + else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + return false; + } + + @undoBatch + internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) { + const dropCreator = annoDragData.dropDocCreator; + annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => { + const dropDoc = dropCreator(annotationOn); + return dropDoc || this.rootDoc; + }; + return true; + } + + // when dropping outside of the current noteTaking context (like a new tab, freeform view, etc...) + @undoBatch + @action + onExternalDrop = async (e: React.DragEvent): Promise<void> => { + const where = [e.clientX, e.clientY]; + let targInd = -1; + const docs = this.getDocsFromXCoord(where[0]) + docs.map((d, i) => { + const pos0 = this.getDocTransform(d).inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + const pos1 = this.getDocTransform(d).inverse().transformPoint(this.getDocWidth(d), this.getDocHeight(d)); + // const pos0 = cd.noteTakingDocTransform().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + // const pos1 = cd.noteTakingDocTransform().inverse().transformPoint(cd.width(), cd.height()); + if (where[0] > pos0[0] && where[0] < pos1[0] && where[1] > pos0[1] && where[1] < pos1[1]) { + targInd = i; + } + }) + // this._docXfs.map((cd, i) => { + // const pos = cd.noteTakingDocTransform().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap); + // const pos1 = cd.noteTakingDocTransform().inverse().transformPoint(cd.width(), cd.height()); + // if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { + // targInd = i; + // } + // }); + super.onExternalDrop(e, {}, () => { + if (targInd !== -1) { + const newDoc = this.childDocs[this.childDocs.length - 1]; + const docs = this.childDocList; + if (docs) { + docs.splice(docs.length - 1, 1); + docs.splice(targInd, 0, newDoc); + } + } + }); + } + + // setDocsForColHeader = (key: string, docs: Doc[]) => { + // this._docsByColumnHeader = new Map(this._docsByColumnHeader.set(key, docs)) + // } + + headings = () => Array.from(this.Sections); + + refList: any[] = []; + + sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { + const type = "number"; + return <CollectionNoteTakingViewColumn + unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)} + observeHeight={ref => { + if (ref) { + this.refList.push(ref); + this.observer = new _global.ResizeObserver(action((entries: any) => { + if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { + const height = this.headerMargin + + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", ""))))); + if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) { + this.props.setHeight(height); + } + } + })); + this.observer.observe(ref); + } + }} + addDocument={this.addDocument} + // docsByColumnHeader={this._docsByColumnHeader} + // setDocsForColHeader={this.setDocsForColHeader} + chromeHidden={this.chromeHidden} + columnHeaders={this.columnHeaders} + Document={this.props.Document} + DataDoc={this.props.DataDoc} + resizeColumns={this.resizeColumns.bind(this)} + renderChildren={this.children} + numGroupColumns={this.numGroupColumns} + gridGap={this.gridGap} + pivotField={this.pivotField} + columnStartXCoords={this.columnStartXCoords} + maxColWidth={this.maxColWdith} + PanelWidth={this.PanelWidth} + key={heading?.heading ?? ""} + headings={this.headings} + heading={heading?.heading ?? ""} + headingObject={heading} + docList={docList} + yMargin={this.yMargin} + type={type} + createDropTarget={this.createDashEventsTarget} + screenToLocalTransform={this.props.ScreenToLocalTransform} + editableViewProps={{ + GetValue: () => "", + SetValue: this.addGroup, + contents: "+ New Column" + }} + />; + } + + // called when adding a new columnHeader + @action + addGroup = (value: string) => { + if (value && this.columnHeaders) { + this.resizeColumns(this.columnHeaders.length + 1) + const schemaHdrField = new SchemaHeaderField(value); + this.columnHeaders.push(schemaHdrField); + DocUtils.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: "schemaHdrField.color" }]); + return true; + } + return false; + } + + sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => { + const descending = StrCast(this.layoutDoc._columnsSort) === "descending"; + const firstEntry = descending ? b : a; + const secondEntry = descending ? a : b; + return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; + } + + onContextMenu = (e: React.MouseEvent): void => { + // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout + if (!e.isPropagationStopped()) { + const subItems: ContextMenuProps[] = []; + subItems.push({ description: `${this.layoutDoc._columnsFill ? "Variable Size" : "Autosize"} Column`, event: () => this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill, icon: "plus" }); + subItems.push({ description: `${this.layoutDoc._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" }); + subItems.push({ description: "Clear All", event: () => this.dataDoc.data = new List([]), icon: "times" }); + ContextMenu.Instance.addItem({ description: "Options...", subitems: subItems, icon: "eye" }); + } + } + + // used to reset column sizes when using the drag handlers + @action + setColumnStartXCoords = (movementX: number, colIndex: number) => { + const coords = [...this.columnStartXCoords] + coords[colIndex] += movementX + this.columnStartXCoords = coords + } + + @computed get renderedSections() { + TraceMobx(); + // let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; + // if (this.pivotField) { + // const entries = Array.from(this.Sections.entries()); + // sections = this.layoutDoc._columnsSort ? entries.sort(this.sortFunc) : entries; + // } + const entries = Array.from(this.Sections.entries()); + const sections = entries.sort(this.sortFunc) + const eles: JSX.Element[] = [] + for (let i = 0; i < sections.length; i++) { + const col = this.sectionNoteTaking(sections[i][0], sections[i][1]) + eles.push(col) + if (i < sections.length - 1) { + eles.push( + <CollectionNoteTakingViewDivider + index={i + 1} + setColumnStartXCoords={this.setColumnStartXCoords.bind(this)} + xMargin={this.xMargin} + /> + ) + } + } + return eles + } + + @computed get buttonMenu() { + const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + // TODO:glr Allow support for multiple buttons + if (menuDoc) { + const width: number = NumCast(menuDoc._width, 30); + const height: number = NumCast(menuDoc._height, 30); + return (<div className="buttonMenu-docBtn" + style={{ width: width, height: height }}> + <DocumentView + Document={menuDoc} + DataDoc={menuDoc} + isContentActive={this.props.isContentActive} + isDocumentActive={returnTrue} + addDocument={this.props.addDocument} + moveDocument={this.props.moveDocument} + addDocTab={this.props.addDocTab} + pinToPres={emptyFunction} + rootSelected={this.props.isSelected} + removeDocument={this.props.removeDocument} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={() => 35} + PanelHeight={() => 35} + renderDepth={this.props.renderDepth} + focus={emptyFunction} + styleProvider={this.props.styleProvider} + layerProvider={this.props.layerProvider} + docViewPath={returnEmptyDoclist} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + docFilters={this.props.docFilters} + docRangeFilters={this.props.docRangeFilters} + searchFilterDocs={this.props.searchFilterDocs} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + /> + </div> + ); + } + } + + + @computed get nativeWidth() { return this.props.NativeWidth?.() ?? Doc.NativeWidth(this.layoutDoc); } + @computed get nativeHeight() { return this.props.NativeHeight?.() ?? Doc.NativeHeight(this.layoutDoc); } + + @computed get scaling() { return !this.nativeWidth ? 1 : this.props.PanelHeight() / this.nativeHeight; } + + @computed get backgroundEvents() { return SnappingManager.GetIsDragging(); } + observer: any; + render() { + TraceMobx(); + const buttonMenu = this.rootDoc.buttonMenu; + const noviceExplainer = this.rootDoc.explainer; + return ( + <> + {buttonMenu || noviceExplainer ? <div className="documentButtonMenu"> + {buttonMenu ? this.buttonMenu : null} + {Doc.UserDoc().noviceMode && noviceExplainer ? + <div className="documentExplanation"> + {noviceExplainer} + </div> + : null + } + </div> : null} + <div className="collectionNoteTakingView" + ref={this.createRef} + style={{ + overflowY: this.props.isContentActive() ? "auto" : "hidden", + background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), + pointerEvents: this.backgroundEvents ? "all" : undefined + }} + onScroll={action(e => this._scroll = e.currentTarget.scrollTop)} + onPointerOver={this.onPointerOver} + onDrop={this.onExternalDrop.bind(this)} + onContextMenu={this.onContextMenu} + onWheel={e => this.props.isContentActive(true) && e.stopPropagation()} + > + {this.renderedSections} + </div> + </> + + ); + } +}
\ No newline at end of file |
