aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts45
-rw-r--r--src/client/documents/Documents.ts10
-rw-r--r--src/client/goldenLayout.js2
-rw-r--r--src/client/util/DocumentManager.ts15
-rw-r--r--src/client/util/DragManager.ts49
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts48
-rw-r--r--src/client/util/RichTextSchema.tsx9
-rw-r--r--src/client/util/SelectionManager.ts16
-rw-r--r--src/client/util/TooltipTextMenu.tsx42
-rw-r--r--src/client/util/prosemirrorPatches.js2
-rw-r--r--src/client/views/DocumentButtonBar.tsx5
-rw-r--r--src/client/views/DocumentDecorations.tsx10
-rw-r--r--src/client/views/InkingCanvas.scss2
-rw-r--r--src/client/views/MainView.tsx12
-rw-r--r--src/client/views/TemplateMenu.tsx4
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx103
-rw-r--r--src/client/views/animationtimeline/Timeline.scss56
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx152
-rw-r--r--src/client/views/animationtimeline/TimelineMenu.tsx11
-rw-r--r--src/client/views/animationtimeline/Track.tsx42
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx6
-rw-r--r--src/client/views/collections/CollectionDockingView.scss5
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx72
-rw-r--r--src/client/views/collections/CollectionPDFView.scss47
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx24
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx6
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx4
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx12
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx6
-rw-r--r--src/client/views/collections/CollectionSubView.tsx7
-rw-r--r--src/client/views/collections/CollectionView.tsx9
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx117
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx629
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx45
-rw-r--r--src/client/views/linking/LinkFollowBox.tsx10
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx4
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx27
-rw-r--r--src/client/views/nodes/DocumentView.tsx21
-rw-r--r--src/client/views/nodes/FieldView.tsx3
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss20
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx149
-rw-r--r--src/client/views/nodes/ImageBox.tsx1
-rw-r--r--src/client/views/nodes/PDFBox.scss132
-rw-r--r--src/client/views/nodes/PDFBox.tsx230
-rw-r--r--src/client/views/nodes/PresBox.tsx6
-rw-r--r--src/client/views/pdf/Annotation.tsx18
-rw-r--r--src/client/views/pdf/PDFMenu.tsx7
-rw-r--r--src/client/views/pdf/PDFViewer.scss123
-rw-r--r--src/client/views/pdf/PDFViewer.tsx705
-rw-r--r--src/client/views/pdf/Page.scss36
-rw-r--r--src/client/views/pdf/Page.tsx293
-rw-r--r--src/client/views/presentationview/PresentationModeMenu.tsx13
-rw-r--r--src/debug/Test.tsx35
-rw-r--r--src/new_fields/Doc.ts15
55 files changed, 1649 insertions, 1826 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 65eb3cffd..2b00a6530 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -235,6 +235,21 @@ export function timenow() {
return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
}
+export function aggregateBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
+ return boundsList.reduce((bounds, b) => {
+ var [sptX, sptY] = [b.x, b.y];
+ let [bptX, bptY] = [sptX + b.width, sptY + b.height];
+ return {
+ x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+ r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+ };
+ }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
+}
+export function 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);
+}
+
export function percent2frac(percent: string) {
return Number(percent.substr(0, percent.length - 1)) / 100;
}
@@ -292,4 +307,34 @@ export function PostToServer(relativeRoute: string, body: any) {
body: body
};
return requestPromise.post(options);
+}
+
+const easeInOutQuad = (currentTime: number, start: number, change: number, duration: number) => {
+ let newCurrentTime = currentTime / (duration / 2);
+
+ if (newCurrentTime < 1) {
+ return (change / 2) * newCurrentTime * newCurrentTime + start;
+ }
+
+ newCurrentTime -= 1;
+ return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
+};
+
+export default function smoothScroll(duration: number, element: HTMLElement, to: number) {
+ const start = element.scrollTop;
+ const change = to - start;
+ const startDate = new Date().getTime();
+
+ const animateScroll = () => {
+ const currentDate = new Date().getTime();
+ const currentTime = currentDate - startDate;
+ element.scrollTop = easeInOutQuad(currentTime, start, change, duration);
+
+ if (currentTime < duration) {
+ requestAnimationFrame(animateScroll);
+ } else {
+ element.scrollTop = to;
+ }
+ };
+ animateScroll();
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2fa0d2dcb..392dca373 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -95,12 +95,13 @@ export namespace Docs {
export namespace Prototypes {
- type LayoutSource = { LayoutString: () => string };
+ type LayoutSource = { LayoutString: (ext?: string) => string };
type CollectionLayoutSource = { LayoutString: (fieldStr: string, fieldExt?: string) => string };
type CollectionViewType = [CollectionLayoutSource, string, string?];
type PrototypeTemplate = {
layout: {
view: LayoutSource,
+ ext?: string, // optional extension field for layout source
collectionView?: CollectionViewType
},
options?: Partial<DocumentOptions>
@@ -144,7 +145,7 @@ export namespace Docs {
options: { height: 32 }
}],
[DocumentType.PDF, {
- layout: { view: PDFBox, collectionView: [CollectionPDFView, data, anno] as CollectionViewType },
+ layout: { view: PDFBox, ext: anno },
options: { nativeWidth: 1200, curPage: 1 }
}],
[DocumentType.ICON, {
@@ -254,7 +255,7 @@ export namespace Docs {
// synthesize the default options, the type and title from computed values and
// whatever options pertain to this specific prototype
let options = { title: title, type: type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
- let primary = layout.view.LayoutString();
+ let primary = layout.view.LayoutString(layout.ext);
let collectionView = layout.collectionView;
if (collectionView) {
options.layout = collectionView[0].LayoutString(collectionView[1], collectionView[2]);
@@ -638,7 +639,6 @@ export namespace DocUtils {
});
}
export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string, anchored1?: boolean) {
- if (LinkManager.Instance.doesLinkExist(source, target)) return undefined;
let sv = DocumentManager.Instance.getDocumentView(source);
if (sv && sv.props.ContainingCollectionDoc === target) return;
if (target === CurrentUserUtils.UserDocument) return undefined;
@@ -651,7 +651,6 @@ export namespace DocUtils {
linkDocProto.sourceContext = sourceContext;
linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
linkDocProto.linkDescription = description;
- linkDocProto.type = DocumentType.LINK;
linkDocProto.anchor1 = source;
linkDocProto.anchor1Page = source.curPage;
@@ -665,6 +664,7 @@ export namespace DocUtils {
Doc.GetProto(source).links = ComputedField.MakeFunction("links(this)");
Doc.GetProto(target).links = ComputedField.MakeFunction("links(this)");
+
}, "make link");
return linkDocProto;
}
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index ad78139c1..29b750720 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -377,7 +377,7 @@
this._nOriginalY = coordinates.y;
this._oDocument.on('mousemove touchmove', this._fMove);
- this._oDocument.one('mouseup touchend', this._fUp);
+ this._oDocument.on('mouseup touchend', this._fUp);
this._timeout = setTimeout(lm.utils.fnBind(this._startDrag, this), this._nDelay);
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index a3c7429b9..c048125c5 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -132,9 +132,7 @@ export class DocumentManager {
let doc = Doc.GetProto(docDelegate);
const contextDoc = await Cast(doc.annotationOn, Doc);
if (contextDoc) {
- const page = NumCast(doc.page, linkPage || 0);
- const curPage = NumCast(contextDoc.curPage, page);
- if (page !== curPage) contextDoc.curPage = page;
+ contextDoc.scrollY = NumCast(doc.y) - NumCast(contextDoc.height) / 2;
}
let docView: DocumentView | null;
@@ -180,7 +178,7 @@ export class DocumentManager {
(dockFunc || CollectionDockingView.AddRightSplit)(contextDoc, undefined);
setTimeout(() => {
this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
- }, 10);
+ }, 1000);
}
}
}
@@ -188,13 +186,8 @@ export class DocumentManager {
@action
zoomIntoScale = (docDelegate: Doc, scale: number) => {
- let doc = Doc.GetProto(docDelegate);
-
- let docView: DocumentView | null;
- docView = DocumentManager.Instance.getDocumentView(doc);
- if (docView) {
- docView.props.zoomToScale(scale);
- }
+ let docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
+ docView && docView.props.zoomToScale(scale);
}
getScaleOfDocView = (docDelegate: Doc) => {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 56496c99b..ddc8fb62c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -349,8 +349,8 @@ export namespace DragManager {
let xs: number[] = [];
let ys: number[] = [];
- const docs: Doc[] =
- dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
+ const docs = dragData instanceof DocumentDragData ? dragData.draggedDocuments :
+ dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
let dragElements = eles.map(ele => {
const w = ele.offsetWidth,
h = ele.offsetHeight;
@@ -379,22 +379,20 @@ export namespace DragManager {
dragElement.style.width = `${rect.width / scaleX}px`;
dragElement.style.height = `${rect.height / scaleY}px`;
- // bcz: if PDFs are rendered with svg's, then this code isn't needed
- // bcz: PDFs don't show up if you clone them when rendered using a canvas.
- // however, PDF's have a thumbnail field that contains an image of their canvas.
- // So we replace the pdf's canvas with the image thumbnail
- // if (docs.length) {
- // var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement;
- // let thumbnail = docs[0].GetT(KeyStore.Thumbnail, ImageField);
- // if (pdfBox && pdfBox.childElementCount && thumbnail) {
- // let img = new Image();
- // img.src = thumbnail.toString();
- // img.style.position = "absolute";
- // img.style.width = `${rect.width / scaleX}px`;
- // img.style.height = `${rect.height / scaleY}px`;
- // pdfBox.replaceChild(img, pdfBox.children[0])
- // }
- // }
+ if (docs.length) {
+ var pdfBox = dragElement.getElementsByTagName("canvas");
+ var pdfBoxSrc = ele.getElementsByTagName("canvas");
+ Array.from(pdfBox).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
+ var pdfView = dragElement.getElementsByClassName("pdfViewer-viewer");
+ var pdfViewSrc = ele.getElementsByClassName("pdfViewer-viewer");
+ let tops = Array.from(pdfViewSrc).map(p => p.scrollTop);
+ let oldopacity = dragElement.style.opacity;
+ dragElement.style.opacity = "0";
+ setTimeout(() => {
+ dragElement.style.opacity = oldopacity;
+ Array.from(pdfView).map((v, i) => v.scrollTo({ top: tops[i] }));
+ }, 0);
+ }
let set = dragElement.getElementsByTagName('*');
if (dragElement.hasAttribute("style")) (dragElement as any).style.pointerEvents = "none";
// tslint:disable-next-line: prefer-for-of
@@ -418,8 +416,8 @@ export namespace DragManager {
hideSource = options.hideSource();
}
}
- eles.map(ele => (ele.hidden = hideSource) &&
- (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = hideSource)));
+
+ eles.map(ele => ele.hidden = hideSource);
let lastX = downX;
let lastY = downY;
@@ -447,12 +445,9 @@ export namespace DragManager {
);
};
- let hideDragElements = () => {
+ let hideDragShowOriginalElements = () => {
dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
- eles.map(ele => {
- ele.hidden = false;
- (ele.parentElement && ele.parentElement.className.indexOf("collectionFreeFormDocumentView") !== -1 && (ele.parentElement.hidden = false));
- });
+ eles.map(ele => ele.hidden = false);
};
let endDrag = () => {
document.removeEventListener("pointermove", moveHandler, true);
@@ -463,12 +458,12 @@ export namespace DragManager {
};
AbortDrag = () => {
- hideDragElements();
+ hideDragShowOriginalElements();
SelectionManager.SetIsDragging(false);
endDrag();
};
const upHandler = (e: PointerEvent) => {
- hideDragElements();
+ hideDragShowOriginalElements();
dispatchDrag(eles, e, dragData, options, finishDrag);
SelectionManager.SetIsDragging(false);
endDrag();
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 3e3d3155c..aab437176 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -11,6 +11,20 @@ const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) :
export type KeyMap = { [key: string]: any };
+export let updateBullets = (tx2: Transaction, schema: Schema) => {
+ let fontSize: number | undefined = undefined;
+ tx2.doc.descendants((node: any, offset: any, index: any) => {
+ if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
+ let path = (tx2.doc.resolve(offset) as any).path;
+ let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
+ if (node.type === schema.nodes.ordered_list) depth++;
+ fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize;
+ let fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
+ tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
+ }
+ });
+ return tx2;
+};
export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap {
let keys: { [key: string]: any } = {}, type;
@@ -93,16 +107,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Mod-s", TooltipTextMenu.insertStar);
- let updateBullets = (tx2: Transaction) => {
- tx2.doc.descendants((node: any, offset: any, index: any) => {
- if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
- let path = (tx2.doc.resolve(offset) as any).path;
- let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
- if (node.type === schema.nodes.ordered_list) depth++;
- tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth }, node.marks);
- }
- });
- };
bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
@@ -110,18 +114,18 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
var range = ref.$from.blockRange(ref.$to);
var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
- updateBullets(tx2);
- marks && tx2.ensureMarks([...marks]);
- marks && tx2.setStoredMarks([...marks]);
- dispatch(tx2);
+ let tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
})) { // couldn't sink into an existing list, so wrap in a new one
let newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
- updateBullets(tx2);
+ let tx3 = updateBullets(tx2, schema);
// when promoting to a list, assume list will format things so don't copy the stored marks.
- marks && tx2.ensureMarks([...marks]);
- marks && tx2.setStoredMarks([...marks]);
- dispatch(tx2);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
})) {
console.log("bullet promote fail");
}
@@ -132,10 +136,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
- updateBullets(tx2);
- marks && tx2.ensureMarks([...marks]);
- marks && tx2.setStoredMarks([...marks]);
- dispatch(tx2);
+ let tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
})) {
console.log("bullet demote fail");
}
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index ba4b92a25..710d55605 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -198,6 +198,8 @@ export const nodes: { [index: string]: NodeSpec } = {
attrs: {
bulletStyle: { default: 0 },
mapStyle: { default: "decimal" },
+ setFontSize: { default: undefined },
+ inheritedFontSize: { default: undefined },
visibility: { default: true }
},
toDOM(node: Node<any>) {
@@ -205,8 +207,9 @@ export const nodes: { [index: string]: NodeSpec } = {
const decMap = bs ? "decimal" + bs : "";
const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
- return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
- ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
+ let fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
+ return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;font-size: ${fsize}` }, 0] :
+ ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}` }];
}
},
@@ -253,7 +256,7 @@ export const marks: { [index: string]: MarkSpec } = {
href: {},
location: { default: null },
title: { default: null },
- docref: { default: false }
+ docref: { default: false } // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
},
inclusive: false,
parseDOM: [{
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 4c97a1056..a02a270ee 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -88,20 +88,4 @@ export namespace SelectionManager {
export function SelectedDocuments(): Array<DocumentView> {
return manager.SelectedDocuments.slice();
}
- export function ViewsSortedHorizontally(): DocumentView[] {
- let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
- if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
- if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
- return 0;
- });
- return sorted;
- }
- export function ViewsSortedVertically(): DocumentView[] {
- let sorted = SelectionManager.SelectedDocuments().slice().sort((doc1, doc2) => {
- if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.y)) return 1;
- if (NumCast(doc1.props.Document.y) < NumCast(doc2.props.Document.y)) return -1;
- return 0;
- });
- return sorted;
- }
}
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index b6de048e4..a83a3949d 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -19,6 +19,7 @@ import { LinkManager } from "./LinkManager";
import { schema } from "./RichTextSchema";
import "./TooltipTextMenu.scss";
import { Cast, NumCast } from '../../new_fields/Types';
+import { updateBullets } from './ProsemirrorExampleTransfer';
const { toggleMark, setBlockType } = require("prosemirror-commands");
const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
@@ -302,12 +303,17 @@ export class TooltipTextMenu {
{
handlers: {
dragComplete: action(() => {
- let linkDoc = dragData.linkDocument;
- let proto = Doc.GetProto(linkDoc);
- if (proto && docView) {
- proto.sourceContext = docView.props.ContainingCollectionDoc;
+ if (dragData.linkDocument) {
+ let linkDoc = dragData.linkDocument;
+ let proto = Doc.GetProto(linkDoc);
+ if (proto && docView) {
+ proto.sourceContext = docView.props.ContainingCollectionDoc;
+ }
+ let text = this.makeLink(linkDoc, ctrlKey ? "onRight" : "inTab");
+ if (linkDoc instanceof Doc && linkDoc.anchor2 instanceof Doc) {
+ proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODODO open to more descriptive descriptions of following in text link
+ }
}
- linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
}),
},
hideSource: false
@@ -389,17 +395,24 @@ export class TooltipTextMenu {
}
}
- makeLinkWithState = (state: EditorState, target: string, location: string) => {
- let link = state.schema.mark(state.schema.marks.link, { href: target, location: location });
- }
+ // makeLinkWithState = (state: EditorState, target: string, location: string) => {
+ // let link = state.schema.mark(state.schema.marks.link, { href: target, location: location });
+ // }
- makeLink = (target: string, location: string) => {
+ makeLink = (targetDoc: Doc, location: string): string => {
+ let target = Utils.prepend("/doc/" + targetDoc[Id]);
let node = this.view.state.selection.$from.nodeAfter;
- let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location });
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location, guid: targetDoc[Id] });
this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
node = this.view.state.selection.$from.nodeAfter;
link = node && node.marks.find(m => m.type.name === "link");
+ if (node) {
+ if (node.text) {
+ return node.text;
+ }
+ }
+ return "";
}
deleteLink = () => {
@@ -506,10 +519,11 @@ export class TooltipTextMenu {
}
}
//actually apply font
- return toggleMark(markType)(view.state, view.dispatch, view);
- }
- else {
- return;
+ if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
+ view.dispatch(updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+ { ...(view.state.selection as NodeSelection).node.attrs, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema));
+ }
+ else toggleMark(markType)(view.state, view.dispatch, view);
}
}
diff --git a/src/client/util/prosemirrorPatches.js b/src/client/util/prosemirrorPatches.js
index 188e3e1c5..269423482 100644
--- a/src/client/util/prosemirrorPatches.js
+++ b/src/client/util/prosemirrorPatches.js
@@ -82,7 +82,7 @@ function sinkListItem(itemType) {
if (dispatch) {
var nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
var inner = prosemirrorModel.Fragment.from(nestedBefore ? itemType.create() : null);
- let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create(parent.attrs, inner)))),
+ let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create({ ...parent.attrs, fontSize: parent.attrs.fontSize ? parent.attrs.fontSize - 4 : undefined }, inner)))),
nestedBefore ? 3 : 1, 0);
var before = range.start, after = range.end;
dispatch(state.tr.step(new prosemirrorTransform.ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index b482e3298..9ca54f738 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -23,6 +23,7 @@ import React = require("react");
import { DocumentView } from './nodes/DocumentView';
import { ParentDocSelector } from './collections/ParentDocumentSelector';
import { CollectionDockingView } from './collections/CollectionDockingView';
+import { DocumentDecorations } from './DocumentDecorations';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -225,7 +226,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
return (
<div className={"linkButtonWrapper"}>
<div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
- DocumentDecorations.hasPushedHack = false;
+ DocumentButtonBar.hasPushedHack = false;
this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
}}>
<FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
@@ -259,7 +260,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
} else {
this.clearPullColor();
- DocumentDecorations.hasPulledHack = false;
+ DocumentButtonBar.hasPulledHack = false;
this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 7ec316bf9..944ae586c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -515,8 +515,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
doc.x = (doc.x || 0) + dX * (actualdW - width);
doc.y = (doc.y || 0) + dY * (actualdH - height);
let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
- let fixedAspect = e.ctrlKey || (!BoolCast(doc.ignoreAspect) && nwidth && nheight);
- if (fixedAspect && e.ctrlKey && BoolCast(doc.ignoreAspect)) {
+ let fixedAspect = e.ctrlKey || (!doc.ignoreAspect && nwidth && nheight);
+ if (fixedAspect && e.ctrlKey && doc.ignoreAspect) {
doc.ignoreAspect = false;
proto.nativeWidth = nwidth = doc.width || 0;
proto.nativeHeight = nheight = doc.height || 0;
@@ -531,7 +531,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
}
doc.width = actualdW;
- if (fixedAspect) doc.height = nheight / nwidth * doc.width;
+ if (fixedAspect && !doc.fitWidth) doc.height = nheight / nwidth * doc.width;
else doc.height = actualdH;
}
else {
@@ -539,7 +539,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true);
}
doc.height = actualdH;
- if (fixedAspect) doc.width = nwidth / nheight * doc.height;
+ if (fixedAspect && !doc.fitWidth) doc.width = nwidth / nheight * doc.height;
else doc.width = actualdW;
}
} else {
@@ -617,7 +617,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
</div>
<div className="documentDecorations-container" style={{
width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
- height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight + 3) + "px",
left: bounds.x - this._resizeBorderWidth / 2,
top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
opacity: this._opacity
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index 2d1142f38..b3edad459 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -1,7 +1,7 @@
@import "globalCssVariables";
.inkingCanvas {
- opacity: 0.99;
+ // opacity: 0.99;
touch-action: none;
.jsx-parser {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 1526dad34..a244e22e7 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -234,7 +234,7 @@ export class MainView extends React.Component {
} else {
DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field => {
field instanceof Doc ? this.openWorkspace(field) :
- this.createNewWorkspace(CurrentUserUtils.MainDocId)
+ this.createNewWorkspace(CurrentUserUtils.MainDocId);
});
}
}
@@ -375,8 +375,9 @@ export class MainView extends React.Component {
}
flyoutWidthFunc = () => this.flyoutWidth;
addDocTabFunc = (doc: Doc, data: Opt<Doc>, where: string) => {
- if (where === "close")
+ if (where === "close") {
return CollectionDockingView.CloseRightSplit(doc);
+ }
if (doc.dockingConfig) {
this.openWorkspace(doc);
return true;
@@ -508,6 +509,7 @@ export class MainView extends React.Component {
<li key="marker"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} title="Highlighter" style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" /></button></li>
<li key="eraser"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} title="Eraser" style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" /></button></li>
<li key="inkControls"><InkingControl /></li>
+ <li key="logout"><button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></li>
</ul>
</div>
</div >;
@@ -523,12 +525,8 @@ export class MainView extends React.Component {
/* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
@computed
get miscButtons() {
- let logoutRef = React.createRef<HTMLDivElement>();
-
return [
this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
- <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
- <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
];
}
@@ -568,7 +566,7 @@ export class MainView extends React.Component {
let next = () => PresBox.CurrentPresentation.next();
let back = () => PresBox.CurrentPresentation.back();
let startOrResetPres = () => PresBox.CurrentPresentation.startOrResetPres();
- let closePresMode = action(() => { PresBox.CurrentPresentation.presMode = false; this.addDocTabFunc(PresBox.CurrentPresentation.props.Document); });
+ let closePresMode = action(() => { PresBox.CurrentPresentation.presMode = false; this.addDocTabFunc(PresBox.CurrentPresentation.props.Document, undefined, "onRight"); });
return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : <PresModeMenu next={next} back={back} presStatus={PresBox.CurrentPresentation.presStatus} startOrResetPres={startOrResetPres} closePresMode={closePresMode} > </PresModeMenu>;
}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index e4ef8313d..9e5e62e03 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -117,13 +117,13 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
@action
toggleChrome = (): void => {
this.props.docs.map(dv => {
- let layout = dv.Document.layout instanceof Doc ? dv.Document.layout as Doc : dv.Document;
+ let layout = dv.Document.layout instanceof Doc ? dv.Document.layout : dv.Document;
layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled");
});
}
render() {
- let layout = this.props.docs[0].Document.layout instanceof Doc ? this.props.docs[0].Document.layout as Doc : this.props.docs[0].Document;
+ let layout = this.props.docs[0].Document.layout instanceof Doc ? this.props.docs[0].Document.layout : this.props.docs[0].Document;
let templateMenu: Array<JSX.Element> = [];
this.props.templates.forEach((checked, template) =>
templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
index 7197f4b49..66ad6a76d 100644
--- a/src/client/views/animationtimeline/Keyframe.tsx
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -8,7 +8,6 @@ import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
import { Cast, NumCast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
import { createSchema, defaultSpec, makeInterface, listSpec } from "../../../new_fields/Schema";
-import { FlyoutProps } from "./Timeline";
import { Transform } from "../../util/Transform";
import { InkField, StrokeData } from "../../../new_fields/InkField";
import { TimelineMenu } from "./TimelineMenu";
@@ -138,48 +137,13 @@ export class Keyframe extends React.Component<IProps> {
@observable private _mouseToggled = false;
@observable private _doubleClickEnabled = false;
- @computed
- private get regiondata() {
- let index = this.regions.indexOf(this.props.RegionData);
- return RegionData(this.regions[index] as Doc);
- }
-
- @computed
- private get regions() {
- return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>;
- }
-
- @computed
- private get firstKeyframe() {
- let first: (Doc | undefined) = undefined;
- DocListCast(this.regiondata.keyframes!).forEach(kf => {
- if (kf.type !== KeyframeFunc.KeyframeType.fade) {
- if (!first || first && NumCast(kf.time) < NumCast(first.time)) {
- first = kf;
- }
- }
- });
- return first;
- }
-
- @computed
- private get lastKeyframe() {
- let last: (Doc | undefined) = undefined;
- DocListCast(this.regiondata.keyframes!).forEach(kf => {
- if (kf.type !== KeyframeFunc.KeyframeType.fade) {
- if (!last || last && NumCast(kf.time) > NumCast(last.time)) {
- last = kf;
- }
- }
- });
- return last;
- }
-
- @computed
- private get keyframes(){
- return DocListCast(this.regiondata.keyframes);
- }
-
+ @computed private get regiondata() { return RegionData(this.regions[this.regions.indexOf(this.props.RegionData)] as Doc);}
+ @computed private get regions() { return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>;}
+ @computed private get keyframes(){ return DocListCast(this.regiondata.keyframes); }
+ @computed private get pixelPosition(){ return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);}
+ @computed private get pixelDuration(){ return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelFadeIn() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get pixelFadeOut(){ return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
@computed
private get inks() {
if (this.props.collection.data_ext) {
@@ -191,38 +155,18 @@ export class Keyframe extends React.Component<IProps> {
}
}
- @computed
- private get pixelPosition(){
- return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
- }
-
- @computed
- private get pixelDuration(){
- return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
- }
-
- @computed
- private get pixelFadeIn() {
- return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
- }
-
- @computed
- private get pixelFadeOut(){
- return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
- }
-
- async componentWillMount() {
- if (!this.regiondata.keyframes) {
- this.regiondata.keyframes = new List<Doc>();
- }
- let fadeIn = await this.makeKeyData(this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade)!;
- let fadeOut = await this.makeKeyData(this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade)!;
- let start = await this.makeKeyData(this.regiondata.position, KeyframeFunc.KeyframeType.fade)!;
- let finish = await this.makeKeyData(this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.fade)!;
- (fadeIn.key! as Doc).opacity = 1;
- (fadeOut.key! as Doc).opacity = 1;
- (start.key! as Doc).opacity = 0.1;
- (finish.key! as Doc).opacity = 0.1;
+ componentWillMount() {
+ runInAction(async () => {
+ if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>();
+ let fadeIn = await this.makeKeyData(this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade)!;
+ let fadeOut = await this.makeKeyData(this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade)!;
+ let start = await this.makeKeyData(this.regiondata.position, KeyframeFunc.KeyframeType.fade)!;
+ let finish = await this.makeKeyData(this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.fade)!;
+ (fadeIn.key! as Doc).opacity = 1;
+ (fadeOut.key! as Doc).opacity = 1;
+ (start.key! as Doc).opacity = 0.1;
+ (finish.key! as Doc).opacity = 0.1;
+ });
}
@action
@@ -336,7 +280,7 @@ export class Keyframe extends React.Component<IProps> {
let bar = this._bar.current!;
let offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
let leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions);
- let firstkf: (Doc | undefined) = this.firstKeyframe;
+ let firstkf: (Doc | undefined) = this.keyframes[0];
if (firstkf && this.regiondata.position + this.regiondata.fadeIn + offset >= NumCast(firstkf!.time)) {
let dif = NumCast(firstkf!.time) - (this.pixelPosition + this.pixelFadeIn);
this.regiondata.position = NumCast(firstkf!.time) - this.regiondata.fadeIn;
@@ -364,8 +308,8 @@ export class Keyframe extends React.Component<IProps> {
let bar = this._bar.current!;
let offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
let rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions);
- if (this.lastKeyframe! && this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= NumCast((this.lastKeyframe! as Doc).time)) {
- let dif = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut - NumCast((this.lastKeyframe! as Doc).time);
+ if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= NumCast((this.keyframes[this.keyframes.length - 1]).time)) {
+ let dif = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut - NumCast((this.keyframes[this.keyframes.length - 1]).time);
this.regiondata.duration -= dif;
} else if (this.regiondata.duration + offset < this.regiondata.fadeIn + this.regiondata.fadeOut) { // nokeyframes, just fades
this.regiondata.duration = this.regiondata.fadeIn + this.regiondata.fadeOut;
@@ -532,6 +476,7 @@ export class Keyframe extends React.Component<IProps> {
e.stopPropagation();
let div = ref.current!;
div.style.opacity = "1";
+ Doc.BrushDoc(this.props.node);
}
onContainerOut = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
@@ -539,6 +484,7 @@ export class Keyframe extends React.Component<IProps> {
e.stopPropagation();
let div = ref.current!;
div.style.opacity = "0";
+ Doc.UnBrushDoc(this.props.node);
}
@@ -623,7 +569,6 @@ export class Keyframe extends React.Component<IProps> {
}
}
render() {
- console.log("RERENDERING");
return (
<div>
<div className="bar" ref={this._bar} style={{ transform: `translate(${this.pixelPosition}px)`,
diff --git a/src/client/views/animationtimeline/Timeline.scss b/src/client/views/animationtimeline/Timeline.scss
index 1457d5a84..09fc593fc 100644
--- a/src/client/views/animationtimeline/Timeline.scss
+++ b/src/client/views/animationtimeline/Timeline.scss
@@ -1,11 +1,6 @@
@import "./../globalCssVariables.scss";
-.minimize{
- position:relative;
- z-index: 1000;
- height: 30px;
- width: 100px;
-}
+
.timeline-toolbox{
position:absolute;
@@ -17,6 +12,8 @@
margin-left:10px;
}
}
+
+
.timeline-container{
width:100%;
height:300px;
@@ -37,12 +34,10 @@
background-color: transparent;
height: 30px;
width:100%;
-
.tick{
height:100%;
width: 1px;
background-color:black;
-
}
}
.scrubber{
@@ -119,4 +114,49 @@
width: 100%;
background-color: grey;
}
+}
+
+.round-toggle {
+ position: absolute;
+ height: 40px;
+ width: 80px;
+ background-color: white;
+ border: 2px solid purple;
+ border-radius: 20px;
+ animation-fill-mode: forwards;
+ animation-duration: 500ms;
+ input{
+ position:absolute;
+ opacity: 0;
+ height: 0;
+ width: 0;
+ }
+ .round-toggle-slider{
+ position:absolute;
+ height: 35px;
+ width: 35px;
+ top: 0.5px;
+ background-color: white;
+ border:1px solid grey;
+ border-radius: 20px;
+ transition: transform 500ms ease-in-out;
+
+ }
+
+}
+@keyframes turnon{
+ from{
+ background-color: white;
+ }
+ to{
+ background-color: purple;
+ }
+}
+@keyframes turnoff{
+ from{
+ background-color: purple;
+ }
+ to{
+ background-color: white;
+ }
} \ No newline at end of file
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index c50ffa51b..8127e4de2 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -15,17 +15,6 @@ import { TimelineOverview } from "./TimelineOverview";
import { FieldViewProps } from "../nodes/FieldView";
import { KeyframeFunc } from "./Keyframe";
-
-
-export interface FlyoutProps {
- x?: number;
- y?: number;
- display?: string;
- regiondata?: Doc;
- regions?: List<Doc>;
-}
-
-
@observer
export class Timeline extends React.Component<FieldViewProps> {
@@ -35,26 +24,23 @@ export class Timeline extends React.Component<FieldViewProps> {
private readonly MAX_CONTAINER_HEIGHT: number = 800;
private readonly DEFAULT_TICK_INCREMENT: number = 1000;
- @observable private _isMinimized = false;
- @observable private _tickSpacing = this.DEFAULT_TICK_SPACING;
- @observable private _tickIncrement = this.DEFAULT_TICK_INCREMENT;
-
@observable private _scrubberbox = React.createRef<HTMLDivElement>();
- @observable private _scrubber = React.createRef<HTMLDivElement>();
@observable private _trackbox = React.createRef<HTMLDivElement>();
@observable private _titleContainer = React.createRef<HTMLDivElement>();
@observable private _timelineContainer = React.createRef<HTMLDivElement>();
- @observable private _timelineWrapper = React.createRef<HTMLDivElement>();
@observable private _infoContainer = React.createRef<HTMLDivElement>();
+ @observable private _roundToggleRef = React.createRef<HTMLDivElement>();
+ @observable private _roundToggleContainerRef = React.createRef<HTMLDivElement>();
@observable private _currentBarX: number = 0;
@observable private _windSpeed: number = 1;
@observable private _isPlaying: boolean = false; //scrubber playing
- @observable private _isFrozen: boolean = true; //timeline freeze
@observable private _totalLength: number = 0;
@observable private _visibleLength: number = 0;
@observable private _visibleStart: number = 0;
- @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT;
+ @observable private _containerHeight: number = this.DEFAULT_CONTAINER_HEIGHT;
+ @observable private _tickSpacing = this.DEFAULT_TICK_SPACING;
+ @observable private _tickIncrement = this.DEFAULT_TICK_INCREMENT;
@observable private _time = 100000; //DEFAULT
@observable private _ticks: number[] = [];
@observable private _playButton = faPlayCircle;
@@ -273,39 +259,7 @@ export class Timeline extends React.Component<FieldViewProps> {
}
}
- @action
- onTimelineDown = (e: React.PointerEvent) => {
- e.preventDefault();
- if (e.nativeEvent.which === 1 && !this._isFrozen) {
- document.addEventListener("pointermove", this.onTimelineMove);
- document.addEventListener("pointerup", () => { document.removeEventListener("pointermove", this.onTimelineMove); });
- }
- }
- @action
- onTimelineMove = (e: PointerEvent) => {
- e.preventDefault();
- e.stopPropagation();
- let timelineContainer = this._timelineWrapper.current!;
- let left = parseFloat(timelineContainer.style.left!);
- let top = parseFloat(timelineContainer.style.top!);
- timelineContainer.style.left = `${left + e.movementX}px`;
- timelineContainer.style.top = `${top + e.movementY}px`;
- }
-
- @action
- minimize = (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- let timelineContainer = this._timelineContainer.current!;
- if (this._isMinimized) {
- this._isMinimized = false;
- timelineContainer.style.visibility = "visible";
- } else {
- this._isMinimized = true;
- timelineContainer.style.visibility = "hidden";
- }
- }
@action
toReadTime = (time: number): string => {
@@ -321,21 +275,6 @@ export class Timeline extends React.Component<FieldViewProps> {
timelineContextMenu = (e:MouseEvent): void => {
let subitems: ContextMenuProps[] = [];
- let timelineContainer = this._timelineWrapper.current!;
- subitems.push({
- description: "Pin to Top", event: action(() => {
- if (!this._isFrozen) {
- timelineContainer.style.left = "0px";
- timelineContainer.style.top = "0px";
- timelineContainer.style.transition = "none";
- }
- }), icon: faArrowUp
- });
- subitems.push({
- description: this._isFrozen ? "Unfreeze Timeline" : "Freeze Timeline", event: action(() => {
- this._isFrozen = !this._isFrozen;
- }), icon: "thumbtack"
- });
subitems.push({
description: this._timelineVisible ? "Hide Timeline" : "Show Timeline", event: action(() => {
this._timelineVisible = !this._timelineVisible;
@@ -358,7 +297,8 @@ export class Timeline extends React.Component<FieldViewProps> {
let currPixel = KeyframeFunc.convertPixelTime(prevTime, "mili", "pixel", this._tickSpacing, this._tickIncrement);
let currCurrent = KeyframeFunc.convertPixelTime(prevCurrent, "mili", "pixel", this._tickSpacing, this._tickIncrement);
this._infoContainer.current!.scrollLeft = currPixel - offset;
- this._visibleStart = currPixel - offset;
+ this._visibleStart = currPixel - offset > 0 ? currPixel - offset : 0;
+ this._visibleStart += this._visibleLength + this._visibleStart > this._totalLength ? this._totalLength - (this._visibleStart + this._visibleLength) :0;
this.changeCurrentBarX(currCurrent);
}
@@ -393,44 +333,70 @@ export class Timeline extends React.Component<FieldViewProps> {
private timelineToolBox = (scale:number) => {
let size = 50 * scale; //50 is default
- return (
+ return (
+
<div key="timeline_toolbox" className="timeline-toolbox" style={{height:`${size}px`}}>
- <div key="timeline_windBack" onClick={this.windBackward}> <FontAwesomeIcon icon={faBackward} style={{height:`${size}px`, width: `${size}px`}} /> </div>
- <div key =" timeline_play" onClick={this.onPlay}> <FontAwesomeIcon icon={this._playButton} style={{height:`${size}px`, width: `${size}px`}} /> </div>
- <div key="timeline_windForward" onClick={this.windForward}> <FontAwesomeIcon icon={faForward} style={{height:`${size}px`, width: `${size}px`}} /> </div>
- <TimelineOverview scale={scale} currentBarX={this._currentBarX} totalLength={this._totalLength} visibleLength={this._visibleLength} visibleStart={this._visibleStart} changeCurrentBarX={this.changeCurrentBarX} movePanX={this.movePanX}/>
+ <div key="timeline_windBack" onClick={this.windBackward}> <FontAwesomeIcon icon={faBackward} style={{height:`${size}px`, width: `${size}px`}} /> </div>
+ <div key =" timeline_play" onClick={this.onPlay}> <FontAwesomeIcon icon={this._playButton} style={{height:`${size}px`, width: `${size}px`}} /> </div>
+ <div key="timeline_windForward" onClick={this.windForward}> <FontAwesomeIcon icon={faForward} style={{height:`${size}px`, width: `${size}px`}} /> </div>
+ <TimelineOverview scale={scale} currentBarX={this._currentBarX} totalLength={this._totalLength} visibleLength={this._visibleLength} visibleStart={this._visibleStart} changeCurrentBarX={this.changeCurrentBarX} movePanX={this.movePanX}/>
+ <div ref={this._roundToggleContainerRef}key="round-toggle" className="round-toggle">
+ <div ref={this._roundToggleRef} className="round-toggle-slider" onPointerDown = {this.toggleChecked}> </div>
+ </div>
</div>
);
}
+
+ @action
+ private toggleChecked = (e:React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ let roundToggle = this._roundToggleRef.current!;
+ let roundToggleContainer = this._roundToggleContainerRef.current!;
+ if (BoolCast(this.props.Document.isAnimating)){
+ roundToggle.style.transform = "translate(0px, 0px)";
+ roundToggle.style.animationName = "turnoff";
+ roundToggleContainer.style.animationName = "turnoff";
+
+ this.props.Document.isAnimating = false;
+ } else {
+ roundToggle.style.transform = "translate(45px, 0px)";
+ roundToggle.style.animationName = "turnon";
+ roundToggleContainer.style.animationName = "turnon";
+ this.props.Document.isAnimating = true;
+ }
+ }
render() {
return (
- <div style={{visibility: this._timelineVisible ? "visible" : "hidden"}}>
- <div key="timeline_wrapper" style={{visibility: BoolCast(this.props.Document.isAnimating && this._timelineVisible) ? "visible" :"hidden", left: "0px", top: "0px", position: "absolute", width: "100%", transform: "translate(0px, 0px)"}} ref={this._timelineWrapper}>
- <button key="timeline_minimize" className="minimize" onClick={this.minimize}>Minimize</button>
- <div key="timeline_container" className="timeline-container" style={{ height: `${this._containerHeight}px`, left: "0px", top: "30px" }} ref={this._timelineContainer} onPointerDown={this.onTimelineDown}>
- {this.timelineToolBox(0.5)}
- <div key ="timeline_info"className="info-container" ref={this._infoContainer} onWheel={this.onWheelZoom}>
- <div key="timeline_scrubberbox" className="scrubberbox" ref={this._scrubberbox} style={{width: `${this._totalLength}px`}} onClick={this.onScrubberClick}>
- {this._ticks.map(element => {
- if(element % this._tickIncrement === 0) return <div className="tick" style={{ transform: `translate(${(element / this._tickIncrement)* this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p>{this.toReadTime(element)}</p></div>;
- })}
+ <div>
+ <div style={{visibility: this._timelineVisible ? "visible" : "hidden"}}>
+ <div key="timeline_wrapper" style={{visibility: BoolCast(this.props.Document.isAnimating && this._timelineVisible) ? "visible" :"hidden", left: "0px", top: "0px", position: "absolute", width: "100%", transform: "translate(0px, 0px)"}}>
+ <div key="timeline_container" className="timeline-container" ref={this._timelineContainer} style={{ height: `${this._containerHeight}px`, left: "0px", top: "30px" }}>
+ <div key ="timeline_info"className="info-container" ref={this._infoContainer} onWheel={this.onWheelZoom}>
+ <div key="timeline_scrubberbox" className="scrubberbox" ref={this._scrubberbox} style={{width: `${this._totalLength}px`}} onClick={this.onScrubberClick}>
+ {this._ticks.map(element => {
+ if(element % this._tickIncrement === 0) return <div className="tick" style={{ transform: `translate(${(element / this._tickIncrement)* this._tickSpacing}px)`, position: "absolute", pointerEvents: "none" }}> <p>{this.toReadTime(element)}</p></div>;
+ })}
+ </div>
+ <div key="timeline_scrubber" className="scrubber" onPointerDown={this.onScrubberDown} style={{ transform: `translate(${this._currentBarX}px)` }}>
+ <div key="timeline_scrubberhead" className="scrubberhead"></div>
+ </div>
+ <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown} style={{width: `${this._totalLength}px`}}>
+ {DocListCast(this.children).map(doc => <Track node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} transform={this.props.ScreenToLocalTransform()} time={this._time} tickSpacing = {this._tickSpacing} tickIncrement ={this._tickIncrement} collection = {this.props.Document} timelineVisible = {this._timelineVisible}/>)}
+ </div>
</div>
- <div key="timeline_scrubber" className="scrubber" ref={this._scrubber} onPointerDown={this.onScrubberDown} style={{ transform: `translate(${this._currentBarX}px)` }}>
- <div key="timeline_scrubberhead" className="scrubberhead"></div>
+ <div key="timeline_title"className="title-container" ref={this._titleContainer}>
+ {DocListCast(this.children).map(doc => <div className="datapane" onPointerOver={() => {Doc.BrushDoc(doc);}} onPointerOut={() => {Doc.UnBrushDoc(doc);}}><p>{doc.title}</p></div>)}
</div>
- <div key="timeline_trackbox" className="trackbox" ref={this._trackbox} onPointerDown={this.onPanDown} style={{width: `${this._totalLength}px`}}>
- {DocListCast(this.children).map(doc => <Track node={doc} currentBarX={this._currentBarX} changeCurrentBarX={this.changeCurrentBarX} transform={this.props.ScreenToLocalTransform()} time={this._time} tickSpacing = {this._tickSpacing} tickIncrement ={this._tickIncrement} collection = {this.props.Document} timelineVisible = {this._timelineVisible}/>)}
+ <div key="timeline_resize" onPointerDown={this.onResizeDown}>
+ <FontAwesomeIcon className="resize" icon={faGripLines}/>
</div>
</div>
- <div key="timeline_title"className="title-container" ref={this._titleContainer}>
- {DocListCast(this.children).map(doc => <div className="datapane"><p>{doc.title}</p></div>)}
- </div>
- <div key="timeline_resize" onPointerDown={this.onResizeDown}>
- <FontAwesomeIcon className="resize" icon={faGripLines} />
- </div>
</div>
+ { this.timelineToolBox(1) }
</div>
- {BoolCast(this.props.Document.isAnimating) ? <div></div>: this.timelineToolBox(1) }
+
+
</div>
);
}
diff --git a/src/client/views/animationtimeline/TimelineMenu.tsx b/src/client/views/animationtimeline/TimelineMenu.tsx
index f3b985297..59c25596e 100644
--- a/src/client/views/animationtimeline/TimelineMenu.tsx
+++ b/src/client/views/animationtimeline/TimelineMenu.tsx
@@ -4,6 +4,7 @@ import {observer} from "mobx-react";
import "./TimelineMenu.scss";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChartLine, faRoad, faClipboard, faPen, faTrash, faTable } from "@fortawesome/free-solid-svg-icons";
+import { Utils } from "../../../Utils";
@observer
@@ -40,7 +41,7 @@ export class TimelineMenu extends React.Component {
if (type === "input"){
let inputRef = React.createRef<HTMLInputElement>();
let text = "";
- this._currentMenu.push(<div className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard} size="lg"/><input className="timeline-menu-input" ref = {inputRef} placeholder={title} onChange={(e) => {
+ this._currentMenu.push(<div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faClipboard} size="lg"/><input className="timeline-menu-input" ref = {inputRef} placeholder={title} onChange={(e) => {
e.stopPropagation();
text = e.target.value;
}} onKeyDown={(e) => {
@@ -52,23 +53,23 @@ export class TimelineMenu extends React.Component {
}}/></div>);
} else if (type === "button") {
let buttonRef = React.createRef<HTMLDivElement>();
- this._currentMenu.push( <div className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine}size="lg"/><p className="timeline-menu-desc" onClick={(e) => {
+ this._currentMenu.push( <div key={Utils.GenerateGuid()} className="timeline-menu-item"><FontAwesomeIcon icon={faChartLine}size="lg"/><p className="timeline-menu-desc" onClick={(e) => {
e.preventDefault();
e.stopPropagation();
event(e);
this.closeMenu();
}}>{title}</p></div>);
- }
+ }
}
@action
addMenu = (title:string) => {
- this._currentMenu.unshift(<div className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>);
+ this._currentMenu.unshift(<div key={Utils.GenerateGuid()} className="timeline-menu-header"><p className="timeline-menu-header-desc">{title}</p></div>);
}
render() {
return (
- <div className="timeline-menu-container" style={{opacity: this._opacity, left: this._x, top: this._y}} >
+ <div key={Utils.GenerateGuid()} className="timeline-menu-container" style={{opacity: this._opacity, left: this._x, top: this._y}} >
{this._currentMenu}
</div>
);
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
index c68d9bb3a..3ec410216 100644
--- a/src/client/views/animationtimeline/Track.tsx
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -32,18 +32,24 @@ export class Track extends React.Component<IProps> {
@observable private _onKeyframe: (Doc | undefined) = undefined;
@observable private _onRegionData: (Doc | undefined) = undefined;
@observable private _storedState: (Doc | undefined) = undefined;
-
- @computed
- private get regions() {
- return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>;
- }
+ @observable private filterList = [
+ "regions",
+ "cursors",
+ "hidden",
+ "nativeHeight",
+ "nativeWidth",
+ "schemaColumns",
+ "baseLayout",
+ "backgroundLayout",
+ "layout",
+ ];
+
+ @computed private get regions() { return Cast(this.props.node.regions, listSpec(Doc)) as List<Doc>;}
componentWillMount() {
- if (!this.props.node.regions) {
- this.props.node.regions = new List<Doc>();
- }
-
-
+ runInAction(() => {
+ if (!this.props.node.regions) this.props.node.regions = new List<Doc>();
+ });
}
componentDidMount() {
@@ -54,11 +60,11 @@ export class Track extends React.Component<IProps> {
this.props.node.hidden = false;
this.props.node.opacity = 1;
});
-
}
componentWillUnmount() {
runInAction(() => {
+ //disposing reactions
if (this._currentBarXReaction) this._currentBarXReaction();
if (this._timelineVisibleReaction) this._timelineVisibleReaction();
});
@@ -166,17 +172,7 @@ export class Track extends React.Component<IProps> {
});
}
- private filterList = [
- "regions",
- "cursors",
- "hidden",
- "nativeHeight",
- "nativeWidth",
- "schemaColumns",
- "baseLayout",
- "backgroundLayout",
- "layout",
- ];
+
@action
private filterKeys = (keys: string[]): string[] => {
@@ -294,7 +290,7 @@ export class Track extends React.Component<IProps> {
return (
<div className="track-container">
<div className="track">
- <div className="inner" ref={this._inner} onDoubleClick={this.onInnerDoubleClick}>
+ <div className="inner" ref={this._inner} onDoubleClick={this.onInnerDoubleClick} onPointerOver = {() => {Doc.BrushDoc(this.props.node);}}onPointerOut={() => {Doc.UnBrushDoc(this.props.node);}}>
{DocListCast(this.regions).map((region) => {
return <Keyframe {...this.props} RegionData={region} />;
})}
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 56d12bd84..0168c466f 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -20,7 +20,8 @@ export enum CollectionViewType {
Docking,
Tree,
Stacking,
- Masonry
+ Masonry,
+ Pivot,
}
export namespace CollectionViewType {
@@ -32,7 +33,8 @@ export namespace CollectionViewType {
["docking", CollectionViewType.Docking],
["tree", CollectionViewType.Tree],
["stacking", CollectionViewType.Stacking],
- ["masonry", CollectionViewType.Masonry]
+ ["masonry", CollectionViewType.Masonry],
+ ["pivot", CollectionViewType.Pivot]
]);
export const valueOf = (value: string) => {
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 0e7e0afa7..6f5abd05b 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -1,8 +1,5 @@
@import "../../views/globalCssVariables.scss";
-.collectiondockingview-content {
- height: 100%;
-}
.lm_active .messageCounter{
color:white;
background: #999999;
@@ -21,7 +18,7 @@
.collectiondockingview-container {
width: 100%;
- height: 100%;
+ height:100%;
border-style: solid;
border-width: $COLLECTION_BORDER_WIDTH;
position: absolute;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 8fcba99e3..b047e77a8 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -9,7 +9,7 @@ import * as ReactDOM from 'react-dom';
import Measure from "react-measure";
import * as GoldenLayout from "../../../client/goldenLayout";
import { DateField } from '../../../new_fields/DateField';
-import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Field, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { FieldId } from "../../../new_fields/RefField";
@@ -30,7 +30,9 @@ import "./CollectionDockingView.scss";
import { SubCollectionViewProps } from "./CollectionSubView";
import React = require("react");
import { ButtonSelector } from './ParentDocumentSelector';
+import { DocumentType } from '../../documents/DocumentTypes';
library.add(faFile);
+const _global = (window /* browser */ || global /* node */) as any;
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
@@ -533,12 +535,11 @@ interface DockedFrameProps {
}
@observer
export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
- _mainCont: HTMLDivElement | undefined = undefined;
+ _mainCont: HTMLDivElement | null = null;
@observable private _panelWidth = 0;
@observable private _panelHeight = 0;
@observable private _document: Opt<Doc>;
@observable private _dataDoc: Opt<Doc>;
-
@observable private _isActive: boolean = false;
get _stack(): any {
@@ -576,6 +577,13 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
componentDidMount() {
+ let observer = new _global.ResizeObserver(action((entries: any) => {
+ for (let entry of entries) {
+ this._panelWidth = entry.contentRect.width;
+ this._panelHeight = entry.contentRect.height;
+ }
+ }));
+ observer.observe(this.props.glContainer._element[0]);
this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged);
this.props.glContainer.on("tab", this.onActiveContentItemChanged);
this.onActiveContentItemChanged();
@@ -594,13 +602,21 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
- panelWidth = () => this._document!.ignoreAspect ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
- panelHeight = () => this._document!.ignoreAspect ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), NumCast(this._document!.nativeHeight, this._panelHeight)));
+ panelWidth = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
+ panelHeight = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), this.nativeHeight()));
- nativeWidth = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0;
- nativeHeight = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0;
+ nativeWidth = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0;
+ nativeHeight = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0;
contentScaling = () => {
+ if (this._document!.type === DocumentType.PDF) {
+ if ((this._document && this._document.fitWidth) ||
+ this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth)) {
+ return this._panelWidth / NumCast(this._document!.nativeWidth);
+ } else {
+ return this._panelHeight / NumCast(this._document!.nativeHeight);
+ }
+ }
const nativeH = this.nativeHeight();
const nativeW = this.nativeWidth();
if (!nativeW || !nativeH) return 1;
@@ -619,6 +635,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth() / this.ScreenToLocalTransform().Scale) / 2 : 0; }
addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
+ SelectionManager.DeselectAll();
if (doc.dockingConfig) {
MainView.Instance.openWorkspace(doc);
return true;
@@ -630,13 +647,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
}
}
- @computed get docView() {
- if (!this._document) {
- return (null);
- }
- let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc;
- return <DocumentView key={this._document[Id]}
- Document={this._document}
+ docView(document: Doc) {
+ let resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc;
+ return <DocumentView key={document[Id]}
+ Document={document}
DataDoc={resolvedDataDoc}
bringToFront={emptyFunction}
addDocument={undefined}
@@ -659,28 +673,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
getScale={returnOne} />;
}
- @computed get content() {
- return (
- <div className="collectionDockingView-content" ref={action((ref: HTMLDivElement) => {
- this._mainCont = ref;
- if (ref) {
- this._panelWidth = Number(getComputedStyle(ref).width!.replace("px", ""));
- this._panelHeight = Number(getComputedStyle(ref).height!.replace("px", ""));
- }
- })}
- style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
- {this.docView}
- </div >);
- }
-
render() {
- if (!this._isActive || !this._document) return null;
- let theContent = this.content;
- return !this._document ? (null) :
- <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
- {({ measureRef }) => <div ref={measureRef}>
- {theContent}
- </div>}
- </Measure>;
+ return (!this._isActive || !this._document) ? (null) :
+ (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref}
+ style={{
+ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`,
+ height: this._document && this._document.fitWidth ? undefined : "100%"
+ }}>
+ {this.docView(this._document)}
+ </div >);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
index 50201bae8..62ec8a5be 100644
--- a/src/client/views/collections/CollectionPDFView.scss
+++ b/src/client/views/collections/CollectionPDFView.scss
@@ -1,26 +1,4 @@
-.collectionPdfView-buttonTray {
- top: 15px;
- left: 20px;
- position: relative;
- transform-origin: left top;
- position: absolute;
-}
-.collectionPdfView-thumb {
- width: 25px;
- height: 25px;
- transform-origin: left top;
- position: absolute;
- background: darkgray;
-}
-
-.collectionPdfView-slider {
- width: 25px;
- height: 25px;
- transform-origin: left top;
- position: absolute;
- background: lightgray;
-}
.collectionPdfView-cont {
width: 100%;
@@ -29,28 +7,5 @@
top: 0;
left: 0;
z-index: -1;
+ overflow: hidden !important;
}
-
-.collectionPdfView-cont-dragging {
- span {
- user-select: none;
- }
-}
-
-.collectionPdfView-backward {
- color: white;
- font-size: 24px;
- top: 0px;
- left: 0px;
- position: absolute;
- background-color: rgba(50, 50, 50, 0.2);
-}
-
-.collectionPdfView-forward {
- color: white;
- font-size: 24px;
- top: 0px;
- left: 45px;
- 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 8eda4d9ee..cc8142ec0 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -1,10 +1,9 @@
-import { computed } from "mobx";
+import { trace } from "mobx";
import { observer } from "mobx-react";
import { Id } from "../../../new_fields/FieldSymbols";
import { emptyFunction } from "../../../Utils";
import { ContextMenu } from "../ContextMenu";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { PDFBox } from "../nodes/PDFBox";
import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import "./CollectionPDFView.scss";
@@ -17,35 +16,18 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt);
}
- private _pdfBox?: PDFBox;
- private _buttonTray: React.RefObject<HTMLDivElement> = React.createRef();
-
- @computed
- get uIButtons() {
- return (
- <div className="collectionPdfView-buttonTray" ref={this._buttonTray} key="tray" style={{ height: "100%" }}>
- <button className="collectionPdfView-backward" onClick={() => this._pdfBox && this._pdfBox.BackPage()}>{"<"}</button>
- <button className="collectionPdfView-forward" onClick={() => this._pdfBox && this._pdfBox.ForwardPage()}>{">"}</button>
- </div>
- );
- }
-
onContextMenu = (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: "PDFOptions", event: emptyFunction, icon: "file-pdf" });
}
}
- setPdfBox = (pdfBox: PDFBox) => { this._pdfBox = pdfBox; };
-
subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
- return (<>
- <CollectionFreeFormView {...this.props} {...renderProps} setPdfBox={this.setPdfBox} CollectionView={this} chromeCollapsed={true} />
- {renderProps.active() ? this.uIButtons : (null)}
- </>);
+ return (<CollectionFreeFormView {...this.props} {...renderProps} CollectionView={this} chromeCollapsed={true} />);
}
render() {
+ trace();
return (
<CollectionBaseView {...this.props} className={"collectionPdfView-cont"} onContextMenu={this.onContextMenu}>
{this.subView}
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 4dac27e60..179e44266 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -34,7 +34,7 @@ export interface CellProps {
row: number;
col: number;
rowProps: CellInfo;
- CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
fieldKey: string;
@@ -151,7 +151,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
fieldExt: "",
ruleProvider: undefined,
ContainingCollectionView: this.props.CollectionView,
- ContainingCollectionDoc: this.props.CollectionView.props.Document,
+ ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document,
isSelected: returnFalse,
select: emptyFunction,
renderDepth: this.props.renderDepth + 1,
@@ -301,7 +301,7 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
render() {
let reference = React.createRef<HTMLDivElement>();
let onItemDown = (e: React.PointerEvent) => {
- (!this.props.CollectionView.props.isSelected() ? undefined :
+ (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined :
SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));
};
return (
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 7bd2a1971..8d931f812 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -246,7 +246,7 @@ export interface SchemaTableProps {
PanelHeight: () => number;
PanelWidth: () => number;
childDocs?: Doc[];
- CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
ContainingCollectionDoc: Opt<Doc>;
fieldKey: string;
@@ -804,7 +804,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
csv.substring(0, csv.length - 1);
let dbName = StrCast(this.props.Document.title);
let res = await Gateway.Instance.PostSchema(csv, dbName);
- if (self.props.CollectionView.props.addDocument) {
+ if (self.props.CollectionView && self.props.CollectionView.props.addDocument) {
let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
if (schemaDoc) {
//self.props.CollectionView.props.addDocument(schemaDoc, false);
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ccf131797..45de0fefa 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -42,7 +42,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
@computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
@computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
- @computed get showAddAGroup() { return (this.sectionFilter && (this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled')); }
+ @computed get showAddAGroup() { return (this.sectionFilter && this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled')); }
@computed get columnWidth() {
return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin,
this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250));
@@ -160,13 +160,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
if (!d) return 0;
let nw = NumCast(d.nativeWidth);
let nh = NumCast(d.nativeHeight);
- if (!d.ignoreAspect && nw && nh) {
+ if (!d.ignoreAspect && !d.fitWidth && nw && nh) {
let aspect = nw && nh ? nh / nw : 1;
let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid);
return wid * aspect;
}
- return d[HeightSym]();
+ return d.fitWidth ? Math.min(this.props.PanelHeight() - 2 * this.yMargin, d[HeightSym]()) : d[HeightSym]();
}
columnDividerDown = (e: React.PointerEvent) => {
@@ -347,7 +347,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
onToggle = (checked: Boolean) => {
- this.props.CollectionView.props.Document.chromeStatus = checked ? "collapsed" : "view-mode";
+ this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus = checked ? "collapsed" : "view-mode");
}
onContextMenu = (e: React.MouseEvent): void => {
@@ -391,10 +391,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
<EditableView {...editableViewProps} />
</div>}
- {this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? <Switch
+ {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch
onChange={this.onToggle}
onClick={this.onToggle}
- defaultChecked={this.props.CollectionView.props.Document.chromeStatus !== 'view-mode'}
+ defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'}
checkedChildren="edit"
unCheckedChildren="view"
/> : null}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index b3b7b40dd..240adf428 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -266,7 +266,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
style={{
width: (style.columnWidth) /
((uniqueHeadings.length +
- ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
+ ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
}}>
{/* the default bucket (no key value) has a tooltip that describes what it is.
Further, it does not have a color and cannot be deleted. */}
@@ -297,7 +297,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
</div> : (null);
for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
return (
- <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
+ <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
{headingView}
<div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
@@ -315,7 +315,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
{this.children(this.props.docList)}
{singleColumn ? (null) : this.props.parent.columnDragger}
</div>
- {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ?
+ {(this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ?
<div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
style={{ width: style.columnWidth / style.numGroupColumns }}>
<EditableView {...newEditableViewProps} />
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index c11dd6150..069269b06 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,12 +1,12 @@
import { action, computed, IReactionDisposer, reaction } from "mobx";
import * as rp from 'request-promise';
import CursorField from "../../../new_fields/CursorField";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { BoolCast, Cast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from "../../../Utils";
@@ -30,10 +30,11 @@ export interface CollectionViewProps extends FieldViewProps {
PanelWidth: () => number;
PanelHeight: () => number;
chromeCollapsed: boolean;
+ setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
}
export interface SubCollectionViewProps extends CollectionViewProps {
- CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
ruleProvider: Doc | undefined;
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 5f4742834..d3072ff1e 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -18,6 +18,7 @@ import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
import { CollectionTreeView } from "./CollectionTreeView";
import { CollectionViewBaseChrome } from './CollectionViewChromes';
+import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
export const COLLECTION_BORDER_WIDTH = 2;
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -59,8 +60,10 @@ export class CollectionView extends React.Component<FieldViewProps> {
case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
+ case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
case CollectionViewType.Freeform:
default:
+ this.props.Document.freeformLayoutEngine = undefined;
return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
}
return (null);
@@ -89,7 +92,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
let existingVm = ContextMenu.Instance.findByDescription("View Modes...");
let subItems: ContextMenuProps[] = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
- subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; delete this.props.Document.usePivotLayout; }, icon: "signature" });
+ subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" });
if (CollectionBaseView.InSafeMode()) {
ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" });
}
@@ -103,10 +106,10 @@ export class CollectionView extends React.Component<FieldViewProps> {
}, icon: "ellipsis-v"
});
subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" });
+ subItems.push({ description: "Pivot", event: () => this.props.Document.viewType = CollectionViewType.Pivot, icon: "columns" });
switch (this.props.Document.viewType) {
case CollectionViewType.Freeform: {
- subItems.push({ description: "Custom", icon: "fingerprint", event: CollectionFreeFormView.AddCustomLayout(this.props.Document, this.props.fieldKey) });
- subItems.push({ description: "Pivot", icon: "copy", event: () => this.props.Document.usePivotLayout = true });
+ subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
break;
}
}
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 20786f690..cefa9eebc 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -260,7 +260,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@observable private pivotKeyDisplay = this.pivotKey;
getPivotInput = () => {
- if (!this.document.usePivotLayout) {
+ if (StrCast(this.document.freeformLayoutEngine) !== "pivot") {
return (null);
}
return (<input className="collectionViewBaseChrome-viewSpecsInput"
@@ -396,6 +396,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree View</option>
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option>
</select>
<div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}>
<input className="collectionViewBaseChrome-viewSpecsInput"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
new file mode 100644
index 000000000..886692172
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -0,0 +1,117 @@
+import { Doc, Field, FieldResult } from "../../../../new_fields/Doc";
+import { NumCast, StrCast, Cast } from "../../../../new_fields/Types";
+import { ScriptBox } from "../../ScriptBox";
+import { CompileScript } from "../../../util/Scripting";
+import { ScriptField } from "../../../../new_fields/ScriptField";
+import { OverlayView, OverlayElementOptions } from "../../OverlayView";
+import { emptyFunction } from "../../../../Utils";
+import React = require("react");
+
+interface PivotData {
+ type: string;
+ text: string;
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ fontSize: number;
+}
+
+export interface ViewDefBounds {
+ x: number;
+ y: number;
+ z?: number;
+ width: number;
+ height: number;
+ transition?: string;
+}
+
+export interface ViewDefResult {
+ ele: JSX.Element;
+ bounds?: ViewDefBounds;
+}
+
+export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
+ let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
+ const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200);
+ const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
+
+ for (const doc of childDocs) {
+ const val = doc[StrCast(pivotDoc.pivotField, "title")];
+ if (val) {
+ !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []);
+ pivotColumnGroups.get(val)!.push(doc);
+ }
+ }
+
+ const minSize = Array.from(pivotColumnGroups.entries()).reduce((min, pair) => Math.min(min, pair[1].length), Infinity);
+ const numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize)));
+ const docMap = new Map<Doc, ViewDefBounds>();
+ const groupNames: PivotData[] = [];
+
+ let x = 0;
+ pivotColumnGroups.forEach((val, key) => {
+ let y = 0;
+ let xCount = 0;
+ groupNames.push({
+ type: "text",
+ text: String(key),
+ x,
+ y: pivotAxisWidth + 50,
+ width: pivotAxisWidth * 1.25 * numCols,
+ height: 100,
+ fontSize: NumCast(pivotDoc.pivotFontSize, 10)
+ });
+ for (const doc of val) {
+ docMap.set(doc, {
+ x: x + xCount * pivotAxisWidth * 1.25,
+ y: -y,
+ width: pivotAxisWidth,
+ height: doc.nativeWidth ? (NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
+ });
+ xCount++;
+ if (xCount >= numCols) {
+ xCount = 0;
+ y += pivotAxisWidth * 1.25;
+ }
+ }
+ x += pivotAxisWidth * 1.25 * (numCols + 1);
+ });
+
+ childPairs.map(pair => {
+ let defaultPosition = {
+ x: NumCast(pair.layout.x),
+ y: NumCast(pair.layout.y),
+ z: NumCast(pair.layout.z),
+ width: NumCast(pair.layout.width),
+ height: NumCast(pair.layout.height)
+ };
+ const pos = docMap.get(pair.layout) || defaultPosition;
+ layoutPoolData.set(pair, { transition: "transform 1s", ...pos });
+ });
+ return { map: layoutPoolData, elements: viewDefsToJSX(groupNames) };
+}
+
+export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void {
+ return () => {
+ let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
+ let overlayDisposer: () => void = emptyFunction; // filled in below after we have a reference to the scriptingBox
+ const scriptField = Cast(doc[key], ScriptField);
+ let scriptingBox = <ScriptBox initialText={scriptField && scriptField.script.originalScript}
+ // tslint:disable-next-line: no-unnecessary-callback-wrapper
+ onCancel={() => overlayDisposer()} // don't get rid of the function wrapper-- we don't want to use the current value of overlayDiposer, but the one set below
+ onSave={(text, onError) => {
+ const script = CompileScript(text, { params, requiredType, typecheck: false });
+ if (!script.compiled) {
+ onError(script.errors.map(error => error.messageText).join("\n"));
+ } else {
+ doc[key] = new ScriptField(script);
+ overlayDisposer();
+ }
+ }} />;
+ overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);
+ };
+ addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined);
+ addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
+ };
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5157d0c75..c6e8d7cf7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,22 +1,23 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faEye } from "@fortawesome/free-regular-svg-icons";
import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, IReactionDisposer, observable, reaction, trace } from "mobx";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCastAsync, Field, FieldResult, HeightSym, Opt, WidthSym, DocListCast } from "../../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
import { Id } from "../../../../new_fields/FieldSymbols";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
import { createSchema, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue, DateCast } from "../../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils";
+import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+import { aggregateBounds, emptyFunction, intersectRect, returnEmptyString, returnOne, Utils } from "../../../../Utils";
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
+import { DocServer } from "../../../DocServer";
import { Docs } from "../../../documents/Documents";
import { DocumentType } from "../../../documents/DocumentTypes";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
import { HistoryUtil } from "../../../util/History";
-import { CompileScript } from "../../../util/Scripting";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
import { undoBatch, UndoManager } from "../../../util/UndoManager";
@@ -26,12 +27,12 @@ import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingCanvas } from "../../InkingCanvas";
import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "../../nodes/DocumentContentsView";
-import { DocumentViewProps, documentSchema } from "../../nodes/DocumentView";
+import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView";
+import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
-import { OverlayElementOptions, OverlayView } from "../../OverlayView";
import PDFMenu from "../../pdf/PDFMenu";
-import { ScriptBox } from "../../ScriptBox";
import { CollectionSubView } from "../CollectionSubView";
+import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
@@ -40,9 +41,6 @@ import React = require("react");
import v5 = require("uuid/v5");
import { Timeline } from "../../animationtimeline/Timeline";
import { number } from "prop-types";
-import { DocServer } from "../../../DocServer";
-import { FormattedTextBox } from "../../nodes/FormattedTextBox";
-import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard);
@@ -54,132 +52,10 @@ export const panZoomSchema = createSchema({
arrangeInit: ScriptField,
useClusters: "boolean",
isRuleProvider: "boolean",
- fitToBox: "boolean"
+ fitToBox: "boolean",
+ panTransformType: "string",
});
-export interface ViewDefBounds {
- x: number;
- y: number;
- z?: number;
- width: number;
- height: number;
-}
-
-export interface ViewDefResult {
- ele: JSX.Element;
- bounds?: ViewDefBounds;
-}
-
-export namespace PivotView {
-
- export interface PivotData {
- type: string;
- text: string;
- x: number;
- y: number;
- width: number;
- height: number;
- fontSize: number;
- }
-
- export const elements = (target: CollectionFreeFormView) => {
- let collection = target.Document;
- const field = StrCast(collection.pivotField) || "title";
- const width = NumCast(collection.pivotWidth) || 200;
- const groups = new Map<FieldResult<Field>, Doc[]>();
-
- for (const doc of target.childDocs) {
- const val = doc[field];
- if (val === undefined) continue;
-
- const l = groups.get(val);
- if (l) {
- l.push(doc);
- } else {
- groups.set(val, [doc]);
- }
- }
-
- let minSize = Infinity;
-
- groups.forEach((val, key) => minSize = Math.min(minSize, val.length));
-
- const numCols = NumCast(collection.pivotNumColumns) || Math.ceil(Math.sqrt(minSize));
- const fontSize = NumCast(collection.pivotFontSize);
-
- const docMap = new Map<Doc, ViewDefBounds>();
- const groupNames: PivotData[] = [];
-
- let x = 0;
- groups.forEach((val, key) => {
- let y = 0;
- let xCount = 0;
- groupNames.push({
- type: "text",
- text: String(key),
- x,
- y: width + 50,
- width: width * 1.25 * numCols,
- height: 100, fontSize: fontSize
- });
- for (const doc of val) {
- docMap.set(doc, {
- x: x + xCount * width * 1.25,
- y: -y,
- width,
- height: width
- });
- xCount++;
- if (xCount >= numCols) {
- xCount = 0;
- y += width * 1.25;
- }
- }
- x += width * 1.25 * (numCols + 1);
- });
-
- let elements = target.viewDefsToJSX(groupNames);
- let docViews = target.childDocs.reduce((prev, doc) => {
- let minim = BoolCast(doc.isMinimized);
- if (minim === undefined || !minim) {
- let defaultPosition = (): ViewDefBounds => {
- return {
- x: NumCast(doc.x),
- y: NumCast(doc.y),
- z: NumCast(doc.z),
- width: NumCast(doc.width),
- height: NumCast(doc.height)
- };
- };
- const pos = docMap.get(doc) || defaultPosition();
- prev.push({
- ele: <CollectionFreeFormDocumentView
- key={doc[Id]}
- x={pos.x}
- y={pos.y}
- width={pos.width}
- height={pos.height}
- transition={"transform 1s"}
- jitterRotation={NumCast(target.props.Document.jitterRotation)}
- {...target.getChildDocumentViewProps(doc)}
- />,
- bounds: {
- x: pos.x,
- y: pos.y,
- z: pos.z,
- width: NumCast(pos.width),
- height: NumCast(pos.height)
- }
- });
- }
- return prev;
- }, elements);
-
- return docViews;
- };
-
-}
-
type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema);
@@ -187,50 +63,26 @@ const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSch
export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private _lastX: number = 0;
private _lastY: number = 0;
- private get _pwidth() { return this.props.PanelWidth(); }
- private get _pheight() { return this.props.PanelHeight(); }
- private _timelineRef = React.createRef<Timeline>();
- private get parentScaling() {
- return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1;
- }
-
- ComputeContentBounds(boundsList: { x: number, y: number, width: number, height: number }[]) {
- let bounds = boundsList.reduce((bounds, b) => {
- var [sptX, sptY] = [b.x, b.y];
- let [bptX, bptY] = [sptX + NumCast(b.width, 1), sptY + NumCast(b.height, 1)];
- return {
- x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
- r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
- };
- }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
- return bounds;
- }
-
- @computed get actualContentBounds() {
- return this.fitToBox && !this.isAnnotationOverlay ? this.ComputeContentBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)) : undefined;
- }
-
- @computed get contentBounds() {
- let bounds = this.actualContentBounds;
- let res = {
- panX: bounds ? (bounds.x + bounds.r) / 2 : this.Document.panX || 0,
- panY: bounds ? (bounds.y + bounds.b) / 2 : this.Document.panY || 0,
- scale: (bounds ? Math.min(this.props.PanelHeight() / (bounds.b - bounds.y), this.props.PanelWidth() / (bounds.r - bounds.x)) : this.Document.scale || 1) / this.parentScaling
- };
- if (res.scale === 0) res.scale = 1;
- return res;
- }
-
- @computed get fitToBox() { return this.props.fitToBox || this.Document.fitToBox; }
- @computed get nativeWidth() { return this.fitToBox ? 0 : this.Document.nativeWidth || 0; }
- @computed get nativeHeight() { return this.fitToBox ? 0 : this.Document.nativeHeight || 0; }
- public get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt')
+ private _clusterDistance: number = 75;
+ private _hitCluster = false;
+ @observable _clusterSets: (Doc[])[] = [];
+ @observable _timelineRef = React.createRef<Timeline>();
+
+ @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
+ @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
+ @computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); }
+ @computed get nativeWidth() { return this.fitToContent ? 0 : this.Document.nativeWidth || 0; }
+ @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
+ private get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt')
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
- private panX = () => this.contentBounds.panX;
- private panY = () => this.contentBounds.panY;
- private zoomScaling = () => this.contentBounds.scale;
- private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this._pwidth / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections
- private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this._pheight / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections
+ private easing = () => this.props.Document.panTransformType === "Ease";
+ private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0;
+ private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document.panY || 0;
+ private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ?
+ Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
+ this.Document.scale || 1);
+ private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections
+ private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections
private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());
private getTransformOverlay = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1);
private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
@@ -248,47 +100,31 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this.addDocument(newBox, false);
}
private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
- this.props.addDocument(newBox, false);
- this.bringToFront(newBox);
- this.updateCluster(newBox);
- return true;
+ let added = this.props.addDocument(newBox, false);
+ added && this.bringToFront(newBox);
+ added && this.updateCluster(newBox);
+ return added;
}
private selectDocuments = (docs: Doc[]) => {
SelectionManager.DeselectAll();
- docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv =>
- SelectionManager.SelectDoc(dv!, true));
+ docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
}
+ public isCurrent(doc: Doc) { return !this.props.Document.isMinimized && (Math.abs(NumCast(doc.page, -1) - NumCast(this.Document.curPage, -1)) < 1.5 || NumCast(doc.page, -1) === -1); }
+
public getActiveDocuments = () => {
- const curPage = FieldValue(this.Document.curPage, -1);
- return this.childLayoutPairs.filter(pair => {
- var page = NumCast(pair.layout!.page, -1);
- return page === curPage || page === -1;
- }).map(pair => pair.layout);
+ return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
}
@computed get fieldExtensionDoc() {
return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
}
- 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);
- }
- _clusterDistance = 75;
- boundsOverlap(doc: Doc, doc2: Doc) {
- var x2 = NumCast(doc2.x) - this._clusterDistance;
- var y2 = NumCast(doc2.y) - this._clusterDistance;
- var w2 = NumCast(doc2.width) + this._clusterDistance;
- var h2 = NumCast(doc2.height) + this._clusterDistance;
- var x = NumCast(doc.x) - this._clusterDistance;
- var y = NumCast(doc.y) - this._clusterDistance;
- var w = NumCast(doc.width) + this._clusterDistance;
- var h = NumCast(doc.height) + this._clusterDistance;
- if (doc.z === doc2.z && this.intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 })) {
- return true;
- }
- return false;
+ @action
+ onDrop = (e: React.DragEvent): void => {
+ var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+ super.onDrop(e, { x: pt[0], y: pt[1] });
}
+
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
@@ -304,7 +140,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let y = (z ? ypo : yp) - de.data.offset[1];
let dropX = NumCast(de.data.droppedDocuments[0].x);
let dropY = NumCast(de.data.droppedDocuments[0].y);
- de.data.droppedDocuments.forEach(d => {
+ de.data.droppedDocuments.forEach(action((d: Doc) => {
d.x = x + NumCast(d.x) - dropX;
d.y = y + NumCast(d.y) - dropY;
if (!NumCast(d.width)) {
@@ -316,7 +152,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
}
this.bringToFront(d);
- });
+ }));
de.data.droppedDocuments.length === 1 && this.updateCluster(de.data.droppedDocuments[0]);
}
@@ -339,18 +175,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return false;
}
- tryDragCluster(e: PointerEvent) {
- let probe = this.getTransform().transformPoint(e.clientX, e.clientY);
- let cluster = this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => {
+ pickCluster(probe: number[]) {
+ return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => {
let cx = NumCast(cd.x) - this._clusterDistance;
let cy = NumCast(cd.y) - this._clusterDistance;
let cw = NumCast(cd.width) + 2 * this._clusterDistance;
let ch = NumCast(cd.height) + 2 * this._clusterDistance;
- if (!cd.z && this.intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 })) {
- return NumCast(cd.cluster);
- }
- return cluster;
+ return !cd.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
+ NumCast(cd.cluster) : cluster;
}, -1);
+ }
+ tryDragCluster(e: PointerEvent) {
+ let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
if (cluster !== -1) {
let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
@@ -375,36 +211,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
return false;
}
- @observable sets: (Doc[])[] = [];
@undoBatch
- @action
updateClusters(useClusters: boolean) {
- this.Document.useClusters = useClusters;
- this.sets.length = 0;
- this.childLayoutPairs.map(pair => pair.layout).map(c => {
- let included = [];
- for (let i = 0; i < this.sets.length; i++) {
- for (let member of this.sets[i]) {
- if (this.boundsOverlap(c, member)) {
- included.push(i);
- break;
- }
- }
- }
- if (included.length === 0) {
- this.sets.push([c]);
- } else if (included.length === 1) {
- this.sets[included[0]].push(c);
- } else {
- this.sets[included[0]].push(c);
- for (let s = 1; s < included.length; s++) {
- this.sets[included[0]].push(...this.sets[included[s]]);
- this.sets[included[s]].length = 0;
- }
- }
- });
- this.sets.map((set, i) => set.map(member => member.cluster = i));
+ this.props.Document.useClusters = useClusters;
+ this._clusterSets.length = 0;
+ this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c));
}
@undoBatch
@@ -412,28 +224,28 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
updateCluster(doc: Doc) {
let childLayouts = this.childLayoutPairs.map(pair => pair.layout);
if (this.props.Document.useClusters) {
- this.sets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
+ this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
let preferredInd = NumCast(doc.cluster);
doc.cluster = -1;
- this.sets.map((set, i) => set.map(member => {
- if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && this.boundsOverlap(doc, member)) {
+ this._clusterSets.map((set, i) => set.map(member => {
+ if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
doc.cluster = i;
}
}));
- if (doc.cluster === -1 && preferredInd !== -1 && (!this.sets[preferredInd] || !this.sets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
+ if (doc.cluster === -1 && preferredInd !== -1 && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
doc.cluster = preferredInd;
}
- this.sets.map((set, i) => {
+ this._clusterSets.map((set, i) => {
if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
doc.cluster = i;
}
});
if (doc.cluster === -1) {
- doc.cluster = this.sets.length;
- this.sets.push([doc]);
+ doc.cluster = this._clusterSets.length;
+ this._clusterSets.push([doc]);
} else {
- for (let i = this.sets.length; i <= doc.cluster; i++) !this.sets[i] && this.sets.push([]);
- this.sets[doc.cluster].push(doc);
+ for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]);
+ this._clusterSets[doc.cluster].push(doc);
}
}
}
@@ -442,13 +254,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let clusterColor = "";
let cluster = NumCast(doc.cluster);
if (this.Document.useClusters) {
- if (this.sets.length <= cluster) {
+ if (this._clusterSets.length <= cluster) {
setTimeout(() => this.updateCluster(doc), 0);
} else {
// choose a cluster color from a palette
let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
clusterColor = colors[cluster % colors.length];
- let set = this.sets.length > cluster ? this.sets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)) : undefined;
+ let set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor));
// override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document
set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
@@ -459,6 +271,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerDown = (e: React.PointerEvent): void => {
+ this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -476,8 +289,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble) {
- if (this.props.Document.useClusters && this.tryDragCluster(e)) {
+ if (!e.cancelBubble && !this.isAnnotationOverlay) {
+ if (this._hitCluster && this.tryDragCluster(e)) {
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
e.preventDefault();
document.removeEventListener("pointermove", this.onPointerMove);
@@ -512,8 +325,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
- let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling() * cscale,
- this._pheight / this.zoomScaling() * cscale);
+ let panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
+ this.props.PanelHeight() / this.zoomScaling() * cscale);
if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2;
if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2;
if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2;
@@ -529,7 +342,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerWheel = (e: React.WheelEvent): void => {
- if (BoolCast(this.props.Document.lockedPosition)) return;
+ if (this.props.Document.lockedPosition || this.isAnnotationOverlay) return;
if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
e.stopPropagation();
}
@@ -546,13 +359,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
this.props.Document.scale = Math.abs(safeScale);
this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
- e.preventDefault();
}
}
@action
setPan(panX: number, panY: number) {
- if (!BoolCast(this.props.Document.lockedPosition)) {
+ if (!this.props.Document.lockedPosition) {
this.props.Document.panTransformType = "None";
var scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
@@ -562,12 +374,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
- @action
- onDrop = (e: React.DragEvent): void => {
- var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
- super.onDrop(e, { x: pt[0], y: pt[1] });
- }
-
bringToFront = (doc: Doc, sendToBack?: boolean) => {
if (sendToBack || doc.isBackground) {
doc.zIndex = 0;
@@ -599,54 +405,45 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
}
SelectionManager.DeselectAll();
- const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
- const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
- const newState = HistoryUtil.getState();
- newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
- HistoryUtil.pushState(newState);
-
- let px = this.Document.panX;
- let py = this.Document.panY;
- let s = this.Document.scale;
- this.setPan(newPanX, newPanY);
-
- this.props.Document.panTransformType = "Ease";
- this.props.focus(this.props.Document);
- if (willZoom) {
- this.setScaleToZoom(doc, scale);
+ if (this.props.Document.scrollHeight) {
+ let annotOn = Cast(doc.annotationOn, Doc) as Doc;
+ let offset = annotOn && (NumCast(annotOn.height) / 2);
+ this.props.Document.scrollY = NumCast(doc.y) - offset;
+ } else {
+ const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
+ const newState = HistoryUtil.getState();
+ newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
+ HistoryUtil.pushState(newState);
+
+ let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
+
+ this.setPan(newPanX, newPanY);
+ this.Document.panTransformType = "Ease";
+ this.props.focus(this.props.Document);
+ willZoom && this.setScaleToZoom(doc, scale);
+
+ afterFocus && setTimeout(() => {
+ if (afterFocus && afterFocus()) {
+ this.Document.panX = savedState.px;
+ this.Document.panY = savedState.py;
+ this.Document.scale = savedState.s;
+ this.Document.panTransformType = savedState.pt;
+ }
+ }, 1000);
}
- console.log("Focused " + this.Document.title + " " + s);
- afterFocus && setTimeout(() => {
- if (afterFocus && afterFocus()) {
- console.log("UnFocused " + this.Document.title + " " + s);
- this.Document.panX = px;
- this.Document.panY = py;
- this.Document.scale = s;
- }
- }, 1000);
+
}
setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
- let p = this.props;
- let PanelHeight = p.PanelHeight();
- let panelWidth = p.PanelWidth();
-
- let docHeight = NumCast(doc.height);
- let docWidth = NumCast(doc.width);
- let targetHeight = scale * PanelHeight;
- let targetWidth = scale * panelWidth;
-
- let maxScaleX: number = targetWidth / docWidth;
- let maxScaleY: number = targetHeight / docHeight;
- let maxApplicableScale = Math.min(maxScaleX, maxScaleY);
- this.Document.scale = maxApplicableScale;
+ this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc.width), this.props.PanelHeight() / NumCast(doc.height));
}
zoomToScale = (scale: number) => {
this.Document.scale = scale;
}
- getScale = () => this.Document.scale ? this.Document.scale : 1;
+ getScale = () => this.Document.scale || 1;
getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
@@ -663,7 +460,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
PanelHeight: childLayout[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
- ContainingCollectionDoc: this.props.CollectionView.props.Document,
+ ContainingCollectionDoc: this.props.Document,
focus: this.focusDocument,
backgroundColor: this.getClusterColor,
parentActive: this.props.active,
@@ -690,7 +487,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
PanelHeight: layoutDoc[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
- ContainingCollectionDoc: this.props.CollectionView.props.Document,
+ ContainingCollectionDoc: this.props.ContainingCollectionDoc,
focus: this.focusDocument,
backgroundColor: returnEmptyString,
parentActive: this.props.active,
@@ -713,110 +510,109 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
viewDefsToJSX = (views: any[]) => {
- let elements: ViewDefResult[] = [];
- if (Array.isArray(views)) {
- elements = views.reduce<typeof elements>((prev, ele) => {
- const jsx = this.viewDefToJSX(ele);
- jsx && prev.push(jsx);
- return prev;
- }, elements);
- }
- return elements;
+ return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!);
}
private viewDefToJSX(viewDef: any): Opt<ViewDefResult> {
if (viewDef.type === "text") {
- const text = Cast(viewDef.text, "string");
+ const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below
const x = Cast(viewDef.x, "number");
const y = Cast(viewDef.y, "number");
const z = Cast(viewDef.z, "number");
const width = Cast(viewDef.width, "number");
const height = Cast(viewDef.height, "number");
const fontSize = Cast(viewDef.fontSize, "number");
- if ([text, x, y, width, height].some(val => val === undefined)) {
- return undefined;
- }
-
- return {
- ele: <div className="collectionFreeform-customText" style={{
- transform: `translate(${x}px, ${y}px)`,
- width, height, fontSize
- }}>{text}</div>, bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
- };
+ return [text, x, y, width, height].some(val => val === undefined) ? undefined :
+ {
+ ele: <div className="collectionFreeform-customText" style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}>
+ {text}
+ </div>,
+ bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
+ };
}
}
- @computed.struct
- get elements() {
- if (this.Document.usePivotLayout) return PivotView.elements(this);
- let curPage = FieldValue(this.Document.curPage, -1);
- const initScript = this.Document.arrangeInit;
- let state: any = undefined;
- let pairs = this.childLayoutPairs;
- let elements: ViewDefResult[] = [];
- if (initScript) {
- const initResult = initScript.script.run({ docs: pairs.map(pair => pair.layout), collection: this.Document }, console.log);
- if (initResult.success) {
- const result = initResult.result;
- const { state: scriptState, views } = result;
- state = scriptState;
- elements = this.viewDefsToJSX(views);
- }
+ lookupLayout = (doc: Doc, dataDoc?: Doc) => {
+ let data: any = undefined;
+ let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] };
+ switch (this.Document.freeformLayoutEngine) {
+ case "pivot": computedElementData = this.doPivotLayout; break;
+ default: computedElementData = this.doFreeformLayout; break;
}
- let docviews = pairs.reduce((prev, pair) => {
- var page = NumCast(pair.layout.page, -1);
- if (!pair.layout.isMinimized && ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1)) {
- const pos = this.getCalculatedPositions({ doc: pair.layout, index: prev.length, collection: this.Document, docs: pairs.map(pair => pair.layout), state });
- state = pos.state === undefined ? state : pos.state;
- prev.push({
- ele: <CollectionFreeFormDocumentView key={pair.layout[Id]}
- ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider}
- jitterRotation={NumCast(this.props.Document.jitterRotation)}
- transition={pos.transition} x={pos.x} y={pos.y} width={pos.width} height={pos.height}
- {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
- bounds: { x: pos.x || 0, y: pos.y || 0, z: pos.z, width: pos.width || 0, height: pos.height || 0 }
- });
+ computedElementData.map.forEach((value: any, key: { layout: Doc, data?: Doc }) => {
+ if (key.layout === doc && key.data === dataDoc) {
+ data = value;
}
- // }
- return prev;
- }, elements);
+ });
+ return data && { x: data.x, y: data.y, z: data.z, width: data.width, height: data.height, transition: data.transition };
+ }
- return docviews;
+ @computed
+ get doPivotLayout() {
+ return computePivotLayout(this.props.Document, this.childDocs,
+ this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), this.viewDefsToJSX);
}
- @computed.struct
- get views() {
- return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele);
+ @computed
+ get doFreeformLayout() {
+ let layoutPoolData: Map<{ layout: Doc, data?: Doc }, any> = new Map();
+ let layoutDocs = this.childLayoutPairs.map(pair => pair.layout);
+ const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log);
+ let state = initResult && initResult.success ? initResult.result.scriptState : undefined;
+ let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
+
+ this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
+ const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
+ state = pos.state === undefined ? state : pos.state;
+ layoutPoolData.set(pair, pos);
+ });
+ return { map: layoutPoolData, elements: elements };
}
- @computed.struct
- get overlayViews() {
- return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele);
+
+ @computed
+ get doLayoutComputation() {
+ let computedElementData: { map: Map<{ layout: Doc, data?: Doc | undefined }, any>, elements: ViewDefResult[] };
+ switch (this.Document.freeformLayoutEngine) {
+ case "pivot": computedElementData = this.doPivotLayout; break;
+ default: computedElementData = this.doFreeformLayout; break;
+ }
+ this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair =>
+ computedElementData.elements.push({
+ ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} dataProvider={this.lookupLayout}
+ ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider}
+ jitterRotation={NumCast(this.props.Document.jitterRotation)} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
+ bounds: this.lookupLayout(pair.layout, pair.data)
+ }));
+
+ return computedElementData;
}
+ @computed.struct get elements() { return this.doLayoutComputation.elements; }
+ @computed.struct get views() { return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
+ @computed.struct get overlayViews() { return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele); }
+
@action
onCursorMove = (e: React.PointerEvent) => {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
- arrangeContents = async () => {
- const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
+ layoutDocsInGrid = () => {
UndoManager.RunInBatch(() => {
- if (docs) {
- let startX = this.Document.panX || 0;
- let x = startX;
- let y = this.Document.panY || 0;
- let i = 0;
- const width = Math.max(...docs.map(doc => NumCast(doc.width)));
- const height = Math.max(...docs.map(doc => NumCast(doc.height)));
- for (const doc of docs) {
- doc.x = x;
- doc.y = y;
- x += width + 20;
- if (++i === 6) {
- i = 0;
- x = startX;
- y += height + 20;
- }
+ const docs = DocListCast(this.Document[this.props.fieldKey]);
+ let startX = this.Document.panX || 0;
+ let x = startX;
+ let y = this.Document.panY || 0;
+ let i = 0;
+ const width = Math.max(...docs.map(doc => NumCast(doc.width)));
+ const height = Math.max(...docs.map(doc => NumCast(doc.height)));
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ x += width + 20;
+ if (++i === 6) {
+ i = 0;
+ x = startX;
+ y += height + 20;
}
}
}, "arrange contents");
@@ -852,10 +648,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
this._timelineRef.current!.timelineContextMenu(e.nativeEvent);
layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
- layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToBox, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" });
+ layoutItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" });
- layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" });
+ layoutItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" });
layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" });
layoutItems.push({
@@ -910,54 +706,26 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
<CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
...this.views
]
-
- public static AddCustomLayout(doc: Doc, dataKey: string): () => void {
- return () => {
- let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => {
- let overlayDisposer: () => void = emptyFunction;
- const script = Cast(doc[key], ScriptField);
- let originalText: string | undefined = undefined;
- if (script) originalText = script.script.originalScript;
- // tslint:disable-next-line: no-unnecessary-callback-wrapper
- let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
- const script = CompileScript(text, {
- params,
- requiredType,
- typecheck: false
- });
- if (!script.compiled) {
- onError(script.errors.map(error => error.messageText).join("\n"));
- return;
- }
- doc[key] = new ScriptField(script);
- overlayDisposer();
- }} />;
- overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, options);
- };
- addOverlay("arrangeInit", { x: 400, y: 100, width: 400, height: 300, title: "Layout Initialization" }, { collection: "Doc", docs: "Doc[]" }, undefined);
- addOverlay("arrangeScript", { x: 400, y: 500, width: 400, height: 300, title: "Layout Script" }, { doc: "Doc", index: "number", collection: "Doc", state: "any", docs: "Doc[]" }, "{x: number, y: number, width?: number, height?: number}");
- };
- }
render() {
// update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
- this.props.Document.fitX = this.actualContentBounds && this.actualContentBounds.x;
- this.props.Document.fitY = this.actualContentBounds && this.actualContentBounds.y;
- this.props.Document.fitW = this.actualContentBounds && (this.actualContentBounds.r - this.actualContentBounds.x);
- this.props.Document.fitH = this.actualContentBounds && (this.actualContentBounds.b - this.actualContentBounds.y);
+ this.props.Document.fitX = this.contentBounds && this.contentBounds.x;
+ this.props.Document.fitY = this.contentBounds && this.contentBounds.y;
+ this.props.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
+ this.props.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
// if fieldExt is set, then children will be stored in the extension document for the fieldKey.
// otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document
Doc.UpdateDocumentExtensionForField(this.props.DataDoc || this.props.Document, this.props.fieldKey);
- const easing = () => this.props.Document.panTransformType === "Ease";
return (
<div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
+ style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (NumCast(this.props.Document.scrollHeight) ? NumCast(this.props.Document.scrollHeight) : "100%") : this.props.PanelHeight() }}
onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
<MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
- addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
- getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
+ addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor}
+ getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
<CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
- easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+ easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
<CollectionFreeFormLinksView {...this.props} key="freeformLinks">
- <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={this._inkKey} >
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} >
{this.childViews}
</InkingCanvas>
</CollectionFreeFormLinksView>
@@ -974,23 +742,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@observer
class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
- @computed get overlayView() {
- return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
- renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
- }
render() {
- return this.overlayView;
+ return <DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
+ renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />;
}
}
@observer
class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
- @computed get backgroundView() {
- return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
- renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
- }
render() {
- return this.props.Document.backgroundLayout ? this.backgroundView : (null);
+ return !this.props.Document.backgroundLayout ? (null) :
+ (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+ renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
}
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index bbea4a555..82193aefa 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -30,6 +30,8 @@ interface MarqueeViewProps {
removeDocument: (doc: Doc) => boolean;
addLiveTextDocument: (doc: Doc) => void;
isSelected: () => boolean;
+ isAnnotationOverlay: boolean;
+ setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
}
@observer
@@ -43,6 +45,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@observable _visible: boolean = false;
_commandExecuted = false;
+ componentDidMount() {
+ this.props.setPreviewCursor && this.props.setPreviewCursor(this.setPreviewCursor);
+ }
+
@action
cleanupInteractions = (all: boolean = false) => {
if (all) {
@@ -145,15 +151,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
@action
onPointerDown = (e: React.PointerEvent): void => {
- this._downX = this._lastX = e.pageX;
- this._downY = this._lastY = e.pageY;
- this._commandExecuted = false;
- PreviewCursor.Visible = false;
- this.cleanupInteractions(true);
+ this._downX = this._lastX = e.clientX;
+ this._downY = this._lastY = e.clientY;
if (e.button === 2 || (e.button === 0 && e.altKey)) {
- document.addEventListener("pointermove", this.onPointerMove, true);
- document.addEventListener("pointerup", this.onPointerUp, true);
- document.addEventListener("keydown", this.marqueeCommand, true);
+ this.setPreviewCursor(e.clientX, e.clientY, true);
if (e.altKey) {
//e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
e.preventDefault();
@@ -176,6 +177,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
e.stopPropagation();
e.preventDefault();
}
+ } else {
+ this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this
}
if (e.altKey) {
e.preventDefault();
@@ -185,16 +188,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@action
onPointerUp = (e: PointerEvent): void => {
if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
- // console.log("pointer up!");
if (this._visible) {
- // console.log("visible");
let mselect = this.marqueeSelect();
if (!e.shiftKey) {
SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
}
this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
}
- //console.log("invisible");
this.cleanupInteractions(true);
if (e.altKey) {
@@ -202,11 +202,28 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
}
+ setPreviewCursor = (x: number, y: number, drag: boolean) => {
+ if (drag) {
+ this._downX = this._lastX = x;
+ this._downY = this._lastY = y;
+ this._commandExecuted = false;
+ PreviewCursor.Visible = false;
+ this.cleanupInteractions(true);
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ document.addEventListener("keydown", this.marqueeCommand, true);
+ } else {
+ this._downX = x;
+ this._downY = y;
+ PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
+ }
+ }
+
@action
onClick = (e: React.MouseEvent): void => {
if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
+ this.setPreviewCursor(e.clientX, e.clientY, false);
// let the DocumentView stopPropagation of this event when it selects this document
} else { // why do we get a click event when the cursor have moved a big distance?
// let's cut it off here so no one else has to deal with it.
@@ -297,8 +314,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
y: bounds.top,
panX: 0,
panY: 0,
- backgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor,
- defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor,
+ backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
+ defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
width: bounds.width,
height: bounds.height,
title: "a nested collection",
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 81b0249dd..cad404d1f 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -18,6 +18,7 @@ import { DocServer } from "../../DocServer";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { docs_v1 } from "googleapis";
+import { Utils } from "../../../Utils";
enum FollowModes {
OPENTAB = "Open in Tab",
@@ -242,6 +243,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
let proto = Doc.GetProto(LinkFollowBox.linkDoc);
let targetContext = await Cast(proto.targetContext, Doc);
let sourceContext = await Cast(proto.sourceContext, Doc);
+ let guid = StrCast(LinkFollowBox.linkDoc[Id]);
const shouldZoom = options ? options.shouldZoom : false;
let dockingFunc = (document: Doc) => { (this._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
@@ -251,6 +253,14 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
}
else if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor1 && sourceContext) {
DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, document => dockingFunc(sourceContext!));
+ if (LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc) {
+ if (guid) {
+ let views = DocumentManager.Instance.getDocumentViews(jumpToDoc);
+ views.length && (views[0].props.Document.scrollToLinkID = guid);
+ } else {
+ jumpToDoc.linkHref = Utils.prepend("/doc/" + StrCast(LinkFollowBox.linkDoc[Id]));
+ }
+ }
}
else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, undefined, undefined,
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 82fe3df23..835554ac0 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -28,9 +28,7 @@ interface LinkMenuItemProps {
export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
private _drag = React.createRef<HTMLDivElement>();
@observable private _showMore: boolean = false;
- @action toggleShowMore() {
- this._showMore = !this._showMore;
- }
+ @action toggleShowMore() { this._showMore = !this._showMore; }
onEdit = (e: React.PointerEvent): void => {
e.stopPropagation();
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index fcf483659..dd063ec9d 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -12,6 +12,7 @@ import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
import { random } from "animejs";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
+ dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
x?: number;
y?: number;
width?: number;
@@ -32,14 +33,14 @@ export const PositionDocument = makeInterface(documentSchema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
_disposer: IReactionDisposer | undefined = undefined;
- @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
- @computed get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
- @computed get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
- @computed get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.Document[WidthSym](); }
- @computed get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.Document[HeightSym](); }
+ get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
+ get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : this.Document.x || 0; }
+ get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : this.Document.y || 0; }
+ get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.props.Document[WidthSym](); }
+ get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.props.Document[HeightSym](); }
+ @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; }
@computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
@computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); }
- @computed get scaleToOverridingWidth() { return this.width / FieldValue(this.Document.width, this.width); }
@computed get renderScriptDim() {
if (this.Document.renderScript) {
@@ -72,11 +73,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
panelHeight = () => this.props.PanelHeight();
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
- .scale(1 / this.contentScaling()).scale(1 / this.scaleToOverridingWidth)
+ .scale(1 / this.contentScaling())
borderRounding = () => {
let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
- let br = StrCast(((this.layoutDoc.layout as Doc) || this.Document).borderRounding);
+ let ld = this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout : undefined;
+ let br = StrCast((ld || this.props.Document).borderRounding);
br = !br && ruleRounding ? ruleRounding : br;
if (br.endsWith("%")) {
let nativeDim = Math.min(NumCast(this.layoutDoc.nativeWidth), NumCast(this.layoutDoc.nativeHeight));
@@ -98,6 +100,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@observable _animPos: number[] | undefined = undefined;
+ finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
+ finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight();
+
render() {
return (
<div className="collectionFreeFormDocumentView-container"
@@ -110,7 +115,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
StrCast(this.layoutDoc.boxShadow, ""),
borderRadius: this.borderRounding(),
transform: this.transform,
- //transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : this.props.transition ? this.props.transition : StrCast(this.layoutDoc.transition),
+ // transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),
width: this.width,
height: this.height,
zIndex: this.Document.zIndex || 0,
@@ -119,8 +124,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
backgroundColor={this.clusterColorFunc}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
+ PanelWidth={this.finalPanelWidth}
+ PanelHeight={this.finalPanelHeight}
/>
</div>
);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2ac68fea3..ea669b23c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -227,15 +227,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
else if (linkedDocs.length) {
SelectionManager.DeselectAll();
let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document) && !d.anchor1anchored);
+ let second = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor2 as Doc, this.props.Document) && !d.anchor2anchored);
let firstUnshown = first.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
+ let secondUnshown = second.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
if (firstUnshown.length) first = [firstUnshown[0]];
- let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]];
+ if (secondUnshown.length) second = [secondUnshown[0]];
+ let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : second.length ? [second[0].anchor1 as Doc, second[0].anchor1 as Doc] : undefined;
// @TODO: shouldn't always follow target context
let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined];
- if (!linkedFwdDocs.some(l => l instanceof Promise)) {
+ if (linkedFwdDocs && !linkedFwdDocs.some(l => l instanceof Promise)) {
let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionDoc) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false,
@@ -258,7 +261,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._hitTemplateDrag = true;
}
}
- if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
+ if (this.active && e.button === 0 && !this.Document.lockedPosition) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -266,11 +269,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onPointerMove = (e: PointerEvent): void => {
if (e.cancelBubble && this.active) {
- document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
}
- else if (!e.cancelBubble && this.active) {
+ else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive()) && !this.Document.lockedPosition) {
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.Document.lockedPosition)) {
+ if (!e.altKey && !this.topMost && e.buttons === 1) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
@@ -300,7 +303,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
await swapViews(this.props.Document, "", "layoutNative");
let options = { title: "data", width: (this.Document.width || 0), x: -(this.Document.width || 0) / 2, y: - (this.Document.height || 0) / 2, };
- let fieldTemplate = this.Document.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) :
+ let fieldTemplate = this.Document.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) : this.Document.type === DocumentType.PDF ? Docs.Create.PdfDocument("http://www.msn.com", options) :
this.Document.type === DocumentType.VID ? Docs.Create.VideoDocument("http://www.cs.brown.edu", options) :
Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
@@ -354,7 +357,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
// const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
de.data.linkSourceDocument !== this.props.Document &&
- (de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc));
+ (de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc, undefined, "in-text link being created")); // TODODO this is where in text links get passed
}
}
@@ -605,7 +608,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
const nativeWidth = this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%";
- const nativeHeight = this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+ const nativeHeight = this.Document.ignoreAspect || this.props.Document.fitWidth ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : nativeWidth !== "100%" ? nativeWidth : "100%";
const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle");
const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption");
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 49fc2263d..b93c78cfd 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -50,7 +50,6 @@ export interface FieldViewProps {
PanelWidth: () => number;
PanelHeight: () => number;
setVideoBox?: (player: VideoBox) => void;
- setPdfBox?: (player: PDFBox) => void;
ContentScaling: () => number;
ChromeHeight?: () => number;
}
@@ -96,7 +95,7 @@ export class FieldView extends React.Component<FieldViewProps> {
return <p>{field.date.toLocaleString()}</p>;
}
else if (field instanceof Doc) {
- return <p><b>{field.title}</b></p>;
+ return <p><b>{field.title && field.title.toString()}</b></p>;
//return <p><b>{field.title + " : id= " + field[Id]}</b></p>;
// let returnHundred = () => 100;
// return (
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 0d7277cbe..45e516015 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -164,13 +164,13 @@ ol { counter-reset: deci1 0;}
.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 }
.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; }
.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10;}
-.decimal1:before { content: counter(deci1) ")"; counter-increment: deci1; display:inline-block; width: 30}
-.decimal2:before { content: counter(deci1) "." counter(deci2) ")"; counter-increment: deci2; display:inline-block; width: 35}
-.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ")"; counter-increment: deci3; display:inline-block; width: 35}
-.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ")"; counter-increment: deci4; display:inline-block; width: 40}
-.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ")"; counter-increment: deci5; display:inline-block; width: 40}
-.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ")"; counter-increment: deci6; display:inline-block; width: 45}
-.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ")"; counter-increment: deci7; display:inline-block; width: 50}
-.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ")"; counter-increment: ualph; display:inline-block; width: 35 }
-.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ")"; counter-increment: lroman;display:inline-block; width: 50 }
-.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ")"; counter-increment: lalpha; display:inline-block; width: 35}
+.decimal1:before { content: counter(deci1) ") "; counter-increment: deci1; display:inline-block; min-width: 30;}
+.decimal2:before { content: counter(deci1) "." counter(deci2) ") "; counter-increment: deci2; display:inline-block; min-width: 35}
+.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ") "; counter-increment: deci3; display:inline-block; min-width: 35}
+.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ") "; counter-increment: deci4; display:inline-block; min-width: 40}
+.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ") "; counter-increment: deci5; display:inline-block; min-width: 40}
+.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ") "; counter-increment: deci6; display:inline-block; min-width: 45}
+.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ") "; counter-increment: deci7; display:inline-block; min-width: 50}
+.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ") "; counter-increment: ualph; display:inline-block; min-width: 35 }
+.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ") "; counter-increment: lroman;display:inline-block; min-width: 50 }
+.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ") "; counter-increment: lalpha; display:inline-block; min-width: 35}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index eb4718581..63a16f90c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -80,6 +80,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
private _nodeClicked: any;
private _undoTyping?: UndoManager.Batch;
private _searchReactionDisposer?: Lambda;
+ private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
private _reactionDisposer: Opt<IReactionDisposer>;
private _textReactionDisposer: Opt<IReactionDisposer>;
private _heightReactionDisposer: Opt<IReactionDisposer>;
@@ -138,6 +139,52 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this.props.isOverlay) {
DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
}
+
+ this._scrollToRegionReactionDisposer = reaction(
+ () => StrCast(this.props.Document.scrollToLinkID),
+ async (scrollToLinkID) => {
+ let findLinkFrag = (frag: Fragment, editor: EditorView) => {
+ const nodes: Node[] = [];
+ frag.forEach((node, index) => {
+ let examinedNode = findLinkNode(node, editor);
+ if (examinedNode && examinedNode.textContent) {
+ nodes.push(examinedNode);
+ start += index;
+ }
+ });
+ return { frag: Fragment.fromArray(nodes), start: start };
+ };
+ let findLinkNode = (node: Node, editor: EditorView) => {
+ if (!node.isText) {
+ const content = findLinkFrag(node.content, editor);
+ return node.copy(content.frag);
+ }
+ const marks = [...node.marks];
+ const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
+ return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
+ };
+
+ let start = -1;
+ if (this._editorView && scrollToLinkID) {
+ let editor = this._editorView;
+ let ret = findLinkFrag(editor.state.doc.content, editor);
+
+ if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) {
+ let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
+ if (ret.frag.firstChild) {
+ selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
+ }
+ editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
+ const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+ setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
+ setTimeout(() => this.unhighlightSearchTerms(), 2000);
+ }
+ this.props.Document.scrollToLinkID = undefined;
+ }
+
+ },
+ { fireImmediately: true }
+ );
}
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
@@ -696,6 +743,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._editorView && this._editorView.destroy();
this._editorView = new EditorView(this._proseRef, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
+ handleScrollToSelection: (editorView) => {
+ let ref = editorView.domAtPos(editorView.state.selection.from);
+ let refNode = ref.node as any;
+ while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement;
+ let r1 = refNode && refNode.getBoundingClientRect();
+ let r3 = self._ref.current!.getBoundingClientRect();
+ r1 && (self._ref.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale);
+ return true;
+ },
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
@@ -736,6 +792,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
componentWillUnmount() {
+ this._scrollToRegionReactionDisposer && this._scrollToRegionReactionDisposer();
this._rulesReactionDisposer && this._rulesReactionDisposer();
this._reactionDisposer && this._reactionDisposer();
this._proxyReactionDisposer && this._proxyReactionDisposer();
@@ -758,6 +815,40 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
e.stopPropagation();
}
let ctrlKey = e.ctrlKey;
+ if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
+ e.preventDefault();
+ }
+ }
+
+ onPointerUp = (e: React.PointerEvent): void => {
+ FormattedTextBoxComment.textBox = this;
+ if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
+ e.stopPropagation();
+ }
+ }
+
+ @action
+ onFocused = (e: React.FocusEvent): void => {
+ document.removeEventListener("keypress", this.recordKeyHandler);
+ document.addEventListener("keypress", this.recordKeyHandler);
+ this.tryUpdateHeight();
+ if (!this.props.isOverlay) {
+ FormattedTextBox.InputBoxOverlay = this;
+ } else {
+ if (this._ref.current) {
+ this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
+ }
+ }
+ }
+ onPointerWheel = (e: React.WheelEvent): void => {
+ // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
+ if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
+ e.stopPropagation();
+ }
+ }
+
+ onClick = (e: React.MouseEvent): void => {
+ let ctrlKey = e.ctrlKey;
if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
let href = (e.target as any).href;
let location: string;
@@ -771,8 +862,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
if (node) {
let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link);
- href = link && link.attrs.href;
- location = link && link.attrs.location;
+ if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above).
+ href = link && link.attrs.href;
+ location = link && link.attrs.location;
+ }
}
if (href) {
if (href.indexOf(Utils.prepend("/doc/")) === 0) {
@@ -783,14 +876,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let proto = Doc.GetProto(linkDoc);
let targetContext = await Cast(proto.targetContext, Doc);
let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
+
if (jumpToDoc) {
if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-
DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
return;
}
}
- if (targetContext) {
+ if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) {
DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
} else if (jumpToDoc) {
DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
@@ -812,39 +905,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
- e.preventDefault();
- }
- }
-
- onPointerUp = (e: React.PointerEvent): void => {
- FormattedTextBoxComment.textBox = this;
- if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
- e.stopPropagation();
- }
- }
-
- @action
- onFocused = (e: React.FocusEvent): void => {
- document.removeEventListener("keypress", this.recordKeyHandler);
- document.addEventListener("keypress", this.recordKeyHandler);
- this.tryUpdateHeight();
- if (!this.props.isOverlay) {
- FormattedTextBox.InputBoxOverlay = this;
- } else {
- if (this._ref.current) {
- this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
- }
- }
- }
- onPointerWheel = (e: React.WheelEvent): void => {
- // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
- if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
- e.stopPropagation();
- }
- }
-
- onClick = (e: React.MouseEvent): void => {
// this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
if (this.props.isSelected() && e.nativeEvent.offsetX < 40) {
let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
@@ -852,7 +912,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let node = this._editorView!.state.doc.nodeAt(pos.pos);
let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined;
if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) {
- this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
+ let hit = this._editorView!.domAtPos(pos.pos).node as any;
+ let beforeEle = document.querySelector("." + hit.className) as Element;
+ let before = beforeEle ? window.getComputedStyle(beforeEle, ':before') : undefined;
+ let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined;
+ if (beforeWidth && e.nativeEvent.offsetX < beforeWidth) {
+ let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined;
+ if (ol && ol.type === schema.nodes.ordered_list && !e.shiftKey) {
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2))));
+ } else {
+ this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
+ }
+ }
}
}
}
@@ -903,7 +974,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (e.key === "Tab" || e.key === "Enter") {
e.preventDefault();
}
- this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
+ this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })));
if (!this._undoTyping) {
this._undoTyping = UndoManager.StartBatch("undoTyping");
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 624593245..004f50590 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -38,6 +38,7 @@ library.add(faFileAudio, faAsterisk);
export const pageSchema = createSchema({
curPage: "number",
+ fitWidth: "boolean"
});
interface Window {
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index c88a94c28..2917c81cb 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -1,77 +1,67 @@
.pdfBox-cont,
.pdfBox-cont-interactive {
- display: flex;
+ display: inline-block;
flex-direction: row;
height: 100%;
- overflow-y: scroll;
- overflow-x: hidden;
- .pdfBox-scrollHack {
- pointer-events: none;
- }
+ width:100%;
+ overflow: hidden;
+ position:absolute;
+ z-index: -1;
}
.pdfBox-cont {
pointer-events: none;
- .pdfPage-textlayer {
- span {
- pointer-events: none !important;
- user-select: none;
+ .collectionFreeFormView-none {
+ pointer-events: none;
+ }
+ .pdfViewer-text {
+ .textLayer {
+ span {
+ user-select: none;
+ }
}
}
}
.pdfBox-cont-interactive {
pointer-events: all;
- .pdfPage-textlayer {
- span {
- pointer-events: all !important;
- user-select: text;
+ .pdfViewer-text {
+ .textLayer {
+ span {
+ user-select: text;
+ }
}
}
}
-.react-pdf__Page {
- transform-origin: left top;
- position: absolute;
- top: 0;
- left: 0;
-}
-
-.react-pdf__Page__textContent span {
- user-select: text;
-}
-
-.react-pdf__Document {
- position: absolute;
-}
-
.pdfBox-settingsCont {
position: absolute;
right: 0;
- top: 0;
+ top: 3;
+ pointer-events: all;
.pdfBox-settingsButton {
border-bottom-left-radius: 50%;
display: flex;
justify-content: space-evenly;
align-items: center;
- height: 70px;
+ height: 30px;
background: none;
padding: 0;
.pdfBox-settingsButton-arrow {
width: 0;
height: 0;
- border-top: 25px solid transparent;
- border-bottom: 25px solid transparent;
- border-right: 25px solid #121721;
+ border-top: 15px solid transparent;
+ border-bottom: 15px solid transparent;
+ border-right: 15px solid #121721;
transition: all 0.5s;
}
.pdfBox-settingsButton-iconCont {
background: #121721;
- height: 50px;
+ height: 30px;
width: 70px;
display: flex;
justify-content: center;
@@ -86,16 +76,15 @@
}
.pdfBox-settingsFlyout {
- width: 600px;
position: absolute;
background: #323232;
box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
- left: -400px;
+ right: 20px;
border-radius: 7px;
padding: 20px;
display: flex;
flex-direction: column;
- font-size: 30px;
+ font-size: 14px;
transition: all 0.5s;
.pdfBox-settingsFlyout-title {
@@ -108,4 +97,69 @@
grid-template-columns: 47.5% 5% 47.5%;
}
}
-} \ No newline at end of file
+}
+
+.pdfBox-overlayCont {
+ position: absolute;
+ width: 100%;
+ height: 40px;
+ background: #121721;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 20px;
+ overflow: hidden;
+ transition: left .5s;
+ pointer-events: all;
+
+ .pdfBox-searchBar {
+ width: 70%;
+ font-size: 14px;
+ }
+}
+
+.pdfBox-overlayButton {
+ border-bottom-left-radius: 50%;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 30px;
+ background: none;
+ padding: 0;
+ position: absolute;
+ pointer-events: all;
+
+ .pdfBox-overlayButton-arrow {
+ width: 0;
+ height: 0;
+ border-top: 15px solid transparent;
+ border-bottom: 15px solid transparent;
+ border-right: 15px solid #121721;
+ transition: all 0.5s;
+ }
+
+ .pdfBox-overlayButton-iconCont,
+ .pdfBox-nextIcon,
+ .pdfBox-prevIcon {
+ background: #121721;
+ height: 30px;
+ width: 70px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-left: -2px;
+ border-radius: 3px;
+ }
+}
+
+.pdfBox-overlayButton:hover {
+ background: none;
+}
+
+.pdfBox-nextIcon {
+ left: 20; top: 5; height: 30px; position: absolute;
+}
+.pdfBox-prevIcon {
+ left: 50; top: 5; height: 30px; position: absolute;
+}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 764051d62..fe71e76fd 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,12 +1,12 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from 'mobx';
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
import 'react-image-lightbox/style.css';
import { Doc, Opt, WidthSym } from "../../../new_fields/Doc";
import { makeInterface } from "../../../new_fields/Schema";
-import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
+import { ScriptField } from '../../../new_fields/ScriptField';
import { Cast, NumCast } from "../../../new_fields/Types";
import { PdfField } from "../../../new_fields/URLField";
import { KeyCodes } from '../../northstar/utils/KeyCodes';
@@ -19,82 +19,59 @@ import { FieldView, FieldViewProps } from './FieldView';
import { pageSchema } from "./ImageBox";
import "./PDFBox.scss";
import React = require("react");
+import { undoBatch } from '../../util/UndoManager';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ContextMenu } from '../ContextMenu';
+import { Utils } from '../../../Utils';
type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
@observer
export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
- public static LayoutString() { return FieldView.LayoutString(PDFBox); }
- private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
- private _reactionDisposer?: IReactionDisposer;
+ public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); }
private _keyValue: string = "";
private _valueValue: string = "";
private _scriptValue: string = "";
+ private _searchString: string = "";
+ private _isChildActive = false;
+ private _pdfViewer: PDFViewer | undefined;
private _keyRef: React.RefObject<HTMLInputElement> = React.createRef();
private _valueRef: React.RefObject<HTMLInputElement> = React.createRef();
private _scriptRef: React.RefObject<HTMLInputElement> = React.createRef();
+ @observable private _searching: boolean = false;
@observable private _flyout: boolean = false;
- @observable private _alt = false;
@observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
+ @observable private _pageControls = false;
@computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-
@computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
componentDidMount() {
- this.props.setPdfBox && this.props.setPdfBox(this);
-
- this.props.Document.curPage = ComputedField.MakeFunction("Math.floor(Number(this.panY) / Number(this.nativeHeight) + 1)");
-
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
if (pdfUrl instanceof PdfField) {
Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf));
}
- this._reactionDisposer = reaction(
- () => this.Document.panY,
- () => this._mainCont.current && this._mainCont.current.scrollTo({ top: this.Document.panY || 0, behavior: "auto" })
- );
- }
-
- componentWillUnmount() {
- this._reactionDisposer && this._reactionDisposer();
- }
-
- public GetPage() {
- return Math.floor((this.Document.panY || 0) / (this.Document.nativeHeight || 0)) + 1;
- }
-
- @action
- public BackPage() {
- let cp = Math.ceil((this.Document.panY || 0) / (this.Document.nativeHeight || 0)) + 1;
- cp = cp - 1;
- if (cp > 0) {
- this.Document.panY = (cp - 1) * (this.Document.nativeHeight || 0);
- }
}
-
- @action
- public GotoPage = (p: number) => {
- if (p > 0 && p <= NumCast(this.dataDoc.numPages)) {
- this.Document.panY = (p - 1) * (this.Document.nativeHeight || 0);
+ loaded = (nw: number, nh: number, np: number) => {
+ this.dataDoc.numPages = np;
+ if (!this.Document.nativeWidth || !this.Document.nativeHeight || !this.Document.scrollHeight) {
+ let oldaspect = (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
+ this.Document.nativeWidth = nw * 96 / 72;
+ this.Document.nativeHeight = this.Document.nativeHeight ? nw * 96 / 72 * oldaspect : nh * 96 / 72;
}
+ !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
}
- @action
- public ForwardPage() {
- let cp = this.GetPage() + 1;
- if (cp <= NumCast(this.dataDoc.numPages)) {
- this.Document.panY = (cp - 1) * (this.Document.nativeHeight || 0);
- }
- }
-
- @action
- setPanY = (y: number) => {
- this.Document.panY = y;
- }
+ public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
+ public prevAnnotation() { this._pdfViewer && this._pdfViewer.prevAnnotation(); }
+ public nextAnnotation() { this._pdfViewer && this._pdfViewer.nextAnnotation(); }
+ public backPage() { this._pdfViewer!.gotoPage(NumCast(this.props.Document.curPage) - 1); }
+ public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); };
+ public forwardPage() { this._pdfViewer!.gotoPage(NumCast(this.props.Document.curPage) + 1); }
+ @undoBatch
@action
private applyFilter = () => {
let scriptText = this._scriptValue ? this._scriptValue :
@@ -102,10 +79,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
this.props.Document.filterScript = ScriptField.MakeFunction(scriptText);
}
- scrollTo = (y: number) => {
- this._mainCont.current && this._mainCont.current.scrollTo({ top: Math.max(y - (this._mainCont.current.offsetHeight / 2), 0), behavior: "auto" });
- }
-
private resetFilters = () => {
this._keyValue = this._valueValue = this._scriptValue = "";
this._keyRef.current && (this._keyRef.current.value = "");
@@ -117,83 +90,112 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
private newValueChange = (e: React.ChangeEvent<HTMLInputElement>) => this._valueValue = e.currentTarget.value;
private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value;
+ whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
+ active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
+ setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; };
+ searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
+
settingsPanel() {
+ let pageBtns = <>
+ <button className="pdfBox-overlayButton-iconCont" key="back" title="Page Back"
+ onPointerDown={(e) => e.stopPropagation()}
+ onClick={() => this.backPage()}
+ style={{ left: 50, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}>
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" />
+ </button>
+ <button className="pdfBox-overlayButton-iconCont" key="fwd" title="Page Forward"
+ onPointerDown={(e) => e.stopPropagation()}
+ onClick={() => this.forwardPage()}
+ style={{ left: 80, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}>
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" />
+ </button>
+ </>;
return !this.props.active() ? (null) :
- (<div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}>
- <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings"
- style={{ marginTop: `${this.Document.panY || 0}px` }}>
- <div className="pdfBox-settingsButton-arrow"
- style={{
- borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`,
- borderBottom: `25px solid ${this._flyout ? "#121721" : "transparent"}`,
- borderRight: `25px solid ${this._flyout ? "transparent" : "#121721"}`,
- transform: `scaleX(${this._flyout ? -1 : 1})`
- }} />
- <div className="pdfBox-settingsButton-iconCont">
- <FontAwesomeIcon style={{ color: "white" }} icon="cog" size="3x" />
- </div>
+ (<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true}
+ onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none", position: "absolute", width: "100%", height: "100%", zIndex: 1, pointerEvents: "none" }}>
+ <div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
+ <button className="pdfBox-overlayButton" title="Open Search Bar" />
+ <input className="pdfBox-searchBar" placeholder="Search" onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} />
+ <button title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}>
+ <FontAwesomeIcon icon="search" size="sm" color="white" /></button>
+ <button className="pdfBox-prevIcon " title="Previous Annotation" onClick={e => this.prevAnnotation()} >
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="sm" />
+ </button>
+ <button className="pdfBox-nextIcon" title="Next Annotation" onClick={e => this.nextAnnotation()} >
+ <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="sm" />
+ </button>
+ </div>
+ <button className="pdfBox-overlayButton" key="search" onClick={action(() => this._searching = !this._searching)} title="Open Search Bar" style={{ bottom: 8, right: 0 }}>
+ <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div>
+ <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
+ <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon={this._searching ? "times" : "search"} size="3x" /></div>
</button>
- <div className="pdfBox-settingsFlyout" style={{ left: `${this._flyout ? -600 : 100}px` }} >
- <div className="pdfBox-settingsFlyout-title">
- Annotation View Settings
- </div>
- <div className="pdfBox-settingsFlyout-kvpInput">
- <input placeholder="Key" className="pdfBox-settingsFlyout-input" onChange={this.newKeyChange}
- style={{ gridColumn: 1 }} ref={this._keyRef} />
- <input placeholder="Value" className="pdfBox-settingsFlyout-input" onChange={this.newValueChange}
- style={{ gridColumn: 3 }} ref={this._valueRef} />
- </div>
- <div className="pdfBox-settingsFlyout-kvpInput">
- <input placeholder="Custom Script" onChange={this.newScriptChange} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} />
- </div>
- <div className="pdfBox-settingsFlyout-kvpInput">
- <button style={{ gridColumn: 1 }} onClick={this.resetFilters}>
- <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" />
- &nbsp; Reset Filters
- </button>
- <button style={{ gridColumn: 3 }} onClick={this.applyFilter}>
- <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" />
- &nbsp; Apply
- </button>
+ <input value={`${NumCast(this.props.Document.curPage)}`}
+ onChange={e => this.gotoPage(Number(e.currentTarget.value))}
+ style={{ left: 20, top: 5, height: "30px", width: "30px", position: "absolute", pointerEvents: "all" }}
+ onClick={action(() => this._pageControls = !this._pageControls)} />
+ {this._pageControls ? pageBtns : (null)}
+ <div className="pdfBox-settingsCont" key="settings" onPointerDown={(e) => e.stopPropagation()}>
+ <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings" >
+ <div className="pdfBox-settingsButton-arrow" style={{ transform: `scaleX(${this._flyout ? -1 : 1})` }} />
+ <div className="pdfBox-settingsButton-iconCont">
+ <FontAwesomeIcon style={{ color: "white", padding: 5 }} icon="cog" size="3x" />
+ </div>
+ </button>
+ <div className="pdfBox-settingsFlyout" style={{ right: `${this._flyout ? 20 : -600}px` }} >
+ <div className="pdfBox-settingsFlyout-title">
+ Annotation View Settings
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <input placeholder="Key" className="pdfBox-settingsFlyout-input" onChange={this.newKeyChange} style={{ gridColumn: 1 }} ref={this._keyRef} />
+ <input placeholder="Value" className="pdfBox-settingsFlyout-input" onChange={this.newValueChange} style={{ gridColumn: 3 }} ref={this._valueRef} />
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <input placeholder="Custom Script" onChange={this.newScriptChange} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} />
+ </div>
+ <div className="pdfBox-settingsFlyout-kvpInput">
+ <button style={{ gridColumn: 1 }} onClick={this.resetFilters}>
+ <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" />
+ &nbsp; Reset Filters
+ </button>
+ <button style={{ gridColumn: 3 }} onClick={this.applyFilter}>
+ <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" />
+ &nbsp; Apply
+ </button>
+ </div>
</div>
</div>
</div>);
}
- loaded = (nw: number, nh: number, np: number) => {
- this.dataDoc.numPages = np;
- if (!this.Document.nativeWidth || !this.Document.nativeHeight || !this.Document.scrollHeight) {
- let oldaspect = (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
- this.Document.nativeWidth = nw;
- this.Document.nativeHeight = this.Document.nativeHeight ? nw * oldaspect : nh;
- this.Document.height = this.Document[WidthSym]() * (nh / nw);
- this.Document.scrollHeight = np * this.Document.nativeHeight;
- }
- }
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
+ let funcs: ContextMenuProps[] = [];
+ pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Fit Width " + (this.Document.fitWidth ? "Off" : "On"), event: () => this.Document.fitWidth = !this.Document.fitWidth, icon: "expand-arrows-alt" });
- @action
- onScroll = (e: React.UIEvent<HTMLDivElement>) => {
- if (e.currentTarget && this.props.ContainingCollectionDoc) {
- this.props.Document.panTransformType = "None";
- this.Document.panY = e.currentTarget.scrollTop;
- }
+ ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
}
-
render() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
- let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
+ let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive");
return (!(pdfUrl instanceof PdfField) || !this._pdf ?
<div>{`pdf, ${this.dataDoc[this.props.fieldKey]}, not found`}</div> :
- <div className={classname}
- onScroll={this.onScroll}
- style={{ marginTop: `${(this.Document.panY || 0)}px` }}
- ref={this._mainCont}>
- <div className="pdfBox-scrollHack" style={{ height: NumCast(this.props.Document.scrollHeight) + ((this.Document.nativeHeight || 0) - (this.Document.nativeHeight || 0) / (this.Document.scale || 1)) }} />
- <PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={this.Document.panY || 0}
- Document={this.props.Document} DataDoc={this.dataDoc}
- addDocTab={this.props.addDocTab} setPanY={this.setPanY} GoToPage={this.GotoPage}
+ <div className={classname} onContextMenu={this.specificContextMenu} onPointerDown={(e: React.PointerEvent) => {
+ let hit = document.elementFromPoint(e.clientX, e.clientY);
+ if (hit && hit.localName === "span" && this.props.isSelected()) { // drag selecting text stops propagation
+ e.button === 0 && e.stopPropagation();
+ }
+ }}>
+ <PDFViewer {...this.props} pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} loaded={this.loaded}
+ setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView}
+ renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth}
+ Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling}
+ addDocTab={this.props.addDocTab} GoToPage={this.gotoPage}
pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
+ isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} />
{this.settingsPanel()}
</div>);
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index e376fbddb..5afd85430 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -161,10 +161,8 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.showButton) {
let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
- if (prevScale !== undefined) {
- if (prevScale !== curScale) {
- DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
- }
+ if (prevScale !== undefined && prevScale !== curScale) {
+ DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
}
}
}
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index a9fa883c8..3ed85f6a5 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -85,7 +85,15 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
@action
onPointerDown = async (e: React.PointerEvent) => {
- if (e.button === 0) {
+ if (e.button === 2 || e.ctrlKey) {
+ PDFMenu.Instance.Status = "annotation";
+ PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this);
+ PDFMenu.Instance.Pinned = false;
+ PDFMenu.Instance.AddTag = this.addTag.bind(this);
+ PDFMenu.Instance.PinToPres = this.pinToPres;
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
+ }
+ else if (e.button === 0) {
let targetDoc = await Cast(this.props.document.target, Doc);
if (targetDoc) {
let context = await Cast(targetDoc.targetContext, Doc);
@@ -96,14 +104,6 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
}
}
}
- if (e.button === 2) {
- PDFMenu.Instance.Status = "annotation";
- PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this);
- PDFMenu.Instance.Pinned = false;
- PDFMenu.Instance.AddTag = this.addTag.bind(this);
- PDFMenu.Instance.PinToPres = this.pinToPres;
- PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
- }
}
addTag = (key: string, value: string): boolean => {
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 3ed81faef..2202351ee 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -1,11 +1,10 @@
import React = require("react");
import "./PDFMenu.scss";
-import { observable, action, runInAction } from "mobx";
+import { observable, action, } from "mobx";
import { observer } from "mobx-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { emptyFunction, returnFalse } from "../../../Utils";
import { Doc } from "../../../new_fields/Doc";
-import { handleBackspace } from "../nodes/PDFBox";
@observer
export default class PDFMenu extends React.Component {
@@ -238,8 +237,8 @@ export default class PDFMenu extends React.Component {
<button key="6" className="pdfMenu-button" title="Pin to Presentation" onPointerDown={this.PinToPres}>
<FontAwesomeIcon icon="map-pin" size="lg" /></button>,
<div key="7" className="pdfMenu-addTag" >
- <input onKeyDown={handleBackspace} onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
- <input onKeyDown={handleBackspace} onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
+ <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
+ <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
</div>,
<button key="8" className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}>
<FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>,
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index a2f3911c5..8027e93a3 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -1,93 +1,66 @@
-
-.pdfViewer-viewer {
- pointer-events:inherit;
+
+.pdfViewer-viewer, .pdfViewer-viewer-zoomed {
+ pointer-events: inherit;
width: 100%;
- .pdfViewer-visibleElements {
- .pdfPage-cont {
- .pdfPage-textLayer {
- div {
- user-select: text;
- }
- span {
- color: transparent;
- position: absolute;
- white-space: pre;
- cursor: text;
- -webkit-transform-origin: 0% 0%;
- transform-origin: 0% 0%;
- }
- }
- }
+ height: 100%;
+ position: absolute;
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ // .canvasWrapper {
+ // transform: scale(0.75);
+ // transform-origin: top left;
+ // }
+ // .textLayer {
+ // transform: scale(0.75);
+ // transform-origin: top left;
+ // }
+
+ .page {
+ position: relative;
}
- .pdfViewer-text {
- transform: scale(1.5);
- transform-origin: top left;
+ .collectionfreeformview-container {
+ pointer-events: none;
+ }
+
+ .pdfViewer-dragAnnotationBox {
+ position:absolute;
+ background-color: transparent;
+ opacity: 0.1;
}
+ .pdfViewer-overlay {
+ transform: scale(2.14359);
+ transform-origin: left top;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ display: inline-block;
+ width:100%;
+ }
.pdfViewer-annotationLayer {
position: absolute;
top: 0;
width: 100%;
pointer-events: none;
+
.pdfPage-annotationBox {
position: absolute;
background-color: red;
opacity: 0.1;
}
}
-
- .pdfViewer-overlayCont {
- position: absolute;
- width: 100%;
- height: 100px;
- background: #121721;
- bottom: 0;
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 20px;
- overflow: hidden;
- transition: left .5s;
- .pdfViewer-overlaySearchBar {
- width: 20%;
- height: 100%;
- font-size: 30px;
- padding: 5px;
- }
- }
-
- .pdfViewer-overlayButton {
- border-bottom-left-radius: 50%;
- display: flex;
- justify-content: space-evenly;
- align-items: center;
- height: 70px;
- background: none;
- padding: 0;
+ .pdfViewer-waiting {
+ width: 70%;
+ height: 70%;
+ margin : 15%;
+ transition: 0.4s opacity ease;
+ opacity: 0.7;
position: absolute;
-
- .pdfViewer-overlayButton-arrow {
- width: 0;
- height: 0;
- border-top: 25px solid transparent;
- border-bottom: 25px solid transparent;
- border-right: 25px solid #121721;
- transition: all 0.5s;
- }
-
- .pdfViewer-overlayButton-iconCont {
- background: #121721;
- height: 50px;
- width: 70px;
- display: flex;
- justify-content: center;
- align-items: center;
- margin-left: -2px;
- border-radius: 3px;
- }
- }
-
- .pdfViewer-overlayButton:hover {
- background: none;
+ z-index: 10;
}
}
+.pdfViewer-viewer-zoomed {
+ overflow-x: scroll;
+}
+ \ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c94b4e3a4..13fd8ea98 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -1,28 +1,33 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, trace, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
-import * as rp from "request-promise";
import { Dictionary } from "typescript-collections";
-import { Doc, DocListCast, FieldResult } from "../../../new_fields/Doc";
+import { Doc, DocListCast, FieldResult, WidthSym, Opt, HeightSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils, numberRange } from "../../../Utils";
+import smoothScroll, { Utils, emptyFunction, returnOne } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
-import { KeyCodes } from "../../northstar/utils/KeyCodes";
-import { CompileScript, CompiledScript } from "../../util/Scripting";
-import Annotation from "./Annotation";
-import Page from "./Page";
+import { DragManager } from "../../util/DragManager";
+import { CompiledScript, CompileScript } from "../../util/Scripting";
+import { Transform } from "../../util/Transform";
+import PDFMenu from "./PDFMenu";
import "./PDFViewer.scss";
import React = require("react");
-import requestPromise = require("request-promise");
+import * as rp from "request-promise";
+import { CollectionPDFView } from "../collections/CollectionPDFView";
+import { CollectionVideoView } from "../collections/CollectionVideoView";
+import { CollectionView } from "../collections/CollectionView";
+import Annotation from "./Annotation";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
+const pdfjsLib = require("pdfjs-dist");
-export const scale = 2;
+pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
interface IViewerProps {
pdf: Pdfjs.PDFDocumentProxy;
@@ -31,15 +36,23 @@ interface IViewerProps {
DataDoc?: Doc;
fieldExtensionDoc: Doc;
fieldKey: string;
+ fieldExt: string;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+ ContentScaling: () => number;
+ select: (isCtrlPressed: boolean) => void;
+ renderDepth: number;
+ isSelected: () => boolean;
loaded: (nw: number, nh: number, np: number) => void;
- panY: number;
- scrollTo: (y: number) => void;
active: () => boolean;
- setPanY?: (n: number) => void;
GoToPage?: (n: number) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
+ setPdfViewer: (view: PDFViewer) => void;
+ ScreenToLocalTransform: () => Transform;
+ ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ whenActiveChanged: (isActive: boolean) => void;
}
/**
@@ -47,36 +60,37 @@ interface IViewerProps {
*/
@observer
export class PDFViewer extends React.Component<IViewerProps> {
- @observable.shallow private _visibleElements: JSX.Element[] = []; // _visibleElements is the array of JSX elements that gets rendered
- @observable private _isPage: string[] = [];// _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder
@observable private _pageSizes: { width: number, height: number }[] = [];
@observable private _annotations: Doc[] = [];
@observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
@observable private _script: CompiledScript = CompileScript("return true") as CompiledScript;
- @observable private _searching: boolean = false;
@observable private Index: number = -1;
-
- private _pageBuffer: number = 1;
+ @observable private _marqueeX: number = 0;
+ @observable private _marqueeY: number = 0;
+ @observable private _marqueeWidth: number = 0;
+ @observable private _marqueeHeight: number = 0;
+ @observable private _marqueeing: boolean = false;
+ @observable private _showWaiting = true;
+ @observable private _showCover = false;
+ @observable private _zoomed = 1;
+
+ public pdfViewer: any;
+ private _isChildActive = false;
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _reactionDisposer?: IReactionDisposer;
+ private _selectionReactionDisposer?: IReactionDisposer;
private _annotationReactionDisposer?: IReactionDisposer;
private _filterReactionDisposer?: IReactionDisposer;
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
- public _pdfViewer: any;
- private _pdfFindController: any;
- private _searchString: string = "";
+ private _marquee: React.RefObject<HTMLDivElement> = React.createRef();
private _selectionText: string = "";
-
- @computed get panY(): number { return this.props.panY; }
-
- // startIndex: where to start rendering pages
- @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.panY) - this._pageBuffer); }
-
- // endIndex: where to end rendering pages
- @computed get endIndex(): number {
- return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.panY + (this._pageSizes[0] ? this._pageSizes[0].height : 0)) + this._pageBuffer);
- }
+ private _startX: number = 0;
+ private _startY: number = 0;
+ private _downX: number = 0;
+ private _downY: number = 0;
+ private _coverPath: any;
@computed get allAnnotations() {
return DocListCast(this.props.fieldExtensionDoc.annotations).filter(
@@ -87,42 +101,32 @@ export class PDFViewer extends React.Component<IViewerProps> {
return this._annotations.filter(anno => this._script.run({ this: anno }, console.log, true).result);
}
- componentDidUpdate = (prevProps: IViewerProps) => this.panY !== prevProps.panY && this.renderPages();
-
componentDidMount = async () => {
- await this.initialLoad();
-
+ // change the address to be the file address of the PNG version of each page
+ // file address of the pdf
+ this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${NumCast(this.props.Document.curPage, 1)}.PNG`)));
+ runInAction(() => this._showWaiting = this._showCover = true);
+ this._selectionReactionDisposer = reaction(() => this.props.isSelected(), () => this.setupPdfJsViewer());
this._reactionDisposer = reaction(
- () => [this.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0],
- () => this.renderPages(),
- { fireImmediately: true });
-
- this._annotationReactionDisposer = reaction(
- () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations),
- annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
- { fireImmediately: true });
-
- this._filterReactionDisposer = reaction(
- () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }),
- action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => {
- let oldScript = this._script.originalScript;
- this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript;
- if (this._script.originalScript !== oldScript) {
- this.Index = -1;
+ () => this.props.Document.scrollY,
+ (scrollY) => {
+ if (scrollY !== undefined) {
+ if (this._showCover || this._showWaiting) {
+ this.setupPdfJsViewer();
+ }
+ this._mainCont.current && smoothScroll(1000, this._mainCont.current, NumCast(this.props.Document.scrollY) || 0);
+ this.props.Document.scrollY = undefined;
}
- annos.forEach(d => d.opacity = this._script.run({ this: d }, console.log, 1).result ? 1 : 0);
- }),
+ },
{ fireImmediately: true }
);
-
- document.removeEventListener("copy", this.copy);
- document.addEventListener("copy", this.copy);
}
componentWillUnmount = () => {
this._reactionDisposer && this._reactionDisposer();
this._annotationReactionDisposer && this._annotationReactionDisposer();
this._filterReactionDisposer && this._filterReactionDisposer();
+ this._selectionReactionDisposer && this._selectionReactionDisposer();
document.removeEventListener("copy", this.copy);
}
@@ -143,42 +147,75 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
- searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
-
- pageLoaded = (page: Pdfjs.PDFPageViewport): void => this.props.loaded(page.width, page.height, this.props.pdf.numPages);
-
setSelectionText = (text: string) => this._selectionText = text;
- getIndex = () => this.Index;
-
@action
initialLoad = async () => {
if (this._pageSizes.length === 0) {
- this._isPage = Array<string>(this.props.pdf.numPages);
this._pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages);
- this._visibleElements = Array<JSX.Element>(this.props.pdf.numPages);
await Promise.all(this._pageSizes.map<Pdfjs.PDFPromise<any>>((val, i) =>
this.props.pdf.getPage(i + 1).then(action((page: Pdfjs.PDFPageProxy) => {
this._pageSizes.splice(i, 1, {
- width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale,
- height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale
+ width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]),
+ height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0])
});
- this._visibleElements.splice(i, 1,
- <div key={`${this.props.url}-placeholder-${i + 1}`} className="pdfviewer-placeholder"
- style={{ width: this._pageSizes[i].width, height: this._pageSizes[i].height }}>
- "PAGE IS LOADING... "
- </div>);
- this.getPlaceholderPage(i);
+ i === this.props.pdf.numPages - 1 && this.props.loaded((page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]),
+ (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]), i);
}))));
- this.props.loaded(Math.max(...this._pageSizes.map(i => i.width)), this._pageSizes[0].height, this.props.pdf.numPages);
-
- let startY = NumCast(this.props.Document.startY, NumCast(this.props.Document.panY));
- this.props.setPanY && this.props.setPanY(startY);
- this.props.scrollTo(startY);
+ Doc.GetProto(this.props.Document).scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
}
}
@action
+ setupPdfJsViewer = async () => {
+ this._selectionReactionDisposer && this._selectionReactionDisposer();
+ this._selectionReactionDisposer = undefined;
+ this._showWaiting = true;
+ this.props.setPdfViewer(this);
+ await this.initialLoad();
+
+ this._annotationReactionDisposer = reaction(
+ () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations),
+ annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
+ { fireImmediately: true });
+
+ this._filterReactionDisposer = reaction(
+ () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }),
+ action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => {
+ let oldScript = this._script.originalScript;
+ this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript;
+ if (this._script.originalScript !== oldScript) {
+ this.Index = -1;
+ }
+ annos.forEach(d => d.opacity = this._script.run({ this: d }, console.log, 1).result ? 1 : 0);
+ }),
+ { fireImmediately: true }
+ );
+
+ document.removeEventListener("copy", this.copy);
+ document.addEventListener("copy", this.copy);
+ document.addEventListener("pagesinit", action(() => {
+ this.pdfViewer.currentScaleValue = this._zoomed = 1;
+ this.gotoPage(NumCast(this.props.Document.curPage, 1));
+ }));
+ document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
+ var pdfLinkService = new PDFJSViewer.PDFLinkService();
+ let pdfFindController = new PDFJSViewer.PDFFindController({
+ linkService: pdfLinkService,
+ });
+ this.pdfViewer = new PDFJSViewer.PDFViewer({
+ container: this._mainCont.current,
+ viewer: this._viewer.current,
+ linkService: pdfLinkService,
+ findController: pdfFindController,
+ renderer: "canvas",
+ });
+ pdfLinkService.setViewer(this.pdfViewer);
+ pdfLinkService.setDocument(this.props.pdf, null);
+ this.pdfViewer.setDocument(this.props.pdf);
+ }
+
+ @action
makeAnnotationDocument = (sourceDoc: Doc | undefined, color: string, createLink: boolean = true): Doc => {
let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
@@ -198,9 +235,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
annoDocs.push(annoDoc);
annoDoc.isButton = true;
anno.remove();
- this.props.addDocument && this.props.addDocument(annoDoc, false);
mainAnnoDoc = annoDoc;
- mainAnnoDocProto = Doc.GetProto(annoDoc);
+ mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
+ mainAnnoDocProto.y = annoDoc.y;
} else {
this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => {
let annoDoc = new Doc();
@@ -231,61 +268,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
@action
- getPlaceholderPage = (page: number) => {
- if (this._isPage[page] !== "none") {
- this._isPage[page] = "none";
- this._visibleElements[page] = (
- <div key={`${this.props.url}-placeholder-${page + 1}`} className="pdfviewer-placeholder"
- style={{ width: this._pageSizes[page].width, height: this._pageSizes[page].height }}>
- "PAGE IS LOADING... "
- </div>);
- }
- }
-
- @action
- getRenderedPage = (page: number) => {
- if (this._isPage[page] !== "page") {
- this._isPage[page] = "page";
- this._visibleElements[page] = (<Page {...this.props}
- size={this._pageSizes[page]}
- numPages={this.props.pdf.numPages}
- setSelectionText={this.setSelectionText}
- page={page}
- key={`${this.props.url}-rendered-${page + 1}`}
- name={`${this.props.pdf.fingerprint + `-page${page + 1}`}`}
- pageLoaded={this.pageLoaded}
- renderAnnotations={this.renderAnnotations}
- createAnnotation={this.createAnnotation}
- sendAnnotations={this.receiveAnnotations}
- makeAnnotationDocuments={this.makeAnnotationDocument}
- getScrollFromPage={this.getScrollFromPage} />);
- }
- }
-
- // change the address to be the file address of the PNG version of each page
- // file address of the pdf
- @action
- getPageImage = async (page: number) => {
- if (this._isPage[page] !== "image") {
- this._isPage[page] = "image";
- try {
- let res = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${page + 1}.PNG`)));
- runInAction(() => this._visibleElements[page] =
- <img key={res.path} src={res.path} onError={() => this.getRenderedPage(page)}
- style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />);
- } catch (e) {
- console.log(e);
- }
- }
- }
-
- renderPages = () => {
- numberRange(this.props.pdf.numPages).filter(p => this._isPage[p] !== undefined).map(i =>
- (i < this.startIndex || i > this.endIndex) ? this.getPlaceholderPage(i) : // pages outside of the pdf use empty stand-in divs
- this.props.active() ? this.getRenderedPage(i) : this.getPageImage(i));
- }
-
- @action
renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => {
if (removeOldAnnotations) {
this._annotations = annotations;
@@ -297,23 +279,30 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
@action
- prevAnnotation = (e: React.MouseEvent) => {
- e.stopPropagation();
+ prevAnnotation = () => {
this.Index = Math.max(this.Index - 1, 0);
- let scrollToAnnotation = this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index];
- this.allAnnotations.forEach(d => Doc.UnBrushDoc(d));
- Doc.BrushDoc(scrollToAnnotation);
- this.props.scrollTo(NumCast(scrollToAnnotation.y));
+ this.scrollToAnnotation(this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]);
}
@action
- nextAnnotation = (e: React.MouseEvent) => {
- e.stopPropagation();
+ nextAnnotation = () => {
this.Index = Math.min(this.Index + 1, this.allAnnotations.length - 1);
- let scrollToAnnotation = this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index];
+ this.scrollToAnnotation(this.allAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y))[this.Index]);
+ }
+
+ @action
+ gotoPage = (p: number) => {
+ this.pdfViewer && this.pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
+ }
+
+ @action
+ scrollToAnnotation = (scrollToAnnotation: Doc) => {
this.allAnnotations.forEach(d => Doc.UnBrushDoc(d));
+ let windowHgt = this.props.PanelHeight() / this.props.ContentScaling();
+ let scrollRange = this._mainCont.current!.scrollHeight - windowHgt;
+ let pgScroll = scrollRange / this._pageSizes.length;
+ this._mainCont.current!.scrollTo(0, NumCast(scrollToAnnotation.y) - pgScroll / 2);
Doc.BrushDoc(scrollToAnnotation);
- this.props.scrollTo(NumCast(scrollToAnnotation.y));
}
sendAnnotations = (page: number) => {
@@ -330,6 +319,11 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
+ @action
+ onScroll = (e: React.UIEvent<HTMLElement>) => {
+ this.pdfViewer && (this.props.Document.curPage = this.pdfViewer.currentPageNumber);
+ }
+
// get the page index that the vertical offset passed in is on
getPageFromScroll = (vOffset: number) => {
let index = 0;
@@ -340,15 +334,11 @@ export class PDFViewer extends React.Component<IViewerProps> {
return index;
}
- getScrollFromPage = (index: number): number => {
- return numberRange(Math.min(this.props.pdf.numPages, index)).reduce((counter, i) => counter + this._pageSizes[i].height, 0);
- }
-
@action
createAnnotation = (div: HTMLDivElement, page: number) => {
if (this._annotationLayer.current) {
if (div.style.top) {
- div.style.top = (parseInt(div.style.top) + this.getScrollFromPage(page)).toString();
+ div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString();
}
this._annotationLayer.current.append(div);
let savedPage = this._savedAnnotations.getValue(page);
@@ -363,11 +353,14 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
@action
- search = (searchString: string) => {
- if (this._pdfViewer._pageViewsReady) {
- this._pdfFindController.executeCommand('findagain', {
+ search = (searchString: string, fwd: boolean) => {
+ if (!searchString) {
+ fwd ? this.nextAnnotation() : this.prevAnnotation();
+ }
+ else if (this.pdfViewer._pageViewsReady) {
+ this.pdfViewer.findController.executeCommand('findagain', {
caseSensitive: false,
- findPrevious: undefined,
+ findPrevious: !fwd,
highlightAll: true,
phraseSearch: true,
query: searchString
@@ -375,122 +368,354 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
else if (this._mainCont.current) {
let executeFind = () => {
- this._pdfFindController.executeCommand('find', {
+ this.pdfViewer.findController.executeCommand('find', {
caseSensitive: false,
- findPrevious: undefined,
+ findPrevious: !fwd,
highlightAll: true,
phraseSearch: true,
query: searchString
});
- }
+ };
this._mainCont.current.addEventListener("pagesloaded", executeFind);
this._mainCont.current.addEventListener("pagerendered", executeFind);
}
}
-
@action
- toggleSearch = (e: React.MouseEvent) => {
- e.stopPropagation();
- this._searching = !this._searching;
-
- if (this._searching) {
- if (!this._pdfFindController && this._mainCont.current && this._viewer.current && !this._pdfViewer) {
- let simpleLinkService = new SimpleLinkService(this);
- this._pdfViewer = new PDFJSViewer.PDFViewer({
- container: this._mainCont.current,
- viewer: this._viewer.current,
- linkService: simpleLinkService
- })
- simpleLinkService.setPDFJSview(this._pdfViewer);
- this._mainCont.current.addEventListener("pagesinit", () => this._pdfViewer.currentScaleValue = 1);
- this._mainCont.current.addEventListener("pagerendered", () => console.log("rendered"));
- this._pdfViewer.setDocument(this.props.pdf);
- this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer);
- this._pdfViewer.findController = this._pdfFindController;
+ onPointerDown = (e: React.PointerEvent): void => {
+ // if alt+left click, drag and annotate
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ if (NumCast(this.props.Document.scale, 1) !== 1) return;
+ if ((e.button !== 0 || e.altKey) && this.active()) {
+ this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true);
+ }
+ this._marqueeing = false;
+ if (!e.altKey && e.button === 0 && this.active()) {
+ PDFMenu.Instance.StartDrag = this.startDrag;
+ PDFMenu.Instance.Highlight = this.highlight;
+ PDFMenu.Instance.Snippet = this.createSnippet;
+ PDFMenu.Instance.Status = "pdf";
+ PDFMenu.Instance.fadeOut(true);
+ if (e.target && (e.target as any).parentElement.className === "textLayer") {
+ if (!e.ctrlKey) {
+ this.receiveAnnotations([], -1);
+ }
+ }
+ else {
+ // set marquee x and y positions to the spatially transformed position
+ if (this._mainCont.current) {
+ let boundingRect = this._mainCont.current.getBoundingClientRect();
+ this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width);
+ this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop;
+ }
+ this._marqueeing = true;
+ let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
+ if (marquees && marquees.length) { // make a copy of the marquee
+ let marquee = marquees[0] as HTMLDivElement;
+ marquee.style.opacity = "0.2";
+ }
+ this.receiveAnnotations([], -1);
}
+ document.removeEventListener("pointermove", this.onSelectMove);
+ document.addEventListener("pointermove", this.onSelectMove);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ document.addEventListener("pointerup", this.onSelectEnd);
}
}
- @computed get visibleElementWrapper() {
- trace();
- return this._visibleElements;
+
+ @action
+ onSelectMove = (e: PointerEvent): void => {
+ if (this._marqueeing && this._mainCont.current) {
+ // transform positions and find the width and height to set the marquee to
+ let boundingRect = this._mainCont.current.getBoundingClientRect();
+ this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width)) - this._startX;
+ this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height)) - this._startY + this._mainCont.current.scrollTop;
+ this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth);
+ this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight);
+ this._marqueeWidth = Math.abs(this._marqueeWidth);
+ this._marqueeHeight = Math.abs(this._marqueeHeight);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ else if (e.target && (e.target as any).parentElement === this._mainCont.current) {
+ e.stopPropagation();
+ }
}
- render() {
- return (<div className="pdfViewer-viewer" ref={this._mainCont} >
- <div className="pdfViewer-visibleElements" style={this._searching ? { position: "absolute", top: 0 } : {}}>
- {this.visibleElementWrapper}
- </div>
- <div className="pdfViewer-text" ref={this._viewer} />
- <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
- {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
- <Annotation {...this.props} anno={anno} key={`${anno[Id]}-annotation`} />)}
- </div>
- <div className="pdfViewer-overlayCont" onPointerDown={(e) => e.stopPropagation()}
- style={{ bottom: -this.props.panY, left: `${this._searching ? 0 : 100}%` }}>
- <button className="pdfViewer-overlayButton" title="Open Search Bar" />
- <input className="pdfViewer-overlaySearchBar" placeholder="Search" onChange={this.searchStringChanged}
- onKeyDown={(e: React.KeyboardEvent) => e.keyCode === KeyCodes.ENTER ? this.search(this._searchString) : e.keyCode === KeyCodes.BACKSPACE ? e.stopPropagation() : true} />
- <button title="Search" onClick={() => this.search(this._searchString)}>
- <FontAwesomeIcon icon="search" size="3x" color="white" /></button>
- </div>
- <button className="pdfViewer-overlayButton" onClick={this.prevAnnotation} title="Previous Annotation"
- style={{ bottom: -this.props.panY + 280, right: 10, display: this.props.active() ? "flex" : "none" }}>
- <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
- <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="3x" /></div>
- </button>
- <button className="pdfViewer-overlayButton" onClick={this.nextAnnotation} title="Next Annotation"
- style={{ bottom: -this.props.panY + 200, right: 10, display: this.props.active() ? "flex" : "none" }}>
- <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
- <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="3x" /></div>
- </button>
- <button className="pdfViewer-overlayButton" onClick={this.toggleSearch} title="Open Search Bar"
- style={{ bottom: -this.props.panY + 10, right: 0, display: this.props.active() ? "flex" : "none" }}>
- <div className="pdfViewer-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()}></div>
- <div className="pdfViewer-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
- <FontAwesomeIcon style={{ color: "white" }} icon={this._searching ? "times" : "search"} size="3x" /></div>
- </button>
- </div >);
+ @action
+ createTextAnnotation = (sel: Selection, selRange: Range) => {
+ if (this._mainCont.current) {
+ let boundingRect = this._mainCont.current.getBoundingClientRect();
+ let clientRects = selRange.getClientRects();
+ for (let i = 0; i < clientRects.length; i++) {
+ let rect = clientRects.item(i);
+ if (rect/* && rect.width !== this._mainCont.current.getBoundingClientRect().width && rect.height !== this._mainCont.current.getBoundingClientRect().height / this.props.pdf.numPages*/) {
+ let scaleY = this._mainCont.current.offsetHeight / boundingRect.height;
+ let scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
+ if (rect.width !== this._mainCont.current.clientWidth) {
+ let annoBox = document.createElement("div");
+ annoBox.className = "pdfPage-annotationBox";
+ // transforms the positions from screen onto the pdf div
+ annoBox.style.top = ((rect.top - boundingRect.top) * scaleY + this._mainCont.current.scrollTop).toString();
+ annoBox.style.left = ((rect.left - boundingRect.left) * scaleX).toString();
+ annoBox.style.width = (rect.width * this._mainCont.current.offsetWidth / boundingRect.width).toString();
+ annoBox.style.height = (rect.height * this._mainCont.current.offsetHeight / boundingRect.height).toString();
+ this.createAnnotation(annoBox, this.getPageFromScroll(rect.top));
+ }
+ }
+ }
+ }
+ let text = selRange.cloneContents().textContent;
+ text && this.setSelectionText(text);
+
+ // clear selection
+ if (sel.empty) { // Chrome
+ sel.empty();
+ } else if (sel.removeAllRanges) { // Firefox
+ sel.removeAllRanges();
+ }
}
-}
-export enum AnnotationTypes { Region }
+ @action
+ onSelectEnd = (e: PointerEvent): void => {
+ if (this._marqueeing) {
+ if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
+ let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
+ if (marquees && marquees.length) { // make a copy of the marquee
+ let copy = document.createElement("div");
+ let marquee = marquees[0] as HTMLDivElement;
+ let style = marquee.style;
+ copy.style.left = style.left;
+ copy.style.top = style.top;
+ copy.style.width = style.width;
+ copy.style.height = style.height;
+ copy.style.border = style.border;
+ copy.style.opacity = style.opacity;
+ copy.className = "pdfPage-annotationBox";
+ this.createAnnotation(copy, this.getPageFromScroll(this._marqueeY));
+ marquee.style.opacity = "0";
+ }
+
+ if (!e.ctrlKey) {
+ PDFMenu.Instance.Status = "snippet";
+ PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
+ }
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+
+ this._marqueeHeight = this._marqueeWidth = 0;
+ }
+ else {
+ let sel = window.getSelection();
+ if (sel && sel.type === "Range") {
+ let selRange = sel.getRangeAt(0);
+ this.createTextAnnotation(sel, selRange);
+ PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+ }
+ }
-class SimpleLinkService {
- _viewer: PDFViewer;
- _pdfjsView: any;
+ if (PDFMenu.Instance.Highlighting) {
+ this.highlight(undefined, "goldenrod");
+ }
+ else {
+ PDFMenu.Instance.StartDrag = this.startDrag;
+ PDFMenu.Instance.Highlight = this.highlight;
+ }
+ document.removeEventListener("pointermove", this.onSelectMove);
+ document.removeEventListener("pointerup", this.onSelectEnd);
+ }
- constructor(viewer: PDFViewer) {
- this._viewer = viewer;
+ @action
+ highlight = (targetDoc: Doc | undefined, color: string) => {
+ // creates annotation documents for current highlights
+ let annotationDoc = this.makeAnnotationDocument(targetDoc, color, false);
+ Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
+ return annotationDoc;
}
- setPDFJSview(v: any) { this._pdfjsView = v; }
- navigateTo() { }
+ /**
+ * This is temporary for creating annotations from highlights. It will
+ * start a drag event and create or put the necessary info into the drag event.
+ */
+ @action
+ startDrag = (e: PointerEvent, ele: HTMLElement): void => {
+ e.preventDefault();
+ e.stopPropagation();
+ let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
+ targetDoc.targetPage = this.getPageFromScroll(this._marqueeY);
+ let annotationDoc = this.highlight(undefined, "red");
+ annotationDoc.linkedToDoc = false;
+ let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
+ DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
+ handlers: {
+ dragComplete: () => {
+ if (!annotationDoc.linkedToDoc) {
+ let annotations = DocListCast(annotationDoc.annotations);
+ annotations && annotations.forEach(anno => anno.target = targetDoc);
+ DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`);
+ }
+ }
+ },
+ hideSource: false
+ });
+ }
- getDestinationHash() { return "#"; }
+ createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
+ let view = Doc.MakeAlias(this.props.Document);
+ let data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
+ data.title = StrCast(data.title) + "_snippet";
+ view.proto = data;
+ view.nativeHeight = marquee.height;
+ view.height = (this.props.Document[WidthSym]() / NumCast(this.props.Document.nativeWidth)) * marquee.height;
+ view.nativeWidth = this.props.Document.nativeWidth;
+ view.startY = marquee.top;
+ view.width = this.props.Document[WidthSym]();
+ DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
+ }
- getAnchorUrl() { return "#"; }
+ // this is called with the document that was dragged and the collection to move it into.
+ // if the target collection is the same as this collection, then the move will be allowed.
+ // otherwise, the document being moved must be able to be removed from its container before
+ // moving it into the target.
+ @action.bound
+ moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
+ if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
+ return true;
+ }
+ return this.removeDocument(doc) ? addDocument(doc) : false;
+ }
- setHash() { }
- isPageVisible(page: number) { return true; }
+ @action.bound
+ removeDocument(doc: Doc): boolean {
+ //TODO This won't create the field if it doesn't already exist
+ let targetDataDoc = this.props.fieldExtensionDoc;
+ let targetField = this.props.fieldExt;
+ let value = Cast(targetDataDoc[targetField], listSpec(Doc), []);
+ let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
+ index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
+ index !== -1 && value.splice(index, 1);
+ return true;
+ }
+ scrollXf = () => {
+ return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._mainCont.current.scrollTop) : this.props.ScreenToLocalTransform();
+ }
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => {
+ this._setPreviewCursor = func;
+ }
+ onClick = (e: React.MouseEvent) => {
+ this._setPreviewCursor &&
+ e.button === 0 &&
+ Math.abs(e.clientX - this._downX) < 3 &&
+ Math.abs(e.clientY - this._downY) < 3 &&
+ this._setPreviewCursor(e.clientX, e.clientY, false);
+ }
+ whenActiveChanged = (isActive: boolean) => {
+ this._isChildActive = isActive;
+ this.props.whenActiveChanged(isActive);
+ }
+ active = () => {
+ return this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
+ }
- executeNamedAction() { }
+ getCoverImage = () => {
+ if (!this.props.Document[HeightSym]()) {
+ setTimeout(() => {
+ this.props.Document.height = this.props.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
+ this.props.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
+ }, 0);
+ }
+ let nativeWidth = NumCast(this.props.Document.nativeWidth);
+ let nativeHeight = NumCast(this.props.Document.nativeHeight);
+ return <img key={this._coverPath.path} src={this._coverPath.path} onLoad={action(() => this._showWaiting = false)}
+ style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />;
+ }
- cachePageRef() { }
- get pagesCount() { return this._viewer._pdfViewer.pagesCount; }
+ @action
+ onZoomWheel = (e: React.WheelEvent) => {
+ e.stopPropagation();
+ if (e.ctrlKey) {
+ let curScale = Number(this.pdfViewer.currentScaleValue);
+ this.pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000));
+ this._zoomed = Number(this.pdfViewer.currentScaleValue);
+ }
+ }
- get page() { return NumCast(this._viewer.props.Document.curPage); }
- set page(p: number) {
- this._pdfjsView._ensurePdfPageLoaded(this._pdfjsView._pages[p - 1]).then(() => {
- this._pdfjsView.renderingQueue.renderView(this._pdfjsView._pages[p - 1]);
- if (this._viewer.props.GoToPage)
- this._viewer.props.GoToPage(p);
- });
+ @computed get annotationLayer() {
+ return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
+ {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
+ <Annotation {...this.props} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ </div>;
+ }
+ @computed get pdfViewerDiv() {
+ return <div className="pdfViewer-text" ref={this._viewer} style={{ transformOrigin: "left top" }} />;
}
+ @computed get standinViews() {
+ return <>
+ {this._showCover ? this.getCoverImage() : (null)}
+ {this._showWaiting ? <img className="pdfViewer-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)}
+ </>;
+ }
+ marqueeWidth = () => this._marqueeWidth;
+ marqueeHeight = () => this._marqueeHeight;
+ marqueeX = () => this._marqueeX;
+ marqueeY = () => this._marqueeY;
+ marqueeing = () => this._marqueeing;
+ render() {
+ trace();
+ return (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
+ {this.pdfViewerDiv}
+ <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
+ <div className="pdfViewer-overlay" style={{ transform: `scale(${this._zoomed})` }}>
+ {this.annotationLayer}
+ <CollectionFreeFormView {...this.props}
+ setPreviewCursor={this.setPreviewCursor}
+ PanelHeight={() => NumCast(this.props.Document.scrollHeight, NumCast(this.props.Document.nativeHeight))}
+ PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : NumCast(this.props.Document.nativeWidth)}
+ focus={emptyFunction}
+ isSelected={this.props.isSelected}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={(doc: Doc, allow: boolean | undefined) => { Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, doc); return true; }}
+ CollectionView={this.props.ContainingCollectionView}
+ ScreenToLocalTransform={this.scrollXf}
+ ruleProvider={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document}
+ chromeCollapsed={true}>
+ </CollectionFreeFormView>
+ </div>
+ {this.standinViews}
+ </div >);
+ }
+}
+
+interface PdfViewerMarqueeProps {
+ isMarqueeing: () => boolean;
+ width: () => number;
+ height: () => number;
+ x: () => number;
+ y: () => number;
+}
+
+@observer
+class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> {
+ render() {
+ return !this.props.isMarqueeing() ? (null) : <div className="pdfViewer-dragAnnotationBox"
+ style={{
+ left: `${this.props.x()}px`, top: `${this.props.y()}px`,
+ width: `${this.props.width()}px`, height: `${this.props.height()}px`,
+ border: `${this.props.width() === 0 ? "" : "2px dashed black"}`
+ }}>
+ </div>;
+ }
+}
- get rotation() { return 0; }
- set rotation(value: any) { }
-} \ No newline at end of file
+export enum AnnotationTypes { Region }
diff --git a/src/client/views/pdf/Page.scss b/src/client/views/pdf/Page.scss
deleted file mode 100644
index d8034b4b4..000000000
--- a/src/client/views/pdf/Page.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-
-.pdfViewer-text {
- .page {
- position: relative;
- }
-}
-.pdfPage-cont {
- position: relative;
-
- .pdfPage-canvasContainer {
- position: absolute;
- }
-
- .pdfPage-dragAnnotationBox {
- position: absolute;
- background-color: transparent;
- opacity: 0.1;
- }
-
- .pdfPage-textLayer {
- position: absolute;
- width: 100%;
- height: 100%;
- div {
- user-select: text;
- }
- span {
- color: transparent;
- position: absolute;
- white-space: pre;
- cursor: text;
- -webkit-transform-origin: 0% 0%;
- transform-origin: 0% 0%;
- }
- }
-} \ No newline at end of file
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
deleted file mode 100644
index 533247170..000000000
--- a/src/client/views/pdf/Page.tsx
+++ /dev/null
@@ -1,293 +0,0 @@
-import { action, IReactionDisposer, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as Pdfjs from "pdfjs-dist";
-import "pdfjs-dist/web/pdf_viewer.css";
-import { Doc, DocListCastAsync, Opt, WidthSym } from "../../../new_fields/Doc";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Docs, DocUtils } from "../../documents/Documents";
-import { DragManager } from "../../util/DragManager";
-import PDFMenu from "./PDFMenu";
-import { scale } from "./PDFViewer";
-import "./Page.scss";
-import React = require("react");
-
-
-interface IPageProps {
- size: { width: number, height: number };
- pdf: Pdfjs.PDFDocumentProxy;
- name: string;
- numPages: number;
- page: number;
- pageLoaded: (page: Pdfjs.PDFPageViewport) => void;
- fieldExtensionDoc: Doc;
- Document: Doc;
- renderAnnotations: (annotations: Doc[], removeOld: boolean) => void;
- sendAnnotations: (annotations: HTMLDivElement[], page: number) => void;
- createAnnotation: (div: HTMLDivElement, page: number) => void;
- makeAnnotationDocuments: (doc: Doc | undefined, color: string, linkTo: boolean) => Doc;
- getScrollFromPage: (page: number) => number;
- setSelectionText: (text: string) => void;
-}
-
-@observer
-export default class Page extends React.Component<IPageProps> {
- @observable private _state: "N/A" | "rendering" = "N/A";
- @observable private _width: number = this.props.size.width;
- @observable private _height: number = this.props.size.height;
- @observable private _page: Opt<Pdfjs.PDFPageProxy>;
- @observable private _currPage: number = this.props.page + 1;
- @observable private _marqueeX: number = 0;
- @observable private _marqueeY: number = 0;
- @observable private _marqueeWidth: number = 0;
- @observable private _marqueeHeight: number = 0;
-
- private _canvas: React.RefObject<HTMLCanvasElement> = React.createRef();
- private _textLayer: React.RefObject<HTMLDivElement> = React.createRef();
- private _marquee: React.RefObject<HTMLDivElement> = React.createRef();
- private _marqueeing: boolean = false;
- private _reactionDisposer?: IReactionDisposer;
- private _startY: number = 0;
- private _startX: number = 0;
-
- componentDidMount = (): void => this.loadPage(this.props.pdf);
-
- componentDidUpdate = (): void => this.loadPage(this.props.pdf);
-
- componentWillUnmount = (): void => this._reactionDisposer && this._reactionDisposer();
-
- loadPage = (pdf: Pdfjs.PDFDocumentProxy): void => {
- pdf.getPage(this._currPage).then(page => this.renderPage(page));
- }
-
- @action
- renderPage = (page: Pdfjs.PDFPageProxy): void => {
- // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes
- if (this._state !== "rendering" && !this._page && this._canvas.current && this._textLayer.current) {
- this._state = "rendering";
- let viewport = page.getViewport(scale);
- this._canvas.current.width = this._width = viewport.width;
- this._canvas.current.height = this._height = viewport.height;
- this.props.pageLoaded(viewport);
- let ctx = this._canvas.current.getContext("2d");
- if (ctx) {
- //@ts-ignore
- page.render({ canvasContext: ctx, viewport: viewport, enableWebGL: true }); // renders the page onto the canvas context
- page.getTextContent().then(res => // renders text onto the text container
- //@ts-ignore
- Pdfjs.renderTextLayer({
- textContent: res,
- container: this._textLayer.current,
- viewport: viewport
- }));
-
- this._page = page;
- }
- }
- }
-
- @action
- highlight = (targetDoc: Doc | undefined, color: string) => {
- // creates annotation documents for current highlights
- let annotationDoc = this.props.makeAnnotationDocuments(targetDoc, color, false);
- Doc.AddDocToList(this.props.fieldExtensionDoc, "annotations", annotationDoc);
- return annotationDoc;
- }
-
- /**
- * This is temporary for creating annotations from highlights. It will
- * start a drag event and create or put the necessary info into the drag event.
- */
- @action
- startDrag = (e: PointerEvent, ele: HTMLElement): void => {
- e.preventDefault();
- e.stopPropagation();
- if (this._textLayer.current) {
- let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
- targetDoc.targetPage = this.props.page;
- let annotationDoc = this.highlight(undefined, "red");
- annotationDoc.linkedToDoc = false;
- let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
- DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
- handlers: {
- dragComplete: async () => {
- if (!BoolCast(annotationDoc.linkedToDoc)) {
- let annotations = await DocListCastAsync(annotationDoc.annotations);
- annotations && annotations.forEach(anno => anno.target = targetDoc);
- DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`);
- }
- }
- },
- hideSource: false
- });
- }
- }
-
- // cleans up events and boolean
- endDrag = (e: PointerEvent): void => {
- e.stopPropagation();
- }
-
- createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
- let view = Doc.MakeAlias(this.props.Document);
- let data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
- data.title = StrCast(data.title) + "_snippet";
- view.proto = data;
- view.nativeHeight = marquee.height;
- view.height = (this.props.Document[WidthSym]() / NumCast(this.props.Document.nativeWidth)) * marquee.height;
- view.nativeWidth = this.props.Document.nativeWidth;
- view.startY = marquee.top + this.props.getScrollFromPage(this.props.page);
- view.width = this.props.Document[WidthSym]();
- DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
- }
-
- @action
- onPointerDown = (e: React.PointerEvent): void => {
- // if alt+left click, drag and annotate
- if (NumCast(this.props.Document.scale, 1) !== 1) return;
- if (!e.altKey && e.button === 0) {
- PDFMenu.Instance.StartDrag = this.startDrag;
- PDFMenu.Instance.Highlight = this.highlight;
- PDFMenu.Instance.Snippet = this.createSnippet;
- PDFMenu.Instance.Status = "pdf";
- PDFMenu.Instance.fadeOut(true);
- if (e.target && (e.target as any).parentElement === this._textLayer.current) {
- e.stopPropagation();
- if (!e.ctrlKey) {
- this.props.sendAnnotations([], -1);
- }
- }
- else {
- // set marquee x and y positions to the spatially transformed position
- if (this._textLayer.current) {
- let boundingRect = this._textLayer.current.getBoundingClientRect();
- this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width);
- this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height);
- }
- this._marqueeing = true;
- this._marquee.current && (this._marquee.current.style.opacity = "0.2");
- this.props.sendAnnotations([], -1);
- }
- document.removeEventListener("pointermove", this.onSelectStart);
- document.addEventListener("pointermove", this.onSelectStart);
- document.removeEventListener("pointerup", this.onSelectEnd);
- document.addEventListener("pointerup", this.onSelectEnd);
- }
- }
-
- @action
- onSelectStart = (e: PointerEvent): void => {
- if (this._marqueeing && this._textLayer.current) {
- // transform positions and find the width and height to set the marquee to
- let boundingRect = this._textLayer.current.getBoundingClientRect();
- this._marqueeWidth = ((e.clientX - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)) - this._startX;
- this._marqueeHeight = ((e.clientY - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)) - this._startY;
- this._marqueeX = Math.min(this._startX, this._startX + this._marqueeWidth);
- this._marqueeY = Math.min(this._startY, this._startY + this._marqueeHeight);
- this._marqueeWidth = Math.abs(this._marqueeWidth);
- e.stopPropagation();
- e.preventDefault();
- }
- else if (e.target && (e.target as any).parentElement === this._textLayer.current) {
- e.stopPropagation();
- }
- }
-
- @action
- onSelectEnd = (e: PointerEvent): void => {
- if (this._marqueeing) {
- this._marqueeing = false;
- if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
- if (this._marquee.current) { // make a copy of the marquee
- let copy = document.createElement("div");
- let style = this._marquee.current.style;
- copy.style.left = style.left;
- copy.style.top = style.top;
- copy.style.width = style.width;
- copy.style.height = style.height;
- copy.style.border = style.border;
- copy.style.opacity = style.opacity;
- copy.className = "pdfPage-annotationBox";
- this.props.createAnnotation(copy, this.props.page);
- this._marquee.current.style.opacity = "0";
- }
-
- if (!e.ctrlKey) {
- PDFMenu.Instance.Status = "snippet";
- PDFMenu.Instance.Marquee = { left: this._marqueeX, top: this._marqueeY, width: this._marqueeWidth, height: this._marqueeHeight };
- }
- PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
- }
-
- this._marqueeHeight = this._marqueeWidth = 0;
- }
- else {
- let sel = window.getSelection();
- if (sel && sel.type === "Range") {
- let selRange = sel.getRangeAt(0);
- this.createTextAnnotation(sel, selRange);
- PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
- }
- }
-
- if (PDFMenu.Instance.Highlighting) {
- this.highlight(undefined, "goldenrod");
- }
- else {
- PDFMenu.Instance.StartDrag = this.startDrag;
- PDFMenu.Instance.Highlight = this.highlight;
- }
- document.removeEventListener("pointermove", this.onSelectStart);
- document.removeEventListener("pointerup", this.onSelectEnd);
- }
-
- @action
- createTextAnnotation = (sel: Selection, selRange: Range) => {
- if (this._textLayer.current) {
- let boundingRect = this._textLayer.current.getBoundingClientRect();
- let clientRects = selRange.getClientRects();
- for (let i = 0; i < clientRects.length; i++) {
- let rect = clientRects.item(i);
- if (rect && rect.width !== this._textLayer.current.getBoundingClientRect().width && rect.height !== this._textLayer.current.getBoundingClientRect().height) {
- let annoBox = document.createElement("div");
- annoBox.className = "pdfPage-annotationBox";
- // transforms the positions from screen onto the pdf div
- annoBox.style.top = ((rect.top - boundingRect.top) * (this._textLayer.current.offsetHeight / boundingRect.height)).toString();
- annoBox.style.left = ((rect.left - boundingRect.left) * (this._textLayer.current.offsetWidth / boundingRect.width)).toString();
- annoBox.style.width = (rect.width * this._textLayer.current.offsetWidth / boundingRect.width).toString();
- annoBox.style.height = (rect.height * this._textLayer.current.offsetHeight / boundingRect.height).toString();
- this.props.createAnnotation(annoBox, this.props.page);
- }
- }
- }
- let text = selRange.cloneContents().textContent;
- text && this.props.setSelectionText(text);
-
- // clear selection
- if (sel.empty) { // Chrome
- sel.empty();
- } else if (sel.removeAllRanges) { // Firefox
- sel.removeAllRanges();
- }
- }
-
- doubleClick = (e: React.MouseEvent) => {
- if (e.target && (e.target as any).parentElement === this._textLayer.current) {
- // do something to select the paragraph ideally
- }
- }
-
- render() {
- return (
- <div className={"pdfPage-cont"} onPointerDown={this.onPointerDown} onDoubleClick={this.doubleClick} style={{ "width": this._width, "height": this._height }}>
- <canvas className="PdfPage-canvasContainer" ref={this._canvas} />
- <div className="pdfPage-dragAnnotationBox" ref={this._marquee}
- style={{
- left: `${this._marqueeX}px`, top: `${this._marqueeY}px`,
- width: `${this._marqueeWidth}px`, height: `${this._marqueeHeight}px`,
- border: `${this._marqueeWidth === 0 ? "" : "10px dashed black"}`
- }}>
- </div>
- <div className="pdfPage-textlayer" ref={this._textLayer} />
- </div>);
- }
-} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationModeMenu.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx
index 4de8da587..0dd2b7731 100644
--- a/src/client/views/presentationview/PresentationModeMenu.tsx
+++ b/src/client/views/presentationview/PresentationModeMenu.tsx
@@ -21,10 +21,12 @@ export interface PresModeMenuProps {
export default class PresModeMenu extends React.Component<PresModeMenuProps> {
@observable private _top: number = 20;
- @observable private _right: number = 0;
+ @observable private _left: number = window.innerWidth - 160;
@observable private _opacity: number = 1;
@observable private _transition: string = "opacity 0.5s";
@observable private _transitionDelay: string = "";
+ private _offsetY: number = 0;
+ private _offsetX: number = 0;
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
@@ -35,8 +37,8 @@ export default class PresModeMenu extends React.Component<PresModeMenuProps> {
*/
@action
dragging = (e: PointerEvent) => {
- this._right -= e.movementX;
- this._top += e.movementY;
+ this._left = e.pageX - this._offsetX;
+ this._top = e.pageY - this._offsetY;
e.stopPropagation();
e.preventDefault();
@@ -63,6 +65,9 @@ export default class PresModeMenu extends React.Component<PresModeMenuProps> {
document.removeEventListener("pointerup", this.dragEnd);
document.addEventListener("pointerup", this.dragEnd);
+ this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
+ this._offsetY = e.nativeEvent.offsetY;
+
e.stopPropagation();
e.preventDefault();
}
@@ -82,7 +87,7 @@ export default class PresModeMenu extends React.Component<PresModeMenuProps> {
render() {
return (
<div className="presMenu-cont" ref={this._mainCont}
- style={{ right: this._right, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
+ style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
<button title="Back" className="presMenu-button" onClick={this.props.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
{this.renderPlayPauseButton()}
<button title="Next" className="presMenu-button" onClick={this.props.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
diff --git a/src/debug/Test.tsx b/src/debug/Test.tsx
index 79f87f4ac..3baedce4b 100644
--- a/src/debug/Test.tsx
+++ b/src/debug/Test.tsx
@@ -2,39 +2,12 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { DocServer } from '../client/DocServer';
import { Doc } from '../new_fields/Doc';
+import * as Pdfjs from "pdfjs-dist";
+import "pdfjs-dist/web/pdf_viewer.css";
+import { Utils } from '../Utils';
+const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
const protoId = "protoDoc";
const delegateId = "delegateDoc";
class Test extends React.Component {
- onCreateClick = () => {
- const proto = new Doc(protoId, true);
- const delegate = Doc.MakeDelegate(proto, delegateId);
- }
-
- onReadClick = async () => {
- console.log("reading");
- const docs = await DocServer.GetRefFields([delegateId, protoId]);
- console.log("done");
- console.log(docs);
- }
-
- onDeleteClick = () => {
- DocServer.DeleteDocuments([protoId, delegateId]);
- }
-
- render() {
- return (
- <div>
- <button onClick={this.onCreateClick}>Create Docs</button>
- <button onClick={this.onReadClick}>Read Docs</button>
- <button onClick={this.onDeleteClick}>Delete Docs</button>
- </div>
- );
- }
}
-
-DocServer.init(window.location.protocol, window.location.hostname, 4321, "test");
-ReactDOM.render(
- <Test />,
- document.getElementById('root')
-); \ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index fef3b8cc5..605877efa 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -13,6 +13,7 @@ import { listSpec } from "./Schema";
import { ComputedField } from "./ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstructor } from "./Types";
import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
+import { intersectRect } from "../Utils";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -343,7 +344,7 @@ export namespace Doc {
let list = Cast(target[key], listSpec(Doc));
if (list) {
if (allowDuplicates !== true) {
- let pind = list.reduce((l, d, i) => d instanceof Doc && Doc.AreProtosEqual(d, doc) ? i : l, -1);
+ let pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1);
if (pind !== -1) {
list.splice(pind, 1);
}
@@ -614,6 +615,18 @@ export namespace Doc {
}), 0);
}
+ export function overlapping(doc: Doc, doc2: Doc, clusterDistance: number) {
+ var x2 = NumCast(doc2.x) - clusterDistance;
+ var y2 = NumCast(doc2.y) - clusterDistance;
+ var w2 = NumCast(doc2.width) + clusterDistance;
+ var h2 = NumCast(doc2.height) + clusterDistance;
+ var x = NumCast(doc.x) - clusterDistance;
+ var y = NumCast(doc.y) - clusterDistance;
+ var w = NumCast(doc.width) + clusterDistance;
+ var h = NumCast(doc.height) + clusterDistance;
+ return doc.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
+ }
+
export function isBrushedHighlightedDegree(doc: Doc) {
if (Doc.IsHighlighted(doc)) {
return 3;