diff options
| author | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-03-17 13:27:33 -0400 |
|---|---|---|
| committer | Sam Wilkins <abdullah_ahmed@brown.edu> | 2019-03-17 13:27:33 -0400 |
| commit | bc97c3d8b057ec73f6726c31cf8bbe37e679e0d0 (patch) | |
| tree | 10031eb180ffde65538b617a7fec74b8be9d3bd1 /src/client/views | |
| parent | 978c2139753e677020d6d3e149ebe51b6443b1dc (diff) | |
| parent | 47da497aded0bafdc5c85c8a79a9a06d0d401e92 (diff) | |
merged with master, resolved conflicts and reformatted workspace menu
Diffstat (limited to 'src/client/views')
53 files changed, 1948 insertions, 778 deletions
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differindex 6bd614c8b..0964d5ff3 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 41994ef79..f6830d9cd 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -1,42 +1,55 @@ +@import "global_variables"; .contextMenu-cont { - position: absolute; - display: flex; - z-index: 1000; - box-shadow: #AAAAAA .2vw .2vw .4vw; - flex-direction: column; + position: absolute; + display: flex; + z-index: 1000; + box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; + flex-direction: column; +} + +.contextMenu-item:first-child { + background: $intermediate-color; + color: $light-color; +} + +.contextMenu-item:first-child::placeholder { + color: $light-color; +} + +.contextMenu-item:first-child:hover { + background: $intermediate-color; + color: $light-color; } .contextMenu-item { - width: auto; - height: auto; - background: #F0F8FF; - display: flex; - justify-content: left; - align-items: center; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - transition: all .1s; - border-width: .11px; - border-style: none; - border-color: rgb(187, 186, 186); - border-bottom-style: solid; - padding: 10px; - white-space: nowrap; - // font-size: 1.5vw; - font-size: 12px; + width: auto; + height: auto; + background: $light-color-secondary; + display: flex; + justify-content: left; + align-items: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all 0.1s; + border-width: 0.11px; + border-style: none; + border-color: $intermediate-color; + border-bottom-style: solid; + padding: 10px; + white-space: nowrap; + font-size: 13px; } .contextMenu-item:hover { - transition: all .1s; - background: #B0E0E6; + transition: all 0.1s; + background: $lighter-alt-accent; } .contextMenu-description { - // font-size: 1.5vw; - text-align: left; - // width: 8vw; -}
\ No newline at end of file + text-align: left; + width: 8vw; +} diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 12352c667..9109b56bb 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -13,6 +13,8 @@ export class ContextMenu extends React.Component { @observable private _pageY: number = 0; @observable private _display: string = "none"; @observable private _searchString: string = ""; + // afaik displaymenu can be called before all the items are added to the menu, so can't determine in displayMenu what the height of the menu will be + @observable private _yRelativeToTop: boolean = true; private ref: React.RefObject<HTMLDivElement>; @@ -59,8 +61,13 @@ export class ContextMenu extends React.Component { intersects = (x: number, y: number): boolean => { if (this.ref.current && this._display !== "none") { - if (x >= this._pageX && x <= this._pageX + this.ref.current.getBoundingClientRect().width) { - if (y >= this._pageY && y <= this._pageY + this.ref.current.getBoundingClientRect().height) { + let menuSize = { width: this.ref.current.getBoundingClientRect().width, height: this.ref.current.getBoundingClientRect().height }; + + let upperLeft = { x: this._pageX, y: this._yRelativeToTop ? this._pageY : window.innerHeight - (this._pageY + menuSize.height) }; + let bottomRight = { x: this._pageX + menuSize.width, y: this._yRelativeToTop ? this._pageY + menuSize.height : window.innerHeight - this._pageY }; + + if (x >= upperLeft.x && x <= bottomRight.x) { + if (y >= upperLeft.y && y <= bottomRight.y) { return true; } } @@ -69,9 +76,12 @@ export class ContextMenu extends React.Component { } render() { + let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } : + { left: this._pageX, bottom: this._pageY, display: this._display }; + return ( - <div className="contextMenu-cont" style={{ left: this._pageX, top: this._pageY, display: this._display }} ref={this.ref}> + <div className="contextMenu-cont" style={style} ref={this.ref}> <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange}></input> {this._items.filter(prop => { return prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1; diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index fb9091dfc..11595aa01 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -1,17 +1,18 @@ +@import "global_variables"; #documentDecorations-container { position: absolute; display: grid; z-index: 1000; - grid-template-rows: 20px 1fr 20px 0px; - grid-template-columns: 20px 1fr 20px; + grid-template-rows: 8px 1fr 8px 30px; + grid-template-columns: 8px 1fr 8px; pointer-events: none; #documentDecorations-centerCont { background: none; } .documentDecorations-resizer { pointer-events: auto; - background: lightblue; - opacity: 0.4; + background: $alt-accent; + opacity: 0.8; } #documentDecorations-topLeftResizer, #documentDecorations-bottomRightResizer { @@ -29,23 +30,89 @@ #documentDecorations-rightResizer { cursor: ew-resize; } - } + +// position: absolute; +// display: grid; +// z-index: 1000; +// grid-template-rows: 20px 1fr 20px 0px; +// grid-template-columns: 20px 1fr 20px; +// pointer-events: none; +// #documentDecorations-centerCont { +// background: none; +// } +// .documentDecorations-resizer { +// pointer-events: auto; +// background: lightblue; +// opacity: 0.4; +// } +// #documentDecorations-topLeftResizer, +// #documentDecorations-bottomRightResizer { +// cursor: nwse-resize; +// } +// #documentDecorations-topRightResizer, +// #documentDecorations-bottomLeftResizer { +// cursor: nesw-resize; +// } +// #documentDecorations-topResizer, +// #documentDecorations-bottomResizer { +// cursor: ns-resize; +// } +// #documentDecorations-leftResizer, +// #documentDecorations-rightResizer { +// cursor: ew-resize; +// } +// } +.linkFlyout { + grid-column: 1/4 +} + +.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-empty { height: 20px; width: 20px; margin-top: 10px; border-radius: 50%; - opacity: 0.6; + opacity: 0.9; pointer-events: auto; - background-color: #2B6091; + 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; } + .linkButton-nonempty { height: 20px; width: 20px; margin-top: 10px; border-radius: 50%; - opacity: 0.6; + opacity: 0.9; pointer-events: auto; - background-color: rgb(35, 165, 42); + 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; }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index dc62f97cf..3bdb7d5b3 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -18,6 +18,8 @@ export class DocumentDecorations extends React.Component { static Instance: DocumentDecorations private _resizer = "" private _isPointerDown = false; + + private _resizeBorderWidth = 16; private _linkButton = React.createRef<HTMLDivElement>(); @observable private _hidden = false; @@ -206,21 +208,24 @@ export class DocumentDecorations extends React.Component { let linkButton = null; if (SelectionManager.SelectedDocuments().length > 0) { let selFirst = SelectionManager.SelectedDocuments()[0]; + let linkToSize = selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length; + let linkFromSize = selFirst.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []).length; + let linkCount = linkToSize + linkFromSize; linkButton = (<Flyout anchorPoint={anchorPoints.RIGHT_TOP} content={ <LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent}> </LinkMenu> }> - <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} ref={this._linkButton} /> + <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} ref={this._linkButton}>{linkCount}</div> </Flyout>); } return ( <div id="documentDecorations-container" style={{ - width: (bounds.r - bounds.x + 40) + "px", - height: (bounds.b - bounds.y + 40) + "px", - left: bounds.x - 20, - top: bounds.y - 20, + width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", + height: (bounds.b - bounds.y + this._resizeBorderWidth + 30) + "px", + left: bounds.x - this._resizeBorderWidth / 2, + top: bounds.y - this._resizeBorderWidth / 2, }}> <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> @@ -232,7 +237,7 @@ export class DocumentDecorations extends React.Component { <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> - {linkButton} + <div title="View Links" className="linkFlyout">{linkButton}</div> </div > ) diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss new file mode 100644 index 000000000..be3c5069a --- /dev/null +++ b/src/client/views/EditableView.scss @@ -0,0 +1,6 @@ +.editableView-container-editing { + overflow-wrap: break-word; + word-wrap: break-word; + hyphens: auto; + max-width: 300px; +}
\ No newline at end of file diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 55a49863d..98a6ed1ba 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -1,6 +1,7 @@ import React = require('react') import { observer } from 'mobx-react'; import { observable, action } from 'mobx'; +import "./EditableView.scss" export interface EditableProps { /** @@ -20,7 +21,7 @@ export interface EditableProps { */ contents: any; height: number - display: string; + display?: string; } /** @@ -50,7 +51,7 @@ export class EditableView extends React.Component<EditableProps> { style={{ display: this.props.display }}></input> } else { return ( - <div className="editableView-container-editing" style={{ display: this.props.display, height: "100%", maxHeight: `${this.props.height}` }} + <div className="editableView-container-editing" style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }} onClick={action(() => this.editing = true)}> {this.props.contents} </div> diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss index f654b194b..e79b146b9 100644 --- a/src/client/views/InkingCanvas.scss +++ b/src/client/views/InkingCanvas.scss @@ -1,8 +1,10 @@ +@import "global_variables"; .inking-canvas { - position: fixed; + position: absolute; top: -50000px; left: -50000px; // z-index: 99; //overlays ink on top of everything svg { + position:absolute; width: 100000px; height: 100000px; .highlight { @@ -13,20 +15,135 @@ .inking-control { position: absolute; - right: 0; - bottom: 75px; - text-align: right; + left: 70px; + bottom: 70px; + margin: 0; + padding: 0; + display: flex; + label, + input, + option { + font-size: 12px; + } + input[type="range"] { + -webkit-appearance: none; + background-color: transparent; + vertical-align: middle; + margin-top: 8px; + &:focus { + outline: none; + } + &::-webkit-slider-runnable-track { + width: 100%; + height: 3px; + border-radius: 1.5px; + cursor: pointer; + background: $intermediate-color; + } + &::-webkit-slider-thumb { + height: 12px; + width: 12px; + border: 1px solid $intermediate-color; + border-radius: 6px; + background: $light-color; + cursor: pointer; + -webkit-appearance: none; + margin-top: -4px; + } + &::-moz-range-track { + width: 100%; + height: 3px; + border-radius: 1.5px; + cursor: pointer; + background: $light-color; + } + &::-moz-range-thumb { + height: 12px; + width: 12px; + border: 1px solid $intermediate-color; + border-radius: 6px; + background: $light-color; + cursor: pointer; + -webkit-appearance: none; + margin-top: -4px; + } + } + input[type="text"] { + border: none; + padding: 0 0px; + background: transparent; + color: $dark-color; + font-size: 12px; + margin-top: 4px; + } .ink-panel { - margin-top: 12px; + margin: 6px 12px 6px 0; + height: 30px; + vertical-align: middle; + line-height: 36px; + padding: 0 10px; + color: $intermediate-color; &:first { margin-top: 0; } } + .ink-tools { + display: flex; + background-color: transparent; + border-radius: 0; + padding: 0; + button { + height: 36px; + padding: 0px; + padding-bottom: 3px; + margin-left: 10px; + background-color: transparent; + color: $intermediate-color; + } + button:hover { + transform: scale(1.15); + } + } .ink-size { display: flex; justify-content: space-between; - input { - width: 85%; + input[type="text"] { + width: 42px; + } + >* { + margin-right: 6px; + &:last-child { + margin-right: 0; + } + } + } + .ink-color { + display: flex; + position: relative; + padding-right: 0; + label { + margin-right: 6px; + } + .ink-color-display { + border-radius: 11px; + width: 22px; + height: 22px; + margin-top: 6px; + cursor: pointer; + text-align: center; // span { + // color: $light-color; + // font-size: 8px; + // user-select: none; + // } + } + .ink-color-picker { + background-color: $light-color; + border-radius: 5px; + padding: 12px; + position: absolute; + bottom: 36px; + left: -3px; + box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; } } }
\ No newline at end of file diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx index 0d87c1239..84c47f616 100644 --- a/src/client/views/InkingCanvas.tsx +++ b/src/client/views/InkingCanvas.tsx @@ -1,4 +1,5 @@ import { observer } from "mobx-react"; +import { observable } from "mobx"; import { action, computed } from "mobx"; import { InkingControl } from "./InkingControl"; import React = require("react"); @@ -6,14 +7,10 @@ import { Transform } from "../util/Transform"; import { Document } from "../../fields/Document"; import { KeyStore } from "../../fields/KeyStore"; import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField"; -import { JsxArgs } from "./nodes/DocumentView"; import { InkingStroke } from "./InkingStroke"; import "./InkingCanvas.scss" -import { CollectionDockingView } from "./collections/CollectionDockingView"; import { Utils } from "../../Utils"; import { FieldWaiting } from "../../fields/Field"; -import { getMapLikeKeys } from "mobx/lib/internal"; - interface InkCanvasProps { getScreenTransform: () => Transform; @@ -22,7 +19,16 @@ interface InkCanvasProps { @observer export class InkingCanvas extends React.Component<InkCanvasProps> { - + static InkOffset: number = 50000; + public static IntersectStrokeRect(stroke: StrokeData, selRect: { left: number, top: number, width: number, height: number }): boolean { + let inside = false; + stroke.pathData.map(val => { + if (selRect.left < val.x - InkingCanvas.InkOffset && selRect.left + selRect.width > val.x - InkingCanvas.InkOffset && + selRect.top < val.y - InkingCanvas.InkOffset && selRect.top + selRect.height > val.y - InkingCanvas.InkOffset) + inside = true; + }); + return inside + } private _isDrawing: boolean = false; private _idGenerator: string = ""; @@ -51,7 +57,6 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { document.removeEventListener("mouseup", this.handleMouseUp); } - @action handleMouseDown = (e: React.PointerEvent): void => { if (e.button != 0 || @@ -62,7 +67,6 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { if (InkingControl.Instance.selectedTool === InkTool.Eraser) { return } - e.stopPropagation() const point = this.relativeCoordinatesForEvent(e); // start the new line, saves a uuid to represent the field of the stroke @@ -74,7 +78,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { color: InkingControl.Instance.selectedColor, width: InkingControl.Instance.selectedWidth, tool: InkingControl.Instance.selectedTool, - page: this.props.Document.GetNumber(KeyStore.CurPage, 0) + page: this.props.Document.GetNumber(KeyStore.CurPage, -1) }); this.inkData = data; this._isDrawing = true; @@ -110,8 +114,8 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { relativeCoordinatesForEvent = (e: React.MouseEvent): { x: number, y: number } => { let [x, y] = this.props.getScreenTransform().transformPoint(e.clientX, e.clientY); - x += 50000 - y += 50000 + x += InkingCanvas.InkOffset; + y += InkingCanvas.InkOffset; return { x, y }; } @@ -145,11 +149,11 @@ export class InkingCanvas extends React.Component<InkCanvasProps> { // parse data from server let paths: Array<JSX.Element> = [] - let curPage = this.props.Document.GetNumber(KeyStore.CurPage, 0) + let curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1) Array.from(lines).map(item => { let id = item[0]; let strokeData = item[1]; - if (strokeData.page == 0 || strokeData.page == curPage) + if (strokeData.page == -1 || strokeData.page == curPage) paths.push(<InkingStroke key={id} id={id} line={strokeData.pathData} color={strokeData.color} diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 955831fb6..fb75ef2a5 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -1,16 +1,25 @@ import { observable, action, computed } from "mobx"; -import { CirclePicker, ColorResult } from 'react-color'; + +import { CirclePicker, ColorResult } from 'react-color' import React = require("react"); import "./InkingCanvas.scss" import { InkTool } from "../../fields/InkField"; import { observer } from "mobx-react"; +import "./InkingCanvas.scss" +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons'; + +library.add(faPen, faHighlighter, faEraser, faBan); @observer export class InkingControl extends React.Component { static Instance: InkingControl = new InkingControl({}); @observable private _selectedTool: InkTool = InkTool.None; - @observable private _selectedColor: string = "#f44336"; + @observable private _selectedColor: string = "rgb(244, 67, 54)"; @observable private _selectedWidth: string = "25"; + @observable private _open: boolean = false; + @observable private _colorPickerDisplay: boolean = false; constructor(props: Readonly<{}>) { super(props); @@ -49,29 +58,50 @@ export class InkingControl extends React.Component { selected = (tool: InkTool) => { if (this._selectedTool === tool) { - return { backgroundColor: "black", color: "white" } + return { color: "#61aaa3" } } return {} } + @action + toggleDisplay = () => { + this._open = !this._open; + } + + @action + toggleColorPicker = () => { + this._colorPickerDisplay = !this._colorPickerDisplay; + } + render() { return ( - <div className="inking-control"> - <div className="ink-tools ink-panel"> - <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}>Pen</button> - <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}>Highlighter</button> - <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}>Eraser</button> - <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}> None</button> - </div> - <div className="ink-size ink-panel"> - <label htmlFor="stroke-width">Size</label> - <input type="range" min="1" max="100" defaultValue="25" name="stroke-width" + <ul className="inking-control" style={this._open ? { display: "flex" } : { display: "none" }}> + <li className="ink-tools ink-panel"> + <div className="ink-tool-buttons"> + <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" title="Pen" /></button> + <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" title="Highlighter" /></button> + <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" title="Eraser" /></button> + <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}><FontAwesomeIcon icon="ban" size="lg" title="Pointer" /></button> + </div> + </li> + <li className="ink-color ink-panel"> + <label>COLOR: </label> + <div className="ink-color-display" style={{ backgroundColor: this._selectedColor }} + onClick={() => this.toggleColorPicker()}> + {/* {this._colorPickerDisplay ? <span>▼</span> : <span>▲</span>} */} + </div> + <div className="ink-color-picker" style={this._colorPickerDisplay ? { display: "block" } : { display: "none" }}> + <CirclePicker onChange={this.switchColor} circleSize={22} width={"220"} /> + </div> + </li> + <li className="ink-size ink-panel"> + <label htmlFor="stroke-width">SIZE: </label> + <input type="text" min="1" max="100" value={this._selectedWidth} name="stroke-width" + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} /> + <input type="range" min="1" max="100" value={this._selectedWidth} name="stroke-width" onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} /> - </div> - <div className="ink-color ink-panel"> - <CirclePicker onChange={this.switchColor} /> - </div> - </div> + </li> + </ul > ) } }
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index d724421d3..87b5c43d8 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -21,8 +21,6 @@ export class InkingStroke extends React.Component<StrokeProps> { @observable private _strokeColor: string = this.props.color; @observable private _strokeWidth: string = this.props.width; - private _canvasColor: string = "#cdcdcd"; - deleteStroke = (e: React.MouseEvent): void => { if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) { this.props.deleteCallback(this.props.id); diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 4334ed299..bb42db202 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -1,22 +1,35 @@ +@import "global_variables"; +@import "nodeModuleOverrides"; html, body { width: 100%; height: 100%; overflow: hidden; - font-family: 'Hind Siliguri', sans-serif; + font-family: $sans-serif; margin: 0; } +#dash-title { + position: absolute; + right: 46.5%; + letter-spacing: 3px; + top: 9px; + font-size: 12px; + color: $alt-accent; + z-index: 9999; +} + h1 { font-size: 50px; position: fixed; top: 30px; left: 50%; transform: translateX(-50%); - color: black; + color: $dark-color; text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; z-index: 9999; - font-family: 'Fjalla One', sans-serif; + font-family: $sans-serif; + font-weight: 700; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -25,27 +38,139 @@ h1 { user-select: none; } +.jsx-parser { + width:100% +} + p { margin: 0px; padding: 0px; } + ::-webkit-scrollbar { -webkit-appearance: none; - height:5px; - width:5px; + height: 5px; + width: 5px; } + ::-webkit-scrollbar-thumb { border-radius: 2px; - background-color: rgba(0,0,0,.5); + background-color: rgba(0, 0, 0, 0.5); } -.main-buttonDiv { +// button stuff +button { + background: $dark-color; + outline: none; + border: 0px; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 75%; + padding: 10px; + transition: transform 0.2s; +} + +button:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; +} + +.clear-db-button { position: absolute; - width: 150px; - left: 0px; + right: 45%; + bottom: 3%; + font-size: 50%; +} + +.round-button { + width: 36px; + height: 36px; + border-radius: 18px; + font-size: 15px; } + +.round-button:hover { + transform: scale(1.15); +} + +.add-button { + position: relative; + margin-right: 10px; +} + .main-undoButtons { position: absolute; width: 150px; right: 0px; } + +//toolbar stuff +#toolbar { + position: absolute; + bottom: 62px; + left: 24px; + .toolbar-button { + display: block; + margin-bottom: 10px; + } +} + +// add nodes menu. Note that the + button is actually an input label, not an actual button. +#add-nodes-menu { + position: absolute; + bottom: 24px; + left: 24px; + label { + background: $dark-color; + color: $light-color; + display: inline-block; + border-radius: 18px; + font-size: 25px; + width: 36px; + height: 36px; + margin-right: 10px; + cursor: pointer; + transition: transform 0.2s; + } + label p { + padding-left: 10.5px; + padding-top: 3px; + } + label:hover { + background: $main-accent; + transform: scale(1.15); + } + input { + display: none; + } + input:not(:checked)~#add-options-content { + display: none; + } + input:checked~label { + transform: rotate(45deg); + transition: transform 0.5s; + cursor: pointer; + } +} + +#add-options-content { + display: table; + opacity: 1; + margin: 0; + padding: 0; + position: relative; + float: right; + bottom: 0.3em; + margin-bottom: -1.68em; +} + +ul#add-options-list { + list-style: none; + padding: 0; + li { + display: inline-block; + padding: 0; + } +}
\ No newline at end of file diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index e90366f3a..e76a5e04b 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -1,4 +1,4 @@ -import { action, configure, observable } from 'mobx'; +import { action, configure, observable, runInAction } from 'mobx'; import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; @@ -24,6 +24,20 @@ import { Field, Opt } from '../../fields/Field'; import { InkingControl } from './InkingControl'; import { RouteStore } from '../../server/RouteStore'; import { json } from 'body-parser'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faFont } from '@fortawesome/free-solid-svg-icons'; +import { faImage } from '@fortawesome/free-solid-svg-icons'; +import { faFilePdf } from '@fortawesome/free-solid-svg-icons'; +import { faObjectGroup } from '@fortawesome/free-solid-svg-icons'; +import { faTable } from '@fortawesome/free-solid-svg-icons'; +import { faGlobeAsia } from '@fortawesome/free-solid-svg-icons'; +import { faUndoAlt } from '@fortawesome/free-solid-svg-icons'; +import { faRedoAlt } from '@fortawesome/free-solid-svg-icons'; +import { faPenNib } from '@fortawesome/free-solid-svg-icons'; +import { faFilm } from '@fortawesome/free-solid-svg-icons'; +import { faMusic } from '@fortawesome/free-solid-svg-icons'; +import Measure from 'react-measure'; @observer export class Main extends React.Component { @@ -31,11 +45,31 @@ export class Main extends React.Component { @observable private mainContainer?: Document; @observable private mainfreeform?: Document; @observable private userWorkspaces: Document[] = []; + @observable public pwidth: number = 0; + @observable public pheight: number = 0; + + private mainDocId: string | undefined; constructor(props: Readonly<{}>) { super(props); // causes errors to be generated when modifying an observable outside of an action configure({ enforceActions: "observed" }); + if (window.location.pathname !== "/home") { + let pathname = window.location.pathname.split("/"); + this.mainDocId = pathname[pathname.length - 1]; + } + + library.add(faFont); + library.add(faImage); + library.add(faFilePdf); + library.add(faObjectGroup); + library.add(faTable); + library.add(faGlobeAsia); + library.add(faUndoAlt); + library.add(faRedoAlt); + library.add(faPenNib); + library.add(faFilm); + library.add(faMusic); this.initEventListeners(); Documents.initProtos(() => { @@ -58,8 +92,8 @@ export class Main extends React.Component { initAuthenticationRouters = () => { // Load the user's active workspace, or create a new one if initial session after signup request.get(this.prepend(RouteStore.getActiveWorkspace), (error, response, body) => { - if (body) { - Server.GetField(body, field => { + if (this.mainDocId || body) { + Server.GetField(this.mainDocId || body, field => { if (field instanceof Document) { this.openWorkspace(field); this.populateWorkspaces(); @@ -71,25 +105,8 @@ export class Main extends React.Component { this.createNewWorkspace(true); } }); - } - - // reportLocation = (e: PointerEvent) => { - // request.post(this.prepend(RouteStore.updateCursor), { - // body: { - // cursorX: e.screenX, - // cursorY: e.screenY, - // docId: this.mainContainer ? this.mainContainer.Id : undefined - // }, - // json: true - // }); - // } - // componentWillUnmount = () => { - - // } - - // pushCursor = () => { - // } + } @action createNewWorkspace = (init: boolean): void => { @@ -128,7 +145,6 @@ export class Main extends React.Component { }); this.mainContainer = doc; this.mainContainer.GetAsync(KeyStore.ActiveFrame, field => this.mainfreeform = field as Document); - // this.pushCursor(); } toggleWorkspaces = () => { @@ -169,47 +185,82 @@ export class Main extends React.Component { let addClick = (creator: () => Document) => action(() => this.mainfreeform!.GetList<Document>(KeyStore.Data, []).push(creator())); - if (!this.mainContainer) { - return <div></div> - } return ( <div style={{ position: "absolute", width: "100%", height: "100%" }}> - <DocumentView Document={this.mainContainer} - AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} - ContentScaling={() => 1} - PanelWidth={() => 0} - PanelHeight={() => 0} - isTopMost={true} - SelectOnLoad={false} - focus={() => { }} - ContainingCollectionView={undefined} /> + <Measure onResize={(r: any) => runInAction(() => { + this.pwidth = r.entry.width; + this.pheight = r.entry.height; + })}> + {({ measureRef }) => { + if (!this.mainContainer) { + return <div></div> + } + return <div ref={measureRef} style={{ position: "absolute", width: "100%", height: "100%" }}> + <DocumentView Document={this.mainContainer} + AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} + ContentScaling={() => 1} + PanelWidth={() => this.pwidth} + PanelHeight={() => this.pheight} + isTopMost={true} + SelectOnLoad={false} + focus={() => { }} + ContainingCollectionView={undefined} /> + </div> + }} + </Measure> <DocumentDecorations /> <ContextMenu /> - <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={this.userWorkspaces} /> - <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} > - <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div> - <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} > - <button onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>Add Web</button></div> - <div className="main-buttonDiv" style={{ bottom: '50px' }} ref={textRef}> - <button onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>Add Text</button></div> - <div className="main-buttonDiv" style={{ bottom: '75px' }} ref={colRef}> - <button onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>Add Collection</button></div> - <div className="main-buttonDiv" style={{ bottom: '100px' }} ref={schemaRef}> - <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div> - <div className="main-buttonDiv" style={{ bottom: '125px' }} > - <button onClick={clearDatabase}>Clear Database</button></div> - <div className="main-buttonDiv" style={{ top: '25px' }} ref={workspacesRef}> - <button onClick={this.toggleWorkspaces}>View Workspaces</button></div> - <div className="main-buttonDiv" style={{ top: '25px', left: '300px' }} ref={logoutRef}> + + <button className="clear-db-button" onClick={clearDatabase}>Clear Database</button> + + {/* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */} + < div id="toolbar" > + <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button> + <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button> + <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button> + </div > + + <div className="main-buttonDiv" style={{ top: '34px', left: '2px', position: 'absolute' }} ref={workspacesRef}> + <button onClick={this.toggleWorkspaces}>Workspaces</button></div> + <div className="main-buttonDiv" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}> <button onClick={() => request.get(this.prepend(RouteStore.logout), () => { })}>Log Out</button></div> - <div className="main-buttonDiv" style={{ bottom: '175px' }} ref={videoRef}> - <button onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}>Add Video</button></div> - <div className="main-buttonDiv" style={{ bottom: '200px' }} ref={audioRef}> - <button onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}>Add Audio</button></div> - <div className="main-buttonDiv" style={{ bottom: '150px' }} ref={pdfRef}> - <button onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>Add PDF</button></div> - <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button> - <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button> + + <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={this.userWorkspaces} /> + {/* for the expandable add nodes menu. Not included with the above because once it expands it expands the whole div with it, making canvas interactions limited. */} + < div id="add-nodes-menu" > + <input type="checkbox" id="add-menu-toggle" /> + <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label> + + <div id="add-options-content"> + <ul id="add-options-list"> + <li><div ref={textRef}><button className="round-button add-button" title="Add Textbox" onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}> + <FontAwesomeIcon icon="font" size="sm" /> + </button></div></li> + <li><div ref={imgRef}><button className="round-button add-button" title="Add Image" onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}> + <FontAwesomeIcon icon="image" size="sm" /> + </button></div></li> + <li><div ref={pdfRef}><button className="round-button add-button" title="Add PDF" onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}> + <FontAwesomeIcon icon="file-pdf" size="sm" /> + </button></div></li> + <li><div ref={videoRef}><button className="round-button add-button" title="Add Video" onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}> + <FontAwesomeIcon icon="film" size="sm" /> + </button></div></li> + <li><div ref={audioRef}><button className="round-button add-button" title="Add Audio" onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}> + <FontAwesomeIcon icon="music" size="sm" /> + </button></div></li> + <li><div ref={webRef}><button className="round-button add-button" title="Add Web Clipping" onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}> + <FontAwesomeIcon icon="globe-asia" size="sm" /> + </button></div></li> + <li><div ref={colRef}><button className="round-button add-button" title="Add Collection" onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}> + <FontAwesomeIcon icon="object-group" size="sm" /> + </button></div></li> + <li><div ref={schemaRef}><button className="round-button add-button" title="Add Schema" onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}> + <FontAwesomeIcon icon="table" size="sm" /> + </button></div></li> + </ul> + </div> + </div > + <InkingControl /> </div> ); diff --git a/src/client/views/_global_variables.scss b/src/client/views/_global_variables.scss new file mode 100644 index 000000000..44a819b79 --- /dev/null +++ b/src/client/views/_global_variables.scss @@ -0,0 +1,17 @@ +@import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Crimson+Text:400,400i,700"); +// colors +$light-color: #fcfbf7; +$light-color-secondary: rgb(241, 239, 235); +$main-accent: #61aaa3; +// $alt-accent: #cdd5ec; +// $alt-accent: #cdeceb; +$alt-accent: #59dff7; +$lighter-alt-accent: rgb(207, 220, 240); +$intermediate-color: #9c9396; +$dark-color: #121721; +// fonts +$sans-serif: "Noto Sans", sans-serif; +// $sans-serif: "Roboto Slab", sans-serif; +$serif: "Crimson Text", serif; +// misc values +$border-radius: 0.3em; diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss new file mode 100644 index 000000000..6f97e60f8 --- /dev/null +++ b/src/client/views/_nodeModuleOverrides.scss @@ -0,0 +1,23 @@ +// this file is for overriding all the css from installed node modules + +// goldenlayout stuff +div .lm_header { + background: $dark-color; + min-height: 2em; +} + +.lm_tab { + margin-top: 0.6em !important; + padding-top: 0.5em !important; + min-height: 1.35em; + padding-bottom: 0px; + border-radius: 5px; + font-family: $sans-serif !important; +} + +.lm_header .lm_controls { + right: 1em !important; +} + +// @TODO the ril__navgiation buttons in the img gallery are a lil messed up but I can't figure out +// why. Low priority for now but it's bugging me. --Julie diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index b5cafe681..19788447e 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -212,6 +212,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stack.remove(); //} })); + stack.header.controlsContainer.find('.lm_popout') //get the close icon + .off('click') //unbind the current click handler + .click(action(function () { + var url = "http://localhost:1050/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId; + let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400"); + })); } render() { diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss index 28242c939..11addc5a1 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -1,35 +1,56 @@ +@import "../global_variables"; + .collectionfreeformview-container { - - .collectionfreeformview > .jsx-parser{ - position:absolute; - height: 100%; - width: 100%; - } - border-style: solid; - box-sizing: border-box; - position: relative; + .collectionfreeformview > .jsx-parser { + position: absolute; + height: 100%; + width: 100%; + } + + //nested freeform views + // .collectionfreeformview-container { + // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px), + // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); + // background-size: 30px 30px; + // } + + border: 0px solid $light-color-secondary; + border-radius: $border-radius; + box-sizing: border-box; + position: relative; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; + .collectionfreeformview { + position: absolute; top: 0; left: 0; width: 100%; height: 100%; - overflow: hidden; - .collectionfreeformview { - position: absolute; - top: 0; - left: 0; - width:100%; - height: 100%; - } -} -.collectionfreeformview-marquee{ - border-style: dashed; - box-sizing: border-box; - position: absolute; - border-width: 1px; - border-color: black; + } } .collectionfreeformview-overlay { + .collectionfreeformview > .jsx-parser { + position: absolute; + height: 100%; + } + .formattedTextBox-cont { + background: $light-color-secondary; + } + position:absolute; + border: 0px solid transparent; + border-radius: $border-radius; + overflow: hidden; + box-sizing: border-box; + top: 0; + left: 0; + width: 100%; + height: 100%; + .collectionfreeformview { .collectionfreeformview > .jsx-parser{ position:absolute; height: 100%; @@ -38,37 +59,39 @@ background:yellow; } - border-style: solid; - box-sizing: border-box; + // overflow: hidden; + // border-style: solid; + // box-sizing: border-box; position: absolute; top: 0; left: 0; width: 100%; height: 100%; - overflow: hidden; - .collectionfreeformview { - position: absolute; - top: 0; - left: 0; - width:100%; - height: 100%; - } + } } +// selection border...? .border { - border-style: solid; - box-sizing: border-box; - width: 100%; - height: 100%; + border-style: solid; + box-sizing: border-box; + width: 98%; + height: 98%; + border-radius: $border-radius; } //this is an animation for the blinking cursor! @keyframes blink { - 0% {opacity: 0} - 49%{opacity: 0} - 50% {opacity: 1} + 0% { + opacity: 0; + } + 49% { + opacity: 0; + } + 50% { + opacity: 1; + } } #prevCursor { - animation: blink 1s infinite; -}
\ No newline at end of file + animation: blink 1s infinite; +} diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 0c74d1852..bb28dd20a 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -5,46 +5,74 @@ import { FieldWaiting } from "../../../fields/Field"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; import { TextField } from "../../../fields/TextField"; -import { Documents } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { InkingCanvas } from "../InkingCanvas"; +import { AudioBox } from "../nodes/AudioBox"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; import { KeyValueBox } from "../nodes/KeyValueBox"; import { PDFBox } from "../nodes/PDFBox"; +import { VideoBox } from "../nodes/VideoBox"; import { WebBox } from "../nodes/WebBox"; import "./CollectionFreeFormView.scss"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import { CollectionViewBase } from "./CollectionViewBase"; +import { MarqueeView } from "./MarqueeView"; +import { PreviewCursor } from "./PreviewCursor"; import React = require("react"); -import { SelectionManager } from "../../util/SelectionManager"; const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? @observer export class CollectionFreeFormView extends CollectionViewBase { - private _canvasRef = React.createRef<HTMLDivElement>(); - @observable - private _lastX: number = 0; - @observable - private _lastY: number = 0; + public _canvasRef = React.createRef<HTMLDivElement>(); private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) - @observable - private _downX: number = 0; - @observable - private _downY: number = 0; + public addLiveTextBox = (newBox: Document) => { + // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself + this._selectOnLoaded = newBox.Id; + //set text to be the typed key and get focus on text box + this.props.addDocument(newBox); + //remove cursor from screen + this.PreviewCursorVisible = false; + } + + public selectDocuments = (docs: Document[]) => { + this.props.CollectionView.SelectedDocs.length = 0; + docs.map(d => this.props.CollectionView.SelectedDocs.push(d.Id)); + } + + public getActiveDocuments = () => { + var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); + const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); + let active: Document[] = []; + if (lvalue && lvalue != FieldWaiting) { + lvalue.Data.map(doc => { + var page = doc.GetNumber(KeyStore.Page, -1); + if (page == curPage || page == -1) { + active.push(doc); + } + }) + } + + return active; + } //determines whether the blinking cursor for indicating whether a text will be made on key down is visible - @observable - private _previewCursorVisible: boolean = false; + @observable public PreviewCursorVisible: boolean = false; + @observable public MarqueeVisible = false; + @observable public DownX: number = 0; + @observable public DownY: number = 0; + @observable private _lastX: number = 0; + @observable private _lastY: number = 0; @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) } @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) } @@ -72,41 +100,34 @@ export class CollectionFreeFormView extends CollectionViewBase { } } - @observable - _marquee = false; + + @action + cleanupInteractions = () => { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + this.MarqueeVisible = false; + } @action onPointerDown = (e: React.PointerEvent): void => { - if (((e.button === 2 && this.props.active()) || !e.defaultPrevented) && - (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) { + this.PreviewCursorVisible = false; + if ((e.button === 2 && this.props.active() && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) { document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); - this._lastX = e.pageX; - this._lastY = e.pageY; - this._downX = e.pageX; - this._downY = e.pageY; + this._lastX = this.DownX = e.pageX; + this._lastY = this.DownY = e.pageY; } } @action onPointerUp = (e: PointerEvent): void => { - if (this._marquee) { - document.removeEventListener("keydown", this.marqueeCommand); - } e.stopPropagation(); - if (this._marquee) { - if (!e.shiftKey) { - SelectionManager.DeselectAll(); - } - var selectedDocs = this.marqueeSelect(); - selectedDocs.map(s => this.props.CollectionView.SelectedDocs.push(s.Id)); - } - else if (!this._marquee && Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) { + if (Math.abs(this.DownX - e.clientX) < 4 && Math.abs(this.DownY - e.clientY) < 4) { //show preview text cursor on tap - this._previewCursorVisible = true; + this.PreviewCursorVisible = true; //select is not already selected if (!this.props.isSelected()) { this.props.select(false); @@ -116,97 +137,31 @@ export class CollectionFreeFormView extends CollectionViewBase { } @action - cleanupInteractions = () => { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - this._marquee = false; - } - - intersectRect(r1: { left: number, right: number, top: number, bottom: number }, - r2: { left: number, right: number, top: number, bottom: number }) { - return !(r2.left > r1.right || - r2.right < r1.left || - r2.top > r1.bottom || - r2.bottom < r1.top); - } - - marqueeSelect() { - this.props.CollectionView.SelectedDocs.length = 0; - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); - let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); - let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - let selRect = { left: p[0], top: p[1], right: p[0] + v[0], bottom: p[1] + v[1] } - - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); - const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); - let selection: Document[] = []; - if (lvalue && lvalue != FieldWaiting) { - lvalue.Data.map(doc => { - var page = doc.GetNumber(KeyStore.Page, 0); - if (page == curPage || page == 0) { - var x = doc.GetNumber(KeyStore.X, 0); - var y = doc.GetNumber(KeyStore.Y, 0); - var w = doc.GetNumber(KeyStore.Width, 0); - var h = doc.GetNumber(KeyStore.Height, 0); - if (this.intersectRect({ left: x, top: y, right: x + w, bottom: y + h }, selRect)) - selection.push(doc) - } - }) - } - return selection; - } - - @action onPointerMove = (e: PointerEvent): void => { if (!e.cancelBubble && this.props.active()) { - e.stopPropagation(); - e.preventDefault(); - let wasMarquee = this._marquee; - this._marquee = e.buttons != 2 && !e.altKey && !e.metaKey; - if (this._marquee && !wasMarquee) { - this._previewCursorVisible = false; - document.addEventListener("keydown", this.marqueeCommand); + if (e.buttons == 1 && !e.altKey && !e.metaKey) { + this.MarqueeVisible = true; } - - if (!this._marquee) { + if (this.MarqueeVisible) { + e.stopPropagation(); + e.preventDefault(); + } + else if ((!this.isAnnotationOverlay || this.zoomScaling != 1) && !e.shiftKey) { let x = this.props.Document.GetNumber(KeyStore.PanX, 0); let y = this.props.Document.GetNumber(KeyStore.PanY, 0); let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - this._previewCursorVisible = false; this.SetPan(x - dx, y - dy); + this._lastX = e.pageX; + this._lastY = e.pageY; + e.stopPropagation(); + e.preventDefault(); } } - this._lastX = e.pageX; - this._lastY = e.pageY; - } - - @action - marqueeCommand = (e: KeyboardEvent) => { - if (e.key == "Backspace") { - this.marqueeSelect().map(d => this.props.removeDocument(d)); - this.cleanupInteractions(); - } - if (e.key == "c") { - let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); - let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - - let selected = this.marqueeSelect().map(m => m); - this.marqueeSelect().map(d => this.props.removeDocument(d)); - //setTimeout(() => { - this.props.CollectionView.addDocument(Documents.FreeformDocument(selected.map(d => { - d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - p[0] - v[0] / 2); - d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - p[1] - v[1] / 2); - d.SetNumber(KeyStore.Page, this.props.Document.GetNumber(KeyStore.Page, 0)); - d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0)); - return d; - }), { x: p[0], y: p[1], panx: 0, pany: 0, width: v[0], height: v[1], title: "a nested collection" })); - // }, 100); - this.cleanupInteractions(); - } } @action onPointerWheel = (e: React.WheelEvent): void => { + this.props.select(false); e.stopPropagation(); e.preventDefault(); let coefficient = 1000; @@ -242,7 +197,6 @@ export class CollectionFreeFormView extends CollectionViewBase { @action private SetPan(panX: number, panY: number) { var x1 = this.getLocalTransform().inverse().Scale; - var x2 = this.getTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY)); this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX); @@ -259,24 +213,6 @@ export class CollectionFreeFormView extends CollectionViewBase { } @action - onKeyDown = (e: React.KeyboardEvent<Element>) => { - //if not these keys, make a textbox if preview cursor is active! - // if (!e.ctrlKey && !e.altKey) { - // if (this._previewCursorVisible) { - // //make textbox and add it to this collection - // let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY); - // let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" }); - // // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself - // this._selectOnLoaded = newBox.Id; - // //set text to be the typed key and get focus on text box - // this.props.CollectionView.addDocument(newBox); - // //remove cursor from screen - // this._previewCursorVisible = false; - // } - // } - } - - @action bringToFront(doc: Document) { const { fieldKey: fieldKey, Document: Document } = this.props; @@ -317,7 +253,7 @@ export class CollectionFreeFormView extends CollectionViewBase { @computed get views() { - var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); + var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); if (lvalue && lvalue != FieldWaiting) { return lvalue.Data.map(doc => { @@ -344,7 +280,7 @@ export class CollectionFreeFormView extends CollectionViewBase { get backgroundView() { return !this.backgroundLayout ? (null) : (<JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox }} bindings={this.props.bindings} jsx={this.backgroundLayout} showWarnings={true} @@ -355,7 +291,7 @@ export class CollectionFreeFormView extends CollectionViewBase { get overlayView() { return !this.overlayLayout ? (null) : (<JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox }} bindings={this.props.bindings} jsx={this.overlayLayout} showWarnings={true} @@ -364,28 +300,17 @@ export class CollectionFreeFormView extends CollectionViewBase { } getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()) + getMarqueeTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH) getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY); noScaling = () => 1; //when focus is lost, this will remove the preview cursor @action - onBlur = (e: React.FocusEvent<HTMLDivElement>): void => { - this._previewCursorVisible = false; + onBlur = (): void => { + this.PreviewCursorVisible = false; } render() { - //determines whether preview text cursor should be visible (ie when user taps this collection it should) - let cursor = null; - if (this._previewCursorVisible) { - //get local position and place cursor there! - let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); - cursor = <div id="prevCursor" onKeyPress={this.onKeyDown} style={{ color: "black", position: "absolute", transformOrigin: "left top", transform: `translate(${x}px, ${y}px)` }}>I</div> - } - - let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); - let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); - var marquee = this._marquee ? <div className="collectionfreeformview-marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }}></div> : (null); - let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); @@ -397,7 +322,6 @@ export class CollectionFreeFormView extends CollectionViewBase { return ( <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`} onPointerDown={this.onPointerDown} - onKeyPress={this.onKeyDown} onWheel={this.onPointerWheel} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} @@ -410,12 +334,14 @@ export class CollectionFreeFormView extends CollectionViewBase { ref={this._canvasRef}> {this.backgroundView} <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} /> - {cursor} + <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox} getTransform={this.getTransform} /> {this.views} - {marquee} </div> + <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} + addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} + getMarqueeTransform={this.getMarqueeTransform} getTransform={this.getTransform} /> {this.overlayView} </div> ); } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss new file mode 100644 index 000000000..0144625c1 --- /dev/null +++ b/src/client/views/collections/CollectionPDFView.scss @@ -0,0 +1,27 @@ +.collectionPdfView-buttonTray { + top : 25px; + left : 20px; + position: relative; + transform-origin: left top; + position: absolute; +} +.collectionPdfView-cont{ + width: 100%; + height: 100%; + position: absolute; + +} +.collectionPdfView-backward { + color : white; + top :0px; + left : 0px; + position: absolute; + background-color: rgba(50, 50, 50, 0.2); +} +.collectionPdfView-forward { + color : white; + top :0px; + left : 35px; + position: absolute; + background-color: rgba(50, 50, 50, 0.2); +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index f22c07060..124d82c8b 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,10 +1,11 @@ -import { action, computed } from "mobx"; +import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { KeyStore } from "../../../fields/KeyStore"; import { ContextMenu } from "../ContextMenu"; import { CollectionView, CollectionViewType } from "./CollectionView"; import { CollectionViewProps } from "./CollectionViewBase"; +import "./CollectionPDFView.scss" import React = require("react"); import { FieldId } from "../../../fields/Field"; @@ -18,22 +19,23 @@ export class CollectionPDFView extends React.Component<CollectionViewProps> { isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; } - public SelectedDocs: FieldId[] = [] - @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : 0; - @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : 0; + private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, -1); } + private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); } + @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : -1; + @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : -1; - @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 0); } - @computed private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); } - @computed private get uIButtons() { + private get uIButtons() { + let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().transformDirection(1, 1)[0]); return ( - <div className="pdfBox-buttonTray" key="tray"> - <button className="pdfButton" onClick={this.onPageBack}>{"<"}</button> - <button className="pdfButton" onClick={this.onPageForward}>{">"}</button> + <div className="collectionPdfView-buttonTray" key="tray" style={{ transform: `scale(${scaling}, ${scaling})` }}> + <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button> + <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button> </div>); } // "inherited" CollectionView API starts here... - + @observable + public SelectedDocs: FieldId[] = [] public active: () => boolean = () => CollectionView.Active(this); addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); } @@ -49,7 +51,7 @@ export class CollectionPDFView extends React.Component<CollectionViewProps> { get subView(): any { return CollectionView.SubView(this); } render() { - return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}> + return (<div className="collectionPdfView-cont" onContextMenu={this.specificContextMenu}> {this.subView} {this.props.isSelected() ? this.uIButtons : (null)} </div>) diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 88a3b73d4..0d615dc01 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,198 +1,162 @@ +@import "../global_variables"; + .collectionSchemaView-container { - border-style: solid; + border: 1px solid $intermediate-color; + border-radius: $border-radius; + box-sizing: border-box; + position: absolute; + width: 100%; + height: 100%; + overflow: hidden; + +.collectionSchemaView-content { + position: absolute; + height:100%; + width:100%; + overflow:auto; +} + .collectionSchemaView-previewRegion { + position: relative; + background: $light-color; + float: left; + height: 100%; + } + .collectionSchemaView-previewHandle { + position: absolute; + height: 37px; + width: 20px; + z-index: 20; + right: 0; + top: 0; + background: $main-accent; + } + .collectionSchemaView-dividerDragger { + position: relative; box-sizing: border-box; + border-left: 1px solid $intermediate-color; + border-right: 1px solid $intermediate-color; + float: left; + height: 100%; + } + .collectionSchemaView-tableContainer { + position: relative; + float: left; + height: 100%; + } + + .ReactTable { position: absolute; + // display: inline-block; + // overflow: auto; width: 100%; height: 100%; - .collectionSchemaView-previewRegion { - position: relative; - background: black; - float: left; - height: 100%; - } - .collectionSchemaView-previewHandle { - position: absolute; - height: 37px; - width: 20px; - z-index: 20; - right: 0; - top: 0; - background: Black ; - } - .collectionSchemaView-dividerDragger{ - position: relative; - background: black; - float: left; - height: 100%; - } - ::-webkit-scrollbar-thumb { - border-radius: 5px; - background-color: rgba(0, 0, 0, .5); - } - .collectionSchemaView-tableContainer { + background: $light-color; + box-sizing: border-box; + border: none !important; + .rt-table { + overflow-y: auto; + overflow-x: auto; + height: 100%; + display: -webkit-inline-box; + direction: ltr; + // direction:rtl; + // display:block; + } + .rt-tbody { + //direction: ltr; + direction: rtl; + } + .rt-tr-group { + direction: ltr; + max-height: 44px; + } + .rt-td { + border-width: 1px; + border-right-color: $intermediate-color; + .imageBox-cont { position: relative; - float: left; - height: 100%; - } - .ReactTable { - position: absolute; // display: inline-block; - // overflow: auto; - width: 100%; + max-height: 100%; + } + .imageBox-cont img { + object-fit: contain; + max-width: 100%; height: 100%; - background: white; - box-sizing: border-box; - .rt-table { - overflow-y: auto; - overflow-x: auto; - height: 100%; - display: -webkit-inline-box; - direction: ltr; // direction:rtl; - // display:block; - } - .rt-tbody { - //direction: ltr; - direction: rtl; - } - .rt-tr-group { - direction: ltr; - max-height: 44px; - } - .rt-td { - border-width: 1; - border-right-color: #aaa; - .imageBox-cont { - position: relative; - max-height: 100%; - } - .imageBox-cont img { - object-fit: contain; - max-width: 100%; - height: 100% - } - } - .rt-tr-group { - border-width: 1; - border-bottom-color: #aaa - } - } - .ReactTable .rt-table { - overflow-y: auto; - overflow-x: auto; + } + .videobox-cont { + object-fit: contain; + width:auto; height: 100%; - display: -webkit-inline-box; - direction: ltr; // direction:rtl; - // display:block; - } - .ReactTable .rt-tbody { - //direction: ltr; - direction: rtl; - } - .ReactTable .rt-tr-group { - direction: ltr; - } - .ReactTable .rt-thead.-header { - background:grey; - } - .ReactTable .rt-th, .ReactTable .rt-td { - max-height: 44; - padding: 3px 7px; - } - .ReactTable .rt-tbody .rt-tr-group:last-child { - border-bottom: grey; - border-bottom-style: solid; - border-bottom-width: 1; - } - .documentView-node:first-child { - background: grey; - .imageBox-cont img { - object-fit: contain; - } - } + } + } + } + .ReactTable .rt-thead.-header { + background: $intermediate-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 12px; + height: 30px; + padding-top: 4px; + } + .ReactTable .rt-th, + .ReactTable .rt-td { + max-height: 44; + padding: 3px 7px; + font-size: 13px; + text-align: center; + } + .ReactTable .rt-tbody .rt-tr-group:last-child { + border-bottom: $intermediate-color; + border-bottom-style: solid; + border-bottom-width: 1; + } + .documentView-node:first-child { + background: $light-color; + .imageBox-cont img { + object-fit: contain; + } + } } .Resizer { - box-sizing: border-box; - background: #000; - opacity: 0.5; - z-index: 1; - background-clip: padding-box; - &.horizontal { - height: 11px; - margin: -5px 0; - border-top: 5px solid rgba(255, 255, 255, 0); - border-bottom: 5px solid rgba(255, 255, 255, 0); - cursor: row-resize; - width: 100%; - &:hover { - border-top: 5px solid rgba(0, 0, 0, 0.5); - border-bottom: 5px solid rgba(0, 0, 0, 0.5); - } - } - &.vertical { - width: 11px; - margin: 0 -5px; - border-left: 5px solid rgba(255, 255, 255, 0); - border-right: 5px solid rgba(255, 255, 255, 0); - cursor: col-resize; - &:hover { - border-left: 5px solid rgba(0, 0, 0, 0.5); - border-right: 5px solid rgba(0, 0, 0, 0.5); - } - } + box-sizing: border-box; + background: #000; + opacity: 0.5; + z-index: 1; + background-clip: padding-box; + &.horizontal { + height: 11px; + margin: -5px 0; + border-top: 5px solid rgba(255, 255, 255, 0); + border-bottom: 5px solid rgba(255, 255, 255, 0); + cursor: row-resize; + width: 100%; &:hover { - -webkit-transition: all 2s ease; - transition: all 2s ease; - } + border-top: 5px solid rgba(0, 0, 0, 0.5); + border-bottom: 5px solid rgba(0, 0, 0, 0.5); + } + } + &.vertical { + width: 11px; + margin: 0 -5px; + border-left: 5px solid rgba(255, 255, 255, 0); + border-right: 5px solid rgba(255, 255, 255, 0); + cursor: col-resize; + &:hover { + border-left: 5px solid rgba(0, 0, 0, 0.5); + border-right: 5px solid rgba(0, 0, 0, 0.5); + } + } + &:hover { + -webkit-transition: all 2s ease; + transition: all 2s ease; + } } .vertical { - section { - width: 100vh; - height: 100vh; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: vertical; - -webkit-box-direction: normal; - -webkit-flex-direction: column; - -ms-flex-direction: column; - flex-direction: column; - } - header { - padding: 1rem; - background: #eee; - } - footer { - padding: 1rem; - background: #eee; - } -} - -.horizontal { - section { - width: 100vh; - height: 100vh; - display: flex; - flex-direction: column; - } - header { - padding: 1rem; - background: #eee; - } - footer { - padding: 1rem; - background: #eee; - } -} - -.parent { - width: 100%; - height: 100%; - -webkit-box-flex: 1; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; + section { + width: 100vh; + height: 100vh; display: -webkit-box; display: -webkit-flex; display: -ms-flexbox; @@ -202,19 +166,71 @@ -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; + } + header { + padding: 1rem; + background: #eee; + } + footer { + padding: 1rem; + background: #eee; + } +} + +.horizontal { + section { + width: 100vh; + height: 100vh; + display: flex; + flex-direction: column; + } + header { + padding: 1rem; + background: #eee; + } + footer { + padding: 1rem; + background: #eee; + } +} + +.parent { + width: 100%; + height: 100%; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; } .header { - background: #aaa; - height: 3rem; - line-height: 3rem; + background: #aaa; + height: 3rem; + line-height: 3rem; } .wrapper { - background: #ffa; - margin: 5rem; - -webkit-box-flex: 1; - -webkit-flex: 1; - -ms-flex: 1; - flex: 1; -}
\ No newline at end of file + background: #ffa; + margin: 5rem; + -webkit-box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} + +.-even { + background: $light-color !important; +} + +.-odd { + background: $light-color-secondary !important; +} diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 17eeaa5a2..957fd0fca 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,5 +1,5 @@ import React = require("react") -import { action, observable } from "mobx"; +import { action, observable, computed } from "mobx"; import { observer } from "mobx-react"; import Measure from "react-measure"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; @@ -24,14 +24,14 @@ import { setupDrag } from "../../util/DragManager"; @observer export class CollectionSchemaView extends CollectionViewBase { private _mainCont = React.createRef<HTMLDivElement>(); - private DIVIDER_WIDTH = 5; + private DIVIDER_WIDTH = 4; @observable _contentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ContentScaling prop of the DocumentView @observable _dividerX = 0; @observable _panelWidth = 0; @observable _panelHeight = 0; @observable _selectedIndex = 0; - @observable _splitPercentage: number = 50; + @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0); } renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { @@ -49,7 +49,7 @@ export class CollectionSchemaView extends CollectionViewBase { let reference = React.createRef<HTMLDivElement>(); let onItemDown = setupDrag(reference, () => props.doc); return ( - <div onPointerDown={onItemDown} key={props.doc.Id} ref={reference}> + <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "36px" }} key={props.doc.Id} ref={reference}> <EditableView display={"inline"} contents={contents} @@ -91,7 +91,8 @@ export class CollectionSchemaView extends CollectionViewBase { return { onClick: action((e: React.MouseEvent, handleOriginal: Function) => { that._selectedIndex = rowInfo.index; - this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize + // bcz - ugh - needed to force Measure to do its thing and call onResize + this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage - 0.05) if (handleOriginal) { handleOriginal() @@ -108,18 +109,18 @@ export class CollectionSchemaView extends CollectionViewBase { @action onDividerMove = (e: PointerEvent): void => { let nativeWidth = this._mainCont.current!.getBoundingClientRect(); - this._splitPercentage = Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100); + this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)); } @action onDividerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onDividerMove); document.removeEventListener('pointerup', this.onDividerUp); - if (this._startSplitPercent == this._splitPercentage) { - this._splitPercentage = this._splitPercentage == 1 ? 66 : 100; + if (this._startSplitPercent == this.splitPercentage) { + this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage == 0 ? 33 : 0); } } onDividerDown = (e: React.PointerEvent) => { - this._startSplitPercent = this._splitPercentage; + this._startSplitPercent = this.splitPercentage; e.stopPropagation(); e.preventDefault(); document.addEventListener("pointermove", this.onDividerMove); @@ -136,12 +137,12 @@ export class CollectionSchemaView extends CollectionViewBase { e.preventDefault(); document.removeEventListener("pointermove", this.onExpanderMove); document.removeEventListener('pointerup', this.onExpanderUp); - if (this._startSplitPercent == this._splitPercentage) { - this._splitPercentage = this._splitPercentage == 100 ? 66 : 100; + if (this._startSplitPercent == this.splitPercentage) { + this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage == 0 ? 33 : 0); } } onExpanderDown = (e: React.PointerEvent) => { - this._startSplitPercent = this._splitPercentage; + this._startSplitPercent = this.splitPercentage; e.stopPropagation(); e.preventDefault(); document.addEventListener("pointermove", this.onExpanderMove); @@ -205,7 +206,7 @@ export class CollectionSchemaView extends CollectionViewBase { ) let previewHandle = !this.props.active() ? (null) : ( <div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />); - let dividerDragger = this._splitPercentage == 100 ? (null) : + let dividerDragger = this.splitPercentage == 0 ? (null) : <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} /> return ( <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > @@ -215,7 +216,8 @@ export class CollectionSchemaView extends CollectionViewBase { this._panelHeight = r.entry.height; })}> {({ measureRef }) => - <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ width: `${this._splitPercentage}%` }}> + <div ref={measureRef} className="collectionSchemaView-tableContainer" + style={{ width: `calc(100% - ${this.splitPercentage}%)` }}> <ReactTable data={children} pageSize={children.length} @@ -237,7 +239,7 @@ export class CollectionSchemaView extends CollectionViewBase { } </Measure> {dividerDragger} - <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}> + <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0)}% - ${this.DIVIDER_WIDTH}px)` }}> {content} </div> {previewHandle} diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index f8d580a7b..fa0f1c761 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,16 +1,25 @@ +@import "../global_variables"; #body { padding: 20px; - background: #bbbbbb; + background: $light-color-secondary; + font-size: 13px; + overflow: scroll; } ul { list-style: none; + padding-left: 20px; } li { margin: 5px 0; } +.collection-child { + margin-top: 10px; + margin-bottom: 10px; +} + .no-indent { padding-left: 0; } @@ -18,10 +27,17 @@ li { .bullet { width: 1.5em; display: inline-block; + color: $intermediate-color; +} + +.coll-title { + font-size: 24px; + margin-bottom: 20px; } .collectionTreeView-dropTarget { - border-style: solid; + border: 0px solid transparent; + border-radius: $border-radius; box-sizing: border-box; height: 100%; } @@ -30,8 +46,16 @@ li { display: inline-table; } +.docContainer:hover { + .delete-button { + display: inline; + } +} + .delete-button { - color: #999999; + color: $intermediate-color; float: right; - margin-left: 1em; + margin-left: 15px; + margin-top: 3px; + display: none; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 9c31bdae2..f9da759fd 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -12,6 +12,10 @@ import { setupDrag } from "../../util/DragManager"; import { FieldWaiting } from "../../../fields/Field"; import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTrashAlt, faCaretRight, faCaretDown } from '@fortawesome/free-solid-svg-icons'; + export interface TreeViewProps { document: Document; deleteDoc: (doc: Document) => void; @@ -23,6 +27,10 @@ export enum BulletType { List } +library.add(faTrashAlt); +library.add(faCaretDown); +library.add(faCaretRight); + @observer /** * Component that takes in a document prop and a boolean whether it's collapsed or not. @@ -50,11 +58,11 @@ class TreeView extends React.Component<TreeViewProps> { switch (type) { case BulletType.Collapsed: - return <div className="bullet" onClick={onClicked}>▶</div> + return <div className="bullet" onClick={onClicked}><FontAwesomeIcon icon="caret-right" /></div> case BulletType.Collapsible: - return <div className="bullet" onClick={onClicked}>▼</div> + return <div className="bullet" onClick={onClicked}><FontAwesomeIcon icon="caret-down" /></div> case BulletType.List: - return <div className="bullet">—</div> + return <div className="bullet"></div> } } @@ -81,7 +89,7 @@ class TreeView extends React.Component<TreeViewProps> { this.props.document.SetData(KeyStore.Title, value, TextField); return true; }} /> - <div className="delete-button" onClick={this.delete}>x</div> + <div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div> </div > } @@ -103,7 +111,7 @@ class TreeView extends React.Component<TreeViewProps> { <TreeView document={value} deleteDoc={this.remove} />) ) subView = - <li key={this.props.document.Id} > + <li className="collection-child" key={this.props.document.Id} > {this.renderBullet(BulletType.Collapsible)} {titleElement} <ul key={this.props.document.Id}> @@ -111,7 +119,7 @@ class TreeView extends React.Component<TreeViewProps> { </ul> </li> } else { - subView = <li key={this.props.document.Id}> + subView = <li className="collection-child" key={this.props.document.Id}> {this.renderBullet(BulletType.Collapsed)} {titleElement} </li> @@ -159,7 +167,7 @@ export class CollectionTreeView extends CollectionViewBase { return ( <div id="body" className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}> - <h3> + <div className="coll-title"> <EditableView contents={titleStr} display={"inline"} height={72} GetValue={() => { @@ -168,7 +176,8 @@ export class CollectionTreeView extends CollectionViewBase { this.props.Document.SetData(KeyStore.Title, value, TextField); return true; }} /> - </h3> + </div> + <hr /> <ul className="no-indent"> {childrenElement} </ul> diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss new file mode 100644 index 000000000..cbb981b13 --- /dev/null +++ b/src/client/views/collections/CollectionVideoView.scss @@ -0,0 +1,40 @@ + +.collectionVideoView-cont{ + width: 100%; + height: 100%; + position: absolute; + +} +.collectionVideoView-time{ + color : white; + top :25px; + left : 25px; + position: absolute; + background-color: rgba(50, 50, 50, 0.2); + transform-origin: left top; +} +.collectionVideoView-play { + width: 25px; + height: 20px; + bottom: 25px; + left : 25px; + position: absolute; + color : white; + background-color: rgba(50, 50, 50, 0.2); + border-radius: 4px; + text-align: center; + transform-origin: left bottom; +} +.collectionVideoView-full { + width: 25px; + height: 20px; + bottom: 25px; + right : 25px; + position: absolute; + color : white; + background-color: rgba(50, 50, 50, 0.2); + border-radius: 4px; + text-align: center; + transform-origin: right bottom; + +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx new file mode 100644 index 000000000..b64ef3c07 --- /dev/null +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -0,0 +1,118 @@ +import { action, computed, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ContextMenu } from "../ContextMenu"; +import { CollectionView, CollectionViewType } from "./CollectionView"; +import { CollectionViewProps } from "./CollectionViewBase"; +import React = require("react"); +import { FieldId } from "../../../fields/Field"; +import "./CollectionVideoView.scss" + + +@observer +export class CollectionVideoView extends React.Component<CollectionViewProps> { + + public static LayoutString(fieldKey: string = "DataKey") { + return `<${CollectionVideoView.name} Document={Document} + ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} + isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; + } + + private _mainCont = React.createRef<HTMLDivElement>(); + + private get uIButtons() { + let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().transformDirection(1, 1)[0]); + return ([ + <div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + <span>{"" + Math.round(this.ctime)}</span> + <span style={{ fontSize: 8 }}>{" " + Math.round((this.ctime - Math.trunc(this.ctime)) * 100)}</span> + </div>, + <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + {this.playing ? "\"" : ">"} + </div>, + <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> + F + </div> + ]); + } + + + // "inherited" CollectionView API starts here... + + @observable + public SelectedDocs: FieldId[] = [] + public active: () => boolean = () => CollectionView.Active(this); + + addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); } + removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); } + + specificContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "VideoOptions", event: () => { } }); + } + } + + get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; } + get subView(): any { return CollectionView.SubView(this); } + + componentDidMount() { + this.updateTimecode(); + } + + get player(): HTMLVideoElement | undefined { + return this._mainCont.current ? this._mainCont.current.getElementsByTagName("video")[0] : undefined; + } + + @action + updateTimecode = () => { + if (this.player) { + this.ctime = this.player.currentTime; + this.props.Document.SetNumber(KeyStore.CurPage, Math.round(this.ctime)); + } + setTimeout(() => this.updateTimecode(), 100) + } + + + @observable + ctime: number = 0 + @observable + playing: boolean = false; + + @action + onPlayDown = () => { + if (this.player) { + if (this.player.paused) { + this.player.play(); + this.playing = true; + } else { + this.player.pause(); + this.playing = false; + } + } + } + @action + onFullDown = (e: React.PointerEvent) => { + if (this.player) { + this.player.requestFullscreen(); + e.stopPropagation(); + e.preventDefault(); + } + } + + @action + onResetDown = () => { + if (this.player) { + this.player.pause(); + this.player.currentTime = 0; + } + + } + + render() { + return (<div className="collectionVideoView-cont" ref={this._mainCont} onContextMenu={this.specificContextMenu}> + {this.subView} + {this.uIButtons} + </div>) + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 17a0fbd23..d9b2722a6 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -22,20 +22,19 @@ export enum CollectionViewType { Tree } -export const COLLECTION_BORDER_WIDTH = 2; +export const COLLECTION_BORDER_WIDTH = 1; @observer export class CollectionView extends React.Component<CollectionViewProps> { - @observable - public SelectedDocs: FieldId[] = []; - public static LayoutString(fieldKey: string = "DataKey") { return `<${CollectionView.name} Document={Document} ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; } + @observable + public SelectedDocs: FieldId[] = []; public active: () => boolean = () => CollectionView.Active(this); addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); } removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); } @@ -50,7 +49,7 @@ export class CollectionView extends React.Component<CollectionViewProps> { @action public static AddDocument(props: CollectionViewProps, doc: Document) { - doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, 0)); + doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, -1)); if (props.Document.Get(props.fieldKey) instanceof Field) { //TODO This won't create the field if it doesn't already exist const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()) diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index ddde05011..4a2761139 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -48,6 +48,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> protected drop(e: Event, de: DragManager.DropEvent) { const docView: DocumentView = de.data["documentView"]; const doc: Document = de.data["document"]; + if (docView && (!docView.props.ContainingCollectionView || docView.props.ContainingCollectionView !== this.props.CollectionView)) { if (docView.props.RemoveDocument) { docView.props.RemoveDocument(docView.props.Document); @@ -62,12 +63,17 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> @action protected onDrop(e: React.DragEvent, options: DocumentOptions): void { - e.stopPropagation() - e.preventDefault() let that = this; let html = e.dataTransfer.getData("text/html"); let text = e.dataTransfer.getData("text/plain"); + + if (text && text.startsWith("<div")) { + return; + } + e.stopPropagation() + e.preventDefault() + if (html && html.indexOf("<img") != 0) { console.log("not good"); let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 }); @@ -82,21 +88,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> const upload = window.location.origin + RouteStore.upload; let item = e.dataTransfer.items[i]; if (item.kind === "string" && item.type.indexOf("uri") != -1) { - e.dataTransfer.items[i].getAsString(function (s) { - action(() => { - var img = Documents.ImageDocument(s, { ...options, nativeWidth: 300, width: 300, }) - - let docs = that.props.Document.GetT(KeyStore.Data, ListField); - if (docs != FieldWaiting) { - if (!docs) { - docs = new ListField<Document>(); - that.props.Document.Set(KeyStore.Data, docs) - } - docs.Data.push(img); - } - })() - - }) + e.dataTransfer.items[i].getAsString(action((s: string) => this.props.addDocument(Documents.WebDocument(s, options)))) } let type = item.type console.log(type) @@ -123,7 +115,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> var doc: any; if (type.indexOf("image") !== -1) { - doc = Documents.ImageDocument(path, { ...options, nativeWidth: 300, width: 300, }) + doc = Documents.ImageDocument(path, { ...options, nativeWidth: 200, width: 200, }) } if (type.indexOf("video") !== -1) { doc = Documents.VideoDocument(path, { ...options, nativeWidth: 300, width: 300, }) diff --git a/src/client/views/collections/MarqueeView.scss b/src/client/views/collections/MarqueeView.scss new file mode 100644 index 000000000..6d9a79344 --- /dev/null +++ b/src/client/views/collections/MarqueeView.scss @@ -0,0 +1,8 @@ + +.marqueeView { + border-style: dashed; + box-sizing: border-box; + position: absolute; + border-width: 1px; + border-color: black; +}
\ No newline at end of file diff --git a/src/client/views/collections/MarqueeView.tsx b/src/client/views/collections/MarqueeView.tsx new file mode 100644 index 000000000..65aaa837f --- /dev/null +++ b/src/client/views/collections/MarqueeView.tsx @@ -0,0 +1,164 @@ +import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { FieldWaiting, Opt } from "../../../fields/Field"; +import { KeyStore } from "../../../fields/KeyStore"; +import { Documents } from "../../documents/Documents"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; +import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import "./MarqueeView.scss"; +import React = require("react"); +import { InkField, StrokeData } from "../../../fields/InkField"; +import { Utils } from "../../../Utils"; +import { InkingCanvas } from "../InkingCanvas"; + +interface MarqueeViewProps { + getMarqueeTransform: () => Transform; + getTransform: () => Transform; + container: CollectionFreeFormView; + addDocument: (doc: Document) => void; + activeDocuments: () => Document[]; + selectDocuments: (docs: Document[]) => void; + removeDocument: (doc: Document) => boolean; +} + +@observer +export class MarqueeView extends React.Component<MarqueeViewProps> +{ + private _reactionDisposer: Opt<IReactionDisposer>; + + @observable _lastX: number = 0; + @observable _lastY: number = 0; + @observable _downX: number = 0; + @observable _downY: number = 0; + + componentDidMount() { + this._reactionDisposer = reaction( + () => this.props.container.MarqueeVisible, + (visible: boolean) => this.onPointerDown(visible, this.props.container.DownX, this.props.container.DownY)) + } + componentWillUnmount() { + if (this._reactionDisposer) { + this._reactionDisposer(); + } + this.cleanupInteractions(); + } + + @action + cleanupInteractions = () => { + document.removeEventListener("pointermove", this.onPointerMove, true) + document.removeEventListener("pointerup", this.onPointerUp, true); + document.removeEventListener("keydown", this.marqueeCommand, true); + } + + @action + onPointerDown = (visible: boolean, downX: number, downY: number): void => { + if (visible) { + this._downX = this._lastX = downX; + this._downY = this._lastY = downY; + document.addEventListener("pointermove", this.onPointerMove, true) + document.addEventListener("pointerup", this.onPointerUp, true); + document.addEventListener("keydown", this.marqueeCommand, true); + } + } + + @action + onPointerMove = (e: PointerEvent): void => { + this._lastX = e.pageX; + this._lastY = e.pageY; + } + + @action + onPointerUp = (e: PointerEvent): void => { + this.cleanupInteractions(); + if (!e.shiftKey) { + SelectionManager.DeselectAll(); + } + this.props.selectDocuments(this.marqueeSelect()); + } + + intersectRect(r1: { left: number, top: number, width: number, height: number }, + r2: { left: number, top: number, width: number, height: number }) { + return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); + } + + get Bounds() { + let left = this._downX < this._lastX ? this._downX : this._lastX; + let top = this._downY < this._lastY ? this._downY : this._lastY; + let topLeft = this.props.getTransform().transformPoint(left, top); + let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) } + } + + @action + marqueeCommand = (e: KeyboardEvent) => { + if (e.key == "Backspace" || e.key == "Delete") { + this.marqueeSelect().map(d => this.props.removeDocument(d)); + this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField); + this.cleanupInteractions(); + } + if (e.key == "c") { + let bounds = this.Bounds; + let selected = this.marqueeSelect().map(d => { + this.props.removeDocument(d); + d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2); + d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2); + d.SetNumber(KeyStore.Page, 0); + d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0)); + return d; + }); + let liftedInk = this.marqueeInkSelect(true); + this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField); + //setTimeout(() => { + this.props.addDocument(Documents.FreeformDocument(selected, { x: bounds.left, y: bounds.top, panx: 0, pany: 0, width: bounds.width, backgroundColor: "Transparent", height: bounds.height, ink: liftedInk, title: "a nested collection" })); + // }, 100); + this.cleanupInteractions(); + } + } + marqueeInkSelect(select: boolean) { + let selRect = this.Bounds; + let centerShiftX = 0 - (selRect.left + selRect.width / 2); // moves each point by the offset that shifts the selection's center to the origin. + let centerShiftY = 0 - (selRect.top + selRect.height / 2); + let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); + if (ink && ink != FieldWaiting) { + let idata = new Map(); + ink.Data.forEach((value: StrokeData, key: string, map: any) => { + let inside = InkingCanvas.IntersectStrokeRect(value, selRect); + if (inside && select) { + idata.set(key, + { + pathData: value.pathData.map(val => { return { x: val.x + centerShiftX, y: val.y + centerShiftY } }), + color: value.color, + width: value.width, + tool: value.tool, + page: -1 + }); + } else if (!inside && !select) { + idata.set(key, value); + } + }) + return idata; + } + } + + marqueeSelect() { + let selRect = this.Bounds; + let selection: Document[] = []; + this.props.activeDocuments().map(doc => { + var x = doc.GetNumber(KeyStore.X, 0); + var y = doc.GetNumber(KeyStore.Y, 0); + var w = doc.GetNumber(KeyStore.Width, 0); + var h = doc.GetNumber(KeyStore.Height, 0); + if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) + selection.push(doc) + }) + return selection; + } + + render() { + let p = this.props.getMarqueeTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY); + let v = this.props.getMarqueeTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); + return (!this.props.container.MarqueeVisible ? (null) : <div className="marqueeView" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/PreviewCursor.scss b/src/client/views/collections/PreviewCursor.scss new file mode 100644 index 000000000..a797411f6 --- /dev/null +++ b/src/client/views/collections/PreviewCursor.scss @@ -0,0 +1,18 @@ + +.previewCursor { + color: black; + position: absolute; + transform-origin: left top; + pointer-events: none; +} + +//this is an animation for the blinking cursor! +@keyframes blink { + 0% {opacity: 0} + 49%{opacity: 0} + 50% {opacity: 1} +} + +#previewCursor { + animation: blink 1s infinite; +}
\ No newline at end of file diff --git a/src/client/views/collections/PreviewCursor.tsx b/src/client/views/collections/PreviewCursor.tsx new file mode 100644 index 000000000..a1411250a --- /dev/null +++ b/src/client/views/collections/PreviewCursor.tsx @@ -0,0 +1,78 @@ +import { trace } from "mobx"; +import "./PreviewCursor.scss"; +import React = require("react"); +import { action, IReactionDisposer, observable, reaction } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { FieldWaiting, Opt } from "../../../fields/Field"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ListField } from "../../../fields/ListField"; +import { Documents } from "../../documents/Documents"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; +import { CollectionFreeFormView } from "./CollectionFreeFormView"; + + +export interface PreviewCursorProps { + getTransform: () => Transform; + container: CollectionFreeFormView; + addLiveTextDocument: (doc: Document) => void; +} + +@observer +export class PreviewCursor extends React.Component<PreviewCursorProps> { + private _reactionDisposer: Opt<IReactionDisposer>; + + @observable _lastX: number = 0; + @observable _lastY: number = 0; + + componentDidMount() { + this._reactionDisposer = reaction( + () => this.props.container.PreviewCursorVisible, + (visible: boolean) => this.onCursorPlaced(visible, this.props.container.DownX, this.props.container.DownY)) + } + componentWillUnmount() { + if (this._reactionDisposer) { + this._reactionDisposer(); + } + this.cleanupInteractions(); + } + + + @action + cleanupInteractions = () => { + document.removeEventListener("keypress", this.onKeyPress, true); + } + + @action + onCursorPlaced = (visible: boolean, downX: number, downY: number): void => { + if (visible) { + document.addEventListener("keypress", this.onKeyPress, true); + this._lastX = downX; + this._lastY = downY; + } else + this.cleanupInteractions(); + } + + @action + onKeyPress = (e: KeyboardEvent) => { + //if not these keys, make a textbox if preview cursor is active! + if (!e.ctrlKey && !e.altKey && !e.defaultPrevented) { + //make textbox and add it to this collection + let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY); + let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" }); + this.props.addLiveTextDocument(newBox); + e.stopPropagation(); + e.preventDefault(); + } + } + + render() { + //get local position and place cursor there! + let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY); + return ( + !this.props.container.PreviewCursorVisible ? (null) : + <div className="previewCursor" id="previewCursor" style={{ transform: `translate(${x}px, ${y}px)` }}>I</div>) + + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index f7d89843d..6daf15f5f 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -18,7 +18,7 @@ export class AudioBox extends React.Component<FieldViewProps> { super(props); } - + componentDidMount() { } @@ -26,16 +26,16 @@ export class AudioBox extends React.Component<FieldViewProps> { componentWillUnmount() { } - + render() { let field = this.props.doc.Get(this.props.fieldKey) - let path = field == FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3": + let path = field == FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3" : field instanceof AudioField ? field.Data.href : "http://techslides.com/demos/samples/sample.mp3"; - + return ( <div> - <audio controls className = "audiobox-cont"> - <source src = {path} type="audio/mpeg"/> + <audio controls className="audiobox-cont"> + <source src={path} type="audio/mpeg" /> Not supported. </audio> </div> diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx new file mode 100644 index 000000000..55b4938a0 --- /dev/null +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -0,0 +1,35 @@ +import { Document } from "../../../fields/Document"; +import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionSchemaView } from "../collections/CollectionSchemaView"; +import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageBox } from "../nodes/ImageBox"; +import { VideoBox } from "../nodes/VideoBox"; +import { AudioBox } from "../nodes/AudioBox"; +import { KeyValueBox } from "./KeyValueBox" +import { WebBox } from "../nodes/WebBox"; +import { PDFBox } from "../nodes/PDFBox"; +import "./DocumentView.scss"; +import React = require("react"); +const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? + +interface JsxBindings { + Document: Document; + layout: string; + [prop: string]: any; +} + +export class DocumentContentsView extends React.PureComponent<JsxBindings> { + render() { + return <JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox }} + bindings={this.props} + jsx={this.props.layout} + showWarnings={true} + onError={(test: any) => { console.log(test) }} + /> + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index ab913897b..85a115f1c 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,23 +1,23 @@ +@import "../global_variables"; .documentView-node { - position: absolute; - background: #cdcdcd; - //overflow: hidden; - &.minimized { - width: 30px; - height: 30px; - } - .top { - background: #232323; - height: 20px; - cursor: pointer; - } - .content { - padding: 20px 20px; - height: auto; - box-sizing: border-box; - } - .scroll-box { - overflow-y: scroll; - height: calc(100% - 20px); - } -}
\ No newline at end of file + position: absolute; + background: $light-color; //overflow: hidden; + &.minimized { + width: 30px; + height: 30px; + } + .top { + background: #232323; + height: 20px; + cursor: pointer; + } + .content { + padding: 20px 20px; + height: auto; + box-sizing: border-box; + } + .scroll-box { + overflow-y: scroll; + height: calc(100% - 20px); + } +} diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index da40f0dc1..7a43c34d0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,31 +1,23 @@ -import { action, computed, IReactionDisposer, runInAction, reaction } from "mobx"; +import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; import { Field, FieldWaiting, Opt } from "../../../fields/Field"; import { Key } from "../../../fields/Key"; import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; +import { TextField } from "../../../fields/TextField"; +import { Documents } from "../../documents/Documents"; +import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; -import { CollectionSchemaView } from "../collections/CollectionSchemaView"; import { CollectionView, CollectionViewType } from "../collections/CollectionView"; -import { CollectionPDFView } from "../collections/CollectionPDFView"; import { ContextMenu } from "../ContextMenu"; -import { FormattedTextBox } from "../nodes/FormattedTextBox"; -import { ImageBox } from "../nodes/ImageBox"; -import { VideoBox } from "../nodes/VideoBox"; -import { AudioBox } from "../nodes/AudioBox"; -import { Documents } from "../../documents/Documents" -import { KeyValueBox } from "./KeyValueBox" -import { WebBox } from "../nodes/WebBox"; -import { PDFBox } from "../nodes/PDFBox"; import "./DocumentView.scss"; import React = require("react"); -import { TextField } from "../../../fields/TextField"; -import { DocumentManager } from "../../util/DocumentManager"; +import { DocumentContentsView } from "./DocumentContentsView"; +import { Utils } from "../../../Utils"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -87,7 +79,6 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { @observer export class DocumentView extends React.Component<DocumentViewProps> { private _mainCont = React.createRef<HTMLDivElement>(); - private _documentBindings: any = null; private _downX: number = 0; private _downY: number = 0; private _reactionDisposer: Opt<IReactionDisposer>; @@ -197,6 +188,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { SelectionManager.SelectDoc(this, e.ctrlKey); } } + stopPropogation = (e: React.SyntheticEvent) => { + e.stopPropagation(); + } deleteClicked = (): void => { if (this.props.RemoveDocument) { @@ -246,11 +240,23 @@ export class DocumentView extends React.Component<DocumentViewProps> { destDoc.GetOrCreateAsync(KeyStore.LinkedFromDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) }); linkDoc.Set(KeyStore.LinkedFromDocs, sourceDoc); - - e.stopPropagation(); } + onDrop = (e: React.DragEvent) => { + if (e.isDefaultPrevented()) { + return; + } + let text = e.dataTransfer.getData("text/plain"); + if (text && text.startsWith("<div")) { + let oldLayout = this.props.Document.GetText(KeyStore.Layout, ""); + let layout = text.replace("{layout}", oldLayout); + this.props.Document.SetText(KeyStore.Layout, layout); + e.stopPropagation(); + e.preventDefault(); + } + } + @action onContextMenu = (e: React.MouseEvent): void => { e.stopPropagation(); @@ -265,6 +271,12 @@ export class DocumentView extends React.Component<DocumentViewProps> { ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked }) ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) }) ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }) + ContextMenu.Instance.addItem({ + description: "Copy ID", + event: () => { + Utils.CopyText(this.props.Document.Id); + } + }); //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) if (!this.topMost) { @@ -277,15 +289,6 @@ export class DocumentView extends React.Component<DocumentViewProps> { SelectionManager.SelectDoc(this, e.ctrlKey); } - get mainContent() { - return <JsxParser - components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, VideoBox, AudioBox, PDFBox }} - bindings={this._documentBindings} - jsx={this.layout} - showWarnings={true} - onError={(test: any) => { console.log(test) }} - /> - } isSelected = () => { return SelectionManager.IsSelected(this); @@ -295,40 +298,52 @@ export class DocumentView extends React.Component<DocumentViewProps> { SelectionManager.SelectDoc(this, ctrlPressed) } - render() { - if (!this.props.Document) return <div></div> - let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); - if (!lkeys || lkeys === "<Waiting>") { - return <p>Error loading layout keys</p>; - } - this._documentBindings = { + @computed + get getProps() { + let bindings: any = { ...this.props, isSelected: this.isSelected, select: this.select, - focus: this.props.focus + layout: this.layout }; for (const key of this.layoutKeys) { - this._documentBindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data + bindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data } for (const key of this.layoutFields) { let field = this.props.Document.Get(key); - this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; + bindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; + } + bindings.bindings = bindings; + + return bindings + } + + render() { + if (!this.props.Document) { + return (null); + } + let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); + if (!lkeys || lkeys === "<Waiting>") { + return <p>Error loading layout keys</p>; } - this._documentBindings.bindings = this._documentBindings; var scaling = this.props.ContentScaling(); var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); + var backgroundcolor = this.props.Document.GetText(KeyStore.BackgroundColor, ""); return ( <div className="documentView-node" ref={this._mainCont} style={{ + background: backgroundcolor, width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%", height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%", transformOrigin: "left top", transform: `scale(${scaling} , ${scaling})` }} + onDrop={this.onDrop} onContextMenu={this.onContextMenu} - onPointerDown={this.onPointerDown} > - {this.mainContent} + onPointerDown={this.onPointerDown} + onPointerUp={this.stopPropogation} > + <DocumentContentsView {...this.getProps} /> </div> ) } diff --git a/src/client/views/nodes/FieldTextBox.scss b/src/client/views/nodes/FieldTextBox.scss index b6ce2fabc..d2cd61b0d 100644 --- a/src/client/views/nodes/FieldTextBox.scss +++ b/src/client/views/nodes/FieldTextBox.scss @@ -1,14 +1,14 @@ .ProseMirror { - margin-top: -1em; - width: 100%; - height: 100%; + margin-top: -1em; + width: 100%; + height: 100%; } .ProseMirror:focus { - outline: none !important + outline: none !important; } .fieldTextBox-cont { - background: white; - padding: 1vw; -}
\ No newline at end of file + background: white; + padding: 1vw; +} diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index ab5849f09..32da2632e 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -1,38 +1,46 @@ +@import "../global_variables"; .ProseMirror { - width: 100%; - height: auto; - min-height: 100% + width: 100%; + height: auto; + min-height: 100%; + font-family: $serif; } .ProseMirror:focus { - outline: none !important + outline: none !important; } .formattedTextBox-cont { - background: white; - padding: 1; - border-width: 1px; - border-radius: 2px; - border-color:black; - box-sizing: border-box; - background: white; - border-style:solid; - overflow-y: scroll; - overflow-x: hidden; - color: initial; - height: 100%; + background: $light-color-secondary; + padding: 0.9em; + border-width: 0px; + border-radius: $border-radius; + border-color: $intermediate-color; + box-sizing: border-box; + border-style: solid; + overflow-y: scroll; + overflow-x: hidden; + color: initial; + height: 100%; } .menuicon { - display: inline-block; - border-right: 1px solid rgba(0, 0, 0, 0.2); - color: #888; - line-height: 1; - padding: 0 7px; - margin: 1px; - cursor: pointer; - text-align: center; - min-width: 1.4em; - } - .strong, .heading { font-weight: bold; } - .em { font-style: italic; }
\ No newline at end of file + display: inline-block; + border-right: 1px solid rgba(0, 0, 0, 0.2); + color: #888; + line-height: 1; + padding: 0 7px; + margin: 1px; + cursor: pointer; + text-align: center; + min-width: 1.4em; +} + +.strong, +.heading { + font-weight: bold; +} + +.em { + font-style: italic; +} diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 5c7aaf9fe..d7026ed67 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -14,6 +14,9 @@ import { Plugin } from 'prosemirror-state' import { Decoration, DecorationSet } from 'prosemirror-view' import { TooltipTextMenu } from "../../util/TooltipTextMenu" import { ContextMenu } from "../../views/ContextMenu"; +import { inpRules } from "../../util/RichTextRules"; +const { buildMenuItems } = require("prosemirror-example-setup"); +const { menuBar } = require("prosemirror-menu"); @@ -31,7 +34,7 @@ import { ContextMenu } from "../../views/ContextMenu"; // and 'doc' property to the document that is being rendered // // When rendered() by React, this extracts the TextController from the Document stored at the -// specified Key and assigns it to an HTML input node. When changes are made tot his node, +// specified Key and assigns it to an HTML input node. When changes are made to this node, // this will edit the document and assign the new value to that field. //] export class FormattedTextBox extends React.Component<FieldViewProps> { @@ -60,6 +63,7 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { let state: EditorState; const config = { schema, + inpRules, //these currently don't do anything, but could eventually be helpful plugins: [ history(), keymap({ "Mod-z": undo, "Mod-y": redo }), diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index ea459b911..487038841 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,22 +1,21 @@ - .imageBox-cont { - padding: 0vw; - position: relative; - text-align: center; - width: 100%; - height: auto; - max-width: 100%; - max-height: 100% + padding: 0vw; + position: relative; + text-align: center; + width: 100%; + height: auto; + max-width: 100%; + max-height: 100%; } .imageBox-cont img { - object-fit: contain; height: 100%; + width:100%; } .imageBox-button { - padding : 0vw; - border: none; - width : 100%; - height: 100%; -}
\ No newline at end of file + padding: 0vw; + border: none; + width: 100%; + height: 100%; +} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 30910fb1f..cad8904d0 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,5 +1,5 @@ -import { action, observable } from 'mobx'; +import { action, observable, trace } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app @@ -70,7 +70,7 @@ export class ImageBox extends React.Component<FieldViewProps> { } lightbox = (path: string) => { - const images = [path, "http://www.cs.brown.edu/~bcz/face.gif"]; + const images = [path]; if (this._isOpen && this.props.isSelected()) { return (<Lightbox mainSrc={images[this._photoIndex]} diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index 1295266e5..63ae75424 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -1,31 +1,57 @@ +@import "../global_variables"; .keyValueBox-cont { - overflow-y:scroll; + overflow-y: scroll; height: 100%; - border: black; - border-width: 1px; - border-style: solid; + background-color: $light-color; + border: 1px solid $intermediate-color; + border-radius: $border-radius; box-sizing: border-box; display: inline-block; .imageBox-cont img { - max-height:45px; + max-height: 45px; height: auto; } + td { + padding: 6px 8px; + border-right: 1px solid $intermediate-color; + border-top: 1px solid $intermediate-color; + &:last-child { + border-right: none; + } + } } + .keyValueBox-table { position: relative; + border-collapse: collapse; } + .keyValueBox-header { - background:gray; + background: $intermediate-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 12px; + height: 30px; + padding-top: 4px; + th { + font-weight: normal; + &:first-child { + border-right: 1px solid $light-color; + } + } } + .keyValueBox-evenRow { - background: white; + background: $light-color; .formattedTextBox-cont { - background: white; + background: $light-color; } } + .keyValueBox-oddRow { - background: lightGray; + background: $light-color-secondary; .formattedTextBox-cont { - background: lightgray; + background: $light-color-secondary; } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index ac8c949a9..283c1f732 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -2,17 +2,62 @@ import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app import { Document } from '../../../fields/Document'; -import { FieldWaiting } from '../../../fields/Field'; +import { FieldWaiting, Field } from '../../../fields/Field'; import { KeyStore } from '../../../fields/KeyStore'; import { FieldView, FieldViewProps } from './FieldView'; import "./KeyValueBox.scss"; import { KeyValuePair } from "./KeyValuePair"; import React = require("react") +import { CompileScript, ToField } from "../../util/Scripting"; +import { Key } from '../../../fields/Key'; +import { observable, action } from "mobx"; @observer export class KeyValueBox extends React.Component<FieldViewProps> { public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr) } + @observable private _keyInput: string = ""; + @observable private _valueInput: string = ""; + + + constructor(props: FieldViewProps) { + super(props); + } + + + + shouldComponentUpdate() { + return false; + } + + @action + onEnterKey = (e: React.KeyboardEvent): void => { + if (e.key == 'Enter') { + if (this._keyInput && this._valueInput) { + let doc = this.props.doc.GetT(KeyStore.Data, Document); + if (!doc || doc == FieldWaiting) { + return + } + let realDoc = doc; + + let script = CompileScript(this._valueInput, undefined, true); + if (!script.compiled) { + return; + } + let field = script(); + if (field instanceof Field) { + realDoc.Set(new Key(this._keyInput), field); + } else { + let dataField = ToField(field); + if (dataField) { + realDoc.Set(new Key(this._keyInput), dataField); + } + } + this._keyInput = "" + this._valueInput = "" + } + } + } onPointerDown = (e: React.PointerEvent): void => { if (e.buttons === 1 && this.props.isSelected()) { @@ -33,7 +78,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { let ids: { [key: string]: string } = {}; let protos = doc.GetAllPrototypes(); for (const proto of protos) { - proto._proxies.forEach((val, key) => { + proto._proxies.forEach((val: any, key: string) => { if (!(key in ids)) { ids[key] = key; } @@ -48,9 +93,26 @@ export class KeyValueBox extends React.Component<FieldViewProps> { return rows; } + @action + keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._keyInput = e.currentTarget.value; + } + + @action + valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { + this._valueInput = e.currentTarget.value; + } - render() { + newKeyValue = () => { + return ( + <tr> + <td><input type="text" value={this._keyInput} placeholder="Key" onChange={this.keyChanged} /></td> + <td><input type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyPress={this.onEnterKey} /></td> + </tr> + ) + } + render() { return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel}> <table className="keyValueBox-table"> <tbody> @@ -59,6 +121,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { <th>Fields</th> </tr> {this.createTable()} + {this.newKeyValue()} </tbody> </table> </div>) diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index a97e98313..111f85a05 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -8,6 +8,8 @@ import { observable, action } from 'mobx'; import { Document } from '../../../fields/Document'; import { Key } from '../../../fields/Key'; import { Server } from "../../Server" +import { EditableView } from "../EditableView"; +import { CompileScript, ToField } from "../../util/Scripting"; // Represents one row in a key value plane @@ -48,10 +50,37 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { bindings: {}, selectOnLoad: false, } + let contents = ( + <FieldView {...props} /> + ); return ( <tr className={this.props.rowStyle}> <td>{this.key.Name}</td> - <td><FieldView {...props} /></td> + <td><EditableView contents={contents} height={36} GetValue={() => { + let field = props.doc.Get(props.fieldKey); + if (field && field instanceof Field) { + return field.ToScriptString(); + } + return field || ""; + }} + SetValue={(value: string) => { + let script = CompileScript(value, undefined, true); + if (!script.compiled) { + return false; + } + let field = script(); + if (field instanceof Field) { + props.doc.Set(props.fieldKey, field); + return true; + } else { + let dataField = ToField(field); + if (dataField) { + props.doc.Set(props.fieldKey, dataField); + return true; + } + } + return false; + }}></EditableView></td> </tr> ) } diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss index 00e5ebb3d..5d5f782d2 100644 --- a/src/client/views/nodes/LinkBox.scss +++ b/src/client/views/nodes/LinkBox.scss @@ -1,13 +1,14 @@ +@import "../global_variables"; .link-container { width: 100%; - height: 30px; + height: 35px; display: flex; flex-direction: row; border-top: 0.5px solid #bababa; } .info-container { - width: 60%; + width: 55%; padding-top: 5px; padding-left: 5px; display: flex; @@ -23,17 +24,42 @@ } .button-container { - width: 40%; + width: 45%; display: flex; flex-direction: row; } .button { - height: 15px; - width: 15px; - margin: 8px 5px; + height: 20px; + width: 20px; + margin: 8px 4px; border-radius: 50%; - opacity: 0.6; + opacity: 0.9; pointer-events: auto; - background-color: #2B6091; + background-color: $dark-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + font-size: 60%; + transition: transform 0.2s; +} + +.button:hover { + background: $main-accent; + cursor: pointer; +} + +.fa-icon-view { + margin-left: 3px; + margin-top: 5px; +} + +.fa-icon-edit { + margin-left: 5px; + margin-top: 5px; +} + +.fa-icon-delete { + margin-left: 6px; + margin-top: 5px; }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 69df676ff..430c1b694 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -11,6 +11,16 @@ import { ListField } from "../../../fields/ListField"; import { DocumentManager } from "../../util/DocumentManager"; import { LinkEditor } from "./LinkEditor"; import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faEye } from '@fortawesome/free-solid-svg-icons'; +import { faEdit } from '@fortawesome/free-solid-svg-icons'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; + + +library.add(faEye); +library.add(faEdit); +library.add(faTimes); interface Props { linkDoc: Document; @@ -79,9 +89,12 @@ export class LinkBox extends React.Component<Props> { </div> <div className="button-container"> - <div className="button" onPointerDown={this.onViewButtonPressed}></div> - <div className="button" onPointerDown={this.onEditButtonPressed}></div> - <div className="button" onPointerDown={this.onDeleteButtonPressed}></div> + <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}> + <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> + <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}> + <FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div> + <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}> + <FontAwesomeIcon className="fa-icon-delete" icon="times" size="sm" /></div> </div> </div> ) diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss index cb191dc8c..fb0c69cff 100644 --- a/src/client/views/nodes/LinkEditor.scss +++ b/src/client/views/nodes/LinkEditor.scss @@ -1,3 +1,4 @@ +@import "../global_variables"; .edit-container { width: 100%; height: auto; @@ -9,21 +10,34 @@ margin-bottom: 10px; padding: 5px; font-size: 12px; + border: 1px solid #bababa; } .description-input { - font-size: 12px; + font-size: 11px; padding: 5px; margin-bottom: 10px; + border: 1px solid #bababa; } .save-button { width: 50px; height: 20px; - background-color: #2B6091; + pointer-events: auto; + background-color: $dark-color; + color: $light-color; + text-transform: uppercase; + letter-spacing: 2px; + padding: 2px; + font-size: 10px; margin: 0 auto; - color: white; + transition: transform 0.2s; text-align: center; line-height: 20px; - font-size: 12px; +} + +.save-button:hover { + background: $main-accent; + transform: scale(1.05); + cursor: pointer; }
\ No newline at end of file diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss index a120ab2a7..dedcce6ef 100644 --- a/src/client/views/nodes/LinkMenu.scss +++ b/src/client/views/nodes/LinkMenu.scss @@ -10,6 +10,7 @@ padding: 5px; margin-bottom: 10px; font-size: 12px; + border: 1px solid #bababa; } #linkMenu-list { diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 5c6b06d00..5eeb40772 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -39,8 +39,8 @@ export class LinkMenu extends React.Component<Props> { <div id="linkMenu-container"> <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> <div id="linkMenu-list"> - {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Source: ")} - {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Destination: ")} + {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Destination: ")} + {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Source: ")} </div> </div> ) diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index 9f92410d4..ad947afd5 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -11,5 +11,5 @@ } .pdfBox-contentContainer { position: absolute; - transform-origin: "left top"; + transform-origin: left top; }
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 1f873f8c5..e6beec5f4 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,5 +1,5 @@ import * as htmlToImage from "html-to-image"; -import { action, computed, observable, reaction, IReactionDisposer } from 'mobx'; +import { action, computed, observable, reaction, IReactionDisposer, trace } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; import Measure from "react-measure"; @@ -87,7 +87,7 @@ export class PDFBox extends React.Component<FieldViewProps> { @observable private _interactive: boolean = false; @observable private _loaded: boolean = false; - @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, 0); } + @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, -1); } componentDidMount() { this._reactionDisposer = reaction( diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 7306450d9..76bbeb37c 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,4 +1,4 @@ .videobox-cont{ width: 100%; - height: 100%; + height: Auto; }
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 22ff5c5ad..8c1ee669f 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,12 +1,13 @@ import React = require("react") -import { FieldViewProps, FieldView } from './FieldView'; +import { observer } from "mobx-react"; import { FieldWaiting } from '../../../fields/Field'; -import { observer } from "mobx-react" -import { VideoField } from '../../../fields/VideoField'; -import "./VideoBox.scss" -import { ContextMenu } from "../../views/ContextMenu"; -import { observable, action } from 'mobx'; -import { KeyStore } from '../../../fields/KeyStore'; +import { VideoField } from '../../../fields/VideoField'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./VideoBox.scss"; +import Measure from "react-measure"; +import { action, trace, observable } from "mobx"; +import { KeyStore } from "../../../fields/KeyStore"; +import { number } from "prop-types"; @observer export class VideoBox extends React.Component<FieldViewProps> { @@ -17,27 +18,44 @@ export class VideoBox extends React.Component<FieldViewProps> { super(props); } - - componentDidMount() { - } + _loaded: boolean = false; - componentWillUnmount() { + @action + setScaling = (r: any) => { + if (this._loaded) { + // bcz: the nativeHeight should really be set when the document is imported. + // also, the native dimensions could be different for different pages of the PDF + // so this design is flawed. + var nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = this.props.doc.GetNumber(KeyStore.NativeHeight, 0); + var newNativeHeight = nativeWidth * r.entry.height / r.entry.width; + if (!nativeHeight && newNativeHeight != nativeHeight && !isNaN(newNativeHeight)) { + this.props.doc.SetNumber(KeyStore.Height, newNativeHeight / nativeWidth * this.props.doc.GetNumber(KeyStore.Width, 0)); + this.props.doc.SetNumber(KeyStore.NativeHeight, newNativeHeight); + } + } else { + this._loaded = true; + } } - + + render() { let field = this.props.doc.Get(this.props.fieldKey) - let path = field == FieldWaiting ? "http://techslides.com/demos/sample-videos/small.mp4": + let path = field == FieldWaiting ? "http://techslides.com/demos/sample-videos/small.mp4" : field instanceof VideoField ? field.Data.href : "http://techslides.com/demos/sample-videos/small.mp4"; - + + //setTimeout(action(() => this._loaded = true), 500); return ( - <div> - <video width = {200} height = {200} controls className = "videobox-cont"> - <source src = {path} type = "video/mp4"/> - Not supported. - </video> - </div> + <Measure onResize={this.setScaling}> + {({ measureRef }) => + <video className="videobox-cont" ref={measureRef}> + <source src={path} type="video/mp4" /> + Not supported. + </video> + } + </Measure> ) } }
\ No newline at end of file |
