diff options
Diffstat (limited to 'src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx')
-rw-r--r-- | src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx | 2188 |
1 files changed, 2188 insertions, 0 deletions
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx new file mode 100644 index 000000000..5d5c35dce --- /dev/null +++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx @@ -0,0 +1,2188 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { ClientUtils, returnAll, returnFalse, returnNone, returnOne, returnZero, setupMoveUpEvents } from '../../../../ClientUtils'; +import { Doc, NumListCast, StrListCast } from '../../../../fields/Doc'; +import { DocCast, ImageCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { ImageField } from '../../../../fields/URLField'; +import { emptyFunction } from '../../../../Utils'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { UndoManager, undoable } from '../../../util/UndoManager'; +import { ObservableReactComponent } from '../../ObservableReactComponent'; +import { DocumentView, DocumentViewInternal } from '../DocumentView'; +import { DataVizBox } from './DataVizBox'; +import './DocCreatorMenu.scss'; +import { Id } from '../../../../fields/FieldSymbols'; +import { Colors, IconButton, Size } from 'browndash-components'; +import { MakeTemplate } from '../../../util/DropConverter'; +import { DragManager } from '../../../util/DragManager'; +import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; +import { Docs } from '../../../documents/Documents'; +import { OpenWhere } from '../OpenWhere'; +import { IDisposer } from 'mobx-utils'; +import { LightboxView } from '../../LightboxView'; +import ReactLoading from 'react-loading'; +import { CollectionStackingView } from '../../collections/CollectionStackingView'; +import { FieldViewProps } from '../FieldView'; +import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { dropActionType } from '../../../util/DropActionTypes'; +import { ImageBox } from '../ImageBox'; +import { a } from '@react-spring/web'; +import { RichTextMenu } from '../formattedText/RichTextMenu'; +import e from 'cors'; +import { Networking } from '../../../Network'; + +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) => docs.map(this.getIcon)); + this._disposers.gpt = reaction(() => this._suggestedTemplates.slice(), (docs) => docs.map(this.getIcon)); + //this._disposers.columns = reaction(() => this._dataViz?.layoutDoc._dataViz_axes, () => {this.generateTemplates('')}) + this._disposers.lightbox = reaction(() => LightboxView.LightboxDoc(), doc => { 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[]) => { 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() => { + // const temp = TemplateLayouts.FourField001; + // const title: Doc = FieldFuncs.TextField({tl: temp.fields[0].tl, br: temp.fields[0].br}, temp.height, temp.width, 'title', 'Title', {backgroundColor: 'transparent'}); + // const image: Doc = FieldFuncs.ImageField({tl: temp.fields[1].tl, br: temp.fields[1].br}, temp.height, temp.width, 'title', '', {borderColor: '#159fe4', borderWidth: '10', cornerRounding: 10, rotation: 40}); + // const caption: Doc = FieldFuncs.TextField({tl: temp.fields[2].tl, br: temp.fields[2].br}, temp.height, temp.width, 'title', 'Caption', {backgroundColor: 'transparent'}); + // const desc: Doc = FieldFuncs.TextField({tl: temp.fields[3].tl, br: temp.fields[3].br}, temp.height, temp.width, 'title', '', {backgroundColor: 'lightblue', borderColor: '#159fe4', borderWidth: '10', cornerRounding: 10}); + + // const doc = Docs.Create.FreeformDocument([title, image, caption, desc], { _height: temp.height, _width: temp.width, title: 'hey', x: 400, y: 400 }); + + // const mainCollection = this._dataViz?.DocumentView?.().containerViewPath?.().lastElement()?.ComponentView as CollectionFreeFormView; + // mainCollection.addDocument(doc); + + // const temp = TemplateLayouts.FourField001; + + // const img: Col = {type: TemplateFieldType.TEXT, title: 'Type', desc: 'description whpoo', size: TemplateFieldSize.LARGE, defaultContent: ''}; + // const capt1: Col = {type: TemplateFieldType.TEXT, title: 'Image', desc: 'description hey', size: TemplateFieldSize.TINY}; + // const capt2: Col = {type: TemplateFieldType.TEXT, title: 'Locality', desc: '', size: TemplateFieldSize.TINY, defaultContent: ''}; + // const desc: Col = {type: TemplateFieldType.TEXT, title: 'Description', desc: '', size: TemplateFieldSize.LARGE, defaultContent: 'This is a description of a rock. It is kind of long. It is very long. It is gratuitous. This description should be shorter. Oh well. This is a description of a rock. It is kind of long. It is very long. It is gratuitous. This description should be shorter. Oh well. This is a description of a rock. It is kind of long. It is very long. It is gratuitous. This description should be shorter. Oh well.'}; + + // const assignments = {'0': img, '1': capt1, '2': capt2, '3': desc} + + // this.createEmptyTemplate(temp, assignments); + // console.log(this.findValidTemplates(this.fieldsInfos, TemplateLayouts.allTemplates)); + + // console.log(this._dataViz?.colsInfo.get("IMG")?.size, this._dataViz?.colsInfo.get("IMG")?.type) + // console.log(this.fieldsInfos) + + 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 }); + 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) => { + this._expandedPreview = info; + } + + 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 ? + <div className='docCreatorMenu-expanded-template-preview'> + <img className='docCreatorMenu-preview-image expanded' src={this._expandedPreview.icon!.url.href.replace(".png", "_o.png")} /> + <div className='right-buttons-panel'> + <button className='docCreatorMenu-menu-button section-reveal-options top-right' onPointerDown={e => this.setUpButtonClick(e, () => 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> + : + <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 *= .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 * .4, height: height}}> + <FontAwesomeIcon icon={icon as any}/> + </div> + {manual ? <input className={`docCreatorMenu-input config ${specClass}`} style={{width: width * .6, height: height}}/> : + <select className={`docCreatorMenu-input config ${specClass}`} style={{width: width * .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 * .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.addField)} 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'> Description </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 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; +} + +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; + +export interface TemplateDocInfos { + title: string; + height: number; + width: number; + opts: TemplateOpts; + fields: Field[]; + decorations: Field[]; +} + +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'; +} + +interface TemplateOpts extends FieldOpts { + +} + +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 * .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}px` ?? '0px', + 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 bool = true; + + 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, + textTransform: opts.fontTransform, + color: opts.color, + _layout_borderRounding: `${opts.cornerRounding}px` ?? '0px', + 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}px` ?? '0px', + 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; + } + +} + +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: [-.95, -1], + br: [.95, -.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: [-.87, -.83], + br: [.87, .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: [-.8, .2], + br: [.8, .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: [-.87, .37], + br: [.87, .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: [-.83, -.95], + br: [.83, -.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: [-.65, -.2], + br: [.65, -.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: [-.65, 0], + br: [.65, .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: [-.83, .2], + br: [.83, .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: [-.8, -.075], + br: [-.525, .075], + opts: { + backgroundColor: '#F8E71C', + rotation: 45 + } + }, { + tl: [-.3075, -.0245], + br: [-.2175, .0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45 + } + }, { + tl: [-.045, -.0245], + br: [.045, .0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45 + } + }, { + tl: [.2175, -.0245], + br: [.3075, .0245], + opts: { + backgroundColor: '#F8E71C', + rotation: 45 + } + }, { + tl: [.525, -.075], + br: [.8, .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: [-.86, -.92], + br: [-.075, -.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: [.075, -.92], + br: [.86, -.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: [-.81, -.64], + br: [.81, .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: [-.86, .6], + br: [.86, .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: [-.852, -.67], + br: [.852, .51], + opts: { + backgroundColor: 'transparent', + borderColor: '#007C0C', + borderWidth: '10', + } + }] + }; + + public static ThreeField001: TemplateDocInfos = { + title: 'threefield1', + width: 575, + height: 770, + opts: { + backgroundColor: '#DDD3A9' + }, + fields: [{ + tl: [-.66, -.747], + br: [.66, .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: [-.7, .2], + br: [.7, .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: [-.95, .5], + br: [.95, .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: [.2, -1.32], + br: [1.8, -.66], + opts: { + backgroundColor: '#CEB155', + rotation: 45, + } + }, { + tl: [-1.8, -1.32], + br: [-.2, -.66], + opts: { + backgroundColor: '#CEB155', + rotation: 135, + } + }, { + tl: [.33, .75], + br: [1.66, 1.25], + opts: { + backgroundColor: '#CEB155', + rotation: 135, + } + }, { + tl: [-1.66, .75], + br: [-.33, 1.25], + opts: { + backgroundColor: '#CEB155', + rotation: 45, + } + }] + }; + + public static ThreeField002: TemplateDocInfos = { + title: 'threefield2', + width: 477, + height: 662, + opts: { + backgroundColor: '#9E9C95' + }, + fields: [{ + tl: [-.875, -.9], + br: [.875, .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: [.1, .775], + br: [.95, .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: [-.95, .775], + br: [-.1, .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: [-.025, .8], + br: [.025, .95], + opts: { + backgroundColor: '#E0E0DA', + } + }] + }; + + + +// 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] +// }] +// }; + +} + +export class ContentField extends Field { + +}
\ No newline at end of file |