aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbob <bcz@cs.brown.edu>2019-06-19 16:17:21 -0400
committerbob <bcz@cs.brown.edu>2019-06-19 16:17:21 -0400
commit5b2a498aca75bd53ffab61f998218bec546b8154 (patch)
tree248881b4b346c9e87704acb592364cef29444d6d
parent358437eeafe42e029ffe27702bde15a3fad54a3b (diff)
parent39e8a7a365442cdc11024c4de8019184fd0057ac (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web
-rw-r--r--src/client/views/ContextMenu.scss23
-rw-r--r--src/client/views/ContextMenu.tsx83
-rw-r--r--src/client/views/ContextMenuItem.tsx7
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/pdf/PDFMenu.tsx55
-rw-r--r--src/client/views/pdf/PDFViewer.tsx77
-rw-r--r--src/client/views/pdf/Page.tsx1
7 files changed, 192 insertions, 56 deletions
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index e363c5158..a1a2b06f1 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -53,6 +53,29 @@
font-size: 20px;
}
+.contextMenu-group {
+ // width: 11vw; //10vw
+ height: 30px; //2vh
+ background: rgb(200, 200, 200);
+ display: flex; //comment out to allow search icon to be inline with search text
+ 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: $intermediate-color; // rgb(187, 186, 186);
+ border-bottom-style: solid;
+ // padding: 10px 0px 10px 0px;
+ white-space: nowrap;
+ font-size: 20px;
+}
+
.contextMenu-item:hover {
transition: all 0.1s;
background: $lighter-alt-accent;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index eb1937683..1133f70a1 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,6 +1,6 @@
import React = require("react");
import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem";
-import { observable, action } from "mobx";
+import { observable, action, computed } from "mobx";
import { observer } from "mobx-react";
import "./ContextMenu.scss";
import { library } from '@fortawesome/fontawesome-svg-core';
@@ -17,11 +17,12 @@ export class ContextMenu extends React.Component {
@observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }];
@observable private _pageX: number = 0;
@observable private _pageY: number = 0;
- @observable private _display: string = "none";
+ @observable private _display: boolean = false;
@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 _searchRef = React.createRef<HTMLInputElement>();
private ref: React.RefObject<HTMLDivElement>;
@@ -36,7 +37,6 @@ export class ContextMenu extends React.Component {
@action
clearItems() {
this._items = [];
- this._display = "none";
}
@action
@@ -62,34 +62,70 @@ export class ContextMenu extends React.Component {
this._searchString = "";
- this._display = "flex";
- }
-
- intersects = (x: number, y: number): boolean => {
- if (this.ref.current && this._display !== "none") {
- let menuSize = { width: this.ref.current.getBoundingClientRect().width, height: this.ref.current.getBoundingClientRect().height };
+ this._display = true;
- 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;
- }
- }
+ if (this._searchRef.current) {
+ this._searchRef.current.focus();
}
- return false;
}
@action
closeMenu = () => {
this.clearItems();
+ this._display = false;
}
- render() {
- let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } :
- { left: this._pageX, bottom: this._pageY, display: this._display };
+ @computed get filteredItems() {
+ const searchString = this._searchString.toLowerCase().split(" ");
+ const matches = (descriptions: string[]): boolean => {
+ return searchString.every(s => descriptions.some(desc => desc.includes(s)));
+ };
+ const createGroupHeader = (contents: any) => {
+ return (
+ <div className="contextMenu-group">
+ <div className="contextMenu-description">{contents}</div>
+ </div>
+ );
+ };
+ const createItem = (item: ContextMenuProps) => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />;
+ const flattenItems = (items: ContextMenuProps[], groupFunc: (contents: any) => JSX.Element, getPath: () => string[]) => {
+ let eles: JSX.Element[] = [];
+
+ for (const item of items) {
+ const description = item.description.toLowerCase();
+ const path = [...getPath(), description];
+ if ("subitems" in item) {
+ const children = flattenItems(item.subitems, contents => groupFunc(<>{item.description} -> {contents}</>), () => path);
+ if (children.length || matches(path)) {
+ eles.push(groupFunc(item.description));
+ eles = eles.concat(children);
+ }
+ } else {
+ if (!matches(path)) {
+ continue;
+ }
+ eles.push(createItem(item));
+ }
+ }
+ return eles;
+ };
+ return flattenItems(this._items, createGroupHeader, () => []);
+ }
+
+ @computed get menuItems() {
+ if (!this._searchString) {
+ return this._items.map(item => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} />);
+ }
+ return this.filteredItems;
+ }
+
+ render() {
+ if (!this._display) {
+ return null;
+ }
+ let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY } :
+ { left: this._pageX, bottom: this._pageY };
return (
<div className="contextMenu-cont" style={style} ref={this.ref}>
@@ -97,10 +133,9 @@ export class ContextMenu extends React.Component {
<span className="icon-background">
<FontAwesomeIcon icon="search" size="lg" />
</span>
- <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} />
+ <input className="contextMenu-item contextMenu-description" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} ref={this._searchRef} autoFocus />
</span>
- {this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1).
- map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.closeMenu} />)}
+ {this.menuItems}
</div>
);
}
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index dc0751049..88ebd95bc 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -21,9 +21,6 @@ export interface SubmenuProps {
closeMenu?: () => void;
}
-export interface ContextMenuItemProps {
- type: ContextMenuProps | SubmenuProps;
-}
export type ContextMenuProps = OriginalMenuProps | SubmenuProps;
@observer
@@ -67,7 +64,6 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> {
return;
}
this.currentTimeout = setTimeout(action(() => this.overItem = false), ContextMenuItem.timeout);
-
}
render() {
@@ -84,8 +80,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> {
</div>
</div>
);
- }
- else {
+ } else if ("subitems" in this.props) {
let submenu = !this.overItem ? (null) :
<div className="contextMenu-subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}>
{this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />)}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 7f979cd3b..e3d4ff8b5 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -118,7 +118,7 @@ export class MainView extends React.Component {
const targets = document.elementsFromPoint(e.x, e.y);
if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
- ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.closeMenu();
}
}), true);
}
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 2462a5f94..39b15fb11 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -15,13 +15,16 @@ export default class PDFMenu extends React.Component {
@observable private _opacity: number = 1;
@observable private _transition: string = "opacity 0.5s";
@observable private _transitionDelay: string = "";
- @observable private _pinned: boolean = false;
+
+ @observable public Pinned: boolean = false;
StartDrag: (e: PointerEvent) => void = emptyFunction;
Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction;
- @observable Highlighting: boolean = false;
+ Delete: () => void = emptyFunction;
+
+ @observable public Highlighting: boolean = false;
+ @observable public Status: "pdf" | "annotation" | "" = "";
- private _timeout: NodeJS.Timeout | undefined;
private _offsetY: number = 0;
private _offsetX: number = 0;
private _mainCont: React.RefObject<HTMLDivElement>;
@@ -66,8 +69,8 @@ export default class PDFMenu extends React.Component {
}
@action
- jumpTo = (x: number, y: number) => {
- if (!this._pinned) {
+ jumpTo = (x: number, y: number, forceJump: boolean = false) => {
+ if (!this.Pinned || forceJump) {
this._transition = this._transitionDelay = "";
this._opacity = 1;
this._left = x;
@@ -77,7 +80,7 @@ export default class PDFMenu extends React.Component {
@action
fadeOut = (forceOut: boolean) => {
- if (!this._pinned) {
+ if (!this.Pinned) {
if (this._opacity === 0.2) {
this._transition = "opacity 0.1s";
this._transitionDelay = "";
@@ -96,7 +99,7 @@ export default class PDFMenu extends React.Component {
@action
pointerLeave = (e: React.PointerEvent) => {
- if (!this._pinned) {
+ if (!this.Pinned) {
this._transition = "opacity 0.5s";
this._transitionDelay = "1s";
this._opacity = 0.2;
@@ -113,8 +116,8 @@ export default class PDFMenu extends React.Component {
@action
togglePin = (e: React.MouseEvent) => {
- this._pinned = !this._pinned;
- if (!this._pinned) {
+ this.Pinned = !this.Pinned;
+ if (!this.Pinned) {
this.Highlighting = false;
}
}
@@ -150,7 +153,7 @@ export default class PDFMenu extends React.Component {
@action
highlightClicked = (e: React.MouseEvent) => {
- if (!this._pinned) {
+ if (!this.Pinned) {
this.Highlight(undefined, "#f4f442");
}
else {
@@ -159,11 +162,35 @@ export default class PDFMenu extends React.Component {
}
}
+ deleteClicked = (e: React.PointerEvent) => {
+ this.Delete();
+ }
+
+ handleContextMenu = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
render() {
+ let buttons = this.Status === "pdf" ? [
+ <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked}
+ style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
+ <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} />
+ </button>,
+ <button className="pdfMenu-button" title="Drag to Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" /></button>,
+ <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin}
+ style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
+ <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} />
+ </button>
+ ] : [
+ <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" /></button>
+ ];
+
return (
- <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont}
+ <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
- <button className="pdfMenu-button" title="Highlight" onClick={this.highlightClicked}
+ {buttons}
+ {/* <button className="pdfMenu-button" title="Highlight" onClick={this.highlightClicked}
style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
<FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} />
</button>
@@ -171,8 +198,8 @@ export default class PDFMenu extends React.Component {
<button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin}
style={this._pinned ? { backgroundColor: "#121212" } : {}}>
<FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this._pinned ? "rotate(45deg)" : "" }} />
- </button>
- <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this._pinned ? "20px" : "0px" }} />
+ </button> */}
+ <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
</div >
);
}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index f0e55705d..7000352e7 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -7,7 +7,7 @@ import { Dictionary } from "typescript-collections";
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types";
import { emptyFunction } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
@@ -138,6 +138,7 @@ class Viewer extends React.Component<IViewerProps> {
makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => {
let annoDocs: Doc[] = [];
+ let mainAnnoDoc = new Doc();
this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => {
for (let anno of value) {
let annoDoc = new Doc();
@@ -147,6 +148,7 @@ class Viewer extends React.Component<IViewerProps> {
if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale;
annoDoc.page = key;
annoDoc.target = sourceDoc;
+ annoDoc.group = mainAnnoDoc;
annoDoc.color = color;
annoDoc.type = AnnotationTypes.Region;
annoDocs.push(annoDoc);
@@ -154,13 +156,12 @@ class Viewer extends React.Component<IViewerProps> {
}
});
- let annoDoc = new Doc();
- annoDoc.annotations = new List<Doc>(annoDocs);
+ mainAnnoDoc.annotations = new List<Doc>(annoDocs);
if (sourceDoc) {
- DocUtils.MakeLink(sourceDoc, annoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title));
+ DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title));
}
this._savedAnnotations.clear();
- return annoDoc;
+ return mainAnnoDoc;
}
drop = async (e: Event, de: DragManager.DropEvent) => {
@@ -508,6 +509,59 @@ interface IAnnotationProps {
class RegionAnnotation extends React.Component<IAnnotationProps> {
@observable private _backgroundColor: string = "red";
+ private _reactionDisposer?: IReactionDisposer;
+ private _mainCont: React.RefObject<HTMLDivElement>;
+
+ constructor(props: IAnnotationProps) {
+ super(props);
+
+ this._mainCont = React.createRef();
+ }
+
+ componentDidMount() {
+ this._reactionDisposer = reaction(
+ () => BoolCast(this.props.document.delete),
+ () => {
+ if (BoolCast(this.props.document.delete)) {
+ if (this._mainCont.current) {
+ this._mainCont.current.style.display = "none";
+ }
+ }
+ },
+ { fireImmediately: true }
+ );
+ }
+
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
+ deleteAnnotation = () => {
+ let annotation = DocListCast(this.props.parent.props.parent.Document.annotations);
+ let group = FieldValue(Cast(this.props.document.group, Doc));
+ if (group && annotation.indexOf(group) !== -1) {
+ let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
+ this.props.parent.props.parent.Document.annotations = new List<Doc>(newAnnotations);
+ }
+
+ if (group) {
+ let groupAnnotations = DocListCast(group.annotations);
+ groupAnnotations.forEach(anno => anno.delete = true);
+ }
+
+ PDFMenu.Instance.fadeOut(true);
+ }
+
+
+ // annotateThis = (e: PointerEvent) => {
+ // e.preventDefault();
+ // e.stopPropagation();
+ // // document that this annotation is linked to
+ // let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" });
+ // let group = FieldValue(Cast(this.props.document.group, Doc));
+ // }
+
+ @action
onPointerDown = (e: React.PointerEvent) => {
if (e.button === 0) {
let targetDoc = Cast(this.props.document.target, Doc, null);
@@ -515,16 +569,17 @@ class RegionAnnotation extends React.Component<IAnnotationProps> {
DocumentManager.Instance.jumpToDocument(targetDoc);
}
}
- // if (e.button === 2) {
- // console.log("right");
- // e.stopPropagation();
- // e.preventDefault();
- // }
+ if (e.button === 2) {
+ PDFMenu.Instance.Status = "annotation";
+ PDFMenu.Instance.Delete = this.deleteAnnotation;
+ PDFMenu.Instance.Pinned = false;
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
+ }
}
render() {
return (
- <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown}
+ <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont}
style={{ top: this.props.y * scale, left: this.props.x * scale, width: this.props.width * scale, height: this.props.height * scale, pointerEvents: "all", backgroundColor: StrCast(this.props.document.color) }}></div>
);
}
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
index 39e737c32..734dff7fc 100644
--- a/src/client/views/pdf/Page.tsx
+++ b/src/client/views/pdf/Page.tsx
@@ -191,6 +191,7 @@ export default class Page extends React.Component<IPageProps> {
else if (e.button === 0) {
PDFMenu.Instance.StartDrag = this.startDrag;
PDFMenu.Instance.Highlight = this.highlight;
+ PDFMenu.Instance.Status = "pdf";
PDFMenu.Instance.fadeOut(true);
let target: any = e.target;
if (target && target.parentElement === this._textLayer.current) {