diff options
Diffstat (limited to 'src/client/views')
| -rw-r--r-- | src/client/views/MainView.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DataVizBox.tsx | 106 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx | 2368 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss (renamed from src/client/views/nodes/DataVizBox/DocCreatorMenu.scss) | 30 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx | 1437 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx | 117 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx | 66 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx | 79 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx | 147 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx | 139 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx | 752 | ||||
| -rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx | 22 |
12 files changed, 2785 insertions, 2480 deletions
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 631c10b89..12dd80ec5 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -59,7 +59,7 @@ import { ImageLabelHandler } from './collections/collectionFreeForm/ImageLabelHa import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu'; import { CollectionLinearView } from './collections/collectionLinear'; import { LinkMenu } from './linking/LinkMenu'; -import { DocCreatorMenu } from './nodes/DataVizBox/DocCreatorMenu'; +import { DocCreatorMenu } from './nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu'; import { SchemaCSVPopUp } from './nodes/DataVizBox/SchemaCSVPopUp'; import { DocButtonState } from './nodes/DocumentLinksButton'; import { DocumentView, DocumentViewInternal } from './nodes/DocumentView'; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index fa3ab73a7..d5e37b3b5 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -32,11 +32,12 @@ import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { FocusViewOptions } from '../FocusViewOptions'; import './DataVizBox.scss'; -import { Col, DataVizTemplateInfo, DocCreatorMenu, LayoutType, TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu'; +import { Col, DataVizTemplateInfo, DocCreatorMenu, LayoutType} from './DocCreatorMenu/DocCreatorMenu'; import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; +import { TemplateFieldSize, TemplateFieldType } from './DocCreatorMenu/TemplateBackend'; export enum DataVizView { TABLE = 'table', @@ -171,7 +172,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const colInfo = this.colsInfo.get(colTitle); if (colInfo) { colInfo.title = newTitle; - console.log(colInfo.title); } else { this.colsInfo.set(colTitle, { title: newTitle, desc: '', type: TemplateFieldType.UNSET, sizes: [] }); } @@ -510,7 +510,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { openDocCreatorMenu = (x: number, y: number) => { DocCreatorMenu.Instance.toggleDisplay(x, y); DocCreatorMenu.Instance.setDataViz(this); - DocCreatorMenu.Instance.setTemplateDocs(this.getPossibleTemplates()); }; specificContextMenu = (e: React.MouseEvent) => { @@ -622,107 +621,6 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } }; - getPossibleTemplates = (): Doc[] => { - const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(this.Document).map(d => DocCast(LinkManager.getOppositeAnchor(d, this.Document))); - const linkedCollections: Doc[] = linkedDocs.filter(doc => doc.type === 'config').map(doc => DocCast(doc.annotationOn)); - const isColumnTitle = (title: string): boolean => { - const colTitles: string[] = Object.keys(this.records[0]); - for (let i = 0; i < colTitles.length; ++i) { - if (colTitles[i] === title) { - return true; - } - } - return false; - }; - const isValidTemplate = (collection: Doc) => { - const childDocs = DocListCast(collection[Doc.LayoutFieldKey(collection)]); - for (let i = 0; i < childDocs.length; ++i) { - if (isColumnTitle(String(childDocs[i].title))) return true; - } - return false; - }; - return linkedCollections.filter(col => isValidTemplate(col)); - }; - - ApplyTemplateTo = (templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) => { - if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) { - if (target.resolvedDataDoc) { - target[targetKey] = new PrefetchProxy(templateDoc); - } else { - titleTarget && (Doc.GetProto(target).title = titleTarget); - const setDoc = [AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target; - setDoc[targetKey] = new PrefetchProxy(templateDoc); - } - } - return target; - }; - - applyLayout = (templateInfo: DataVizTemplateInfo, docs: Doc[]) => { - if (templateInfo.layout.type === LayoutType.Stacked) return; - const columns: number = templateInfo.columns; - const xGap: number = templateInfo.layout.xMargin; - const yGap: number = templateInfo.layout.yMargin; - // const repeat: number = templateInfo.layout.repeat; - const startX: number = templateInfo.referencePos.x; - const startY: number = templateInfo.referencePos.y; - const templWidth = Number(templateInfo.doc._width); - const templHeight = Number(templateInfo.doc._height); - - let i: number = 0; - let docsChanged: number = 0; - let curX: number = startX; - let curY: number = startY; - - while (docsChanged < docs.length) { - while (i < columns && docsChanged < docs.length) { - docs[docsChanged].x = curX; - docs[docsChanged].y = curY; - curX += templWidth + xGap; - ++docsChanged; - ++i; - } - - i = 0; - curX = startX; - curY += templHeight + yGap; - } - }; - - // @action addSavedLayout = (layout: DataVizTemplateLayout) => { - // const saved = Cast(this.layoutDoc.dataViz_savedTemplates, listSpec('RefField')); - - // } - - @action - createDocsFromTemplate = (templateInfo: DataVizTemplateInfo) => { - if (!templateInfo.doc) return; - const mainCollection = this.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; - const fields: string[] = Array.from(Object.keys(this.records[0])); - const selectedRows = NumListCast(this.layoutDoc.dataViz_selectedRows); - const docs: Doc[] = selectedRows.map(row => { - const values: string[] = []; - fields.forEach(col => values.push(this.records[row][col])); - - const proto = new Doc(); - proto.author = ClientUtils.CurrentUserEmail(); - values.forEach((val, i) => { - proto[fields[i]] = val as FieldType; - }); - - const target = Doc.MakeDelegate(proto); - const targetKey = StrCast(templateInfo.doc!.layout_fieldKey, 'layout'); - const applied = this.ApplyTemplateTo(templateInfo.doc!, target, targetKey, templateInfo.doc!.title + `${row}`); - target.layout_fieldKey = targetKey; - - //this.applyImagesTo(target, fields); - return applied; - }); - - docs.forEach(doc => mainCollection.addDocument(doc)); - - this.applyLayout(templateInfo, docs); - }; - /** * creates a new dataviz document filter from this one * it appears to the right of this document, with the diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx deleted file mode 100644 index 4d9173211..000000000 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx +++ /dev/null @@ -1,2368 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Colors } from '@dash/components'; -import { action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; -import { observer } from 'mobx-react'; -import { IDisposer } from 'mobx-utils'; -import * as React from 'react'; -import ReactLoading from 'react-loading'; -import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils'; -import { emptyFunction } from '../../../../Utils'; -import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../fields/Doc'; -import { Id } from '../../../../fields/FieldSymbols'; -import { Cast, DocCast, ImageCast, StrCast } from '../../../../fields/Types'; -import { ImageField } from '../../../../fields/URLField'; -import { Networking } from '../../../Network'; -import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; -import { Docs } from '../../../documents/Documents'; -import { DragManager } from '../../../util/DragManager'; -import { MakeTemplate } from '../../../util/DropConverter'; -import { SnappingManager } from '../../../util/SnappingManager'; -import { UndoManager, undoable } from '../../../util/UndoManager'; -import { LightboxView } from '../../LightboxView'; -import { ObservableReactComponent } from '../../ObservableReactComponent'; -import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; -import { DocumentView, DocumentViewInternal } from '../DocumentView'; -import { FieldViewProps } from '../FieldView'; -import { OpenWhere } from '../OpenWhere'; -import { DataVizBox } from './DataVizBox'; -import './DocCreatorMenu.scss'; -import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider'; -import { Transform } from '../../../util/Transform'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { Upload } from '../../../../server/SharedMediaTypes'; - -export enum LayoutType { - Stacked = 'stacked', - Grid = 'grid', - Row = 'row', - Column = 'column', - Custom = 'custom', -} - -@observer -export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { - static Instance: DocCreatorMenu; - - private _disposers: { [name: string]: IDisposer } = {}; - - private _ref: HTMLDivElement | null = null; - - @observable _templateDocs: Doc[] = []; - @observable _selectedTemplate: Doc | undefined = undefined; - @observable _columns: Col[] = []; - @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = []; - - @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.Grid, yMargin: 0, xMargin: 0, repeat: 0 }; - @observable _layoutPreview: boolean = true; - @observable _layoutPreviewScale: number = 1; - @observable _savedLayouts: DataVizTemplateLayout[] = []; - @observable _expandedPreview: { icon: ImageField; doc: Doc } | undefined = undefined; - - @observable _suggestedTemplates: Doc[] = []; - @observable _GPTOpt: boolean = false; - @observable _userPrompt: string = ''; - @observable _callCount: number = 0; - @observable _GPTLoading: boolean = false; - - @observable _pageX: number = 0; - @observable _pageY: number = 0; - @observable _indicatorX: number | undefined = undefined; - @observable _indicatorY: number | undefined = undefined; - - @observable _hoveredLayoutPreview: number | undefined = undefined; - @observable _mouseX: number = -1; - @observable _mouseY: number = -1; - @observable _startPos?: { x: number; y: number }; - @observable _shouldDisplay: boolean = false; - - @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates'; - @observable _dragging: boolean = false; - @observable _draggingIndicator: boolean = false; - @observable _dataViz?: DataVizBox; - @observable _interactionLock: any; - @observable _snapPt: any; - @observable _resizeHdlId: string = ''; - @observable _resizing: boolean = false; - @observable _offset: { x: number; y: number } = { x: 0, y: 0 }; - @observable _resizeUndo: UndoManager.Batch | undefined = undefined; - @observable _initDimensions: { width: number; height: number; x?: number; y?: number } = { width: 300, height: 400, x: undefined, y: undefined }; - @observable _menuDimensions: { width: number; height: number } = { width: 400, height: 400 }; - @observable _editing: boolean = false; - - constructor(props: any) { - super(props); - makeObservable(this); - DocCreatorMenu.Instance = this; - //setTimeout(() => this.generateTemplates('')); - } - - @action setDataViz = (dataViz: DataVizBox) => { - this._dataViz = dataViz; - }; - @action setTemplateDocs = (docs: Doc[]) => { - this._templateDocs = docs.map(doc => (doc.annotationOn ? DocCast(doc.annotationOn) : doc)); - }; - @action setGSuggestedTemplates = (docs: Doc[]) => { - this._suggestedTemplates = docs; - }; - - @computed get docsToRender() { - return this._selectedTemplate ? NumListCast(this._dataViz?.layoutDoc.dataViz_selectedRows) : []; - } - - @computed get rowsCount() { - switch (this._layout.type) { - case LayoutType.Row: - case LayoutType.Stacked: - return 1; - case LayoutType.Column: - return this.docsToRender.length; - case LayoutType.Grid: - return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0; - default: - return 0; - } - } - - @computed get columnsCount() { - switch (this._layout.type) { - case LayoutType.Row: - return this.docsToRender.length; - case LayoutType.Column: - case LayoutType.Stacked: - return 1; - case LayoutType.Grid: - return this._layout.columns ?? 0; - default: - return 0; - } - } - - @computed get selectedFields() { - return StrListCast(this._dataViz?.layoutDoc._dataViz_axes); - } - - @computed get fieldsInfos(): Col[] { - const colInfo = this._dataViz?.colsInfo; - return this.selectedFields - .map(field => { - const fieldInfo = colInfo?.get(field); - - const col: Col = { - title: field, - type: fieldInfo?.type ?? TemplateFieldType.UNSET, - desc: fieldInfo?.desc ?? '', - sizes: fieldInfo?.sizes ?? [TemplateFieldSize.MEDIUM], - }; - - if (fieldInfo?.defaultContent !== undefined) { - col.defaultContent = fieldInfo.defaultContent; - } - - return col; - }) - .concat(this._columns); - } - - @computed get canMakeDocs() { - return this._selectedTemplate !== undefined && this._layout !== undefined; - } - - get bounds(): { t: number; b: number; l: number; r: number } { - const rect = this._ref?.getBoundingClientRect(); - const bounds = { t: rect?.top ?? 0, b: rect?.bottom ?? 0, l: rect?.left ?? 0, r: rect?.right ?? 0 }; - return bounds; - } - - setUpButtonClick = (e: any, func: Function) => { - setupMoveUpEvents( - this, - e, - returnFalse, - emptyFunction, - undoable(clickEv => { - clickEv.stopPropagation(); - clickEv.preventDefault(); - func(); - }, 'create docs') - ); - }; - - @action - onPointerDown = (e: PointerEvent) => { - this._mouseX = e.clientX; - this._mouseY = e.clientY; - }; - - @action - onPointerUp = (e: PointerEvent) => { - if (this._resizing) { - this._initDimensions.width = this._menuDimensions.width; - this._initDimensions.height = this._menuDimensions.height; - this._initDimensions.x = this._pageX; - this._initDimensions.y = this._pageY; - document.removeEventListener('pointermove', this.onResize); - SnappingManager.SetIsResizing(undefined); - this._resizing = false; - } - if (this._dragging) { - document.removeEventListener('pointermove', this.onDrag); - this._dragging = false; - } - if (e.button !== 2 && !e.ctrlKey) return; - const curX = e.clientX; - const curY = e.clientY; - if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) { - this._shouldDisplay = false; - } - }; - - componentDidMount() { - document.addEventListener('pointerdown', this.onPointerDown, true); - document.addEventListener('pointerup', this.onPointerUp); - this._disposers.templates = reaction( - () => this._templateDocs.slice(), - docs => this.updateIcons(docs) - ); - this._disposers.gpt = reaction( - () => this._suggestedTemplates.slice(), - docs => this.updateIcons(docs) - ); - //this._disposers.columns = reaction(() => this._dataViz?.layoutDoc._dataViz_axes, () => {this.generateTemplates('')}) - this._disposers.lightbox = reaction( - () => LightboxView.LightboxDoc(), - doc => { - // NOTE: bcz; commented this out because the doc creator would appear everytime I close out of the lightbox - // doc ? this._shouldDisplay && this.closeMenu() : !this._shouldDisplay && this.openMenu(); - } - ); - //this._disposers.fields = reaction(() => this._dataViz?.axes, cols => this._selectedCols = cols?.map(col => { return {title: col, type: '', desc: ''}})) - } - - componentWillUnmount() { - Object.values(this._disposers).forEach(disposer => disposer?.()); - document.removeEventListener('pointerdown', this.onPointerDown, true); - document.removeEventListener('pointerup', this.onPointerUp); - } - - updateIcons = (docs: Doc[]) => { - console.log('called'); - docs.map(this.getIcon); - }; - - @action - updateSelectedCols = (cols: string[]) => { - this._selectedCols; - }; - - @action - toggleDisplay = (x: number, y: number) => { - if (this._shouldDisplay) { - this._shouldDisplay = false; - } else { - this._pageX = x; - this._pageY = y; - this._shouldDisplay = true; - } - }; - - @action - closeMenu = () => { - this._shouldDisplay = false; - }; - - @action - openMenu = () => { - const allTemplates = this._templateDocs.concat(this._suggestedTemplates); - this._shouldDisplay = true; - this.updateIcons(allTemplates); - }; - - @action - onResizePointerDown = (e: React.PointerEvent): void => { - this._resizing = true; - document.addEventListener('pointermove', this.onResize); - SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them - e.stopPropagation(); - const id = (this._resizeHdlId = e.currentTarget.className); - const pad = id.includes('Left') || id.includes('Right') ? Number(getComputedStyle(e.target as any).width.replace('px', '')) / 2 : 0; - const bounds = e.currentTarget.getBoundingClientRect(); - this._offset = { - x: id.toLowerCase().includes('left') ? bounds.right - e.clientX - pad : bounds.left - e.clientX + pad, // - y: id.toLowerCase().includes('top') ? bounds.bottom - e.clientY - pad : bounds.top - e.clientY + pad, - }; - this._resizeUndo = UndoManager.StartBatch('drag resizing'); - this._snapPt = { x: e.pageX, y: e.pageY }; - }; - - @action - onResize = (e: any): boolean => { - const dragHdl = this._resizeHdlId.split(' ')[1]; - const thisPt = DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y); - - const { scale, refPt, transl } = this.getResizeVals(thisPt, dragHdl); - !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate) - this._interactionLock = true; - const scaleAspect = {x: scale.x, y: scale.y}; - this.resizeView(refPt, scaleAspect, transl); // prettier-ignore - await new Promise<any>(res => { setTimeout(() => { res(this._interactionLock = undefined)})}); - }); // prettier-ignore - return true; - }; - - @action - onDrag = (e: any): boolean => { - this._pageX = e.pageX - (this._startPos?.x ?? 0); - this._pageY = e.pageY - (this._startPos?.y ?? 0); - this._initDimensions.x = this._pageX; - this._initDimensions.y = this._pageY; - return true; - }; - - getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => { - const [w, h] = [this._initDimensions.width, this._initDimensions.height]; - const [moveX, moveY] = [thisPt.x - this._snapPt.x, thisPt.y - this._snapPt.y]; - let vals: { scale: { x: number; y: number }; refPt: [number, number]; transl: { x: number; y: number } }; - switch (dragHdl) { - case 'topLeft': vals = { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.r, this.bounds.b], transl: {x: moveX, y: moveY } }; break; - case 'topRight': vals = { scale: { x: 1 + moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; - case 'top': vals = { scale: { x: 1, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; - case 'left': vals = { scale: { x: 1 - moveX / w, y: 1 }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; - case 'bottomLeft': vals = { scale: { x: 1 - moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; - case 'right': vals = { scale: { x: 1 + moveX / w, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; - case 'bottomRight':vals = { scale: { x: 1 + moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; - case 'bottom': vals = { scale: { x: 1, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; - default: vals = { scale: { x: 1, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; - } // prettier-ignore - return vals; - }; - - resizeView = (refPt: number[], scale: { x: number; y: number }, translation: { x: number; y: number }) => { - const refCent = [refPt[0], refPt[1]]; // fixed reference point for resize (ie, a point that doesn't move) - if (this._initDimensions.x === undefined) this._initDimensions.x = this._pageX; - if (this._initDimensions.y === undefined) this._initDimensions.y = this._pageY; - const { height, width, x, y } = this._initDimensions; - - this._menuDimensions.width = Math.max(300, scale.x * width); - this._menuDimensions.height = Math.max(200, scale.y * height); - this._pageX = x + translation.x; - this._pageY = y + translation.y; - }; - - async getIcon(doc: Doc) { - const docView = DocumentView.getDocumentView(doc); - if (docView) { - docView.ComponentView?.updateIcon?.(); - return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 500)); - } - return undefined; - } - - @action updateSelectedTemplate = (template: Doc) => { - if (this._selectedTemplate === template) { - this._selectedTemplate = undefined; - return; - } else { - this._selectedTemplate = template; - MakeTemplate(template); - } - }; - - @action updateSelectedSavedLayout = (layout: DataVizTemplateLayout) => { - this._layout.xMargin = layout.layout.xMargin; - this._layout.yMargin = layout.layout.yMargin; - this._layout.type = layout.layout.type; - this._layout.columns = layout.columns; - }; - - isSelectedLayout = (layout: DataVizTemplateLayout) => { - return this._layout.xMargin === layout.layout.xMargin && this._layout.yMargin === layout.layout.yMargin && this._layout.type === layout.layout.type && this._layout.columns === layout.columns; - }; - - @action - generateTemplates = async (inputText: string) => { - ++this._callCount; - const origCount = this._callCount; - - let prompt: string = `(#${origCount}) Please generate for the fields:`; - this.selectedFields?.forEach(field => (prompt += ` ${field},`)); - prompt += ` (-----NOT A FIELD-----) Additional prompt: ${inputText}`; - - this._GPTLoading = true; - - try { - const res = await gptAPICall(prompt, GPTCallType.TEMPLATE); - - if (res && this._callCount === origCount) { - this._suggestedTemplates = []; - const templates: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[] = JSON.parse(res); - this.createGeneratedTemplates(templates, 500, 500); - } - } catch (err) { - console.error(err); - } - }; - - @action - createGeneratedTemplates = (layouts: { template_type: string; fieldVals: { title: string; tlx: string; tly: string; brx: string; bry: string }[] }[], tempWidth: number, tempHeight: number) => { - const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; - const GPTTemplates: Doc[] = []; - - layouts.forEach(layout => { - const fields: Doc[] = layout.fieldVals.map(field => { - const left: number = (Number(field.tlx) * tempWidth) / 2; - const top: number = Number(field.tly) * tempHeight / 2; //prettier-ignore - const right: number = (Number(field.brx) * tempWidth) / 2; - const bottom: number = Number(field.bry) * tempHeight / 2; //prettier-ignore - const height = bottom - top; - const width = right - left; - const doc = !field.title.includes('$$') - ? Docs.Create.TextDocument('', { _height: height, _width: width, title: field.title, x: left, y: top, text_fontSize: `${height / 2}` }) - : Docs.Create.ImageDocument('', { _height: height, _width: width, title: field.title.replace(/\$\$/g, ''), x: left, y: top }); - return doc; - }); - - const template = Docs.Create.FreeformDocument(fields, { _height: tempHeight, _width: tempWidth, title: layout.template_type, x: 400000, y: 400000 }); - - mainCollection.addDocument(template); - - GPTTemplates.push(template); - }); - - setTimeout(() => { - this.setGSuggestedTemplates(GPTTemplates); /*GPTTemplates.forEach(template => mainCollection.removeDocument(template))*/ - }, 100); - - this.forceUpdate(); - }; - - editTemplate = (doc: Doc) => { - //this.closeMenu(); - DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight); - DocumentView.DeselectAll(); - Doc.UnBrushDoc(doc); - }; - - removeTemplate = (doc: Doc) => { - this._templateDocs.splice(this._templateDocs.indexOf(doc), 1); - }; - - testTemplate = async () => { - this.updateIcons(this._suggestedTemplates.slice()); - this.forceUpdate(); - - // try { - // const res = await gptImageCall('Image of panda eating a cookie'); - - // if (res) { - // const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res }); - - // console.log(result); - // } - // } catch (e) { - // console.log(e); - // } - }; - - @action addField = () => { - const newFields: Col[] = this._columns.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]); - this._columns = newFields; - }; - - @action removeField = (field: { title: string; type: string; desc: string }) => { - if (this._dataViz?.axes.includes(field.title)) { - this._dataViz.selectAxes(this._dataViz.axes.filter(col => col !== field.title)); - } else { - const toRemove = this._columns.filter(f => f === field); - if (!toRemove) return; - - if (toRemove.length > 1) { - while (toRemove.length > 1) { - toRemove.pop(); - } - } - - if (this._columns.length === 1) { - this._columns = []; - } else { - this._columns.splice(this._columns.indexOf(toRemove[0]), 1); - } - } - }; - - @action setColTitle = (column: Col, title: string) => { - if (this.selectedFields.includes(column.title)) { - this._dataViz?.setColumnTitle(column.title, title); - } else { - column.title = title; - } - this.forceUpdate(); - }; - - @action setColType = (column: Col, type: TemplateFieldType) => { - if (this.selectedFields.includes(column.title)) { - this._dataViz?.setColumnType(column.title, type); - } else { - column.type = type; - } - this.forceUpdate(); - }; - - modifyColSizes = (column: Col, size: TemplateFieldSize, valid: boolean) => { - if (this.selectedFields.includes(column.title)) { - this._dataViz?.modifyColumnSizes(column.title, size, valid); - } else { - if (!valid && column.sizes.includes(size)) { - column.sizes.splice(column.sizes.indexOf(size), 1); - } else if (valid && !column.sizes.includes(size)) { - column.sizes.push(size); - } - } - this.forceUpdate(); - }; - - setColDesc = (column: Col, desc: string) => { - if (this.selectedFields.includes(column.title)) { - this._dataViz?.setColumnDesc(column.title, desc); - } else { - column.desc = desc; - } - this.forceUpdate(); - }; - - generateGPTImage = async (prompt: string): Promise<string | undefined> => { - console.log(prompt); - - try { - const res = await gptImageCall(prompt); - - if (res) { - const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: res })) as Upload.FileInformation[]; - const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client); - return source; - } - } catch (e) { - console.log(e); - } - }; - - matchesForTemplate = (template: TemplateDocInfos, cols: Col[]): number[][] => { - const colMatchesField = (col: Col, field: Field) => { - return field.sizes?.some(size => col.sizes?.includes(size)) && field.types?.includes(col.type); - }; - - const matches: number[][] = Array(template.fields.length) - .fill([]) - .map(() => []); - - template.fields.forEach((field, i) => { - cols.forEach((col, v) => { - if (colMatchesField(col, field)) { - matches[i].push(v); - } - }); - }); - - return matches; - }; - - maxMatches = (fieldsCt: number, matches: number[][]) => { - const used: boolean[] = Array(fieldsCt).fill(false); - const mt: number[] = Array(fieldsCt).fill(-1); - - const augmentingPath = (v: number): boolean => { - if (used[v]) return false; - used[v] = true; - for (const to of matches[v]) { - if (mt[to] === -1 || augmentingPath(mt[to])) { - mt[to] = v; - return true; - } - } - return false; - }; - - for (let v = 0; v < fieldsCt; ++v) { - used.fill(false); - augmentingPath(v); - } - - let count: number = 0; - - for (let i = 0; i < fieldsCt; ++i) { - if (mt[i] !== -1) ++count; - } - - return count; - }; - - findValidTemplates = (cols: Col[], templates: TemplateDocInfos[]) => { - let validTemplates: any[] = []; - templates.forEach(template => { - const numFields = template.fields.length; - if (!(numFields === cols.length)) return; - const matches = this.matchesForTemplate(template, cols); - if (this.maxMatches(numFields, matches) === numFields) { - validTemplates.push(template.title); - } - }); - - validTemplates = validTemplates.map(title => TemplateLayouts.getTemplateByTitle(title)); - - return validTemplates; - }; - - // createColumnField = (template: TemplateDocInfos, field: Field, column: Col): Doc => { - - // if (field.subfields) { - // const doc = FieldFuncs.FreeformField({ - // tl: field.tl, - // br: field.br }, - // template.height, - // template.width, - // column.title, - // '', - // field.opts - // ); - - // field.subfields[1].forEach(f => { - // const fDoc = () - // }) - - // } - - // return new Doc; - // } - - /** - * Populates a preset template framework with content from a datavizbox or any AI-generated content. - * @param template the preloaded template framework being filled in - * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz - * @returns a doc containing the fully rendered template - */ - fillPresetTemplate = async (template: TemplateDocInfos, assignments: { [field: string]: Col }): Promise<Doc> => { - const wordLimit = (size: TemplateFieldSize) => { - switch (size) { - case TemplateFieldSize.TINY: - return 2; - case TemplateFieldSize.SMALL: - return 5; - case TemplateFieldSize.MEDIUM: - return 20; - case TemplateFieldSize.LARGE: - return 50; - case TemplateFieldSize.HUGE: - return 100; - default: - return 10; - } - }; - - const renderTextCalls = async (): Promise<Doc[]> => { - const rendered: Doc[] = []; - - if (GPTTextCalls.length) { - try { - const prompt = fieldContent + GPTTextAssignment; - - const res = await gptAPICall(prompt, GPTCallType.FILL); - - if (res) { - const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res); - //console.log('assignments', GPTAssignments, 'assignment string', GPTAssignmentString, 'field content', fieldContent, 'response', res, 'assignments', assignments); - Object.entries(assignments).forEach(([title, info]) => { - const field: Field = template.fields[Number(info.number)]; - const col = this.getColByTitle(title); - - const doc = FieldUtils.TextField( - { - tl: field.tl, - br: field.br, - }, - template.height, - template.width, - col.title, - info.content ?? '', - field.opts - ); - - rendered.push(doc); - }); - } - } catch (err) { - console.log(err); - } - } - - return rendered; - }; - - const createGeneratedImage = async (fieldNum: string, col: Col, prompt: string) => { - const url = await this.generateGPTImage(prompt); - const field: Field = template.fields[Number(fieldNum)]; - const doc = FieldUtils.ImageField( - { - tl: field.tl, - br: field.br, - }, - template.height, - template.width, - col.title, - url ?? '', - field.opts - ); - - return doc; - }; - - const renderImageCalls = async (): Promise<Doc[]> => { - const rendered: Doc[] = []; - const calls = GPTIMGCalls; - - if (calls.length) { - try { - const renderedImages: Doc[] = await Promise.all( - calls.map(async ([fieldNum, col]) => { - const sysPrompt = - 'Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ' + - fieldContent + - ' **** The user prompt is: ' + - col.desc; - - const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT); - console.log(sysPrompt, prompt); - - return createGeneratedImage(fieldNum, col, prompt); - }) - ); - - const renderedTemplates: Doc[] = await Promise.all(renderedImages); - renderedTemplates.forEach(doc => rendered.push(doc)); - } catch (e) { - console.log(e); - } - } - - return rendered; - }; - - const fields: Doc[] = []; - - const GPTAssignments = Object.entries(assignments).filter(([f, col]) => this._columns.includes(col)); - const nonGPTAssignments: [string, Col][] = Object.entries(assignments).filter(a => !GPTAssignments.includes(a)); - const GPTTextCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.TEXT); - const GPTIMGCalls = GPTAssignments.filter(([str, col]) => col.type === TemplateFieldType.VISUAL); - - const stringifyGPTInfo = (calls: [string, Col][]): string => { - let string: string = '*** COLUMN INFO:'; - calls.forEach(([fieldNum, col]) => { - string += `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`; - }); - return (string += ' ***'); - }; - - const GPTTextAssignment = stringifyGPTInfo(GPTTextCalls); - - let fieldContent: string = ''; - - Object.entries(nonGPTAssignments).forEach(([f, strCol]) => { - const field: Field = template.fields[Number(f)]; - const col = strCol[1]; - - const doc = (col.type === TemplateFieldType.VISUAL ? FieldUtils.ImageField : FieldUtils.TextField)( - { - tl: field.tl, - br: field.br, - }, - template.height, - template.width, - col.title, - col.defaultContent ?? '', - field.opts - ); - - fieldContent += `--- Field #${f} (title: ${col.title}): ${col.defaultContent ?? ''} ---`; - - fields.push(doc); - }); - - template.decorations.forEach(dec => { - const doc = FieldUtils.FreeformField( - { - tl: dec.tl, - br: dec.br, - }, - template.height, - template.width, - '', - '', - dec.opts - ); - - fields.push(doc); - }); - - const createMainDoc = (): Doc => { - const main = Docs.Create.FreeformDocument(fields, { - _height: template.height, - _width: template.width, - title: template.title, - backgroundColor: template.opts.backgroundColor, - _layout_borderRounding: `${template.opts.cornerRounding}px` ?? '0px', - borderWidth: template.opts.borderWidth, - borderColor: template.opts.borderColor, - x: 40000, - y: 40000, - }); - - const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; - mainCollection.addDocument(main); - - return main; - }; - - const textCalls = await renderTextCalls(); - const imageCalls = await renderImageCalls(); - - textCalls.forEach(doc => { - fields.push(doc); - }); - imageCalls.forEach(doc => { - fields.push(doc); - }); - - return createMainDoc(); - }; - - compileFieldDescriptions = (templates: TemplateDocInfos[]): string => { - let descriptions: string = ''; - templates.forEach(template => { - descriptions += `---------- NEW TEMPLATE TO INCLUDE: Description of template ${template.title}'s fields: `; - template.fields.forEach((field, index) => { - descriptions += `{Field #${index}: ${field.description}} `; - }); - }); - - return descriptions; - }; - - compileColDescriptions = (cols: Col[]): string => { - let descriptions: string = ' ------------- COL DESCRIPTIONS START HERE:'; - cols.forEach(col => (descriptions += `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `)); - - return descriptions; - }; - - getColByTitle = (title: string) => { - return this.fieldsInfos.filter(col => col.title === title)[0]; - }; - - @action - assignColsToFields = async (templates: TemplateDocInfos[], cols: Col[]): Promise<[TemplateDocInfos, { [field: number]: Col }][]> => { - const fieldDescriptions: string = this.compileFieldDescriptions(templates); - const colDescriptions: string = this.compileColDescriptions(cols); - - const inputText = fieldDescriptions.concat(colDescriptions); - - ++this._callCount; - const origCount = this._callCount; - - let prompt: string = `(${origCount}) ${inputText}`; - - this._GPTLoading = true; - - try { - const res = await gptAPICall(prompt, GPTCallType.TEMPLATE); - - if (res && this._callCount === origCount) { - const assignments: { [templateTitle: string]: { [field: string]: string } } = JSON.parse(res); - const brokenDownAssignments: [TemplateDocInfos, { [field: number]: Col }][] = []; - - Object.entries(assignments).forEach(([tempTitle, assignment]) => { - const template = TemplateLayouts.getTemplateByTitle(tempTitle); - if (!template) return; - const toObj = Object.entries(assignment).reduce( - (a, [fieldNum, colTitle]) => { - a[Number(fieldNum)] = this.getColByTitle(colTitle); - return a; - }, - {} as { [field: number]: Col } - ); - brokenDownAssignments.push([template, toObj]); - }); - return brokenDownAssignments; - } - } catch (err) { - console.error(err); - } - - return []; - }; - - generatePresetTemplates = async () => { - this._dataViz?.updateColDefaults(); - - const cols = this.fieldsInfos; - const templates = this.findValidTemplates(cols, TemplateLayouts.allTemplates); - - const assignments: [TemplateDocInfos, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols); - - const renderedTemplatePromises: Promise<Doc>[] = assignments.map(([template, assignments]) => this.fillPresetTemplate(template, assignments)); - - const renderedTemplates: Doc[] = await Promise.all(renderedTemplatePromises); - - setTimeout(() => { - this.setGSuggestedTemplates(renderedTemplates); - this._GPTLoading = false; - }); - }; - - @action setExpandedView = (info: { icon: ImageField; doc: Doc } | undefined) => { - if (info) { - const doc = info.doc; - const wrapper: Doc = Docs.Create.FreeformDocument([info.doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: '' }); - const newInfo = { icon: new ImageField(''), doc: wrapper }; - this._expandedPreview = newInfo; - } else { - this._expandedPreview = info; - } - }; - - get editingWindow() { - const doc = this._expandedPreview?.doc ?? new Doc(); - const rendered = ( - <div className="docCreatorMenu-expanded-template-preview"> - <CollectionFreeFormView - Document={this._expandedPreview!.doc} - docViewPath={returnEmptyDocViewList} - childLayoutTemplate={() => Cast(doc.childLayoutTemplate, Doc, null)} - isContentActive={emptyFunction} - isAnyChildContentActive={() => true} - select={emptyFunction} - isSelected={returnFalse} - fieldKey={Doc.LayoutFieldKey(doc)} - addDocument={returnFalse} - moveDocument={returnFalse} - removeDocument={returnFalse} - PanelWidth={() => this._menuDimensions.width - 10} - PanelHeight={() => this._menuDimensions.height - 60} - ScreenToLocalTransform={() => new Transform(-this._pageX, -this._pageY, 1)} - renderDepth={5} - whenChildContentsActiveChanged={emptyFunction} - focus={emptyFunction} - styleProvider={DefaultStyleProvider} - addDocTab={this._props.addDocTab} - // eslint-disable-next-line no-use-before-define - pinToPres={() => undefined} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - fitContentsToBox={returnTrue} - xPadding={0} - yPadding={0} - /> - </div> - ); - - return ( - <div className="docCreatorMenu-expanded-template-preview"> - <div className="top-panel" /> - {rendered} - <div className="right-buttons-panel"> - <button - className="docCreatorMenu-menu-button section-reveal-options top-right" - onPointerDown={e => - this.setUpButtonClick(e, () => { - this._expandedPreview && this.updateIcons(this._suggestedTemplates.slice()); - this.setExpandedView(undefined); - }) - }> - <FontAwesomeIcon icon="minimize" /> - </button> - <button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => this._expandedPreview && this._templateDocs.push(this._expandedPreview.doc))}> - <FontAwesomeIcon icon="plus" color="white" /> - </button> - </div> - </div> - ); - } - - get templatesPreviewContents() { - const renderedTemplates: Doc[] = []; - - const GPTOptions = <div></div>; - - //<img className='docCreatorMenu-preview-image expanded' src={this._expandedPreview.icon!.url.href.replace(".png", "_o.png")} /> - - return ( - <div className={`docCreatorMenu-templates-view`}> - {this._expandedPreview ? ( - this.editingWindow - ) : ( - <div> - <div className="docCreatorMenu-section" style={{ height: this._GPTOpt ? 200 : 200 }}> - <div className="docCreatorMenu-section-topbar"> - <div className="docCreatorMenu-section-title">Suggested Templates</div> - <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'dashboard')))}> - <FontAwesomeIcon icon="gear" /> - </button> - </div> - <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._GPTLoading || this._menuDimensions.width > 400 ? 'center' : '' }}> - {this._GPTLoading ? ( - <div className="loading-spinner"> - <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> - </div> - ) : ( - this._suggestedTemplates - ?.map(doc => ({ icon: ImageCast(doc.icon), doc })) - .filter(info => info.icon && info.doc) - .map(info => ( - <div - className="docCreatorMenu-preview-window" - style={{ - border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', - boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '', - }} - onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}> - <button - className="option-button left" - onPointerDown={e => - this.setUpButtonClick(e, () => { - this.setExpandedView(info); - }) - }> - <FontAwesomeIcon icon="magnifying-glass" color="white" /> - </button> - <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this._templateDocs.push(info.doc))}> - <FontAwesomeIcon icon="plus" color="white" /> - </button> - <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} /> - </div> - )) - )} - </div> - <div className="docCreatorMenu-GPT-options"> - <div className="docCreatorMenu-GPT-options-container"> - <button className="docCreatorMenu-menu-button" onPointerDown={e => this.setUpButtonClick(e, () => this.generatePresetTemplates())}> - <FontAwesomeIcon icon="arrows-rotate" /> - </button> - </div> - {this._GPTOpt ? GPTOptions : null} - </div> - </div> - <hr className="docCreatorMenu-option-divider full no-margin" /> - <div className="docCreatorMenu-section"> - <div className="docCreatorMenu-section-topbar"> - <div className="docCreatorMenu-section-title">Your Templates</div> - <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => (this._GPTOpt = !this._GPTOpt))}> - <FontAwesomeIcon icon="gear" /> - </button> - </div> - <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._menuDimensions.width > 400 ? 'center' : '' }}> - <div className="docCreatorMenu-preview-window empty" onPointerDown={e => this.testTemplate()}> - <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" /> - </div> - {this._templateDocs - .map(doc => ({ icon: ImageCast(doc.icon), doc })) - .filter(info => info.icon && info.doc) - .map(info => { - if (renderedTemplates.includes(info.doc)) return undefined; - renderedTemplates.push(info.doc); - return ( - <div - className="docCreatorMenu-preview-window" - style={{ - border: this._selectedTemplate === info.doc ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', - boxShadow: this._selectedTemplate === info.doc ? `0 0 15px rgba(68, 118, 247, .8)` : '', - }} - onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(info.doc)))}> - <button - className="option-button left" - onPointerDown={e => - this.setUpButtonClick(e, () => { - this.editTemplate(info.doc); - }) - }> - <FontAwesomeIcon icon="pencil" color="black" /> - </button> - <button - className="option-button right" - onPointerDown={e => - this.setUpButtonClick(e, () => { - this.removeTemplate(info.doc); - }) - }> - <FontAwesomeIcon icon="trash" color="black" /> - </button> - <img className="docCreatorMenu-preview-image" src={info.icon!.url.href.replace('.png', '_o.png')} /> - </div> - ); - })} - </div> - </div> - </div> - )} - </div> - ); - } - - get savedLayoutsPreviewContents() { - return ( - <div className="docCreatorMenu-preview-container"> - {this._savedLayouts.map((layout, index) => ( - <div - className="docCreatorMenu-preview-window" - style={{ - border: this.isSelectedLayout(layout) ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', - boxShadow: this.isSelectedLayout(layout) ? `0 0 15px rgba(68, 118, 247, .8)` : '', - }} - onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedSavedLayout(layout)))}> - {this.layoutPreviewContents(87, layout, true, index)} - </div> - ))} - </div> - ); - } - - @action updateXMargin = (input: string) => { - this._layout.xMargin = Number(input); - }; - @action updateYMargin = (input: string) => { - this._layout.yMargin = Number(input); - }; - @action updateColumns = (input: string) => { - this._layout.columns = Number(input); - }; - - get layoutConfigOptions() { - const optionInput = (icon: string, func: Function, def?: number, key?: string, noMargin?: boolean) => { - return ( - <div className="docCreatorMenu-option-container small no-margin" key={key} style={{ marginTop: noMargin ? '0px' : '' }}> - <div className="docCreatorMenu-option-title config layout-config"> - <FontAwesomeIcon icon={icon as any} /> - </div> - <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" /> - </div> - ); - }; - - switch (this._layout.type) { - case LayoutType.Row: - return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '0')}</div>; - case LayoutType.Column: - return <div className="docCreatorMenu-configuration-bar">{optionInput('arrows-up-down', this.updateYMargin, this._layout.yMargin, '1')}</div>; - case LayoutType.Grid: - return ( - <div className="docCreatorMenu-configuration-bar"> - {optionInput('arrows-up-down', this.updateYMargin, this._layout.xMargin, '2')} - {optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '3')} - {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)} - </div> - ); - case LayoutType.Stacked: - return null; - default: - break; - } - } - - // doc = () => { - // return Docs.Create.FreeformDocument([], { _height: 200, _width: 200, title: 'title'}); - // } - - screenToLocalTransform = () => this._props.ScreenToLocalTransform(); - - layoutPreviewContents = (outerSpan: number, altLayout?: DataVizTemplateLayout, small: boolean = false, id?: number) => { - const doc: Doc | undefined = altLayout ? altLayout.template : this._selectedTemplate; - if (!doc) return; - - const layout = altLayout ? altLayout.layout : this._layout; - - const docWidth: number = Number(doc._width); - const docHeight: number = Number(doc._height); - const horizontalSpan: number = (docWidth + layout.xMargin) * (altLayout ? altLayout.columns : this.columnsCount) - layout.xMargin; - const verticalSpan: number = (docHeight + layout.yMargin) * (altLayout ? altLayout.rows : this.rowsCount) - layout.yMargin; - const largerSpan: number = horizontalSpan > verticalSpan ? horizontalSpan : verticalSpan; - const scaledDown = (input: number) => { - return input / ((largerSpan / outerSpan) * this._layoutPreviewScale); - }; - const fontSize = Math.min(scaledDown(docWidth / 3), scaledDown(docHeight / 3)); - - return ( - // <div className='divvv' style={{width: 100, height: 100, border: `1px solid white`}}> - // <CollectionFreeFormView - // // eslint-disable-next-line react/jsx-props-no-spreading - // {...this._props} - // Document={new Doc()} - // isContentActive={returnFalse} - // setContentViewBox={emptyFunction} - // NativeWidth={() => 100} - // NativeHeight={() => 100} - // pointerEvents={SnappingManager.IsDragging ? returnAll : returnNone} - // isAnnotationOverlay - // isAnnotationOverlayScrollable - // childDocumentsActive={returnFalse} - // fieldKey={this._props.fieldKey + '_annotations'} - // dropAction={dropActionType.move} - // select={emptyFunction} - // addDocument={returnFalse} - // removeDocument={returnFalse} - // moveDocument={returnFalse} - // renderDepth={this._props.renderDepth + 1}> - // {null} - // </CollectionFreeFormView> - // </div> - <div className="docCreatorMenu-layout-preview-window-wrapper" id={String(id) ?? undefined}> - <div className="docCreatorMenu-zoom-button-container"> - <button className="docCreatorMenu-zoom-button" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 1.25)))}> - <FontAwesomeIcon icon={'minus'} /> - </button> - <button className="docCreatorMenu-zoom-button zoom-in" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreviewScale *= 0.75)))}> - <FontAwesomeIcon icon={'plus'} /> - </button> - {altLayout ? ( - <button className="docCreatorMenu-zoom-button trash" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this._savedLayouts.splice(this._savedLayouts.indexOf(altLayout), 1)))}> - <FontAwesomeIcon icon={'trash'} /> - </button> - ) : null} - </div> - { - <div - id={String(id) ?? undefined} - className={`docCreatorMenu-layout-preview-window ${small ? 'small' : ''}`} - style={{ - gridTemplateColumns: `repeat(${altLayout ? altLayout.columns : this.columnsCount}, ${scaledDown(docWidth)}px`, - gridTemplateRows: `${scaledDown(docHeight)}px`, - gridAutoRows: `${scaledDown(docHeight)}px`, - rowGap: `${scaledDown(layout.yMargin)}px`, - columnGap: `${scaledDown(layout.xMargin)}px`, - }}> - {this._layout.type === LayoutType.Stacked ? ( - <div - className="docCreatorMenu-layout-preview-item" - style={{ - width: scaledDown(docWidth), - height: scaledDown(docHeight), - fontSize: fontSize, - }}> - All - </div> - ) : ( - this.docsToRender.map(num => ( - <div - onMouseEnter={() => this._dataViz?.setSpecialHighlightedRow(num)} - onMouseLeave={() => this._dataViz?.setSpecialHighlightedRow(undefined)} - className="docCreatorMenu-layout-preview-item" - style={{ - width: scaledDown(docWidth), - height: scaledDown(docHeight), - fontSize: fontSize, - }}> - {num} - </div> - )) - )} - </div> - } - </div> - ); - }; - - get optionsMenuContents() { - const layoutEquals = (layout: DataVizTemplateLayout) => {}; //TODO: ADD LATER - - const layoutOption = (option: LayoutType, optStyle?: {}, specialFunc?: Function) => { - return ( - <div - className="docCreatorMenu-dropdown-option" - style={optStyle} - onPointerDown={e => - this.setUpButtonClick(e, () => { - specialFunc?.(); - runInAction(() => (this._layout.type = option)); - }) - }> - {option} - </div> - ); - }; - - const selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => { - return ( - <div className="docCreatorMenu-option-container"> - <div className={`docCreatorMenu-option-title config ${specClass}`} style={{ width: width * 0.4, height: height }}> - <FontAwesomeIcon icon={icon as any} /> - </div> - {manual ? ( - <input className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }} /> - ) : ( - <select className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }}> - {options} - </select> - )} - </div> - ); - }; - - const repeatOptions = [0, 1, 2, 3, 4, 5]; - - return ( - <div className="docCreatorMenu-menu-container"> - <div className="docCreatorMenu-option-container layout"> - <div className="docCreatorMenu-dropdown-hoverable"> - <div className="docCreatorMenu-option-title">{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}</div> - <div className="docCreatorMenu-dropdown-content"> - {layoutOption(LayoutType.Stacked)} - {layoutOption(LayoutType.Grid, undefined, () => { - if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); - })} - {layoutOption(LayoutType.Row)} - {layoutOption(LayoutType.Column)} - {layoutOption(LayoutType.Custom, { borderBottom: `0px` })} - </div> - </div> - <button className="docCreatorMenu-menu-button preview-toggle" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._layoutPreview = !this._layoutPreview)))}> - <FontAwesomeIcon icon={this._layoutPreview ? 'minus' : 'magnifying-glass'} /> - </button> - </div> - {this._layout.type ? this.layoutConfigOptions : null} - {this._layoutPreview ? this.layoutPreviewContents(this._menuDimensions.width * 0.75) : null} - {selectionBox( - 60, - 20, - 'repeat', - undefined, - repeatOptions.map(num => <option onPointerDown={e => (this._layout.repeat = num)}>{`${num}x`}</option>) - )} - <hr className="docCreatorMenu-option-divider" /> - <div className="docCreatorMenu-general-options-container"> - <button - className="docCreatorMenu-save-layout-button" - onPointerDown={e => - setupMoveUpEvents( - this, - e, - returnFalse, - emptyFunction, - undoable(clickEv => { - clickEv.stopPropagation(); - if (!this._selectedTemplate) return; - const layout: DataVizTemplateLayout = { - template: this._selectedTemplate, - layout: { type: this._layout.type, xMargin: this._layout.xMargin, yMargin: this._layout.yMargin, repeat: 0 }, - columns: this.columnsCount, - rows: this.rowsCount, - docsNumList: this.docsToRender, - }; - if (!this._savedLayouts.includes(layout)) { - this._savedLayouts.push(layout); - } - }, 'make docs') - ) - }> - <FontAwesomeIcon icon="floppy-disk" /> - </button> - <button - className="docCreatorMenu-create-docs-button" - style={{ backgroundColor: this.canMakeDocs ? '' : 'rgb(155, 155, 155)', border: this.canMakeDocs ? '' : 'solid 2px rgb(180, 180, 180)' }} - onPointerDown={e => - setupMoveUpEvents( - this, - e, - returnFalse, - emptyFunction, - undoable(clickEv => { - clickEv.stopPropagation(); - if (!this._selectedTemplate) return; - const templateInfo: DataVizTemplateInfo = { doc: this._selectedTemplate, layout: this._layout, referencePos: { x: this._pageX + 450, y: this._pageY }, columns: this.columnsCount }; - this._dataViz?.createDocsFromTemplate(templateInfo); - }, 'make docs') - ) - }> - <FontAwesomeIcon icon="plus" /> - </button> - </div> - </div> - ); - } - - get dashboardContents() { - const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge']; - - const fieldPanel = (field: Col) => { - return ( - <div className="field-panel"> - <div className="top-bar"> - <span className="field-title">{`${field.title} Field`}</span> - <button className="docCreatorMenu-menu-button section-reveal-options no-margin" onPointerDown={e => this.setUpButtonClick(e, () => this.removeField(field))} style={{ position: 'absolute', right: '0px' }}> - <FontAwesomeIcon icon="minus" /> - </button> - </div> - <div className="opts-bar"> - <div className="opt-box"> - <div className="top-bar"> Title </div> - <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} defaultValue={field.title} placeholder={'Enter title'} onChange={e => this.setColTitle(field, e.target.value)} /> - </div> - <div className="opt-box"> - <div className="top-bar"> Type </div> - <div className="content"> - <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : ''}</span> - <div className="bubbles"> - <input - className="bubble" - type="radio" - name="type" - onClick={() => { - this.setColType(field, TemplateFieldType.TEXT); - }} - /> - <div className="text">Text</div> - <input - className="bubble" - type="radio" - name="type" - onClick={() => { - this.setColType(field, TemplateFieldType.VISUAL); - }} - /> - <div className="text">File</div> - </div> - </div> - </div> - </div> - <div className="sizes-box"> - <div className="top-bar"> Valid Sizes </div> - <div className="content"> - <div className="bubbles"> - {sizes.map(size => ( - <> - <input - className="bubble" - type="checkbox" - name="type" - checked={field.sizes.includes(size as TemplateFieldSize)} - onChange={e => { - this.modifyColSizes(field, size as TemplateFieldSize, e.target.checked); - }} - /> - <div className="text">{size}</div> - </> - ))} - </div> - </div> - </div> - <div className="desc-box"> - <div className="top-bar"> Prompt </div> - <textarea - className="content" - onChange={e => this.setColDesc(field, e.target.value)} - defaultValue={field.desc === this._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc} - placeholder={this._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'} - /> - </div> - </div> - ); - }; - - return ( - <div className="docCreatorMenu-dashboard-view"> - <div className="topbar"> - <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, this.addField)}> - <FontAwesomeIcon icon="plus" /> - </button> - <button className="docCreatorMenu-menu-button section-reveal-options float-right" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'templates')))}> - <FontAwesomeIcon icon="arrow-left" /> - </button> - </div> - <div className="panels-container">{this.fieldsInfos.map(field => fieldPanel(field))}</div> - </div> - ); - } - - get renderSelectedViewType() { - switch (this._menuContent) { - case 'templates': - return this.templatesPreviewContents; - case 'options': - return this.optionsMenuContents; - case 'saved': - return this.savedLayoutsPreviewContents; - case 'dashboard': - return this.dashboardContents; - default: - return undefined; - } - } - - get resizePanes() { - const ref = this._ref?.getBoundingClientRect(); - const height: number = ref?.height ?? 0; - const width: number = ref?.width ?? 0; - - return [ - <div className='docCreatorMenu-resizer top' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: -7}}/>, - <div className='docCreatorMenu-resizer right' onPointerDown={this.onResizePointerDown} style={{height: height, left: width - 3, top: 0}}/>, - <div className='docCreatorMenu-resizer bottom' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: height - 3}}/>, - <div className='docCreatorMenu-resizer left' onPointerDown={this.onResizePointerDown} style={{height: height, left: -7, top: 0}}/>, - <div className='docCreatorMenu-resizer topRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: -10, cursor: 'nesw-resize'}}/>, - <div className='docCreatorMenu-resizer topLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: -10, cursor: 'nwse-resize'}}/>, - <div className='docCreatorMenu-resizer bottomRight' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: height - 5, cursor: 'nwse-resize'}}/>, - <div className='docCreatorMenu-resizer bottomLeft' onPointerDown={this.onResizePointerDown} style={{left: -10, top: height - 5, cursor: 'nesw-resize'}}/> - ]; //prettier-ignore - } - - render() { - const topButton = (icon: string, opt: string, func: Function, tag: string) => { - return ( - <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}> - <div - className="top-button-content" - onPointerDown={e => - this.setUpButtonClick(e, () => - runInAction(() => { - func(); - }) - ) - }> - <FontAwesomeIcon icon={icon as any} /> - </div> - </div> - ); - }; - - const onPreviewSelected = () => { - this._menuContent = 'templates'; - }; - const onSavedSelected = () => { - this._menuContent = 'dashboard'; - }; - const onOptionsSelected = () => { - this._menuContent = 'options'; - if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); - }; - - return ( - <div className="docCreatorMenu"> - {!this._shouldDisplay ? undefined : ( - <div - className="docCreatorMenu-cont" - ref={r => (this._ref = r)} - style={{ - display: '', - left: this._pageX, - top: this._pageY, - width: this._menuDimensions.width, - height: this._menuDimensions.height, - background: SnappingManager.userBackgroundColor, - color: SnappingManager.userColor, - }}> - {this.resizePanes} - <div - className="docCreatorMenu-menu" - onPointerDown={e => - setupMoveUpEvents( - this, - e, - e => { - this._dragging = true; - this._startPos = { x: 0, y: 0 }; - this._startPos.x = e.pageX - (this._ref?.getBoundingClientRect().left ?? 0); - this._startPos.y = e.pageY - (this._ref?.getBoundingClientRect().top ?? 0); - document.addEventListener('pointermove', this.onDrag); - return true; - }, - emptyFunction, - undoable(clickEv => { - clickEv.stopPropagation(); - }, 'drag menu') - ) - }> - <div className="docCreatorMenu-top-buttons-container"> - {topButton('table-cells', 'templates', onPreviewSelected, 'left')} - {topButton('bars', 'options', onOptionsSelected, 'middle')} - {topButton('floppy-disk', 'saved', onSavedSelected, 'right')} - </div> - <button className="docCreatorMenu-menu-button close-menu" onPointerDown={e => this.setUpButtonClick(e, this.closeMenu)}> - <FontAwesomeIcon icon={'minus'} /> - </button> - </div> - {this.renderSelectedViewType} - </div> - )} - </div> - ); - } -} - -export interface DataVizTemplateInfo { - doc: Doc; - layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; - columns: number; - referencePos: { x: number; y: number }; -} - -export interface DataVizTemplateLayout { - template: Doc; - docsNumList: number[]; - layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; - columns: number; - rows: number; -} - -export enum TemplateFieldType { - TEXT = 'text', - VISUAL = 'visual', - UNSET = 'unset', -} - -export enum TemplateFieldSize { - TINY = 'tiny', - SMALL = 'small', - MEDIUM = 'medium', - LARGE = 'large', - HUGE = 'huge', -} - -export type Col = { - sizes: TemplateFieldSize[]; - desc: string; - title: string; - type: TemplateFieldType; - defaultContent?: string; -}; -export interface FieldOpts { - backgroundColor?: string; - color?: string; - cornerRounding?: number; - borderWidth?: string; - borderColor?: string; - contentXCentering?: 'h-left' | 'h-center' | 'h-right'; - contentYCentering?: 'top' | 'center' | 'bottom'; - opacity?: number; - rotation?: number; - //animation?: boolean; - fontBold?: boolean; - fontTransform?: 'uppercase' | 'lowercase'; - fieldViewType?: 'freeform' | 'stacked'; -} - -export type Field = { - tl: [number, number]; - br: [number, number]; - opts: FieldOpts; - subfields?: Field[]; - types?: TemplateFieldType[]; - sizes?: TemplateFieldSize[]; - isDecoration?: boolean; - description?: string; -}; - -// class ContentField implements Field { -// tl: [number, number]; -// br: [number, number]; -// opts: FieldOpts; -// subfields?: Field[]; -// types?: TemplateFieldType[]; -// sizes?: TemplateFieldSize[]; -// description?: string; - -// constructor( tl: [number, number], br: [number, number], -// opts: FieldOpts, subfields?: Field[], -// types?: TemplateFieldType[], -// sizes?: TemplateFieldSize[], -// description?: string) { -// this.tl = tl; -// this.br = br; -// this.opts = opts; -// this.subfields = subfields; -// this.types = types; -// this.sizes = sizes; -// this.description = description; -// } - -// render = (content: any): Doc => { -// return new Doc; -// } -// } - -type DecorationField = Field; - -type InkDecoration = {}; - -type TemplateDecorations = Field | InkDecoration; - -interface TemplateOpts extends FieldOpts {} - -export interface TemplateDocInfos { - title: string; - height: number; - width: number; - opts: TemplateOpts; - fields: Field[]; - decorations: Field[]; -} - -export class TemplateLayouts { - public static get allTemplates(): TemplateDocInfos[] { - return Object.values(TemplateLayouts).filter(value => typeof value === 'object' && value !== null && 'title' in value) as TemplateDocInfos[]; - } - - public static getTemplateByTitle = (title: string): TemplateDocInfos | undefined => { - switch (title) { - case 'fourfield1': - return TemplateLayouts.FourField001; - case 'fourfield2': - return TemplateLayouts.FourField002; - // case 'fourfield3': - // return TemplateLayouts.FourField003; - case 'fourfield4': - return TemplateLayouts.FourField004; - case 'threefield1': - return TemplateLayouts.ThreeField001; - case 'threefield2': - return TemplateLayouts.ThreeField002; - default: - break; - } - - return undefined; - }; - - public static FourField001: TemplateDocInfos = { - title: 'fourfield1', - width: 416, - height: 700, - opts: { - backgroundColor: '#C0B887', - cornerRounding: 20, - borderColor: '#6B461F', - borderWidth: '12', - }, - fields: [ - { - tl: [-0.95, -1], - br: [0.95, -0.85], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A title field for very short text that contextualizes the content.', - opts: { - backgroundColor: 'transparent', - color: '#F1F0E9', - contentXCentering: 'h-center', - fontBold: true, - }, - }, - { - tl: [-0.87, -0.83], - br: [0.87, 0.2], - types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'The main focus of the template; could be an image, long text, etc.', - opts: { - cornerRounding: 20, - borderColor: '#8F5B25', - borderWidth: '6', - backgroundColor: '#CECAB9', - }, - }, - { - tl: [-0.8, 0.2], - br: [0.8, 0.3], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - description: 'A caption for field #2, very short to short text that contextualizes the content of field #2', - opts: { - backgroundColor: 'transparent', - contentXCentering: 'h-center', - color: '#F1F0E9', - }, - }, - { - tl: [-0.87, 0.37], - br: [0.87, 0.96], - types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A medium-sized field for medium/long text.', - opts: { - cornerRounding: 15, - borderColor: '#8F5B25', - borderWidth: '6', - backgroundColor: '#CECAB9', - }, - }, - ], - decorations: [], - }; - - public static FourField002: TemplateDocInfos = { - title: 'fourfield2', - width: 425, - height: 778, - opts: { - backgroundColor: '#242425', - }, - fields: [ - { - tl: [-0.83, -0.95], - br: [0.83, -0.2], - types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], - description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus.', - opts: { - borderWidth: '8', - borderColor: '#F8E71C', - }, - }, - { - tl: [-0.65, -0.2], - br: [0.65, -0.02], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A tiny field for just a word or two of plain text.', - opts: { - backgroundColor: 'transparent', - color: 'white', - contentXCentering: 'h-center', - fontTransform: 'uppercase', - }, - }, - { - tl: [-0.65, 0], - br: [0.65, 0.18], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A tiny field for just a word or two of plain text.', - opts: { - backgroundColor: 'transparent', - color: 'white', - contentXCentering: 'h-center', - fontTransform: 'uppercase', - }, - }, - { - tl: [-0.83, 0.2], - br: [0.83, 0.95], - types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus, or share focus with field 1.', - opts: { - borderWidth: '8', - borderColor: '#F8E71C', - color: 'white', - backgroundColor: '#242425', - }, - }, - ], - decorations: [ - { - tl: [-0.8, -0.075], - br: [-0.525, 0.075], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - { - tl: [-0.3075, -0.0245], - br: [-0.2175, 0.0245], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - { - tl: [-0.045, -0.0245], - br: [0.045, 0.0245], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - { - tl: [0.2175, -0.0245], - br: [0.3075, 0.0245], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - { - tl: [0.525, -0.075], - br: [0.8, 0.075], - opts: { - backgroundColor: '#F8E71C', - rotation: 45, - }, - }, - ], - }; - - // public static FourField003: TemplateDocInfos = { - // title: 'fourfield3', - // width: 477, - // height: 662, - // opts: { - // backgroundColor: '#9E9C95' - // }, - // fields: [{ - // tl: [-.875, -.9], - // br: [.875, .7], - // types: [TemplateFieldType.VISUAL], - // sizes: [TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - // description: '', - // opts: { - // borderWidth: '15', - // borderColor: '#E0E0DA', - // } - // }, { - // tl: [-.95, .8], - // br: [-.1, .95], - // types: [TemplateFieldType.TEXT], - // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - // description: '', - // opts: { - // backgroundColor: 'transparent', - // color: 'white', - // contentXCentering: 'h-right', - // } - // }, { - // tl: [.1, .8], - // br: [.95, .95], - // types: [TemplateFieldType.TEXT], - // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - // description: '', - // opts: { - // backgroundColor: 'transparent', - // color: 'red', - // fontTransform: 'uppercase', - // contentXCentering: 'h-left' - // } - // }, { - // tl: [0, -.9], - // br: [.85, -.66], - // types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], - // sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - // description: '', - // opts: { - // backgroundColor: 'transparent', - // contentXCentering: 'h-right' - // } - // }], - // decorations: [{ - // tl: [-.025, .8], - // br: [.025, .95], - // opts: { - // backgroundColor: '#E0E0DA', - // } - // }] - // }; - - public static FourField004: TemplateDocInfos = { - title: 'fourfield4', - width: 414, - height: 583, - opts: { - backgroundColor: '#6CCAF0', - borderColor: '#1088C3', - borderWidth: '10', - }, - fields: [ - { - tl: [-0.86, -0.92], - br: [-0.075, -0.77], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A tiny field for just a word or two of plain text.', - opts: { - backgroundColor: '#E2B4F5', - borderWidth: '9', - borderColor: '#9222F1', - contentXCentering: 'h-center', - }, - }, - { - tl: [0.075, -0.92], - br: [0.86, -0.77], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY], - description: 'A tiny field for just a word or two of plain text.', - opts: { - backgroundColor: '#F5B4DD', - borderWidth: '9', - borderColor: '#E260F3', - contentXCentering: 'h-center', - }, - }, - { - tl: [-0.81, -0.64], - br: [0.81, 0.48], - types: [TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A large to huge field for visual content that is the main content of the template.', - opts: { - borderWidth: '16', - borderColor: '#A2BD77', - }, - }, - { - tl: [-0.86, 0.6], - br: [0.86, 0.92], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], - description: 'A medium to large field for text that describes the visual content above', - opts: { - borderWidth: '9', - borderColor: '#F0D601', - backgroundColor: '#F3F57D', - }, - }, - ], - decorations: [ - { - tl: [-0.852, -0.67], - br: [0.852, 0.51], - opts: { - backgroundColor: 'transparent', - borderColor: '#007C0C', - borderWidth: '10', - }, - }, - ], - }; - - public static ThreeField001: TemplateDocInfos = { - title: 'threefield1', - width: 575, - height: 770, - opts: { - backgroundColor: '#DDD3A9', - }, - fields: [ - { - tl: [-0.66, -0.747], - br: [0.66, 0.247], - types: [TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A medium to large field for visual content that is the central focus.', - opts: { - borderColor: 'yellow', - borderWidth: '8', - rotation: 45, - }, - }, - { - tl: [-0.7, 0.2], - br: [0.7, 0.46], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - description: 'A very small text field for one to a few words. A good caption for the image.', - opts: { - backgroundColor: 'transparent', - contentXCentering: 'h-center', - }, - }, - { - tl: [-0.95, 0.5], - br: [0.95, 0.95], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], - description: 'A medium to large text field for a thorough description of the image. ', - opts: { - backgroundColor: 'transparent', - color: 'white', - }, - }, - ], - decorations: [ - { - tl: [0.2, -1.32], - br: [1.8, -0.66], - opts: { - backgroundColor: '#CEB155', - rotation: 45, - }, - }, - { - tl: [-1.8, -1.32], - br: [-0.2, -0.66], - opts: { - backgroundColor: '#CEB155', - rotation: 135, - }, - }, - { - tl: [0.33, 0.75], - br: [1.66, 1.25], - opts: { - backgroundColor: '#CEB155', - rotation: 135, - }, - }, - { - tl: [-1.66, 0.75], - br: [-0.33, 1.25], - opts: { - backgroundColor: '#CEB155', - rotation: 45, - }, - }, - ], - }; - - public static ThreeField002: TemplateDocInfos = { - title: 'threefield2', - width: 477, - height: 662, - opts: { - backgroundColor: '#9E9C95', - }, - fields: [ - { - tl: [-0.875, -0.9], - br: [0.875, 0.7], - types: [TemplateFieldType.VISUAL], - sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], - description: 'A medium to large visual field for the main content of the template', - opts: { - borderWidth: '15', - borderColor: '#E0E0DA', - }, - }, - { - tl: [0.1, 0.775], - br: [0.95, 0.975], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - description: 'A very small text field for one to a few words. The content should represent a general categorization of the image.', - opts: { - backgroundColor: 'transparent', - color: '#AF0D0D', - fontTransform: 'uppercase', - fontBold: true, - contentXCentering: 'h-left', - }, - }, - { - tl: [-0.95, 0.775], - br: [-0.1, 0.975], - types: [TemplateFieldType.TEXT], - sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], - description: 'A very small text field for one to a few words. The content should contextualize field 2.', - opts: { - backgroundColor: 'transparent', - color: 'black', - contentXCentering: 'h-right', - }, - }, - ], - decorations: [ - { - tl: [-0.025, 0.8], - br: [0.025, 0.95], - opts: { - backgroundColor: '#E0E0DA', - }, - }, - ], - }; -} - -export class FieldUtils { - public static contentFields = (fields: Field[]) => { - let toRet: Field[] = []; - fields.forEach(field => { - if (!field.isDecoration) { - toRet.push(field); - } - toRet = toRet.concat(FieldUtils.contentFields(field.subfields ?? [])); - }); - - return toRet; - }; - - public static calculateFontSize = (contWidth: number, contHeight: number, text: string, uppercase: boolean): number => { - const words: string[] = text.split(/\s+/).filter(Boolean); - - let currFontSize = 1; - let rowsCount = 1; - let currTextHeight = currFontSize * rowsCount * 2; - - while (currTextHeight <= contHeight) { - let wordIndex = 0; - let currentRowWidth = 0; - let wordsInCurrRow = 0; - rowsCount = 1; - - while (wordIndex < words.length) { - const word = words[wordIndex]; - const wordWidth = word.length * currFontSize * 0.5; - //console.log(wordWidth) - - if (currentRowWidth + wordWidth <= contWidth) { - currentRowWidth += wordWidth; - ++wordsInCurrRow; - } else { - if (words.length !== 1 && words.length > wordsInCurrRow) { - rowsCount++; - currentRowWidth = wordWidth; - wordsInCurrRow = 1; - } else { - break; - } - } - - wordIndex++; - } - - currTextHeight = rowsCount * currFontSize * 2; - //console.log(rowsCount, currFontSize, currTextHeight) - - currFontSize += 1; - } - - return currFontSize - 1; - }; - - private static getDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number): { width: number; height: number; coord: { x: number; y: number } } => { - const l = (coords.tl[0] * parentHeight) / 2; - const t = coords.tl[1] * parentWidth / 2; //prettier-ignore - const r = (coords.br[0] * parentHeight) / 2; - const b = coords.br[1] * parentWidth / 2; //prettier-ignore - const width = r - l; - const height = b - t; - const coord = { x: l, y: t }; - //console.log(coords, parentWidth, parentHeight, height); - return { width, height, coord }; - }; - - public static FreeformField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => { - const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - - const docWithBasicOpts = Docs.Create.FreeformDocument([], { - isDefaultTemplateDoc: true, - _height: height, - _width: width, - title: title, - x: coord.x, - y: coord.y, - backgroundColor: opts.backgroundColor ?? '', - _layout_borderRounding: `${opts.cornerRounding ?? 0}px`, - borderColor: opts.borderColor, - borderWidth: opts.borderWidth, - opacity: opts.opacity, - hCentering: opts.contentXCentering, - _rotation: opts.rotation, - }); - - return docWithBasicOpts; - }; - - public static TextField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => { - const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - - const docWithBasicOpts = Docs.Create.TextDocument(content, { - isDefaultTemplateDoc: true, - _height: height, - _width: width, - title: title, - x: coord.x, - y: coord.y, - text_fontSize: `${FieldUtils.calculateFontSize(width, height, content, true)}`, - backgroundColor: opts.backgroundColor ?? '', - text_fontColor: opts.color, - contentBold: opts.fontBold, - text_transform: opts.fontTransform, - color: opts.color, - _layout_borderRounding: `${opts.cornerRounding ?? 0}px`, - borderColor: opts.borderColor, - borderWidth: opts.borderWidth, - opacity: opts.opacity, - hCentering: opts.contentXCentering, - _rotation: opts.rotation, - }); - - docWithBasicOpts._layout_hideScroll = true; - - return docWithBasicOpts; - }; - - public static ImageField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, content: string, opts: FieldOpts) => { - const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - - const doc = Docs.Create.ImageDocument(content, { - isDefaultTemplateDoc: true, - _height: height, - _width: width, - title: title, - x: coord.x, - y: coord.y, - _layout_fitWidth: false, - backgroundColor: opts.backgroundColor ?? '', - _layout_borderRounding: `${opts.cornerRounding ?? 0}px`, - borderColor: opts.borderColor, - borderWidth: opts.borderWidth, - opacity: opts.opacity, - _rotation: opts.rotation, - }); - - //setTimeout(() => {doc._height = height; doc._width = width}, 10); - - return doc; - }; - - public static CarouselField = (coords: { tl: [number, number]; br: [number, number] }, parentWidth: number, parentHeight: number, title: string, fields: Doc[]) => { - const { width, height, coord } = FieldUtils.getDimensions(coords, parentWidth, parentHeight); - - const doc = Docs.Create.Carousel3DDocument(fields, { _height: height, _width: width, title: title, x: coord.x, y: coord.y, text_fontSize: `${height / 2}` }); - - return doc; - }; -} - -// public static FourField002: TemplateDocInfos = { -// width: 450, -// height: 600, -// fields: [{ -// tl: [-.6, -.9], -// br: [.6, -.8], -// types: [FieldType.TEXT], -// sizes: [FieldSize.TINY] -// }, { -// tl: [-.9, -.7], -// br: [.9, .2], -// types: [FieldType.TEXT, FieldType.VISUAL], -// sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE] -// }, { -// tl: [-.9, .3], -// br: [-.05, .9], -// types: [FieldType.TEXT], -// sizes: [FieldSize.TINY] -// }, { -// tl: [.05, .3], -// br: [.9, .9], -// types: [FieldType.TEXT, FieldType.VISUAL], -// sizes: [FieldSize.MEDIUM, FieldSize.LARGE, FieldSize.HUGE] -// }] -// }; - -// public static TwoFieldPlusCarousel: TemplateDocInfos = { -// width: 500, -// height: 600, -// fields: [{ -// tl: [-.9, -.99], -// br: [.9, -.7], -// types: [FieldType.TEXT], -// sizes: [FieldSize.TINY] -// }, { -// tl: [-.9, -.65], -// br: [.9, .35], -// types: [], -// sizes: [] -// }, { -// tl: [-.9, .4], -// br: [.9, .95], -// types: [FieldType.TEXT], -// sizes: [FieldSize.TINY] -// }] -// }; -// } diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss index 4ea904b8e..57f4a1e94 100644 --- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.scss +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.scss @@ -7,7 +7,7 @@ .docCreatorMenu-cont { position: absolute; - z-index: 100000; + z-index: 1000; // box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%); // background: whitesmoke; // color: black; @@ -45,9 +45,10 @@ font-size: 12px; width: 18px; height: 18px; - border-radius: 2px; font-size: 12px; margin-left: auto; + margin-right: 5px; + margin-bottom: 3px; } &.options { @@ -292,6 +293,7 @@ display: flex; flex-direction: column; justify-content: flex-start; + color: black; position: relative; width: 100%; height: 100%; @@ -324,6 +326,7 @@ height: 113px; margin-top: 10px; margin-left: 10px; + color: none; border: 1px solid rgb(163, 163, 163); border-radius: 5px; box-shadow: 5px 5px rgb(29, 29, 31); @@ -350,6 +353,7 @@ border: 0px; padding: 0px; font-size: 15px; + z-index: 1000; &.right { position: absolute; @@ -422,6 +426,7 @@ align-items: center; overflow-y: scroll; position: relative; + color: black; height: 125px; width: calc(100% - 10px); -ms-overflow-style: none; @@ -524,8 +529,6 @@ background: whitesmoke; background-color: rgb(34, 34, 37); border-radius: 5px; - border-top-right-radius: 0px; - border-bottom-right-radius: 0px; border: 1px solid rgb(180, 180, 180); padding: 0px; font-size: 12px; @@ -634,6 +637,8 @@ height: calc(100% - 30px); border: 1px solid rgb(180, 180, 180); border-radius: 5px; + -ms-overflow-style: none; + scrollbar-width: none; .docCreatorMenu-option-container{ width: 180px; @@ -686,13 +691,24 @@ } .docCreatorMenu-layout-preview-window-wrapper { + flex: 0 0 auto; display: flex; justify-content: center; align-items: center; - width: 85%; - height: auto; + color: black; + width: calc(100% - 50px); + height: calc(100% - 50px); position: relative; - padding: 0px; + border: 1px solid rgb(180, 180, 180); + padding: 10px; + margin-left: 20px; + margin-right: 20px; + + &.loading { + width: 100px; + height: 100px; + border: none; + } &:hover .docCreatorMenu-zoom-button-container { display: block; diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx new file mode 100644 index 000000000..7bc1b8064 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/DocCreatorMenu.tsx @@ -0,0 +1,1437 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Colors } from 'browndash-components'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import { IDisposer } from 'mobx-utils'; +import * as React from 'react'; +import ReactLoading from 'react-loading'; +import { ClientUtils, returnEmptyFilter, returnFalse, setupMoveUpEvents } from '../../../../../ClientUtils'; +import { emptyFunction } from '../../../../../Utils'; +import { Doc, NumListCast, StrListCast, returnEmptyDoclist } from '../../../../../fields/Doc'; +import { Id } from '../../../../../fields/FieldSymbols'; +import { ImageCast, StrCast } from '../../../../../fields/Types'; +import { ImageField } from '../../../../../fields/URLField'; +import { Networking } from '../../../../Network'; +import { GPTCallType, gptAPICall, gptImageCall } from '../../../../apis/gpt/GPT'; +import { Docs, DocumentOptions } from '../../../../documents/Documents'; +import { DragManager } from '../../../../util/DragManager'; +import { SnappingManager } from '../../../../util/SnappingManager'; +import { UndoManager, undoable } from '../../../../util/UndoManager'; +import { ObservableReactComponent } from '../../../ObservableReactComponent'; +import { CollectionFreeFormView } from '../../../collections/collectionFreeForm/CollectionFreeFormView'; +import { DocumentView, DocumentViewInternal } from '../../DocumentView'; +import { FieldViewProps } from '../../FieldView'; +import { OpenWhere } from '../../OpenWhere'; +import { DataVizBox } from '../DataVizBox'; +import './DocCreatorMenu.scss'; +import { DefaultStyleProvider } from '../../../StyleProvider'; +import { Transform } from '../../../../util/Transform'; +import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from './TemplateBackend'; +import { TemplateManager } from './TemplateManager'; +import { Template } from './Template'; +import { Field, FieldContentType } from './FieldTypes/Field'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; + +export enum LayoutType { + FREEFORM = 'Freeform', + CAROUSEL = 'Carousel', + CAROUSEL3D = '3D Carousel', + MASONRY = 'Masonry', + CARD = 'Card View', +} + +export interface DataVizTemplateInfo { + doc: Doc; + layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; + columns: number; + referencePos: { x: number; y: number }; +} + +export interface DataVizTemplateLayout { + template: Doc; + docsNumList: number[]; + layout: { type: LayoutType; xMargin: number; yMargin: number; repeat: number }; + columns: number; + rows: number; +} + +export type Col = { + sizes: TemplateFieldSize[]; + desc: string; + title: string; + type: TemplateFieldType; + defaultContent?: string; +}; + +@observer +export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> { + static Instance: DocCreatorMenu; + + private _disposers: { [name: string]: IDisposer } = {}; + + private _ref: HTMLDivElement | null = null; + + private templateManager: TemplateManager; + + @observable _fullyRenderedDocs: Doc[] = []; + @observable _renderedDocCollectionPreview: Doc | undefined = undefined; + @observable _renderedDocCollection: Doc | undefined = undefined; + @observable _docsRendering: boolean = false; + + @observable _userTemplates: {template: Template, doc: Doc}[] = []; //!!! used to keep track of all templates, should be refactored to work with actual templates and not docs + @observable _selectedTemplate: Template | undefined = undefined; + @observable _currEditingTemplate: Template | undefined = undefined; + + @observable _userCreatedFields: Col[] = []; + @observable _selectedCols: { title: string; type: string; desc: string }[] | undefined = []; + + @observable _layout: { type: LayoutType; yMargin: number; xMargin: number; columns?: number; repeat: number } = { type: LayoutType.FREEFORM, yMargin: 10, xMargin: 10, columns: 3, repeat: 0 }; + @observable _layoutPreviewScale: number = 1; + @observable _savedLayouts: DataVizTemplateLayout[] = []; + @observable _expandedPreview: Doc | undefined = undefined; + + @observable _suggestedTemplates: Template[] = []; + @observable _suggestedTemplatePreviews: {doc: Doc, template: Template}[] = []; + @observable _GPTOpt: boolean = false; + @observable _callCount: number = 0; + @observable _GPTLoading: boolean = false; + + @observable _pageX: number = 0; + @observable _pageY: number = 0; + + @observable _hoveredLayoutPreview: number | undefined = undefined; + @observable _mouseX: number = -1; + @observable _mouseY: number = -1; + @observable _startPos?: { x: number; y: number }; + @observable _shouldDisplay: boolean = false; + + @observable _menuContent: 'templates' | 'options' | 'saved' | 'dashboard' = 'templates'; + @observable _dragging: boolean = false; + @observable _draggingIndicator: boolean = false; + @observable _dataViz?: DataVizBox; + @observable _interactionLock: boolean | undefined; + @observable _snapPt: {x: number, y: number} = {x: 0, y: 0}; + @observable _resizeHdlId: string = ''; + @observable _resizing: boolean = false; + @observable _offset: { x: number; y: number } = { x: 0, y: 0 }; + @observable _resizeUndo: UndoManager.Batch | undefined = undefined; + @observable _initDimensions: { width: number; height: number; x?: number; y?: number } = { width: 300, height: 400, x: undefined, y: undefined }; + @observable _menuDimensions: { width: number; height: number } = { width: 400, height: 400 }; + @observable _editing: boolean = false; + + constructor(props: any) { + super(props); + makeObservable(this); + DocCreatorMenu.Instance = this; + this.templateManager = new TemplateManager(TemplateLayouts.allTemplates); + } + + @action setDataViz = (dataViz: DataVizBox) => { + this._dataViz = dataViz; + this._selectedTemplate = undefined; + this._renderedDocCollection = undefined; + this._renderedDocCollectionPreview = undefined; + this._fullyRenderedDocs = []; + this._suggestedTemplatePreviews = []; + this._suggestedTemplates = []; + this._userCreatedFields = []; + }; + @action addUserTemplate = (template: Template) => { + this._userTemplates.push({template: template.cloneBase(), doc: template.getRenderedDoc()}); + }; + @action removeUserTemplate = (template: Template) => { + this._userTemplates = this._userTemplates.filter(info => info.template !== template); + } + @action updateTemplatePreview = (template: Template) => { + template.renderUpdates(); + const preview = {template: template, doc: template.getRenderedDoc()}; + this._suggestedTemplatePreviews = this._suggestedTemplatePreviews.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore + this._userTemplates = this._userTemplates.map(t => { return t.template === preview.template ? preview : t }); //prettier-ignore + }; + @action setSuggestedTemplates = (templates: Template[]) => { + this._suggestedTemplates = templates; + this._suggestedTemplatePreviews = templates.map(template => {return {template: template, doc: template.getRenderedDoc()}}); //prettier-ignore + }; + + @computed get docsToRender() { + return this._selectedTemplate ? NumListCast(this._dataViz?.layoutDoc.dataViz_selectedRows) : []; + } + + @computed get rowsCount() { + switch (this._layout.type) { + case LayoutType.FREEFORM: + return Math.ceil(this.docsToRender.length / (this._layout.columns ?? 1)) ?? 0; + case LayoutType.CAROUSEL3D: + return 1.8; + default: + return 1; + } + } + + @computed get columnsCount() { + switch (this._layout.type) { + case LayoutType.FREEFORM: + return this._layout.columns ?? 0; + case LayoutType.CAROUSEL3D: + return 3; + default: + return 1; + } + } + + @computed get selectedFields() { + return StrListCast(this._dataViz?.layoutDoc._dataViz_axes); + } + + @computed get fieldsInfos(): Col[] { + const colInfo = this._dataViz?.colsInfo; + return this.selectedFields + .map(field => { + const fieldInfo = colInfo?.get(field); + + const col: Col = { + title: field, + type: fieldInfo?.type ?? TemplateFieldType.UNSET, + desc: fieldInfo?.desc ?? '', + sizes: fieldInfo?.sizes ?? [TemplateFieldSize.MEDIUM], + }; + + if (fieldInfo?.defaultContent !== undefined) { + col.defaultContent = fieldInfo.defaultContent; + } + + return col; + }) + .concat(this._userCreatedFields); + } + + @computed get canMakeDocs() { + return this._selectedTemplate !== undefined && this._layout !== undefined; + } + + get bounds(): { t: number; b: number; l: number; r: number } { + const rect = this._ref?.getBoundingClientRect(); + const bounds = { t: rect?.top ?? 0, b: rect?.bottom ?? 0, l: rect?.left ?? 0, r: rect?.right ?? 0 }; + return bounds; + } + + setUpButtonClick = (e: any, func: () => void) => { + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + clickEv.preventDefault(); + func(); + }, 'create docs') + ); + }; + + @action + onPointerDown = (e: PointerEvent) => { + this._mouseX = e.clientX; + this._mouseY = e.clientY; + }; + + @action + onPointerUp = (e: PointerEvent) => { + if (this._resizing) { + this._initDimensions.width = this._menuDimensions.width; + this._initDimensions.height = this._menuDimensions.height; + this._initDimensions.x = this._pageX; + this._initDimensions.y = this._pageY; + document.removeEventListener('pointermove', this.onResize); + SnappingManager.SetIsResizing(undefined); + this._resizing = false; + } + if (this._dragging) { + document.removeEventListener('pointermove', this.onDrag); + this._dragging = false; + } + if (e.button !== 2 && !e.ctrlKey) return; + const curX = e.clientX; + const curY = e.clientY; + if (Math.abs(this._mouseX - curX) > 1 || Math.abs(this._mouseY - curY) > 1) { + this._shouldDisplay = false; + } + }; + + componentDidMount() { + document.addEventListener('pointerdown', this.onPointerDown, true); + document.addEventListener('pointerup', this.onPointerUp); + } + + componentWillUnmount() { + Object.values(this._disposers).forEach(disposer => disposer?.()); + document.removeEventListener('pointerdown', this.onPointerDown, true); + document.removeEventListener('pointerup', this.onPointerUp); + } + + @action + toggleDisplay = (x: number, y: number) => { + if (this._shouldDisplay) { + this._shouldDisplay = false; + } else { + this._pageX = x; + this._pageY = y; + this._shouldDisplay = true; + } + }; + + @action + closeMenu = () => { + this._shouldDisplay = false; + }; + + @action + openMenu = () => { + this._shouldDisplay = true; + }; + + @action + onResizePointerDown = (e: React.PointerEvent): void => { + this._resizing = true; + document.addEventListener('pointermove', this.onResize); + SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them + e.stopPropagation(); + const id = (this._resizeHdlId = e.currentTarget.className); + const pad = id.includes('Left') || id.includes('Right') ? Number(getComputedStyle(e.target as any).width.replace('px', '')) / 2 : 0; + const bounds = e.currentTarget.getBoundingClientRect(); + this._offset = { + x: id.toLowerCase().includes('left') ? bounds.right - e.clientX - pad : bounds.left - e.clientX + pad, // + y: id.toLowerCase().includes('top') ? bounds.bottom - e.clientY - pad : bounds.top - e.clientY + pad, + }; + this._resizeUndo = UndoManager.StartBatch('drag resizing'); + this._snapPt = { x: e.pageX, y: e.pageY }; + }; + + @action + onResize = (e: any): boolean => { + const dragHdl = this._resizeHdlId.split(' ')[1]; + const thisPt = DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y); + + const { scale, refPt, transl } = this.getResizeVals(thisPt, dragHdl); + !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate) + this._interactionLock = true; + const scaleAspect = {x: scale.x, y: scale.y}; + this.resizeView(refPt, scaleAspect, transl); // prettier-ignore + await new Promise<boolean | undefined>(res => { setTimeout(() => { res(this._interactionLock = undefined)})}); + }); // prettier-ignore + return true; + }; + + @action + onDrag = (e: any): boolean => { + this._pageX = e.pageX - (this._startPos?.x ?? 0); + this._pageY = e.pageY - (this._startPos?.y ?? 0); + this._initDimensions.x = this._pageX; + this._initDimensions.y = this._pageY; + return true; + }; + + getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => { + const [w, h] = [this._initDimensions.width, this._initDimensions.height]; + const [moveX, moveY] = [thisPt.x - this._snapPt!.x, thisPt.y - this._snapPt!.y]; + let vals: { scale: { x: number; y: number }; refPt: [number, number]; transl: { x: number; y: number } }; + switch (dragHdl) { + case 'topLeft': vals = { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.r, this.bounds.b], transl: {x: moveX, y: moveY } }; break; + case 'topRight': vals = { scale: { x: 1 + moveX / w, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; + case 'top': vals = { scale: { x: 1, y: 1 -moveY / h }, refPt: [this.bounds.l, this.bounds.b], transl: {x: 0, y: moveY } }; break; + case 'left': vals = { scale: { x: 1 - moveX / w, y: 1 }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; + case 'bottomLeft': vals = { scale: { x: 1 - moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.r, this.bounds.t], transl: {x: moveX, y: 0 } }; break; + case 'right': vals = { scale: { x: 1 + moveX / w, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + case 'bottomRight':vals = { scale: { x: 1 + moveX / w, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + case 'bottom': vals = { scale: { x: 1, y: 1 + moveY / h }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + default: vals = { scale: { x: 1, y: 1 }, refPt: [this.bounds.l, this.bounds.t], transl: {x: 0, y: 0 } }; break; + } // prettier-ignore + return vals; + }; + + resizeView = (refPt: number[], scale: { x: number; y: number }, translation: { x: number; y: number }) => { + if (this._initDimensions.x === undefined) this._initDimensions.x = this._pageX; + if (this._initDimensions.y === undefined) this._initDimensions.y = this._pageY; + const { height, width, x, y } = this._initDimensions; + + this._menuDimensions.width = Math.max(300, scale.x * width); + this._menuDimensions.height = Math.max(200, scale.y * height); + this._pageX = x + translation.x; + this._pageY = y + translation.y; + }; + + async getIcon(doc: Doc) { + const docView = DocumentView.getDocumentView(doc); + if (docView) { + docView.ComponentView?.updateIcon?.(); + return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 500)); + } + return undefined; + } + + @action updateSelectedTemplate = async (template: Template) => { + if (this._selectedTemplate === template) { + this._selectedTemplate = undefined; + return; + } else { + this._selectedTemplate = template; + template.renderUpdates(); + this._fullyRenderedDocs = await this.createDocsFromTemplate(template) ?? []; + this.updateRenderedDocCollection(); + } + }; + + @action updateSelectedSavedLayout = (layout: DataVizTemplateLayout) => { + this._layout.xMargin = layout.layout.xMargin; + this._layout.yMargin = layout.layout.yMargin; + this._layout.type = layout.layout.type; + this._layout.columns = layout.columns; + }; + + isSelectedLayout = (layout: DataVizTemplateLayout) => { + return this._layout.xMargin === layout.layout.xMargin && this._layout.yMargin === layout.layout.yMargin && this._layout.type === layout.layout.type && this._layout.columns === layout.columns; + }; + + editTemplate = (doc: Doc) => { + DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight); + DocumentView.DeselectAll(); + Doc.UnBrushDoc(doc); + }; + + @action addField = () => { + const newFields: Col[] = this._userCreatedFields.concat([{ title: '', type: TemplateFieldType.UNSET, desc: '', sizes: [] }]); + this._userCreatedFields = newFields; + }; + + @action removeField = (field: { title: string; type: string; desc: string }) => { + if (this._dataViz?.axes.includes(field.title)) { + this._dataViz.selectAxes(this._dataViz.axes.filter(col => col !== field.title)); + } else { + const toRemove = this._userCreatedFields.filter(f => f === field); + if (!toRemove) return; + + if (toRemove.length > 1) { + while (toRemove.length > 1) { + toRemove.pop(); + } + } + + if (this._userCreatedFields.length === 1) { + this._userCreatedFields = []; + } else { + this._userCreatedFields.splice(this._userCreatedFields.indexOf(toRemove[0]), 1); + } + } + }; + + @action setColTitle = (column: Col, title: string) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.setColumnTitle(column.title, title); + } else { + column.title = title; + } + this.forceUpdate(); + }; + + @action setColType = (column: Col, type: TemplateFieldType) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.setColumnType(column.title, type); + } else { + column.type = type; + } + this.forceUpdate(); + }; + + modifyColSizes = (column: Col, size: TemplateFieldSize, valid: boolean) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.modifyColumnSizes(column.title, size, valid); + } else { + if (!valid && column.sizes.includes(size)) { + column.sizes.splice(column.sizes.indexOf(size), 1); + } else if (valid && !column.sizes.includes(size)) { + column.sizes.push(size); + } + } + this.forceUpdate(); + }; + + setColDesc = (column: Col, desc: string) => { + if (this.selectedFields.includes(column.title)) { + this._dataViz?.setColumnDesc(column.title, desc); + } else { + column.desc = desc; + } + this.forceUpdate(); + }; + + generateGPTImage = async (prompt: string): Promise<string | undefined> => { + try { + const res = await gptImageCall(prompt); + + if (res) { + const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res }); + const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client); + return source; + } + } catch (e) { + console.log(e); + } + }; + + /** + * Populates a preset template framework with content from a datavizbox or any AI-generated content. + * @param template the preloaded template framework being filled in + * @param assignments a list of template field numbers (from top to bottom) and their assigned columns from the linked dataviz + * @returns a doc containing the fully rendered template + */ + applyGPTContentToTemplate = async (template: Template, assignments: { [field: string]: Col }): Promise<Template | undefined> => { + + const GPTTextCalls = Object.entries(assignments).filter(([str, col]) => col.type === TemplateFieldType.TEXT && this._userCreatedFields.includes(col)); + const GPTIMGCalls = Object.entries(assignments).filter(([str, col]) => col.type === TemplateFieldType.VISUAL && this._userCreatedFields.includes(col)); + + if (GPTTextCalls.length) { + const promises = GPTTextCalls.map(([str, col]) => { + return this.renderGPTTextCall(template, col, Number(str)); + }); + + await Promise.all(promises); + } + + if (GPTIMGCalls.length) { + const promises = GPTIMGCalls.map(async ([fieldNum, col]) => { + return this.renderGPTImageCall(template, col, Number(fieldNum)); + }); + + await Promise.all(promises); + }; + + return template; + }; + + compileFieldDescriptions = (templates: Template[]): string => { + let descriptions: string = ''; + templates.forEach(template => { + descriptions += `---------- NEW TEMPLATE TO INCLUDE: The title is: ${template.mainField.getTitle()}. Its fields are: `; + descriptions += template.descriptionSummary; + }); + + return descriptions; + }; + + compileColDescriptions = (cols: Col[]): string => { + let descriptions: string = ' ------------- COL DESCRIPTIONS START HERE:'; + cols.forEach(col => (descriptions += `{title: ${col.title}, sizes: ${String(col.sizes)}, type: ${col.type}, descreiption: ${col.desc}} `)); + + return descriptions; + }; + + getColByTitle = (title: string) => { + return this.fieldsInfos.filter(col => col.title === title)[0]; + }; + + @action + assignColsToFields = async (templates: Template[], cols: Col[]): Promise<[Template, { [field: number]: Col }][]> => { + const fieldDescriptions: string = this.compileFieldDescriptions(templates); + const colDescriptions: string = this.compileColDescriptions(cols); + + const inputText = fieldDescriptions.concat(colDescriptions); + + ++this._callCount; + const origCount = this._callCount; + + const prompt: string = `(${origCount}) ${inputText}`; + + this._GPTLoading = true; + + try { + const res = await gptAPICall(prompt, GPTCallType.TEMPLATE); + + if (res) { + const assignments: { [templateTitle: string]: { [fieldID: string]: string } } = JSON.parse(res); + const brokenDownAssignments: [Template, { [fieldID: number]: Col }][] = []; + + Object.entries(assignments).forEach(([tempTitle, assignment]) => { + const template = templates.filter(t => t.mainField.getTitle() === tempTitle)[0]; + if (!template) return; + const toObj = Object.entries(assignment).reduce( + (a, [fieldID, colTitle]) => { + const col = this.getColByTitle(colTitle); + if (!this._userCreatedFields.includes(col)){ // do the following for any fields not added by the user; will change in the future, for now only GPT content works with user-added fields + const field = template.getFieldByID(Number(fieldID)); + field.setContent(col.defaultContent ?? '', col.type === TemplateFieldType.VISUAL ? FieldContentType.IMAGE : FieldContentType.STRING); + field.setTitle(col.title); + } else { + a[Number(fieldID)] = this.getColByTitle(colTitle); + } + return a; + }, + {} as { [field: number]: Col } + ); + brokenDownAssignments.push([template, toObj]); + }); + + return brokenDownAssignments; + } + } catch (err) { + console.error(err); + } + + return []; + }; + + generatePresetTemplates = async () => { + this._dataViz?.updateColDefaults(); + + const cols = this.fieldsInfos; + const templates = this.templateManager.getValidTemplates(cols); + + const assignments: [Template, { [field: number]: Col }][] = await this.assignColsToFields(templates, cols); + + const renderedTemplatePromises: Promise<Template | undefined>[] = assignments.map(([template, asns]) => this.applyGPTContentToTemplate(template, asns)); + + await Promise.all(renderedTemplatePromises); + + setTimeout(() => { + this.setSuggestedTemplates(templates); + this._GPTLoading = false; + }); + }; + + renderGPTImageCall = async (template: Template, col: Col, fieldNumber: number): Promise<boolean> => { + const generateAndLoadImage = async (fieldNum: string, column: Col, prompt: string) => { + const url = await this.generateGPTImage(prompt); + const field: Field = template.getFieldByID(Number(fieldNum)); + + field.setContent(url ?? '', FieldContentType.IMAGE); + field.setTitle(column.title); + }; + + const fieldContent: string = template.compiledContent; + + try { + const sysPrompt = + 'Your job is to create a prompt for an AI image generator to help it generate an image based on existing content in a template and a user prompt. Your prompt should focus heavily on visual elements to help the image generator; avoid unecessary info that might distract it. ONLY INCLUDE THE PROMPT, NO OTHER TEXT OR EXPLANATION. The existing content is as follows: ' + + fieldContent + + ' **** The user prompt is: ' + + col.desc; + + const prompt = await gptAPICall(sysPrompt, GPTCallType.COMPLETEPROMPT); + + await generateAndLoadImage(String(fieldNumber), col, prompt); + } catch (e) { + console.log(e); + } + return true; + } + + renderGPTTextCall = async (template: Template, col: Col, fieldNum: number): Promise<boolean> => { + const wordLimit = (size: TemplateFieldSize) => { + switch (size) { + case TemplateFieldSize.TINY: + return 2; + case TemplateFieldSize.SMALL: + return 5; + case TemplateFieldSize.MEDIUM: + return 20; + case TemplateFieldSize.LARGE: + return 50; + case TemplateFieldSize.HUGE: + return 100; + default: + return 10; + } + }; + + const textAssignment = `--- title: ${col.title}, prompt: ${col.desc}, word limit: ${wordLimit(col.sizes[0])} words, assigned field: ${fieldNum} ---`; + + const fieldContent: string = template.compiledContent; + + try { + const prompt = fieldContent + textAssignment; + + const res = await gptAPICall(`${++this._callCount}: ${prompt}`, GPTCallType.FILL); + + if (res) { + const assignments: { [title: string]: { number: string; content: string } } = JSON.parse(res); + Object.entries(assignments).forEach(([title, info]) => { + const field: Field = template.getFieldByID(Number(info.number)); + const column = this.getColByTitle(title); + + field.setContent(info.content ?? '', FieldContentType.STRING); + field.setTitle(column.title); + }); + } + } catch (err) { + console.log(err); + } + + return true; + } + + createDocsFromTemplate = async (template: Template) => { + const dv = this._dataViz; + + if (!dv) return; + + this._docsRendering = true; + + const fields: string[] = Array.from(Object.keys(dv.records[0])); + const selectedRows = NumListCast(dv.layoutDoc.dataViz_selectedRows); + + const rowContents: { [title: string]: string }[] = selectedRows.map(row => { + const values: { [title: string]: string } = {}; + fields.forEach(col => { + values[col] = dv.records[row][col]; + }); + + return values; + }); + + const processContent = async (content: {[title: string]: string}) => { + const templateCopy = template.cloneBase(); + + fields.filter(title => title).forEach(title => { + const field = templateCopy.getFieldByTitle(title); + if (field === undefined) { return }; + field.setContent(content[title]); + }); + + const gptPromises = this._userCreatedFields.filter(field => field.type === TemplateFieldType.TEXT).map(field => { + const title = field.title; + const templateField = templateCopy.getFieldByTitle(title); + if (templateField === undefined) { return }; + const templatefieldID = templateField.getID; + + return this.renderGPTTextCall(templateCopy, field, templatefieldID); + }); + + const imagePromises = this._userCreatedFields.filter(field => field.type === TemplateFieldType.VISUAL).map(field => { + const title = field.title; + const templateField = templateCopy.getFieldByTitle(title); + if (templateField === undefined) { return }; + const templatefieldID = templateField.getID; + + return this.renderGPTImageCall(templateCopy, field, templatefieldID); + }); + + await Promise.all(gptPromises); + + await Promise.all(imagePromises); + + return templateCopy.getRenderedDoc(); + }; + + const promises = rowContents.map(content => processContent(content)); + + const renderedDocs = await Promise.all(promises); + + this._docsRendering = false; + + return renderedDocs; + } + + + addRenderedCollectionToMainview = () => { + const collection = this._renderedDocCollection; + if (!collection) return; + const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; + collection.x = this._pageX - this._menuDimensions.width; + collection.y = this._pageY - this._menuDimensions.height; + mainCollection.addDocument(collection); + this.closeMenu(); + } + + @action setExpandedView = (template: Template | undefined) => { + if (template) { + this._currEditingTemplate = template; + this._expandedPreview = template.mainField.renderedDoc(); //Docs.Create.FreeformDocument([doc], { _height: NumListCast(doc._height)[0], _width: NumListCast(doc._width)[0], title: ''}); + } else { + this._currEditingTemplate = undefined; + this._expandedPreview = undefined; + } + }; + + get editingWindow(){ + const rendered = !this._expandedPreview ? null : + <div className="docCreatorMenu-expanded-template-preview"> + <DocumentView + Document={this._expandedPreview} + isContentActive={emptyFunction} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => this._menuDimensions.width - 10} + PanelHeight={() => this._menuDimensions.height - 60} + ScreenToLocalTransform={() => new Transform(-this._pageX - 5,-this._pageY - 35, 1)} + renderDepth={5} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + /> + </div> + + return ( + <div className="docCreatorMenu-expanded-template-preview"> + <div className="top-panel"/> + {rendered} + <div className="right-buttons-panel"> + <button className="docCreatorMenu-menu-button section-reveal-options top-right" onPointerDown={e => this.setUpButtonClick(e, () => { + this._currEditingTemplate && this.updateTemplatePreview(this._currEditingTemplate); + this.setExpandedView(undefined)} + )}> + <FontAwesomeIcon icon="minimize" /> + </button> + <button className="docCreatorMenu-menu-button section-reveal-options top-right-lower" onPointerDown={e => this.setUpButtonClick(e, () => {this._currEditingTemplate?.resetToBase(); this.setExpandedView(this._currEditingTemplate);})}> + <FontAwesomeIcon icon="arrows-rotate" color="white" /> + </button> + </div> + </div> + ); + } + + get templatesPreviewContents() { + + const GPTOptions = <div></div>; + + return ( + <div className={`docCreatorMenu-templates-view`}> + {this._expandedPreview ? ( + this.editingWindow + ) : ( + <div> + <div className="docCreatorMenu-section" style={{ height: this._GPTOpt ? 200 : 200 }}> + <div className="docCreatorMenu-section-topbar"> + <div className="docCreatorMenu-section-title">Suggested Templates</div> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'dashboard')))}> + <FontAwesomeIcon icon="gear" /> + </button> + </div> + <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._GPTLoading || this._menuDimensions.width > 400 ? 'center' : '' }}> + {this._GPTLoading ? ( + <div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + </div> + ) : ( + this._suggestedTemplatePreviews + .map(({doc, template}) => ( + <div + className="docCreatorMenu-preview-window" + key='0' + style={{ + border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', + boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '', + }} + onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}> + <button + className="option-button left" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this.setExpandedView(template); + }) + }> + <FontAwesomeIcon icon="magnifying-glass" color="white" /> + </button> + <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.addUserTemplate(template))}> + <FontAwesomeIcon icon="plus" color="white" /> + </button> + <DocumentView + Document={doc} + isContentActive={emptyFunction} // !!! should be return false + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => this._selectedTemplate === template ? 104 : 111} + PanelHeight={() => this._selectedTemplate === template ? 104 : 111} + ScreenToLocalTransform={() => new Transform(-this._pageX - 5,-this._pageY - 35, 1)} + renderDepth={1} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + )) + )} + </div> + <div className="docCreatorMenu-GPT-options"> + <div className="docCreatorMenu-GPT-options-container"> + <button className="docCreatorMenu-menu-button" onPointerDown={e => this.setUpButtonClick(e, () => this.generatePresetTemplates())}> + <FontAwesomeIcon icon="arrows-rotate" /> + </button> + </div> + {this._GPTOpt ? GPTOptions : null} + </div> + </div> + <hr className="docCreatorMenu-option-divider full no-margin" /> + <div className="docCreatorMenu-section"> + <div className="docCreatorMenu-section-topbar"> + <div className="docCreatorMenu-section-title">Your Templates</div> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, () => (this._GPTOpt = !this._GPTOpt))}> + <FontAwesomeIcon icon="gear" /> + </button> + </div> + <div className="docCreatorMenu-templates-preview-window" style={{ justifyContent: this._menuDimensions.width > 400 ? 'center' : '' }}> + <div className="docCreatorMenu-preview-window empty"> + <FontAwesomeIcon icon="plus" color="rgb(160, 160, 160)" /> + </div> + {this._userTemplates + .map(({template, doc}) => ( + <div + className="docCreatorMenu-preview-window" + key='0' + style={{ + border: this._selectedTemplate === template ? `solid 3px ${Colors.MEDIUM_BLUE}` : '', + boxShadow: this._selectedTemplate === template ? `0 0 15px rgba(68, 118, 247, .8)` : '', + }} + onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => this.updateSelectedTemplate(template)))}> + <button + className="option-button left" + onPointerDown={e => + this.setUpButtonClick(e, () => { + this.setExpandedView(template); + }) + }> + <FontAwesomeIcon icon="magnifying-glass" color="white" /> + </button> + <button className="option-button right" onPointerDown={e => this.setUpButtonClick(e, () => this.removeUserTemplate(template))}> + <FontAwesomeIcon icon="minus" color="white" /> + </button> + <DocumentView + Document={doc} + isContentActive={emptyFunction} // !!! should be return false + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => this._selectedTemplate === template ? 104 : 111} + PanelHeight={() => this._selectedTemplate === template ? 104 : 111} + ScreenToLocalTransform={() => new Transform(-this._pageX - 5, -this._pageY - 35, 1)} + renderDepth={1} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + ))} + </div> + </div> + </div> + )} + </div> + ); + } + + + @action updateXMargin = (input: string) => { + this._layout.xMargin = Number(input); + setTimeout(() => { + if (!this._renderedDocCollection || !this._fullyRenderedDocs) return; + this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs); + }); + }; + @action updateYMargin = (input: string) => { + this._layout.yMargin = Number(input); + setTimeout(() => { + if (!this._renderedDocCollection || !this._fullyRenderedDocs) return; + this.applyLayout(this._renderedDocCollection, this._fullyRenderedDocs); + }); + }; + @action updateColumns = (input: string) => { + this._layout.columns = Number(input); + this.updateRenderedDocCollection(); + }; + + get layoutConfigOptions() { + const optionInput = (icon: string, func: (input: string) => void, def?: number, key?: string, noMargin?: boolean) => { + return ( + <div className="docCreatorMenu-option-container small no-margin" key={key} style={{ marginTop: noMargin ? '0px' : '' }}> + <div className="docCreatorMenu-option-title config layout-config"> + <FontAwesomeIcon icon={icon as IconProp} /> + </div> + <input defaultValue={def} onInput={e => func(e.currentTarget.value)} className="docCreatorMenu-input config layout-config" /> + </div> + ); + }; + + switch (this._layout.type) { + case LayoutType.FREEFORM: + return ( + <div className="docCreatorMenu-configuration-bar"> + {optionInput('arrows-up-down', this.updateYMargin, this._layout.xMargin, '2')} + {optionInput('arrows-left-right', this.updateXMargin, this._layout.xMargin, '3')} + {optionInput('table-columns', this.updateColumns, this._layout.columns, '4', true)} + </div> + ); + default: + break; + } + } + + screenToLocalTransform = () => this._props.ScreenToLocalTransform(); + + applyLayout = (collection: Doc, docs: Doc[]) => { + const { horizontalSpan, verticalSpan } = this.previewInfo; + collection._height = verticalSpan; + collection._width = horizontalSpan; + + const layout = this._layout; + const columns: number = layout.columns ?? this.columnsCount; + const xGap: number = layout.xMargin; + const yGap: number = layout.yMargin; + // const repeat: number = templateInfo.layout.repeat; + const startX: number = -Number(collection._width)/2; + const startY: number = -Number(collection._height)/2; + const docHeight: number = Number(docs[0]._height); + const docWidth: number = Number(docs[0]._width); + + if (columns === 0 || docs.length === 0){ + return; + } + + let i: number = 0; + let docsChanged: number = 0; + let curX: number = startX; + let curY: number = startY; + + while (docsChanged < docs.length) { + while (i < columns && docsChanged < docs.length) { + docs[docsChanged].x = curX; + docs[docsChanged].y = curY; + curX += docWidth + xGap; + ++docsChanged; + ++i; + } + i = 0; + curX = startX; + curY += docHeight + yGap; + } + }; + + @computed + get previewInfo(){ + const docHeight: number = Number(this._fullyRenderedDocs[0]._height); + const docWidth: number = Number(this._fullyRenderedDocs[0]._width); + const layout = this._layout; + return { + docHeight: docHeight, + docWidth: docWidth, + horizontalSpan: (docWidth + layout.xMargin) * (this.columnsCount) - layout.xMargin, + verticalSpan: (docHeight + layout.yMargin) * (this.rowsCount) - layout.yMargin, + } + } + + /** + * Updates the preview that shows how all docs will be rendered in the chosen collection type. + @type the type of collection the docs should render to (ie. freeform, carousel, card) + */ + updateRenderedDocCollection = () => { + if (!this._fullyRenderedDocs) return; + + const { horizontalSpan, verticalSpan } = this.previewInfo; + + const collectionFactory = (): (docs: Doc[], options: DocumentOptions) => Doc => { + switch (this._layout.type) { + case LayoutType.CAROUSEL3D: + return Docs.Create.Carousel3DDocument; + case LayoutType.FREEFORM: + return Docs.Create.FreeformDocument; + case LayoutType.CARD: + return Docs.Create.CardDeckDocument; + case LayoutType.MASONRY: + return Docs.Create.MasonryDocument; + case LayoutType.CAROUSEL: + return Docs.Create.CarouselDocument; + default: + return Docs.Create.FreeformDocument; + } + } + + const collection: Doc = collectionFactory()(this._fullyRenderedDocs, { + isDefaultTemplateDoc: true, + _height: verticalSpan, + _width: horizontalSpan, + title: 'title', + backgroundColor: 'gray', + }); + + this.applyLayout(collection, this._fullyRenderedDocs); + + this._renderedDocCollection = collection; + } + + layoutPreviewContents = () => { + + return this._docsRendering ? ( + <div className="docCreatorMenu-layout-preview-window-wrapper loading"> + <div className="loading-spinner"> + <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} /> + </div> + </div> + ) : !this._renderedDocCollection? null : ( + <div className="docCreatorMenu-layout-preview-window-wrapper"> + <DocumentView + Document={this._renderedDocCollection} + isContentActive={emptyFunction} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={() => this._menuDimensions.width - 80} + PanelHeight={() => this._menuDimensions.height - 105} + ScreenToLocalTransform={() => new Transform(-this._pageX - 5,-this._pageY - 35, 1)} + renderDepth={5} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={DefaultStyleProvider} + addDocTab={this._props.addDocTab} + pinToPres={() => undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + fitContentsToBox={returnFalse} + fitWidth={returnFalse} + hideDecorations={true} + /> + </div> + ) + }; + + get optionsMenuContents() { + const layoutOption = (option: LayoutType, optStyle?: object, specialFunc?: () => void) => { + return ( + <div + className="docCreatorMenu-dropdown-option" + style={optStyle} + onPointerDown={e => + this.setUpButtonClick(e, () => { + specialFunc?.(); + runInAction(() => { + this._layout.type = option; + this.updateRenderedDocCollection(); + }); + }) + }> + {option} + </div> + ); + }; + + const selectionBox = (width: number, height: number, icon: string, specClass?: string, options?: JSX.Element[], manual?: boolean): JSX.Element => { + return ( + <div className="docCreatorMenu-option-container"> + <div className={`docCreatorMenu-option-title config ${specClass}`} style={{ width: width * 0.4, height: height }}> + <FontAwesomeIcon icon={icon as IconProp} /> + </div> + {manual ? ( + <input className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }} /> + ) : ( + <select className={`docCreatorMenu-input config ${specClass}`} style={{ width: width * 0.6, height: height }}> + {options} + </select> + )} + </div> + ); + }; + + const repeatOptions = [0, 1, 2, 3, 4, 5]; + + return ( + <div className="docCreatorMenu-menu-container"> + <div className="docCreatorMenu-option-container layout"> + <div className="docCreatorMenu-dropdown-hoverable"> + <div className="docCreatorMenu-option-title">{this._layout.type ? this._layout.type.toUpperCase() : 'Choose Layout'}</div> + <div className="docCreatorMenu-dropdown-content"> + {layoutOption(LayoutType.FREEFORM, undefined, () => { + if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); + })} + {layoutOption(LayoutType.CAROUSEL)} + {layoutOption(LayoutType.CAROUSEL3D)} + {layoutOption(LayoutType.MASONRY)} + </div> + </div> + </div> + {this._layout.type ? this.layoutConfigOptions : null} + {this.layoutPreviewContents()} + {selectionBox( + 60, + 20, + 'repeat', + undefined, + repeatOptions.map(num => <option key={num} onPointerDown={() => (this._layout.repeat = num)}>{`${num}x`}</option>) + )} + <hr className="docCreatorMenu-option-divider" /> + <div className="docCreatorMenu-general-options-container"> + <button + className="docCreatorMenu-save-layout-button" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + if (!this._selectedTemplate) return; + const layout: DataVizTemplateLayout = { + template: this._selectedTemplate.getRenderedDoc(), + layout: { type: this._layout.type, xMargin: this._layout.xMargin, yMargin: this._layout.yMargin, repeat: 0 }, + columns: this.columnsCount, + rows: this.rowsCount, + docsNumList: this.docsToRender, + }; + if (!this._savedLayouts.includes(layout)) { + this._savedLayouts.push(layout); + } + }, 'make docs') + ) + }> + <FontAwesomeIcon icon="floppy-disk" /> + </button> + <button + className="docCreatorMenu-create-docs-button" + style={{ backgroundColor: this.canMakeDocs ? '' : 'rgb(155, 155, 155)', border: this.canMakeDocs ? '' : 'solid 2px rgb(180, 180, 180)' }} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + returnFalse, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + if (!this._selectedTemplate) return; + this.addRenderedCollectionToMainview(); + }, 'make docs') + ) + }> + <FontAwesomeIcon icon="plus" /> + </button> + </div> + </div> + ); + } + + get dashboardContents() { + const sizes: string[] = ['tiny', 'small', 'medium', 'large', 'huge']; + + const fieldPanel = (field: Col, id: number) => { + return ( + <div className="field-panel" key={id}> + <div className="top-bar"> + <span className="field-title">{`${field.title} Field`}</span> + <button className="docCreatorMenu-menu-button section-reveal-options no-margin" onPointerDown={e => this.setUpButtonClick(e, () => this.removeField(field))} style={{ position: 'absolute', right: '0px' }}> + <FontAwesomeIcon icon="minus" /> + </button> + </div> + <div className="opts-bar"> + <div className="opt-box"> + <div className="top-bar"> Title </div> + <textarea className="content" style={{ width: '100%', height: 'calc(100% - 20px)' }} value={field.title} placeholder={'Enter title'} onChange={e => this.setColTitle(field, e.target.value)} /> + </div> + <div className="opt-box"> + <div className="top-bar"> Type </div> + <div className="content"> + <span className="type-display">{field.type === TemplateFieldType.TEXT ? 'Text Field' : field.type === TemplateFieldType.VISUAL ? 'File Field' : ''}</span> + <div className="bubbles"> + <input + className="bubble" + type="radio" + name="type" + onClick={() => { + this.setColType(field, TemplateFieldType.TEXT); + }} + /> + <div className="text">Text</div> + <input + className="bubble" + type="radio" + name="type" + onClick={() => { + this.setColType(field, TemplateFieldType.VISUAL); + }} + /> + <div className="text">File</div> + </div> + </div> + </div> + </div> + <div className="sizes-box"> + <div className="top-bar"> Valid Sizes </div> + <div className="content"> + <div className="bubbles"> + {sizes.map(size => ( + <> + <input + className="bubble" + type="checkbox" + name="type" + checked={field.sizes.includes(size as TemplateFieldSize)} + onChange={e => { + this.modifyColSizes(field, size as TemplateFieldSize, e.target.checked); + }} + /> + <div className="text">{size}</div> + </> + ))} + </div> + </div> + </div> + <div className="desc-box"> + <div className="top-bar"> Prompt </div> + <textarea + className="content" + onChange={e => this.setColDesc(field, e.target.value)} + defaultValue={field.desc === this._dataViz?.GPTSummary?.get(field.title)?.desc ? '' : field.desc} + placeholder={this._dataViz?.GPTSummary?.get(field.title)?.desc ?? 'Add a description/prompt to help with template generation.'} + /> + </div> + </div> + ); + }; + + return ( + <div className="docCreatorMenu-dashboard-view"> + <div className="topbar"> + <button className="docCreatorMenu-menu-button section-reveal-options" onPointerDown={e => this.setUpButtonClick(e, this.addField)}> + <FontAwesomeIcon icon="plus" /> + </button> + <button className="docCreatorMenu-menu-button section-reveal-options float-right" onPointerDown={e => this.setUpButtonClick(e, () => runInAction(() => (this._menuContent = 'templates')))}> + <FontAwesomeIcon icon="arrow-left" /> + </button> + </div> + <div className="panels-container">{this.fieldsInfos.map((field, i) => fieldPanel(field, i))}</div> + </div> + ); + } + + get renderSelectedViewType() { + switch (this._menuContent) { + case 'templates': + return this.templatesPreviewContents; + case 'options': + return this.optionsMenuContents; + case 'dashboard': + return this.dashboardContents; + default: + return undefined; + } + } + + get resizePanes() { + const ref = this._ref?.getBoundingClientRect(); + const height: number = ref?.height ?? 0; + const width: number = ref?.width ?? 0; + + return [ + <div className='docCreatorMenu-resizer top' key='0' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: -7}}/>, + <div className='docCreatorMenu-resizer left' key='1' onPointerDown={this.onResizePointerDown} style={{height: height, left: -7, top: 0}}/>, + <div className='docCreatorMenu-resizer right' key='2' onPointerDown={this.onResizePointerDown} style={{height: height, left: width - 3, top: 0}}/>, + <div className='docCreatorMenu-resizer bottom' key='3' onPointerDown={this.onResizePointerDown} style={{width: width, left: 0, top: height - 3}}/>, + <div className='docCreatorMenu-resizer topLeft' key='4' onPointerDown={this.onResizePointerDown} style={{left: -10, top: -10, cursor: 'nwse-resize'}}/>, + <div className='docCreatorMenu-resizer topRight' key='5' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: -10, cursor: 'nesw-resize'}}/>, + <div className='docCreatorMenu-resizer bottomLeft' key='6' onPointerDown={this.onResizePointerDown} style={{left: -10, top: height - 5, cursor: 'nesw-resize'}}/>, + <div className='docCreatorMenu-resizer bottomRight' key='7' onPointerDown={this.onResizePointerDown} style={{left: width - 5, top: height - 5, cursor: 'nwse-resize'}}/>, + ]; //prettier-ignore + } + + render() { + const topButton = (icon: string, opt: string, func: () => void, tag: string) => { + return ( + <div className={`top-button-container ${tag} ${opt === this._menuContent ? 'selected' : ''}`}> + <div + className="top-button-content" + onPointerDown={e => + this.setUpButtonClick(e, () => + runInAction(() => { + func(); + }) + ) + }> + <FontAwesomeIcon icon={icon as IconProp} /> + </div> + </div> + ); + }; + + const onPreviewSelected = () => { + this._menuContent = 'templates'; + }; + const onSavedSelected = () => { + this._menuContent = 'dashboard'; + }; + const onOptionsSelected = () => { + this._menuContent = 'options'; + if (!this._layout.columns) this._layout.columns = Math.ceil(Math.sqrt(this.docsToRender.length)); + }; + + return ( + <div className="docCreatorMenu"> + {!this._shouldDisplay ? undefined : ( + <div + className="docCreatorMenu-cont" + ref={r => (this._ref = r)} + style={{ + display: '', + left: this._pageX, + top: this._pageY, + width: this._menuDimensions.width, + height: this._menuDimensions.height, + background: SnappingManager.userBackgroundColor, + color: SnappingManager.userColor, + }}> + {this.resizePanes} + <div + className="docCreatorMenu-menu" + onPointerDown={e => + setupMoveUpEvents( + this, + e, + event => { + this._dragging = true; + this._startPos = { x: 0, y: 0 }; + this._startPos.x = event.pageX - (this._ref?.getBoundingClientRect().left ?? 0); + this._startPos.y = event.pageY - (this._ref?.getBoundingClientRect().top ?? 0); + document.addEventListener('pointermove', this.onDrag); + return true; + }, + emptyFunction, + undoable(clickEv => { + clickEv.stopPropagation(); + }, 'drag menu') + ) + }> + <div className="docCreatorMenu-top-buttons-container"> + {topButton('lightbulb', 'templates', onPreviewSelected, 'left')} + {topButton('magnifying-glass', 'options', onOptionsSelected, 'middle')} + {topButton('bars', 'saved', onSavedSelected, 'right')} + </div> + <button className="docCreatorMenu-menu-button close-menu" onPointerDown={e => this.setUpButtonClick(e, this.closeMenu)}> + <FontAwesomeIcon icon={'minus'} /> + </button> + </div> + {this.renderSelectedViewType} + </div> + )} + </div> + ); + } +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx new file mode 100644 index 000000000..c5254c17d --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/DynamicField.tsx @@ -0,0 +1,117 @@ +import { Doc } from "../../../../../../fields/Doc"; +import { Docs } from "../../../../../documents/Documents"; +import { Field, FieldDimensions, FieldSettings, ViewType } from "./Field"; +import { FieldUtils } from "./FieldUtils"; +import { StaticField } from "./StaticField"; + +export class DynamicField implements Field { + private subfields: Field[] = []; + + private id: number; + private settings: FieldSettings; + private title: string = ''; + + private parent: Field; + private dimensions: FieldDimensions; + + constructor(settings: FieldSettings, id: number, parent?: Field) { + this.id = id; + this.settings = settings; + if (settings.title) { this.title = settings.title }; + if (!parent) { + this.parent = this; + this.dimensions = {width: this.settings.br[0] - this.settings.tl[0], height: this.settings.br[1] - this.settings.tl[1], coord: {x: this.settings.tl[0], y: this.settings.tl[1]}}; + } else { + this.parent = parent; + this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions); + } + this.subfields = this.setupSubfields(); + } + + setContent = () => { return }; + getContent = () => { return '' }; + + setTitle = (title: string) => { this.title = title }; + getTitle = () => { return this.title }; + + get getSubfields() { return this.subfields }; + get getAllSubfields() { + let fields: Field[] = []; + this.subfields?.forEach(field => { + fields.push(field); + fields = fields.concat(field.getAllSubfields) + }); + return fields; + }; + + get getDimensions() { return this.dimensions }; + get getID() { return this.id }; + get getViewType() { return this.settings.viewType }; + + get getDescription(): string { + return this.settings.description ?? ''; + } + + matches = (): Array<number> => { + return []; + } + + updateRenderedDoc = () => { + return new Doc(); + } + + setupSubfields = (): Field[] => { + const fields: Field[] = []; + this.settings.subfields?.forEach((fieldSettings, index) => { + let field: Field; + const type = fieldSettings.viewType; + + const id = Number(String(this.id) + String(index)); + + if (type == ViewType.CAROUSEL3D || type === ViewType.FREEFORM) { + field = new DynamicField(fieldSettings, id, this); + } else { + field = new StaticField(fieldSettings, this, id); + } + fields.push(field); + }); + return fields; + } + + applyAttributes = (field: Field) => { + field.setTitle(this.title); + field.updateRenderedDoc(this.renderedDoc()); + } + + getChildDimensions = (coords: { tl: [number, number]; br: [number, number] }): FieldDimensions => { + const l = (coords.tl[0] * this.dimensions.height) / 2; + const t = coords.tl[1] * this.dimensions.width / 2; //prettier-ignore + const r = (coords.br[0] * this.dimensions.height) / 2; + const b = coords.br[1] * this.dimensions.width / 2; //prettier-ignore + const width = r - l; + const height = b - t; + const coord = { x: l, y: t }; + return { width, height, coord }; + }; + + renderedDoc = (): Doc => { + let doc: Doc; + switch (this.settings.viewType) { + case ViewType.CAROUSEL3D: + doc = Docs.Create.Carousel3DDocument(this.subfields.map(field => field.renderedDoc()), { + title: this.title, + }); + FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings); + return doc; + case ViewType.FREEFORM: + doc = Docs.Create.FreeformDocument(this.subfields.map(field => field.renderedDoc()), { + title: this.title, + }); + FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings); + return doc; + default: + return new Doc(); + } + } + +} diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx new file mode 100644 index 000000000..ea9b566b3 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/Field.tsx @@ -0,0 +1,66 @@ +import { Doc } from "../../../../../../fields/Doc"; +import { Col } from "../DocCreatorMenu"; +import { TemplateFieldSize, TemplateFieldType } from "../TemplateBackend"; + +export enum FieldContentType { + STRING = 'string', + IMAGE = 'image', +} + +export enum ViewType { + CAROUSEL3D = 'carousel3d', + FREEFORM = 'freeform', + STATIC = 'static', + DEC = 'decoration' +} + +export type FieldDimensions = { + width: number; + height: number; + coord: {x: number, y: number}; +} + +export interface FieldOpts { + backgroundColor?: string; + color?: string; + cornerRounding?: number; + borderWidth?: string; + borderColor?: string; + contentXCentering?: 'h-left' | 'h-center' | 'h-right'; + contentYCentering?: 'top' | 'center' | 'bottom'; + opacity?: number; + rotation?: number; + fontBold?: boolean; + fontTransform?: 'uppercase' | 'lowercase'; + fieldViewType?: 'freeform' | 'stacked'; +} + +export type FieldSettings = { + tl: [number, number]; + br: [number, number]; + opts: FieldOpts; + viewType: ViewType; + title?: string; + subfields?: FieldSettings[]; + types?: TemplateFieldType[]; + sizes?: TemplateFieldSize[]; + description?: string; +}; + +export interface Field { + getContent: () => string; + setContent: (content: string, type?: FieldContentType) => void; + getDimensions: FieldDimensions; + getSubfields: Field[]; + getAllSubfields: Field[]; + getID: number; + getViewType: ViewType; + getDescription: string; + getTitle: () => string; + setTitle: (title: string) => void; + setupSubfields: () => Field[]; + applyAttributes: (field: Field) => void; + renderedDoc: () => Doc; + matches: (cols: Col[]) => number[]; + updateRenderedDoc: (oldDoc?: Doc) => Doc; +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx new file mode 100644 index 000000000..3886774d2 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/FieldUtils.tsx @@ -0,0 +1,79 @@ +import { Doc } from "../../../../../../fields/Doc"; +import { ComputedField, ScriptField } from "../../../../../../fields/ScriptField"; +import { Col } from "../DocCreatorMenu"; +import { TemplateFieldSize, TemplateFieldType, TemplateLayouts } from "../TemplateBackend"; +import { FieldDimensions, FieldSettings } from "./Field"; + +export class FieldUtils { + public static getLocalDimensions = (coords: { tl: [number, number]; br: [number, number] }, parentDimensions: FieldDimensions): FieldDimensions => { + const l = (coords.tl[0] * parentDimensions.width) / 2; + const t = coords.tl[1] * parentDimensions.height / 2; //prettier-ignore + const r = (coords.br[0] * parentDimensions.width) / 2; + const b = coords.br[1] * parentDimensions.height / 2; //prettier-ignore + const width = r - l; + const height = b - t; + const coord = { x: l, y: t }; + return { width, height, coord }; + }; + + public static applyBasicOpts = (doc: Doc, parentDimensions: FieldDimensions, settings: FieldSettings, oldDoc?: Doc) => { + const opts = settings.opts; + doc.isDefaultTemplateDoc = oldDoc ? oldDoc.isDefaultTemplateDoc : true; + doc._layout_hideScroll = oldDoc ? oldDoc._layout_hideScroll : true; + doc.x = oldDoc ? oldDoc.x : parentDimensions.coord.x; + doc.y = oldDoc ? oldDoc.y : parentDimensions.coord.y; + doc._height = oldDoc ? oldDoc.height : parentDimensions.height; + doc._width = oldDoc ? oldDoc.width : parentDimensions.width; + doc.backgroundColor = oldDoc ? oldDoc.backgroundColor : opts.backgroundColor ?? ''; + doc._layout_borderRounding = !opts.cornerRounding ? '0px' : ScriptField.MakeFunction(`${opts.cornerRounding} * this.width + 'px'`); + doc.borderColor = oldDoc ? oldDoc.borderColor : opts.borderColor; + doc.borderWidth = oldDoc ? oldDoc.borderWidth : opts.borderWidth; + doc.opacity = oldDoc ? oldDoc.opacity : opts.opacity; + doc._rotation = oldDoc ? oldDoc._rotation : opts.rotation; + doc.hCentering = oldDoc ? oldDoc.hCentering : opts.contentXCentering; + doc.nativeWidth = parentDimensions.width; + doc.nativeHeight = parentDimensions.height; + doc._layout_nativeDimEditable = true; + }; + + public static calculateFontSize = (contWidth: number, contHeight: number, text: string, uppercase: boolean): number => { + const words: string[] = text.split(/\s+/).filter(Boolean); + + let currFontSize = 1; + let rowsCount = 1; + let currTextHeight = currFontSize * rowsCount * 2; + + while (currTextHeight <= contHeight) { + let wordIndex = 0; + let currentRowWidth = 0; + let wordsInCurrRow = 0; + rowsCount = 1; + + while (wordIndex < words.length) { + const word = words[wordIndex]; + const wordWidth = word.length * currFontSize * 0.7; + + if (currentRowWidth + wordWidth <= contWidth) { + currentRowWidth += wordWidth; + ++wordsInCurrRow; + } else { + if (words.length !== 1 && words.length > wordsInCurrRow) { + rowsCount++; + currentRowWidth = wordWidth; + wordsInCurrRow = 1; + } else { + break; + } + } + + wordIndex++; + } + + currTextHeight = rowsCount * currFontSize * 2; + + currFontSize += 1; + } + + return currFontSize - 1; + }; +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx new file mode 100644 index 000000000..47b43f051 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/FieldTypes/StaticField.tsx @@ -0,0 +1,147 @@ +import { Doc } from "../../../../../../fields/Doc"; +import { Docs } from "../../../../../documents/Documents"; +import { Col } from "../DocCreatorMenu"; +import { DynamicField } from "./DynamicField"; +import { FieldUtils } from "./FieldUtils"; +import { Field, FieldContentType, FieldDimensions, FieldSettings, ViewType } from "./Field"; + +export class StaticField { + private content: string; + private contentType: FieldContentType | undefined; + private subfields: Field[] = []; + private renderedDocument: Doc; + + private id: number; + private title: string = ''; + + private settings: FieldSettings; + + private parent: Field; + private dimensions: FieldDimensions; + + constructor(settings: FieldSettings, parent: Field, id: number) { + this.settings = settings; + if (settings.title) { this.title = settings.title }; + this.id = id; + this.parent = parent; + this.dimensions = FieldUtils.getLocalDimensions({tl: settings.tl, br: settings.br}, this.parent.getDimensions); + this.content = ''; + this.subfields = this.setupSubfields(); + this.renderedDocument = this.updateRenderedDoc(); + }; + + get getSubfields(): Field[] { return this.subfields ?? []; }; + + get getAllSubfields(): Field[] { + let fields: Field[] = []; + this.subfields?.forEach(field => { + fields.push(field); + fields = fields.concat(field.getAllSubfields); + }); + return fields; + }; + + get getDimensions() { return this.dimensions }; + get getID() { return this.id }; + get getViewType() { return this.settings.viewType }; + + get getDescription(): string { + return this.settings.description ?? ''; + } + + renderedDoc = () => { + return this.renderedDocument; + } + + setContent = (newContent: string, type?: FieldContentType) => { + this.content = newContent; + if (type) this.contentType = type; + this.updateRenderedDoc(this.renderedDocument); + }; + getContent() { return this.content }; + + setTitle = (title: string) => { + this.title = title; + this.renderedDocument.title = title; + this.updateRenderedDoc(this.renderedDocument); + }; + getTitle = () => { return this.title }; + + applyAttributes = (field: Field) => { //!!! can be updated later for more robust clonign; this is all ythat's needed now + field.setTitle(this.title); + field.setContent('', this.contentType); + field.updateRenderedDoc(this.renderedDoc()); + } + + setupSubfields = (): Field[] => { + const fields: Field[] = []; + this.settings.subfields?.forEach((fieldSettings, index) => { + let field: Field; + const type = fieldSettings.viewType; + + const id = Number(String(this.id) + String(index)); + + if (type === ViewType.FREEFORM || type === ViewType.CAROUSEL3D) { + field = new DynamicField(fieldSettings, id, this); + } else { + field = new StaticField(fieldSettings, this, id); + }; + + fields.push(field); + }); + return fields; + }; + + matches = (cols: Col[]): number[] => { + const colMatchesField = (col: Col) => { + const isMatch: boolean = ( + this.settings.sizes?.some(size => col.sizes?.includes(size)) + && this.settings.types?.includes(col.type)) + ?? false; + return isMatch; + } + + const matches: Array<number> = []; + + cols.forEach((col, v) => { + if (colMatchesField(col)) { + matches.push(v); + } + }); + + return matches; + }; + + updateRenderedDoc = (oldDoc?: Doc): Doc => { + const opts = this.settings.opts; + + if (!this.contentType) { this.contentType = FieldContentType.STRING }; + + let doc: Doc; + + switch (this.contentType) { + case FieldContentType.STRING: + doc = Docs.Create.TextDocument(String(this.content), { + title: this.title, + text_fontColor: oldDoc ? String(oldDoc.color) : opts.color, + contentBold: oldDoc ? Boolean(oldDoc.fontBold) : opts.fontBold, + textTransform: oldDoc ? String(oldDoc.fontTransform) : opts.fontTransform, + color: oldDoc ? String(oldDoc.color) : opts.color, + _text_fontSize: `${FieldUtils.calculateFontSize(this.dimensions.width, this.dimensions.height, String(this.content), true)}` + }); + FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc); + break; + case FieldContentType.IMAGE: + doc = Docs.Create.ImageDocument(String(this.content), { + title: this.title, + _layout_fitWidth: false, + }); + FieldUtils.applyBasicOpts(doc, this.dimensions, this.settings, oldDoc); + break; + } + + this.renderedDocument = doc; + + return doc; + }; +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx new file mode 100644 index 000000000..0a5097d4a --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/Template.tsx @@ -0,0 +1,139 @@ +import { Doc, FieldType } from "../../../../../fields/Doc"; +import { Col } from "./DocCreatorMenu"; +import { DynamicField } from "./FieldTypes/DynamicField"; +import { Field, FieldSettings, ViewType } from "./FieldTypes/Field"; +import { } from "./FieldTypes/FieldUtils"; +import { } from "./FieldTypes/StaticField"; + +export class Template { + + mainField: DynamicField; + settings: FieldSettings; + + constructor(templateInfo: FieldSettings) { + this.mainField = this.setupMainField(templateInfo); + this.settings = templateInfo; + } + + get childFields(): Field[] { return this.mainField.getSubfields }; + get allFields(): Field[] { return this.mainField.getAllSubfields }; + get contentFields(): Field[] { return this.allFields.filter(field => field.getViewType === ViewType.STATIC) }; + get doc(){ return this.mainField.renderedDoc(); }; + + cloneBase = () => { + const clone: Template = new Template(this.settings); + clone.allFields.forEach(field => { + const matchingField: Field = this.allFields.filter(f => f.getID === field.getID)[0]; + matchingField.applyAttributes(field); + }) + return clone; + } + + getRenderedDoc = () => { + const doc: Doc = this.mainField.renderedDoc(); + this.contentFields.forEach(field => { + const title: string = field.getTitle(); + const val: FieldType = field.getContent() as FieldType; + if (!title || !val) return; + doc[title] = val; + }); + return doc; + } + + getFieldByID = (id: number): Field => { + return this.allFields.filter(field => field.getID === id)[0]; + } + + getFieldByTitle = (title: string) => { + return this.allFields.filter(field => field.getTitle() === title)[0]; + } + + setupMainField = (templateInfo: FieldSettings) => { + return new DynamicField(templateInfo, 1); + } + + get descriptionSummary(): string { + let summary: string = ''; + this.contentFields.forEach(field => { + summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getDescription ?? ''} ---`; + }); + return summary; + } + + get compiledContent(): string { + let summary: string = ''; + this.contentFields.forEach(field => { + summary += `--- Field #${field.getID} (title: ${field.getTitle()}): ${field.getContent() ?? ''} ---`; + }); + return summary; + } + + renderUpdates = () => { + this.allFields.forEach(field => { + field.updateRenderedDoc(field.renderedDoc()); + }); + }; + + resetToBase = () => { + this.allFields.forEach(field => { + field.updateRenderedDoc(); + }) + } + + isValidTemplate = (cols: Col[]) => { + const matches: number[][] = this.getMatches(cols); + const maxMatches: number = this.maxMatches(matches); + return maxMatches === this.contentFields.length; + } + + getMatches = (cols: Col[]): number[][] => { + const numFields = this.contentFields.length; + + if (cols.length !== numFields) return []; + + const matches: number[][] = Array(numFields) + .fill([]) + .map(() => []); + + this.contentFields.forEach((field, i) => { + matches[i] = (field.matches(cols)); + }); + + return matches; + } + + maxMatches = (matches: number[][]) => { + if (matches.length === 0) return 0; + + const fieldsCt = this.contentFields.length; + const used: boolean[] = Array(fieldsCt).fill(false); + const mt: number[] = Array(fieldsCt).fill(-1); + + const augmentingPath = (v: number): boolean => { + if (used[v]) return false; + used[v] = true; + + for (const to of matches[v]) { + if (mt[to] === -1 || augmentingPath(mt[to])) { + mt[to] = v; + return true; + } + } + return false; + }; + + for (let v = 0; v < fieldsCt; ++v) { + used.fill(false); + augmentingPath(v); + } + + let count: number = 0; + + for (let i = 0; i < fieldsCt; ++i) { + if (mt[i] !== -1) ++count; + } + + return count; + }; + +}
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx new file mode 100644 index 000000000..d3282eda3 --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateBackend.tsx @@ -0,0 +1,752 @@ +import { FieldSettings, ViewType } from "./FieldTypes/Field"; +import { } from "./FieldTypes/StaticField"; + +export enum TemplateFieldType { + TEXT = 'text', + VISUAL = 'visual', + UNSET = 'unset', +} + +export enum TemplateFieldSize { + TINY = 'tiny', + SMALL = 'small', + MEDIUM = 'medium', + LARGE = 'large', + HUGE = 'huge', +} + +export class TemplateLayouts { + public static get allTemplates(): FieldSettings[] { + return Object.values(TemplateLayouts); + } + + public static FourField001: FieldSettings = { + title: 'fourfield001', + tl: [0, 0], + br: [416, 700], + viewType: ViewType.FREEFORM, + opts: { + backgroundColor: '#C0B887', + cornerRounding: .05, + //borderColor: '#6B461F', + //borderWidth: '12', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.95, -1], + br: [0.95, -0.85], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A title field for very short text that contextualizes the content.', + opts: { + backgroundColor: 'transparent', + color: '#F1F0E9', + contentXCentering: 'h-center', + fontBold: true, + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.87, -0.83], + br: [0.87, 0.2], + types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'The main focus of the template; could be an image, long text, etc.', + opts: { + cornerRounding: .05, + borderColor: '#8F5B25', + borderWidth: '6', + backgroundColor: '#CECAB9', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.8, 0.2], + br: [0.8, 0.3], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A caption for field #2, very short text.', + opts: { + backgroundColor: 'transparent', + contentXCentering: 'h-center', + color: '#F1F0E9', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.87, 0.37], + br: [0.87, 0.96], + types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium-sized field for medium/long text.', + opts: { + cornerRounding: .05, + borderColor: '#8F5B25', + borderWidth: '6', + backgroundColor: '#CECAB9', + }, + }, + ], + }; + + public static FourField002: FieldSettings = { + title: 'fourfield002', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [425, 778], + opts: { + backgroundColor: '#242425', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.83, -0.95], + br: [0.83, -0.2], + types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large-sized field suitable for an image or longer text that should be the main focus.', + opts: { + borderWidth: '8', + borderColor: '#F8E71C', + backgroundColor: '#242425', + color: 'white', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.65, -0.2], + br: [0.65, -0.02], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: 'transparent', + color: 'white', + contentXCentering: 'h-center', + fontTransform: 'uppercase', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.65, 0], + br: [0.65, 0.18], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: 'transparent', + color: 'white', + contentXCentering: 'h-center', + fontTransform: 'uppercase', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.83, 0.2], + br: [0.83, 0.95], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large-sized field suitable for longer text that should contextualize field 1.', + opts: { + borderWidth: '8', + borderColor: '#F8E71C', + color: 'white', + backgroundColor: '#242425', + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.8, -0.075], + br: [-0.525, 0.075], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.3075, -0.0245], + br: [-0.2175, 0.0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.045, -0.0245], + br: [0.045, 0.0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + viewType: ViewType.DEC, + tl: [0.2175, -0.0245], + br: [0.3075, 0.0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + { + viewType: ViewType.DEC, + tl: [0.525, -0.075], + br: [0.8, 0.075], + opts: { + backgroundColor: '#F8E71C', + rotation: 45, + }, + }, + ], + }; + + // public static FourField003: TemplateDocInfos = { + // title: 'fourfield3', + // width: 477, + // height: 662, + // opts: { + // backgroundColor: '#9E9C95' + // }, + // fields: [{ + // tl: [-.875, -.9], + // br: [.875, .7], + // types: [TemplateFieldType.VISUAL], + // sizes: [TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + // description: '', + // opts: { + // borderWidth: '15', + // borderColor: '#E0E0DA', + // } + // }, { + // tl: [-.95, .8], + // br: [-.1, .95], + // types: [TemplateFieldType.TEXT], + // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + // description: '', + // opts: { + // backgroundColor: 'transparent', + // color: 'white', + // contentXCentering: 'h-right', + // } + // }, { + // tl: [.1, .8], + // br: [.95, .95], + // types: [TemplateFieldType.TEXT], + // sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + // description: '', + // opts: { + // backgroundColor: 'transparent', + // color: 'red', + // fontTransform: 'uppercase', + // contentXCentering: 'h-left' + // } + // }, { + // tl: [0, -.9], + // br: [.85, -.66], + // types: [TemplateFieldType.TEXT, TemplateFieldType.VISUAL], + // sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + // description: '', + // opts: { + // backgroundColor: 'transparent', + // contentXCentering: 'h-right' + // } + // }], + // decorations: [{ + // tl: [-.025, .8], + // br: [.025, .95], + // opts: { + // backgroundColor: '#E0E0DA', + // } + // }] + // }; + + public static FourField004: FieldSettings = { + title: 'fourfield04', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [414,583], + opts: { + backgroundColor: '#6CCAF0', + //borderColor: '#1088C3', + //borderWidth: '10', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.86, -0.92], + br: [-0.075, -0.77], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: '#E2B4F5', + borderWidth: '9', + borderColor: '#9222F1', + contentXCentering: 'h-center', + }, + }, + { + viewType: ViewType.STATIC, + tl: [0.075, -0.92], + br: [0.86, -0.77], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY], + description: 'A tiny field for just a word or two of plain text.', + opts: { + backgroundColor: '#F5B4DD', + borderWidth: '9', + borderColor: '#E260F3', + contentXCentering: 'h-center', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.81, -0.64], + br: [0.81, 0.48], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A large to huge field for visual content that is the main content of the template.', + opts: { + borderWidth: '16', + borderColor: '#A2BD77', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.86, 0.6], + br: [0.86, 0.92], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large field for text that describes the visual content above', + opts: { + borderWidth: '9', + borderColor: '#F0D601', + backgroundColor: '#F3F57D', + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.852, -0.67], + br: [0.852, 0.51], + opts: { + backgroundColor: 'transparent', + borderColor: '#007C0C', + borderWidth: '10', + }, + }, + ], + }; + + public static FourField005: FieldSettings = { + title: 'fourfield05', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [400,550], + opts: { + backgroundColor: '#95A575', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.9, -.925], + br: [-.075, -.775], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A small text field for a title or word(s) that categorize the rest of the content.', + opts: { + borderColor: '#3B4A2C', + borderWidth: '8', + contentXCentering: "h-center", + backgroundColor: '#B8DC90', + }, + }, + { + viewType: ViewType.STATIC, + tl: [.075, -.925], + br: [.9, -.775], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A small text field for a title that categorizes the rest of the content.', + opts: { + borderColor: '#3B4A2C', + borderWidth: '8', + contentXCentering: "h-center", + backgroundColor: '#B8DC90', + }, + }, + { + viewType: ViewType.DEC, + tl: [-.82, -.4], + br: [-.5, -.2], + opts: { + backgroundColor: '#94B058', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.66, -.65], + br: [0.66, .25], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large field in the center of the template, for the main visual content.', + opts: { + borderColor: '#3B4A2C', + borderWidth: '8', + backgroundColor: '#B8DC90', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-.875, .425], + br: [0.875, .925], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large field at the bottom of the template, for the main text content.', + opts: { + borderColor: '#3B4A2C', + borderWidth: '8', + contentXCentering: "h-center", + backgroundColor: '#B8DC90', + }, + }, + { + viewType: ViewType.DEC, + tl: [-1.1, -.62], + br: [-.9, -.5], + opts: { + backgroundColor: '#7A9D31', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [-1.1, 0], + br: [-.9, .15], + opts: { + backgroundColor: '#94B058', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [-.93, -.265], + br: [-.715, -.125], + opts: { + backgroundColor: '#728745', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [.7, -.45], + br: [.85, -.3], + opts: { + backgroundColor: '#7A9D31', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [.8, .03], + br: [1.2, .33], + opts: { + backgroundColor: '#728745', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + { + viewType: ViewType.DEC, + tl: [.875, -.13], + br: [1.2, .12], + opts: { + backgroundColor: '#94B058', + borderColor: '#3B4A2C', + borderWidth: '8', + }, + }, + ] + } + + public static FourFieldCarousel: FieldSettings = { + title: 'title_fourfieldcarousel', + viewType: ViewType.FREEFORM, + tl:[0,0], + br:[500, 600], + opts: { + backgroundColor: '#DDD3A9', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.8, -.9], + br: [0.8, -.5], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A small text field for a title that categorizes the rest of the content.', + opts: { + borderColor: 'yellow', + borderWidth: '8', + contentXCentering: "h-center", + backgroundColor: 'transparent', + }, + }, + { + viewType: ViewType.CAROUSEL3D, + tl: [-0.9, -.3], + br: [0.9, .9], + opts: { + borderColor: 'yellow', + borderWidth: '8', + backgroundColor: 'transparent', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-.3, -.6], + br: [.3, .6], + types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large field for content that will share central focus with other content in the carousel.', + opts: { + borderColor: 'yellow', + borderWidth: '8', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-.3, -.6], + br: [.3, .6], + types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large field for content that will share central focus with other content in the carousel.', + opts: { + borderColor: 'black', + borderWidth: '8', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-.3, -.6], + br: [.3, .6], + types: [TemplateFieldType.VISUAL, TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large field for content that will share central focus with other content in the carousel.', + opts: { + borderColor: 'yellow', + borderWidth: '8', + }, + }, + ] + }, + ] + } + + public static ThreeField001: FieldSettings = { + title: 'threefield001', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [575, 770], + opts: { + backgroundColor: '#DDD3A9', + }, + subfields: [ + { + viewType: ViewType.FREEFORM, + tl: [-0.66, -0.747], + br: [0.66, 0.247], + description: 'A medium to large field for visual content that is the central focus.', + opts: { + borderColor: 'yellow', + borderWidth: '8', + backgroundColor: '#DDD3A9', + rotation: 45, + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-1.25, -1.25], + br: [1.25, 1.25], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large field for visual content that is the central focus.', + opts: { + rotation: -45, + }, + }, + ] + }, + { + viewType: ViewType.STATIC, + tl: [-0.7, 0.2], + br: [0.7, 0.46], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A very small text field for one to a few words. A good caption for the image.', + opts: { + backgroundColor: 'transparent', + contentXCentering: 'h-center', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.95, 0.5], + br: [0.95, 0.95], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE], + description: 'A medium to large text field for a thorough description of the image. ', + opts: { + backgroundColor: 'transparent', + color: 'white', + }, + }, + { + viewType: ViewType.FREEFORM, + tl: [0.2, -1.32], + br: [1.8, -0.66], + opts: { + backgroundColor: '#CEB155', + rotation: 45, + }, + subfields: [ + { + viewType: ViewType.DEC, + tl: [-1, -.7], + br: [1, -.625], + opts: { + backgroundColor: 'yellow', + }, + }, + ] + }, + { + viewType: ViewType.FREEFORM, + tl: [-1.8, -1.32], + br: [-0.2, -0.66], + opts: { + backgroundColor: '#CEB155', + rotation: 135, + }, + subfields: [ + { + viewType: ViewType.DEC, + tl: [-1, -.7], + br: [1, -.625], + opts: { + backgroundColor: 'yellow', + }, + }, + ] + }, + { + viewType: ViewType.FREEFORM, + tl: [0.33, 0.75], + br: [1.66, 1.25], + opts: { + backgroundColor: '#CEB155', + rotation: 135, + }, + subfields: [ + { + viewType: ViewType.DEC, + tl: [-1, -.7], + br: [1, -.625], + opts: { + backgroundColor: 'yellow', + }, + }, + ] + }, + { + viewType: ViewType.FREEFORM, + tl: [-1.66, 0.75], + br: [-0.33, 1.25], + opts: { + backgroundColor: '#CEB155', + rotation: 45, + }, + subfields: [ + { + viewType: ViewType.DEC, + tl: [-1, -.7], + br: [1, -.625], + opts: { + backgroundColor: 'yellow', + }, + }, + ] + }, + ], + }; + + public static ThreeField002: FieldSettings = { + title: 'threefield002', + viewType: ViewType.FREEFORM, + tl: [0,0], + br: [477, 662], + opts: { + backgroundColor: '#9E9C95', + }, + subfields: [ + { + viewType: ViewType.STATIC, + tl: [-0.875, -0.9], + br: [0.875, 0.7], + types: [TemplateFieldType.VISUAL], + sizes: [TemplateFieldSize.MEDIUM, TemplateFieldSize.LARGE, TemplateFieldSize.HUGE], + description: 'A medium to large visual field for the main content of the template', + opts: { + borderWidth: '15', + borderColor: '#E0E0DA', + }, + }, + { + viewType: ViewType.STATIC, + tl: [0.1, 0.775], + br: [0.95, 0.975], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A very small text field for one to a few words. The content should represent a general categorization of the image.', + opts: { + backgroundColor: 'transparent', + color: '#AF0D0D', + fontTransform: 'uppercase', + fontBold: true, + contentXCentering: 'h-left', + }, + }, + { + viewType: ViewType.STATIC, + tl: [-0.95, 0.775], + br: [-0.1, 0.975], + types: [TemplateFieldType.TEXT], + sizes: [TemplateFieldSize.TINY, TemplateFieldSize.SMALL], + description: 'A very small text field for one to a few words. The content should contextualize field 2.', + opts: { + backgroundColor: 'transparent', + color: 'black', + contentXCentering: 'h-right', + }, + }, + { + viewType: ViewType.DEC, + tl: [-0.025, 0.8], + br: [0.025, 0.95], + opts: { + backgroundColor: '#E0E0DA', + }, + }, + ], + }; +} + + diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx new file mode 100644 index 000000000..50ae4d72a --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu/TemplateManager.tsx @@ -0,0 +1,22 @@ +import { Col } from "./DocCreatorMenu"; +import { FieldSettings } from "./FieldTypes/Field"; +import { Template } from "./Template"; + +export class TemplateManager { + + templates: Template[] = []; + + constructor(templateSettings: FieldSettings[]) { + this.templates = this.initializeTemplates(templateSettings); + } + + initializeTemplates = (templateSettings: FieldSettings[]): Template[] => { + const initializedTemplates: Template[] = []; + templateSettings.forEach(settings => initializedTemplates.push(new Template(settings))); + return initializedTemplates; + } + + getValidTemplates = (cols: Col[]): Template[] => { + return this.templates.filter(template => template.isValidTemplate(cols)); + } +}
\ No newline at end of file |
