aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx')
-rw-r--r--src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx2188
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