diff options
author | Bob Zeleznik <zzzman@gmail.com> | 2019-04-28 22:26:03 -0400 |
---|---|---|
committer | Bob Zeleznik <zzzman@gmail.com> | 2019-04-28 22:26:03 -0400 |
commit | bf4a6e0962264e6ce934dcc727cccef6437f7713 (patch) | |
tree | 9586f47ef674407e1dea311f9b1e22013b995ede /src | |
parent | a5ab5caa244da261814bc10f0ae88b302da93951 (diff) | |
parent | 227e8443a9fc9ca0c3f2e02e85dbd80b26a8b7b2 (diff) |
Merge remote-tracking branch 'origin/templating'
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 9 | ||||
-rw-r--r-- | src/client/views/.DS_Store | bin | 8196 -> 6148 bytes | |||
-rw-r--r-- | src/client/views/DocumentDecorations.scss | 96 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 20 | ||||
-rw-r--r-- | src/client/views/TemplateMenu.tsx | 75 | ||||
-rw-r--r-- | src/client/views/Templates.tsx | 66 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 48 | ||||
-rw-r--r-- | src/fields/KeyStore.ts | 2 | ||||
-rw-r--r-- | src/fields/TemplateField.ts | 43 | ||||
-rw-r--r-- | src/server/Message.ts | 2 | ||||
-rw-r--r-- | src/server/ServerUtil.ts | 2 |
11 files changed, 321 insertions, 42 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 879b114b6..a7514f1d6 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -32,6 +32,8 @@ import { action } from "mobx"; import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel"; import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel"; import { AggregateFunction } from "../northstar/model/idea/idea"; +import { Template } from "../views/Templates"; +import { TemplateField } from "../../fields/TemplateField"; import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; import { IconBox } from "../views/nodes/IconBox"; import { IconField } from "../../fields/IconFIeld"; @@ -49,7 +51,9 @@ export interface DocumentOptions { pany?: number; page?: number; scale?: number; + baseLayout?: string; layout?: string; + templates?: Array<Template>; layoutKeys?: Key[]; viewType?: number; backgroundColor?: string; @@ -106,7 +110,9 @@ export namespace Documents { if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); } if (options.backgroundColor !== undefined) { doc.SetText(KeyStore.BackgroundColor, options.backgroundColor); } if (options.ink !== undefined) { doc.Set(KeyStore.Ink, new InkField(options.ink)); } + if (options.baseLayout !== undefined) { doc.SetText(KeyStore.BaseLayout, options.baseLayout); } if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); } + if (options.templates !== undefined) { doc.Set(KeyStore.Templates, new TemplateField(options.templates)); } if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); } if (options.copyDraggedItems !== undefined) { doc.SetBoolean(KeyStore.CopyDraggedItems, options.copyDraggedItems); } if (options.borderRounding !== undefined) { doc.SetNumber(KeyStore.BorderRounding, options.borderRounding); } @@ -124,7 +130,7 @@ export namespace Documents { } function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document { - return assignOptions(new Document(protoId), { ...options, title: title, layout: layout }); + return assignOptions(new Document(protoId), { ...options, title: title, layout: layout, baseLayout: layout }); } function SetInstanceOptions<T, U extends Field & { Data: T }>(doc: Document, options: DocumentOptions, value: [T, { new(): U }] | Document, id?: string) { var deleg = doc.MakeDelegate(id); @@ -142,6 +148,7 @@ export namespace Documents { { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }); imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString()); imageProto.SetNumber(KeyStore.CurPage, 0); + imageProto.SetData(KeyStore.LayoutFields, [KeyStore.Title], ListField); return imageProto; } diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differindex 0964d5ff3..5008ddfcf 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index dd646e77c..27b94e6d2 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -3,18 +3,19 @@ .documentDecorations { position: absolute; } + .documentDecorations-container { z-index: $docDecorations-zindex; position: absolute; top: 0; - left:0; + left: 0; display: grid; grid-template-rows: 20px 8px 1fr 8px; grid-template-columns: 8px 16px 1fr 8px 8px; pointer-events: none; #documentDecorations-centerCont { - grid-column:3; + grid-column: 3; background: none; } @@ -39,8 +40,8 @@ #documentDecorations-bottomRightResizer, #documentDecorations-topRightResizer, #documentDecorations-rightResizer { - grid-column-start:5; - grid-column-end:7; + grid-column-start: 5; + grid-column-end: 7; } #documentDecorations-topLeftResizer, @@ -64,7 +65,7 @@ } .title{ background: lightblue; - grid-column-start:3; + grid-column-start: 3; grid-column-end: 4; pointer-events: auto; overflow: hidden; @@ -72,7 +73,7 @@ } .documentDecorations-closeButton { - background:$alt-accent; + background: $alt-accent; opacity: 0.8; grid-column-start: 4; grid-column-end: 6; @@ -80,8 +81,9 @@ text-align: center; cursor: pointer; } + .documentDecorations-minimizeButton { - background:$alt-accent; + background: $alt-accent; opacity: 0.8; grid-column-start: 1; grid-column-end: 3; @@ -94,6 +96,7 @@ width: $MINIMIZED_ICON_SIZE; height: $MINIMIZED_ICON_SIZE; } + .documentDecorations-background { background: lightblue; position: absolute; @@ -105,21 +108,9 @@ margin-left: 25px; } -.linkButton-empty:hover { - background: $main-accent; - transform: scale(1.05); - cursor: pointer; -} - -.linkButton-nonempty:hover { - background: $main-accent; - transform: scale(1.05); - cursor: pointer; -} - .linkButton-linker { - position:absolute; - bottom:0px; + position: absolute; + bottom: 0px; left: 0px; height: 20px; width: 20px; @@ -139,13 +130,14 @@ justify-content: center; align-items: center; } + .linkButton-tray { grid-column: 1/4; } + .linkButton-empty { height: 20px; width: 20px; - margin-top: 10px; border-radius: 50%; opacity: 0.9; pointer-events: auto; @@ -159,23 +151,51 @@ display: flex; justify-content: center; align-items: center; + + &:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; + } } -.linkButton-nonempty { - height: 20px; - width: 20px; - margin-top: 10px; - border-radius: 50%; - opacity: 0.9; +.templating-menu { + position: absolute; + bottom: 0; + left: 50px; pointer-events: auto; - background-color: $dark-color; - color: $light-color; - text-transform: uppercase; - letter-spacing: 2px; - font-size: 75%; - transition: transform 0.2s; - text-align: center; - display: flex; - justify-content: center; - align-items: center; + + .templating-button { + width: 20px; + height: 20px; + border-radius: 50%; + opacity: 0.9; + background-color: $dark-color; + color: $light-color; + text-align: center; + cursor: pointer; + + &:hover { + background: $main-accent; + transform: scale(1.05); + } + } + + #template-list { + position: absolute; + top: 0; + left: 30px; + width: 150px; + line-height: 25px; + max-height: 175px; + font-family: $sans-serif; + font-size: 12px; + background-color: $light-color-secondary; + padding: 2px 12px; + list-style: none; + + input { + margin-right: 10px; + } + } }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index a1cf0087f..bbd92c6c5 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -14,7 +14,9 @@ import './DocumentDecorations.scss'; import { MainOverlayTextBox } from "./MainOverlayTextBox"; import { DocumentView } from "./nodes/DocumentView"; import { LinkMenu } from "./nodes/LinkMenu"; +import { TemplateMenu } from "./TemplateMenu"; import React = require("react"); +import { Template, Templates } from "./Templates"; import { CompileScript } from "../util/Scripting"; import { IconBox } from "./nodes/IconBox"; import { FieldValue, Field } from "../../fields/Field"; @@ -31,7 +33,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> private keyinput: React.RefObject<HTMLInputElement>; private _documents: DocumentView[] = SelectionManager.SelectedDocuments(); private _resizeBorderWidth = 16; - private _linkBoxHeight = 30; + private _linkBoxHeight = 20; private _titleHeight = 20; private _linkButton = React.createRef<HTMLDivElement>(); private _linkerButton = React.createRef<HTMLDivElement>(); @@ -470,6 +472,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> // buttonOnPointerUp = (e: React.PointerEvent): void => { // e.stopPropagation(); // } + render() { var bounds = this.Bounds; let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; @@ -498,6 +501,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div> </Flyout>); } + + let templates: Map<Template, boolean> = new Map(); + let doc = SelectionManager.SelectedDocuments()[0]; + Array.from(Object.values(Templates.TemplateList)).map(template => { + let docTemps = doc.templates; + let checked = false; + docTemps.forEach(temp => { + if (template.Name === temp.Name) { + checked = true; + } + }); + templates.set(template, checked); + }); + return (<div className="documentDecorations"> <div className="documentDecorations-background" style={{ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", @@ -533,6 +550,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div> <div className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>∞</div> + <TemplateMenu doc={doc} templates={templates} /> </div > </div> ); diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx new file mode 100644 index 000000000..8eb2fc6c6 --- /dev/null +++ b/src/client/views/TemplateMenu.tsx @@ -0,0 +1,75 @@ +import { observable, computed, action } from "mobx"; +import React = require("react"); +import { observer } from "mobx-react"; +import './DocumentDecorations.scss'; +import { Template } from "./Templates"; +import { DocumentView } from "./nodes/DocumentView"; +const higflyout = require("@hig/flyout"); +export const { anchorPoints } = higflyout; +export const Flyout = higflyout.default; + +@observer +class TemplateToggle extends React.Component<{ template: Template, checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>, template: Template) => void }> { + render() { + if (this.props.template) { + return ( + <li> + <input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event, this.props.template)} /> + {this.props.template.Name} + </li> + ); + } else { + return (null); + } + } +} + +export interface TemplateMenuProps { + doc: DocumentView; + templates: Map<Template, boolean>; +} + +@observer +export class TemplateMenu extends React.Component<TemplateMenuProps> { + + @observable private _hidden: boolean = true; + @observable private _templates: Map<Template, boolean> = this.props.templates; + + + @action + toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => { + if (event.target.checked) { + this.props.doc.addTemplate(template); + this._templates.set(template, true); + } else { + this.props.doc.removeTemplate(template); + this._templates.set(template, false); + } + } + + @action + componentWillReceiveProps(nextProps: TemplateMenuProps) { + this._templates = nextProps.templates; + } + + @action + toggleTemplateActivity = (): void => { + this._hidden = !this._hidden; + } + + render() { + let templateMenu: Array<JSX.Element> = []; + this._templates.forEach((checked, template) => { + templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />); + }); + + return ( + <div className="templating-menu" > + <div className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div> + <ul id="template-list" style={{ display: this._hidden ? "none" : "block" }}> + {templateMenu} + </ul> + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx new file mode 100644 index 000000000..616bc2566 --- /dev/null +++ b/src/client/views/Templates.tsx @@ -0,0 +1,66 @@ +import React = require("react"); + +export enum TemplatePosition { + InnerTop, + InnerBottom, + InnerRight, + InnerLeft, + OutterTop, + OutterBottom, + OutterRight, + OutterLeft, +} + +export class Template { + constructor(name: string, position: TemplatePosition, layout: string) { + this._name = name; + this._position = position; + this._layout = layout; + } + + private _name: string; + private _position: TemplatePosition; + private _layout: string; + + get Name(): string { + return this._name; + } + + get Position(): TemplatePosition { + return this._position; + } + + get Layout(): string { + return this._layout; + } +} + +export namespace Templates { + // export const BasicLayout = new Template("Basic layout", "{layout}"); + + export const OuterCaption = new Template("Outer caption", TemplatePosition.OutterBottom, + `<div><div style="margin:auto; height:calc(100%); width:100%;">{layout}</div><div style="height:(100% + 50px); width:100%; position:absolute"><FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={CaptionKey} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/></div></div>` + ); + + export const InnerCaption = new Template("Inner caption", TemplatePosition.InnerBottom, + `<div><div style="margin:auto; height:calc(100% - 50px); width:100%;">{layout}</div><div style="height:50px; width:100%; position:absolute"><FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={CaptionKey} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/></div></div>` + ); + + export const SideCaption = new Template("Side caption", TemplatePosition.OutterRight, + `<div><div style="margin:auto; height:100%; width:100%;">{layout}</div><div style="height:100%; width:300px; position:absolute; top: 0; right: -300px;"><FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={CaptionKey} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/></div> </div>` + ); + + export const Title = new Template("Title", TemplatePosition.InnerTop, + `<div><div style="margin:auto; height:calc(100% - 50px); width:100%;">{layout}</div><div style="height:50px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .6); color: white; padding:12px">{Title}</div></div>` + ); + + export const TemplateList: Template[] = [Title, OuterCaption, InnerCaption, SideCaption]; + + export function sortTemplates(a: Template, b: Template) { + if (a.Position < b.Position) { return -1; } + if (a.Position > b.Position) { return 1; } + return 0; + } + +} + diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 03d9a4322..bae14193e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,10 +1,11 @@ import { action, computed, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; -import { Field, Opt } from "../../../fields/Field"; +import { Field, FieldWaiting, Opt } from "../../../fields/Field"; import { Key } from "../../../fields/Key"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; +import { TemplateField } from "../../../fields/TemplateField"; import { ServerUtils } from "../../../server/ServerUtil"; import { emptyFunction, Utils } from "../../../Utils"; import { Documents } from "../../documents/Documents"; @@ -18,6 +19,7 @@ import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; +import { Template, Templates } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); @@ -86,6 +88,7 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { export class DocumentView extends React.Component<DocumentViewProps> { private _downX: number = 0; private _downY: number = 0; + @computed get base(): string { return this.props.Document.GetText(KeyStore.BaseLayout, "<p>Error loading base layout data</p>"); } private _mainCont = React.createRef<HTMLDivElement>(); private _dropDisposer?: DragManager.DragDropDisposer; @@ -95,6 +98,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); } @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); } @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); } + @computed get templates(): Array<Template> { + let field = this.props.Document.GetT(KeyStore.Templates, TemplateField); + return !field || field === FieldWaiting ? [] : field.Data; + } + set templates(templates: Array<Template>) { this.props.Document.SetData(KeyStore.Templates, templates, TemplateField); } + screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); @action componentDidMount() { @@ -240,6 +249,43 @@ export class DocumentView extends React.Component<DocumentViewProps> { } } + updateLayout = (): void => { + let base = this.base; + let layout = this.base; + + this.templates.forEach(template => { + let temp = template.Layout; + layout = temp.replace("{layout}", base); + base = layout; + }); + + this.props.Document.SetText(KeyStore.Layout, layout); + } + + @action + addTemplate = (template: Template) => { + let templates = this.templates; + templates.push(template); + templates = templates.splice(0, templates.length).sort(Templates.sortTemplates); + this.templates = templates; + this.updateLayout(); + } + + @action + removeTemplate = (template: Template) => { + let templates = this.templates; + for (let i = 0; i < templates.length; i++) { + let temp = templates[i]; + if (temp.Name === template.Name) { + templates.splice(i, 1); + break; + } + } + templates = templates.splice(0, templates.length).sort(Templates.sortTemplates); + this.templates = templates; + this.updateLayout(); + } + @action onContextMenu = (e: React.MouseEvent): void => { e.stopPropagation(); diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index aa18a9f33..a7652f1bf 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -19,7 +19,9 @@ export namespace KeyStore { export const Data = new Key("Data"); export const Annotations = new Key("Annotations"); export const ViewType = new Key("ViewType"); + export const BaseLayout = new Key("BaseLayout"); export const Layout = new Key("Layout"); + export const Templates = new Key("Templates"); export const BackgroundColor = new Key("BackgroundColor"); export const BackgroundLayout = new Key("BackgroundLayout"); export const OverlayLayout = new Key("OverlayLayout"); diff --git a/src/fields/TemplateField.ts b/src/fields/TemplateField.ts new file mode 100644 index 000000000..72ae13c2e --- /dev/null +++ b/src/fields/TemplateField.ts @@ -0,0 +1,43 @@ +import { BasicField } from "./BasicField"; +import { Types } from "../server/Message"; +import { FieldId } from "./Field"; +import { Template, TemplatePosition } from "../client/views/Templates"; + + +export class TemplateField extends BasicField<Array<Template>> { + constructor(data: Array<Template> = [], id?: FieldId, save: boolean = true) { + super(data, save, id); + } + + ToScriptString(): string { + return `new TemplateField("${this.Data}")`; + } + + Copy() { + return new TemplateField(this.Data); + } + + ToJson() { + let templates: Array<{ name: string, position: TemplatePosition, layout: string }> = []; + this.Data.forEach(template => { + templates.push({ name: template.Name, layout: template.Layout, position: template.Position }); + }); + return { + type: Types.Templates, + data: templates, + id: this.Id, + }; + } + + UpdateFromServer(data: any) { + this.data = new Array(data); + } + + static FromJson(id: string, data: any): TemplateField { + let templates: Array<Template> = []; + data.forEach((template: { name: string, position: number, layout: string }) => { + templates.push(new Template(template.name, template.position, template.layout)); + }); + return new TemplateField(templates, id, false); + } +}
\ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index 15916ef12..854ae0168 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -15,7 +15,7 @@ export class Message<T> { export enum Types { Number, List, Key, Image, Web, Document, Text, Icon, RichText, DocumentReference, - Html, Video, Audio, Ink, PDF, Tuple, HistogramOp, Boolean, Script, + Html, Video, Audio, Ink, PDF, Tuple, HistogramOp, Boolean, Script, Templates } export interface Transferable { diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts index 79ca5e55d..eb5749dff 100644 --- a/src/server/ServerUtil.ts +++ b/src/server/ServerUtil.ts @@ -18,6 +18,7 @@ import { NumberField } from "./../fields/NumberField"; import { RichTextField } from "./../fields/RichTextField"; import { TextField } from "./../fields/TextField"; import { Transferable, Types } from "./Message"; +import { TemplateField } from "../fields/TemplateField"; import { IconField } from "../fields/IconFIeld"; export class ServerUtils { @@ -52,6 +53,7 @@ export class ServerUtils { case Types.Video: return new VideoField(new URL(json.data), json.id, false); case Types.Tuple: return new TupleField(json.data, json.id, false); case Types.Ink: return InkField.FromJson(json.id, json.data); + case Types.Templates: return TemplateField.FromJson(json.id, json.data); case Types.Document: return Document.FromJson(json.data, json.id, false); default: throw Error( |