aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts6
-rw-r--r--src/client/documents/Documents.ts25
-rw-r--r--src/client/util/DocumentManager.ts14
-rw-r--r--src/client/views/MainView.tsx12
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx68
-rw-r--r--src/client/views/collections/CollectionPileView.tsx8
-rw-r--r--src/client/views/collections/CollectionStackingView.scss42
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx2
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx2
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx9
-rw-r--r--src/client/views/collections/CollectionView.tsx16
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss60
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx202
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss26
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx95
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx12
-rw-r--r--src/client/views/linking/LinkEditor.tsx2
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx6
-rw-r--r--src/client/views/nodes/ComparisonBox.scss16
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx19
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx4
-rw-r--r--src/client/views/nodes/DocumentView.tsx17
-rw-r--r--src/client/views/nodes/PresBox.tsx2
-rw-r--r--src/client/views/nodes/WebBox.tsx4
-rw-r--r--src/client/views/pdf/PDFViewer.scss4
-rw-r--r--src/client/views/pdf/PDFViewer.tsx18
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx1
-rw-r--r--src/client/views/search/SearchBox.tsx4
-rw-r--r--src/fields/Doc.ts40
-rw-r--r--src/fields/ScriptField.ts2
-rw-r--r--src/fields/documentSchemas.ts2
-rw-r--r--src/fields/util.ts10
-rw-r--r--src/scraping/buxton/final/BuxtonImporter.ts74
-rw-r--r--src/scraping/buxton/final/assets/pdfs/3DCad_Brochure.pdfbin0 -> 107790 bytes
-rw-r--r--src/server/ApiManagers/PDFManager.ts43
35 files changed, 546 insertions, 321 deletions
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 517ccdce9..b816d1617 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -45,7 +45,11 @@ export enum Confidence {
export namespace CognitiveServices {
const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => {
- const apiKey = process.env[service.toUpperCase()];
+ let apiKey = process.env[service.toUpperCase()];
+ // A HACK FOR A DEMO VIDEO - syip2
+ if (service === "handwriting") {
+ apiKey = "61088486d76c4b12ba578775a5f55422";
+ }
if (!apiKey) {
console.log(`No API key found for ${service}: ensure youe root directory has .env file with _CLIENT_${service.toUpperCase()}.`);
return undefined;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 754d1eed1..8513da76c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -432,17 +432,28 @@ export namespace Docs {
parentProto.data = new List<Doc>();
}
if (device) {
- const { __images } = device;
+ const { title, __images, additionalMedia } = device;
delete device.__images;
+ delete device.additionalMedia;
const { ImageDocument, StackingDocument } = Docs.Create;
const constructed = __images.map(({ url, nativeWidth, nativeHeight }) => ({ url: Utils.prepend(url), nativeWidth, nativeHeight }));
- const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => ImageDocument(url, {
- title: `image${i}.${extname(url)}`,
- _nativeWidth: nativeWidth,
- _nativeHeight: nativeHeight
- }));
+ const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => {
+ const imageDoc = ImageDocument(url, {
+ title: `image${i}.${extname(url)}`,
+ _nativeWidth: nativeWidth,
+ _nativeHeight: nativeHeight
+ });
+ const media = additionalMedia[i];
+ if (media) {
+ for (const key of Object.keys(media)) {
+ imageDoc[`additionalMedia_${key}`] = Utils.prepend(`/files/${key}/buxton/${media[key]}`);
+ }
+ }
+ return imageDoc;
+ });
// the main document we create
- const doc = StackingDocument(deviceImages, { title: device.title, _LODdisable: true, hero: new ImageField(constructed[0].url) });
+ const doc = StackingDocument(deviceImages, { title, _LODdisable: true, hero: new ImageField(constructed[0].url) });
+ doc.nameAliases = new List<string>([title.toLowerCase()]);
// add the parsed attributes to this main document
Docs.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } });
Doc.AddDocToList(parentProto, "data", doc);
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ab087335e..67f2f244c 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -126,13 +126,13 @@ export class DocumentManager {
finished?.();
}
public jumpToDocument = async (
- targetDoc: Doc,
- willZoom: boolean,
- createViewFunc = DocumentManager.addRightSplit,
- docContext?: Doc,
- linkId?: string,
- closeContextIfNotFound: boolean = false,
- originatingDoc: Opt<Doc> = undefined,
+ targetDoc: Doc, // document to display
+ willZoom: boolean, // whether to zoom doc to take up most of screen
+ createViewFunc = DocumentManager.addRightSplit, // how to create a view of the doc if it doesn't exist
+ docContext?: Doc, // context to load that should contain the target
+ linkId?: string, // link that's being followed
+ closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there
+ originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc
finished?: () => void
): Promise<void> => {
const getFirstDocView = DocumentManager.Instance.getFirstDocumentView;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 358de2333..a1d1b0ece 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -51,6 +51,8 @@ import { PreviewCursor } from './PreviewCursor';
import { ScriptField } from '../../fields/ScriptField';
import { TimelineMenu } from './animationtimeline/TimelineMenu';
import { SnappingManager } from '../util/SnappingManager';
+import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { DocumentManager } from '../util/DocumentManager';
@observer
export class MainView extends React.Component {
@@ -83,6 +85,14 @@ export class MainView extends React.Component {
window.removeEventListener("keydown", KeyManager.Instance.handle);
window.addEventListener("keydown", KeyManager.Instance.handle);
window.addEventListener("paste", KeyManager.Instance.paste as any);
+ document.addEventListener("dash", (e: any) => { // event used by chrome plugin to tell Dash which document to focus on
+ const id = FormattedTextBox.GetDocFromUrl(e.detail);
+ DocServer.GetRefField(id).then(doc => {
+ if (doc instanceof Doc) {
+ DocumentManager.Instance.jumpToDocument(doc, false, undefined);
+ }
+ });
+ });
}
componentWillUnMount() {
@@ -534,7 +544,7 @@ export class MainView extends React.Component {
}
@computed get snapLines() {
- return <div className="mainView-snapLines">
+ return !Doc.UserDoc().showSnapLines ? (null) : <div className="mainView-snapLines">
<svg style={{ width: "100%", height: "100%" }}>
{SnappingManager.horizSnapLines().map(l => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
{SnappingManager.vertSnapLines().map(l => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
index 92b0f05b3..b562bd957 100644
--- a/src/client/views/animationtimeline/Keyframe.tsx
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -12,11 +12,9 @@ import { Transform } from "../../util/Transform";
import { TimelineMenu } from "./TimelineMenu";
import { Docs } from "../../documents/Documents";
import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { undoBatch, UndoManager } from "../../util/UndoManager";
import { emptyPath } from "../../../Utils";
-
/**
* Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also
*/
@@ -100,16 +98,11 @@ export namespace KeyframeFunc {
export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing: number, tickIncrement: number) => {
const time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;
switch (unit) {
- case "mili":
- return time;
- case "sec":
- return dir === "pixel" ? time / 1000 : time * 1000;
- case "min":
- return dir === "pixel" ? time / 60000 : time * 60000;
- case "hr":
- return dir === "pixel" ? time / 3600000 : time * 3600000;
- default:
- return time;
+ case "mili": return time;
+ case "sec": return dir === "pixel" ? time / 1000 : time * 1000;
+ case "min": return dir === "pixel" ? time / 60000 : time * 60000;
+ case "hr": return dir === "pixel" ? time / 3600000 : time * 3600000;
+ default: return time;
}
};
}
@@ -187,10 +180,10 @@ export class Keyframe extends React.Component<IProps> {
const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade);
const fadeOut = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut, KeyframeFunc.KeyframeType.fade);
const finish = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.duration, KeyframeFunc.KeyframeType.end);
- (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;
+ (fadeIn as Doc).opacity = 1;
+ (fadeOut as Doc).opacity = 1;
+ (start as Doc).opacity = 0.1;
+ (finish as Doc).opacity = 0.1;
this.forceUpdate(); //not needed, if setTimeout is gone...
}, 1000);
}
@@ -333,31 +326,28 @@ export class Keyframe extends React.Component<IProps> {
*/
@action
makeKeyframeMenu = (kf: Doc, e: MouseEvent) => {
- TimelineMenu.Instance.addItem("button", "Show Data", () => {
- runInAction(() => {
+ TimelineMenu.Instance.addItem("button", "Toggle Fade Only", () => {
+ kf.type = kf.type === KeyframeFunc.KeyframeType.fade ? KeyframeFunc.KeyframeType.default : KeyframeFunc.KeyframeType.fade;
+ }),
+ TimelineMenu.Instance.addItem("button", "Show Data", action(() => {
const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 });
CollectionDockingView.AddRightSplit(kvp, emptyPath);
- });
- }),
- TimelineMenu.Instance.addItem("button", "Delete", () => {
- runInAction(() => {
- (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
- this.forceUpdate();
- });
- }),
- TimelineMenu.Instance.addItem("input", "Move", (val) => {
- runInAction(() => {
- let cannotMove: boolean = false;
- const kfIndex: number = this.keyframes.indexOf(kf);
- if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) {
- cannotMove = true;
- }
- if (!cannotMove) {
- this.keyframes[kfIndex].time = parseInt(val, 10);
- this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
- }
- });
- });
+ })),
+ TimelineMenu.Instance.addItem("button", "Delete", action(() => {
+ (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
+ this.forceUpdate();
+ })),
+ TimelineMenu.Instance.addItem("input", "Move", action((val) => {
+ let cannotMove: boolean = false;
+ const kfIndex: number = this.keyframes.indexOf(kf);
+ if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.keyframes[kfIndex].time = parseInt(val, 10);
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+ }));
TimelineMenu.Instance.addMenu("Keyframe");
TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
}
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 020a87b2e..fc48e0327 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -23,7 +23,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
@observable _childClickedScript: Opt<ScriptField>;
componentDidMount() {
if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") {
- this.Document._layoutEngine = "pass";
+ this.Document._pileLayoutEngine = "pass";
}
this._originalChrome = StrCast(this.layoutDoc._chromeStatus);
this.layoutDoc._chromeStatus = "disabled";
@@ -34,7 +34,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
this.layoutDoc._chromeStatus = this._originalChrome;
}
- layoutEngine = () => StrCast(this.Document._layoutEngine);
+ layoutEngine = () => StrCast(this.Document._pileLayoutEngine);
@computed get contents() {
return <div className="collectionPileView-innards" style={{ pointerEvents: this.layoutEngine() === "starburst" ? undefined : "none" }} >
@@ -53,7 +53,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
Doc.pileup(this.childDocs);
this.layoutDoc._panX = 0;
this.layoutDoc._panY = -10;
- this.props.Document._layoutEngine = 'pass';
+ this.props.Document._pileLayoutEngine = 'pass';
} else {
const defaultSize = 25;
this.layoutDoc._overflow = 'visible';
@@ -67,7 +67,7 @@ export class CollectionPileView extends CollectionSubView(doc => doc) {
}
this.layoutDoc._panX = this.layoutDoc._panY = 0;
this.layoutDoc._width = this.layoutDoc._height = defaultSize;
- this.props.Document._layoutEngine = 'starburst';
+ this.props.Document._pileLayoutEngine = 'starburst';
}
});
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 5eaf29316..203c51163 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -7,11 +7,13 @@
.collectionStackingView {
display: flex;
}
+
.collectionStackingMasonry-cont {
- position:relative;
- height:100%;
- width:100%;
+ position: relative;
+ height: 100%;
+ width: 100%;
}
+
.collectionStackingView,
.collectionMasonryView {
height: 100%;
@@ -22,12 +24,17 @@
overflow-x: hidden;
flex-wrap: wrap;
transition: top .5s;
+
>div {
position: relative;
display: block;
}
+
.collectionStackingViewFieldColumn {
- height:max-content;
+ height: max-content;
+ }
+ .collectionStackingViewFieldColumnDragging {
+ height:100%;
}
.collectionSchemaView-previewDoc {
@@ -129,27 +136,34 @@
background: red;
}
}
+
.collectionStackingView-miniHeader {
width: 100%;
+
.editableView-container-editing-oneLine {
min-height: 20px;
display: flex;
align-items: center;
flex-direction: row;
}
- span::before , span::after{
+
+ span::before,
+ span::after {
content: "";
width: 50%;
border-top: dashed gray 1px;
position: relative;
display: inline-block;
}
+
span::before {
margin-right: 10px;
}
- span::after{
+
+ span::after {
margin-left: 10px;
}
+
span {
position: relative;
text-align: center;
@@ -157,10 +171,11 @@
overflow: visible;
width: 100%;
display: flex;
- color:gray;
+ color: gray;
align-items: center;
}
}
+
.collectionStackingView-sectionHeader {
text-align: center;
margin: auto;
@@ -277,7 +292,7 @@
height: 20px;
border-radius: 10px;
margin: 3px;
- width:max-content;
+ width: max-content;
&.active {
color: red;
@@ -294,15 +309,18 @@
display: none;
}
}
+
.collectionStackingView-sectionHeader:hover {
.collectionStackingView-sectionColor {
- display:unset;
+ display: unset;
}
+
.collectionStackingView-sectionOptions {
- display:unset;
+ display: unset;
}
+
.collectionStackingView-sectionDelete {
- display:unset;
+ display: unset;
}
}
@@ -403,4 +421,4 @@
.rc-switch-checked .rc-switch-inner {
left: 8px;
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 6949670d6..7fd19a23c 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -193,7 +193,7 @@ export class CollectionStackingView extends CollectionSubView(StackingDocument)
getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
const height = () => this.getDocHeight(doc);
- const opacity = () => this.Document.currentTimecode === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, this.Document.currentTimecode || 0)?.opacity;
+ const opacity = () => this.Document.currentFrame === undefined ? this.props.childOpacity?.() : CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document.currentFrame))?.opacity;
return <ContentFittingDocumentView
Document={doc}
DataDoc={dataDoc || (doc[DataSym] !== doc && doc[DataSym])}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 53435ccc9..a269b21f5 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -352,7 +352,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
const chromeStatus = this.props.parent.props.Document._chromeStatus;
return (
- <div className="collectionStackingViewFieldColumn" key={heading}
+ <div className={"collectionStackingViewFieldColumn" + (SnappingManager.GetIsDragging() ? "Dragging" : "")} key={heading}
style={{
width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`,
height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined,
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3e99af724..b2e1c0f73 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -717,7 +717,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
}
ContextMenu.Instance.addItem({
description: "Buxton Layout", icon: "eye", event: () => {
- const { ImageDocument } = Docs.Create;
+ const { ImageDocument, PdfDocument } = Docs.Create;
const { Document } = this.props;
const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif";
const detailView = Cast(Cast(Doc.UserDoc()["template-button-detail"], Doc, null)?.dragFactory, Doc, null);
@@ -726,13 +726,14 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
heroView._showTitle = "title";
heroView._showTitleHover = "titlehover";
- const doubleClickView = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target
+ const fallback = ImageDocument("http://cs.brown.edu/~bcz/face.gif", { _width: 400 }); // replace with desired double click target
+ let pdfContent: string;
DocListCast(this.dataDoc[this.props.fieldKey]).map(d => {
DocListCast(d.data).map((img, i) => {
const caption = (d.captions as any)[i];
if (caption) {
Doc.GetProto(img).caption = caption;
- Doc.GetProto(img).doubleClickView = doubleClickView;
+ Doc.GetProto(img).doubleClickView = (pdfContent = StrCast(img.additionalMedia_pdfs)) ? PdfDocument(pdfContent, { title: pdfContent }) : fallback;
}
});
Doc.GetProto(d).type = "buxton";
@@ -748,7 +749,7 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
Document.childLayoutTemplate = heroView;
Document.childClickedOpenTemplateView = new PrefetchProxy(detailView);
Document._viewType = CollectionViewType.Time;
- Document._forceActive = true;
+ Document.forceActive = true;
Document._pivotField = "company";
Document.childDropAction = "alias";
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 191bbba3a..a0f6ad9c7 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -8,7 +8,7 @@ import * as React from 'react';
import Lightbox from 'react-image-lightbox-with-rotate';
import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
import { DateField } from '../../../fields/DateField';
-import { DataSym, Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, Field, Opt, AclSym, AclAddonly, AclReadonly } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
@@ -128,10 +128,16 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
const docList = DocListCast(targetDataDoc[this.props.fieldKey]);
const added = docs.filter(d => !docList.includes(d));
if (added.length) {
- added.map(doc => doc.context = this.props.Document);
- added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add));
- targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]);
- targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
+ if (this.dataDoc[AclSym] === AclReadonly) {
+ return false;
+ } else if (this.dataDoc[AclSym] === AclAddonly) {
+ added.map(doc => Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc));
+ } else {
+ added.map(doc => doc.context = this.props.Document);
+ added.map(add => Doc.AddDocToList(Cast(Doc.UserDoc().myCatalog, Doc, null), "data", add));
+ targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, ...added]);
+ targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
+ }
}
return true;
}
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index e4581eb46..03bd9a01a 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -8,10 +8,12 @@
z-index: 9001;
transition: top .5s;
background: lightgrey;
+ transform-origin: top left;
.collectionViewChrome {
display: flex;
- padding-bottom: 10px;
+ padding-bottom: 1px;
+ height:32px;
border-bottom: .5px solid rgb(180, 180, 180);
overflow: hidden;
@@ -21,12 +23,12 @@
.collectionViewBaseChrome-viewPicker {
font-size: 75%;
//text-transform: uppercase;
- letter-spacing: 2px;
+ //letter-spacing: 2px;
background: rgb(238, 238, 238);
color: grey;
outline-color: black;
border: none;
- padding: 12px 10px 11px 10px;
+ //padding: 12px 10px 11px 10px;
}
.collectionViewBaseChrome-viewPicker:active {
@@ -47,6 +49,7 @@
.collectionViewBaseChrome-cmdPicker {
margin-left: 3px;
margin-right: 0px;
+ font-size: 75%;
background: rgb(238, 238, 238);
border: none;
color: grey;
@@ -56,6 +59,7 @@
background-color: gray;
display: flex;
flex-direction: row;
+ height:30px;
.commandEntry-drop {
color:white;
width:25px;
@@ -95,8 +99,8 @@
margin: auto;
background: gray;
color: white;
- width: 40px;
- height: 40px;
+ width: 30px;
+ height: 30px;
align-items: center;
justify-content: center;
}
@@ -195,8 +199,7 @@
.collectionTreeViewChrome-pivotField-label {
vertical-align: center;
padding-left: 10px;
- padding-top: 10px;
- padding-bottom: 10px;
+ margin:auto;
}
.collectionStackingViewChrome-pivotField,
@@ -204,14 +207,15 @@
color: white;
width:100%;
min-width: 100px;
- text-align: center;
+ display: flex;
+ align-items: center;
background: rgb(238, 238, 238);
.editable-view-input,
input,
.editableView-container-editing-oneLine,
.editableView-container-editing {
- padding: 12px 10px 11px 10px;
+ margin:auto;
border: 0px;
color: grey;
text-align: center;
@@ -235,6 +239,44 @@
}
}
+.collectionFreeFormViewChrome-cont {
+ width: 60px;
+ display: flex;
+ position: relative;
+ align-items: center;
+ .fwdKeyframe, .numKeyframe, .backKeyframe {
+ cursor: pointer;
+ position: absolute;
+ width: 20;
+ height: 30;
+ bottom: 0;
+ background: gray;
+ display: flex;
+ align-items: center;
+ color:white;
+ }
+ .backKeyframe {
+ left:0;
+ svg {
+ display:block;
+ margin:auto;
+ }
+ }
+ .numKeyframe {
+ left:20;
+ display: flex;
+ flex-direction: column;
+ padding: 5px;
+ }
+ .fwdKeyframe {
+ left:40;
+ svg {
+ display:block;
+ margin:auto;
+ }
+ }
+}
+
.collectionSchemaViewChrome-cont {
display: flex;
font-size: 10.5px;
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 53860d8ad..199902923 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -15,6 +15,7 @@ import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
import { CollectionViewType } from "./CollectionView";
import { CollectionView } from "./CollectionView";
import "./CollectionViewChromes.scss";
+import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
const datepicker = require('js-datepicker');
interface CollectionViewChromeProps {
@@ -44,7 +45,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
initialize: emptyFunction,
};
_narrativeCommand = {
- params: ["target", "source"], title: "=> click clicked open view",
+ params: ["target", "source"], title: "=> child click view",
script: "this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])",
immediate: (source: Doc[]) => this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0]),
initialize: emptyFunction,
@@ -84,22 +85,16 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
private _viewRef = React.createRef<HTMLInputElement>();
@observable private _currentKey: string = "";
- componentDidMount = () => {
- runInAction(() => {
- // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
- const chromeStatus = this.props.CollectionView.props.Document._chromeStatus;
- if (chromeStatus) {
- if (chromeStatus === "disabled") {
- throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
- }
- else if (chromeStatus === "collapsed") {
- if (this.props.collapse) {
- this.props.collapse(true);
- }
- }
- }
- });
- }
+ componentDidMount = action(() => {
+ // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
+ switch (this.props.CollectionView.props.Document._chromeStatus) {
+ case "disabled":
+ throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
+ case "collapsed":
+ this.props.collapse?.(true);
+ break;
+ }
+ })
@undoBatch
viewChanged = (e: React.ChangeEvent) => {
@@ -198,10 +193,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
}
}
- subChrome = () => {
+ @computed get subChrome() {
const collapsed = this.document._chromeStatus !== "enabled";
if (collapsed) return null;
switch (this.props.type) {
+ case CollectionViewType.Freeform: return (<CollectionFreeFormViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
@@ -237,7 +233,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
const vtype = this.props.CollectionView.collectionViewType;
const c = {
params: ["target"], title: vtype,
- script: `this.target._viewType = ${StrCast(this.props.CollectionView.props.Document._viewType)}`,
+ script: `this.target._viewType = '${StrCast(this.props.CollectionView.props.Document._viewType)}'`,
immediate: (source: Doc[]) => this.props.CollectionView.props.Document._viewType = Doc.getDocTemplate(source?.[0]),
initialize: emptyFunction,
};
@@ -255,14 +251,61 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
}, emptyFunction, emptyFunction);
}
+ @computed get templateChrome() {
+ const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
+ return <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}>
+ <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
+ <div className="commandEntry-drop">
+ <FontAwesomeIcon icon="bullseye" size="2x" />
+ </div>
+ <select
+ className="collectionViewBaseChrome-cmdPicker"
+ onPointerDown={stopPropagation}
+ onChange={this.commandChanged}
+ value={this._currentKey}>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""} />
+ {this._buttonizableCommands.map(cmd =>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option>
+ )}
+ </select>
+ </div>
+ </div>;
+ }
+
+ @computed get viewModes() {
+ const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
+ return <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}>
+ <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}>
+ <div className="commandEntry-drop">
+ <FontAwesomeIcon icon="bullseye" size="2x" />
+ </div>
+ <select
+ className="collectionViewBaseChrome-viewPicker"
+ onPointerDown={stopPropagation}
+ onChange={this.viewChanged}
+ value={StrCast(this.props.CollectionView.props.Document._viewType)}>
+ {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : (
+ <option
+ key={Utils.GenerateGuid()}
+ className="collectionViewBaseChrome-viewOption"
+ onPointerDown={stopPropagation}
+ value={type}>
+ {type[0].toUpperCase() + type.substring(1)}
+ </option>
+ ))}
+ </select>
+ </div>
+ </div>;
+ }
+
render() {
const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
+ const scale = Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale);
return (
<div className="collectionViewChrome-cont" style={{
top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined,
- transform: collapsed ? "" : `scale(${Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)})`,
- transformOrigin: "top left",
- width: `${this.props.PanelWidth() / Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)}px`
+ transform: collapsed ? "" : `scale(${scale})`,
+ width: `${this.props.PanelWidth() / scale}px`
}}>
<div className="collectionViewChrome" style={{ border: "unset", pointerEvents: collapsed ? "none" : undefined }}>
<div className="collectionViewBaseChrome">
@@ -277,52 +320,15 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
title="Collapse collection chrome" onClick={this.toggleCollapse}>
<FontAwesomeIcon icon="caret-up" size="2x" />
</button>
- <div className="collectionViewBaseChrome-viewModes" style={{ display: collapsed ? "none" : undefined }}>
- <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}>
- <div className="commandEntry-drop">
- <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon>
- </div>
- <select
- className="collectionViewBaseChrome-viewPicker"
- onPointerDown={stopPropagation}
- onChange={this.viewChanged}
- value={StrCast(this.props.CollectionView.props.Document._viewType)}>
- {Object.values(CollectionViewType).map(type => ["invalid", "docking"].includes(type) ? (null) : (
- <option
- key={Utils.GenerateGuid()}
- className="collectionViewBaseChrome-viewOption"
- onPointerDown={stopPropagation}
- value={type}>
- {type[0].toUpperCase() + type.substring(1)}
- </option>
- ))}
- </select>
- </div>
- </div>
+ {this.viewModes}
<div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}>
<div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.toggleViewSpecs} >
<FontAwesomeIcon icon="filter" size="2x" />
</div>
</div>
- <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}>
- <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
- <div className="commandEntry-drop">
- <FontAwesomeIcon icon="bullseye" size="2x" />
- </div>
- <select
- className="collectionViewBaseChrome-cmdPicker"
- onPointerDown={stopPropagation}
- onChange={this.commandChanged}
- value={this._currentKey}>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={"empty"} value={""}>{""}</option>
- {this._buttonizableCommands.map(cmd =>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} key={cmd.title} value={cmd.title}>{cmd.title}</option>
- )}
- </select>
- </div>
- </div>
+ {this.templateChrome}
</div>
- {this.subChrome()}
+ {this.subChrome}
</div>
</div>
);
@@ -330,6 +336,56 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
}
@observer
+export class CollectionFreeFormViewChrome extends React.Component<CollectionViewChromeProps> {
+
+ get Document() { return this.props.CollectionView.props.Document; }
+ @computed get dataField() {
+ return this.props.CollectionView.props.Document[Doc.LayoutFieldKey(this.props.CollectionView.props.Document)];
+ }
+ @computed get childDocs() {
+ return DocListCast(this.dataField);
+ }
+ @undoBatch
+ @action
+ nextKeyframe = (): void => {
+ const currentFrame = NumCast(this.Document.currentFrame);
+ if (currentFrame === undefined) {
+ this.Document.currentFrame = 0;
+ CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
+ }
+ CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+ this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1);
+ this.Document.lastFrame = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastFrame));
+ }
+ @undoBatch
+ @action
+ prevKeyframe = (): void => {
+ const currentFrame = NumCast(this.Document.currentFrame);
+ if (currentFrame === undefined) {
+ this.Document.currentFrame = 0;
+ CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
+ }
+ CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+ this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1);
+ }
+ render() {
+ return this.Document.isAnnotationOverlay ? (null) :
+ <div className="collectionFreeFormViewChrome-cont">
+ <div key="back" title="back frame" className="backKeyframe" onClick={this.prevKeyframe}>
+ <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
+ </div>
+ <div key="num" title="toggle view all" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }}
+ onClick={action(() => this.Document.editing = !this.Document.editing)} >
+ {NumCast(this.Document.currentFrame)}
+ </div>
+ <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={this.nextKeyframe}>
+ <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
+ </div>
+ </div>;
+ }
+}
+
+@observer
export class CollectionStackingViewChrome extends React.Component<CollectionViewChromeProps> {
@observable private _currentKey: string = "";
@observable private suggestions: string[] = [];
@@ -372,6 +428,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
this.suggestions = [];
}
+ @action
setValue = (value: string) => {
this.props.CollectionView.props.Document._pivotField = value;
return true;
@@ -474,19 +531,20 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
@observer
export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> {
- get dataExtension() {
- return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "_ext"] as Doc;
+ get sortAscending() {
+ return this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"];
}
- @computed private get descending() {
- return this.dataExtension && Cast(this.dataExtension.sortAscending, "boolean", null);
+ set sortAscending(value) {
+ this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey + "-sortAscending"] = value;
+ }
+ @computed private get ascending() {
+ return Cast(this.sortAscending, "boolean", null);
}
@action toggleSort = () => {
- if (this.dataExtension) {
- if (this.dataExtension.sortAscending) this.dataExtension.sortAscending = undefined;
- else if (this.dataExtension.sortAscending === undefined) this.dataExtension.sortAscending = false;
- else this.dataExtension.sortAscending = true;
- }
+ if (this.sortAscending) this.sortAscending = undefined;
+ else if (this.sortAscending === undefined) this.sortAscending = false;
+ else this.sortAscending = true;
}
render() {
@@ -496,7 +554,7 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro
<div className="collectionTreeViewChrome-sortLabel">
Sort
</div>
- <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.descending === undefined ? "90" : this.descending ? "180" : "0"}deg)` }}>
+ <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.ascending === undefined ? "90" : this.ascending ? "180" : "0"}deg)` }}>
<FontAwesomeIcon icon="caret-up" size="2x" color="white" />
</div>
</button>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 5478a1c4a..d9011c9d3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -41,32 +41,6 @@
// touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions.
touch-action: none;
- .fwdKeyframe, .numKeyframe, .backKeyframe{
- cursor: pointer;
- position: absolute;
- width: 20;
- height:20;
- bottom:0;
- background:gray;
- }
- .backKeyframe {
- right:45;
- svg {
- display:block;
- margin:auto;
- }
- }
- .numKeyframe {
- right:25;
- text-align:center;
- }
- .fwdKeyframe {
- right:5;
- svg {
- display:block;
- margin:auto;
- }
- }
.collectionfreeformview-placeholder {
background: gray;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4840bb7e7..c753a703d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,7 +1,7 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faEye, faEyeSlash } from "@fortawesome/free-regular-svg-icons";
import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, _allowStateChangesInsideComputed } from "mobx";
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, _allowStateChangesInsideComputed, trace } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
import { Doc, HeightSym, Opt, WidthSym, DocListCast } from "../../../../fields/Doc";
@@ -76,6 +76,7 @@ const PanZoomDocument = makeInterface(panZoomSchema, collectionSchema, documentS
export type collectionFreeformViewProps = {
forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox)
viewDefDivClick?: ScriptField;
+ childPointerEvents?: boolean;
};
@observer
@@ -117,12 +118,28 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
this.Document.scale || 1)
- private centeringShiftX = () => !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / this.contentScaling : 0; // shift so pan position is at center of window for non-overlay collections
- private centeringShiftY = () => !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / this.contentScaling : 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);
- private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
+ @computed get cachedCenteringShiftX(): number {
+ return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / this.contentScaling : 0; // shift so pan position is at center of window for non-overlay collections
+ }
+ @computed get cachedCenteringShiftY(): number {
+ return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / this.contentScaling : 0;// shift so pan position is at center of window for non-overlay collections
+ }
+ @computed get cachedGetLocalTransform(): Transform {
+ return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
+ }
+ @computed get cachedGetContainerTransform(): Transform {
+ return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
+ }
+ @computed get cachedGetTransform(): Transform {
+ return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
+ }
+
+ private centeringShiftX = () => this.cachedCenteringShiftX;
+ private centeringShiftY = () => this.cachedCenteringShiftY;
+ private getTransform = () => this.cachedGetTransform.copy();
+ private getLocalTransform = () => this.cachedGetLocalTransform.copy();
+ private getContainerTransform = () => this.cachedGetContainerTransform.copy();
+ private getTransformOverlay = () => this.getContainerTransform().translate(1, 1);
private addLiveTextBox = (newBox: Doc) => {
FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
this.addDocument(newBox);
@@ -161,30 +178,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
return retVal;
})
- @undoBatch
- @action
- nextKeyframe = (): void => {
- const currentFrame = this.Document.currentFrame;
- if (currentFrame === undefined) {
- this.Document.currentFrame = 0;
- CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
- }
- CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
- this.Document.currentFrame = Math.max(0, (currentFrame || 0) + 1);
- this.Document.lastFrame = Math.max(NumCast(this.Document.currentFrame), NumCast(this.Document.lastFrame));
- }
- @undoBatch
- @action
- prevKeyframe = (): void => {
- const currentFrame = this.Document.currentFrame;
- if (currentFrame === undefined) {
- this.Document.currentFrame = 0;
- CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
- }
- CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
- this.Document.currentFrame = Math.max(0, (currentFrame || 0) - 1);
- }
-
private selectDocuments = (docs: Doc[]) => {
SelectionManager.DeselectAll();
docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
@@ -501,9 +494,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
console.log("end");
if (this._inkToTextStartX && this._inkToTextStartY) {
const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
- const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "text" && s.color);
+ const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === "rtf" && s.color);
const sets = setDocs.map((sd) => {
- return Cast(sd.data, RichTextField)?.Text as string;
+ return Cast(sd.text, RichTextField)?.Text as string;
});
if (sets.length && sets[0]) {
this._wordPalette.clear();
@@ -539,6 +532,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
});
+ console.log(this._wordPalette)
CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => {
console.log(results);
const wordResults = results.filter((r: any) => r.category === "inkWord");
@@ -1123,10 +1117,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
replica={entry[1].replica}
dataProvider={this.childDataProvider}
sizeProvider={this.childSizeProvider}
- pointerEvents={
- this.backgroundActive ?
- true :
- (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? false : undefined}
+ pointerEvents={this.backgroundActive || this.props.childPointerEvents ?
+ true :
+ (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? false : undefined}
jitterRotation={NumCast(this.props.Document._jitterRotation) || ((Doc.UserDoc().renderStyle === "comic" ? 10 : 0))}
//fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
fitToBox={BoolCast(this.props.freezeChildDimensions)} // bcz: check this
@@ -1218,6 +1211,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
+ optionItems.push({ description: "toggle snap line display", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" });
optionItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
optionItems.push({ description: `${this.fitToContent ? "Unset" : "Set"} Fit To Container`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
@@ -1349,8 +1343,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
getContainerTransform={this.getContainerTransform}
getTransform={this.getTransform}
isAnnotationOverlay={this.isAnnotationOverlay}>
- <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} shifted={!this.nativeHeight && !this.isAnnotationOverlay}
- easing={this.easing} viewDefDivClick={this.props.viewDefDivClick} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+ <CollectionFreeFormViewPannableContents
+ centeringShiftX={this.centeringShiftX}
+ centeringShiftY={this.centeringShiftY}
+ shifted={!this.nativeHeight && !this.isAnnotationOverlay}
+ easing={this.easing}
+ transition={Cast(this.layoutDoc.transition, "string", null)}
+ viewDefDivClick={this.props.viewDefDivClick}
+ zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
{this.children}
</CollectionFreeFormViewPannableContents>
{this._timelineVisible ? <Timeline ref={this._timelineRef} {...this.props} /> : (null)}
@@ -1390,18 +1390,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
{!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ?
this.placeholder : this.marqueeView}
<CollectionFreeFormOverlayView elements={this.elementFunc} />
- {this.isAnnotationOverlay || !this.props.isSelected() || this.props.Document._viewType === CollectionViewType.Pile ? (null) :
- <>
- <div key="back" className="backKeyframe" onClick={this.prevKeyframe}>
- <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
- </div>
- <div key="num" className="numKeyframe" style={{ backgroundColor: this.Document.editing ? "#759c75" : "#c56565" }} onClick={action(() => this.Document.editing = !this.Document.editing)} >
- {NumCast(this.Document.currentFrame)}
- </div>
- <div key="fwd" className="fwdKeyframe" onClick={this.nextKeyframe}>
- <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
- </div>
- </>}
+
<div className={"pullpane-indicator"}
style={{
display: this._pullDirection ? "block" : "none",
@@ -1444,6 +1433,7 @@ interface CollectionFreeFormViewPannableContentsProps {
viewDefDivClick?: ScriptField;
children: () => JSX.Element[];
shifted: boolean;
+ transition?: string;
}
@observer
@@ -1458,7 +1448,8 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
return <div className={freeformclass}
style={{
width: this.props.shifted ? 0 : undefined, height: this.props.shifted ? 0 : undefined,
- transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`
+ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`,
+ transition: this.props.transition
}}>
{this.props.children()}
</div>;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index ed70ac9e8..cdfeeaa6b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -357,10 +357,14 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const selected = this.marqueeSelect(false);
if (e instanceof KeyboardEvent ? e.key === "c" : true) {
selected.map(action(d => {
- //this.props.removeDocument(d);
- d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
- d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
- d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ const dx = NumCast(d.x);
+ const dy = NumCast(d.y);
+ delete d.x;
+ delete d.y;
+ delete d.activeFrame;
+ delete d.displayTimecode; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ d.x = dx - bounds.left - bounds.width / 2;
+ d.y = dy - bounds.top - bounds.height / 2;
return d;
}));
this.props.removeDocument(selected);
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 5fb4cf3c6..13b9a2459 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -298,7 +298,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
<div className="linkEditor">
{this.props.hideback ? (null) : <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>}
<div className="linkEditor-info">
- <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
+ <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto?.title ?? destination.title ?? "untitled"}</b></p>
<button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
</div>
{groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 682aed8f5..910aa744d 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -96,12 +96,12 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
opacityindexed?.length <= timecode + 1 && opacityindexed.push(undefined as any as number);
doc.transition = "all 1s";
});
- setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
+ setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010);
}
public static gotoKeyframe(docs: Doc[]) {
docs.forEach(doc => doc.transition = "all 1s");
- setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
+ setTimeout(() => docs.forEach(doc => doc.transition = "inherit"), 1010);
}
public static setupKeyframes(docs: Doc[], timecode: number, progressivize: boolean = false) {
@@ -119,6 +119,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
doc.x = ComputedField.MakeInterpolated("x", "activeFrame");
doc.y = ComputedField.MakeInterpolated("y", "activeFrame");
doc.opacity = ComputedField.MakeInterpolated("opacity", "activeFrame");
+ doc.transition = "inherit";
});
}
@@ -153,6 +154,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
width: this.width,
height: this.height,
zIndex: this.ZInd,
+ mixBlendMode: StrCast(this.layoutDoc.mixBlendMode) as any,
display: this.ZInd === -99 ? "none" : undefined,
pointerEvents: this.props.Document.isBackground || this.Opacity === 0 ? "none" : this.props.pointerEvents ? "all" : undefined
}} >
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 7849c9976..810a824cf 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -1,4 +1,5 @@
-.comparisonBox-interactive, .comparisonBox {
+.comparisonBox-interactive,
+.comparisonBox {
border-radius: inherit;
width: 100%;
height: 100%;
@@ -19,13 +20,13 @@
}
}
- .slide-bar {
+ .slide-bar {
position: absolute;
height: 100%;
width: 3px;
display: inline-block;
background: white;
- cursor: ew-resize;
+
.slide-handle {
position: absolute;
display: none;
@@ -33,7 +34,9 @@
width: 30px;
bottom: 0px;
left: -10.5px;
- .left-handle, .right-handle{
+
+ .left-handle,
+ .right-handle {
width: 15px;
}
}
@@ -77,10 +80,13 @@
}
}
}
+
.comparisonBox-interactive {
pointer-events: unset;
+ cursor: ew-resize;
+
.slide-bar {
- .slide-handle {
+ .slide-handle {
display: flex;
}
}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 77e07ec0c..cce60628d 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,7 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction, Lambda, IReactionDisposer } from 'mobx';
+import { action, observable } from 'mobx';
import { observer } from "mobx-react";
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc } from '../../../fields/Doc';
import { documentSchema } from '../../../fields/documentSchemas';
import { createSchema, makeInterface } from '../../../fields/Schema';
import { NumCast, Cast, StrCast } from '../../../fields/Types';
@@ -32,13 +32,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C
protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
this._disposers[disposerId]?.();
if (ele) {
+ // create disposers identified by disposerId to remove drag & drop listeners
this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc);
}
}
@undoBatch
private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
- event.stopPropagation();
+ event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments;
if (droppedDocs?.length) {
this.dataDoc[fieldKey] = droppedDocs[0];
@@ -47,6 +48,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C
private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, action(() => {
+ // on click, animate slider movement to the targetWidth
this._animating = "all 1s";
this.layoutDoc._clipWidth = targetWidth * 100 / this.props.PanelWidth();
setTimeout(action(() => this._animating = ""), 1000);
@@ -64,7 +66,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C
@undoBatch
clearDoc = (e: React.MouseEvent, fieldKey: string) => {
- e.stopPropagation;
+ e.stopPropagation; // prevent click event action (slider movement) in registerSliding
delete this.dataDoc[fieldKey];
}
@@ -72,7 +74,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C
const clipWidth = NumCast(this.layoutDoc._clipWidth) + "%";
const childProps: DocumentViewProps = { ...this.props, pointerEvents: false, parentActive: this.props.active };
const clearButton = (which: string) => {
- return <div className={`clear-button ${which}`} onPointerDown={e => e.stopPropagation()} onClick={e => this.clearDoc(e, `${which}Doc`)}>
+ return <div className={`clear-button ${which}`}
+ onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding
+ onClick={e => this.clearDoc(e, `${which}Doc`)}>
<FontAwesomeIcon className={`clear-button ${which}`} icon={"times"} size="sm" />
</div>;
};
@@ -95,13 +99,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps, C
};
return (
- <div className={`comparisonBox${this.active() || SnappingManager.GetIsDragging() ? "-interactive" : ""}`}>
+ <div className={`comparisonBox${this.active() || SnappingManager.GetIsDragging() ? "-interactive" : ""}` /* change className to easily disable/enable pointer events in CSS */}>
{displayBox("after", 1, this.props.PanelWidth() - 5)}
<div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, "gray") }}>
{displayBox("before", 0, 5)}
</div>
- <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)` }}>
+ <div className="slide-bar" style={{ left: `calc(${clipWidth} - 0.5px)` }}
+ onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ >
<div className="slide-handle" />
</div>
</div >);
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index ef56e6fcd..126e9ac14 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,6 +1,6 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { Doc, Opt, Field } from "../../../fields/Doc";
+import { Doc, Opt, Field, AclSym, AclPrivate } from "../../../fields/Doc";
import { Cast, StrCast, NumCast } from "../../../fields/Types";
import { OmitKeys, Without, emptyPath } from "../../../Utils";
import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
@@ -185,7 +185,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
const bindings = this.CreateBindings(onClick, onInput);
// layoutFrame = splits.length > 1 ? splits[0] + splits[1].replace(/{([^{}]|(?R))*}/, replacer4) : ""; // might have been more elegant if javascript supported recursive patterns
- return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc) ? (null) :
+ return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || this.layoutDoc[AclSym] === AclPrivate) ? (null) :
<ObserverJsxParser
key={42}
blacklistedAttrs={[]}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 00d19752f..e245e045c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -4,7 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DataSym, AclSym, AclReadonly, AclPrivate } from "../../../fields/Doc";
import { Document } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
@@ -742,13 +742,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const more = cm.findByDescription("More...");
const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : [];
+ moreItems.push({ description: "Make Add Only", event: () => this.dataDoc.ACL = this.layoutDoc.ACL = "addOnly", icon: "concierge-bell" });
+ moreItems.push({ description: "Make Read Only", event: () => this.dataDoc.ACL = this.layoutDoc.ACL = "readOnly", icon: "concierge-bell" });
+ moreItems.push({ description: "Make Private", event: () => this.dataDoc.ACL = this.layoutDoc.ACL = "noAccess", icon: "concierge-bell" });
moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
moreItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
moreItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
if (!ClientUtils.RELEASE) {
// let copies: ContextMenuProps[] = [];
- moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
+ moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" });
// cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" });
}
if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) {
@@ -965,9 +968,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return this.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
}
childScaling = () => (this.layoutDoc._fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
- panelWidth = () => this.props.PanelWidth();
- panelHeight = () => this.props.PanelHeight();
- screenToLocalTransform = () => this.props.ScreenToLocalTransform();
@computed get contents() {
TraceMobx();
return (<>
@@ -988,10 +988,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
addDocument={this.props.addDocument}
removeDocument={this.props.removeDocument}
moveDocument={this.props.moveDocument}
- ScreenToLocalTransform={this.screenToLocalTransform}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
renderDepth={this.props.renderDepth}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={this.props.PanelHeight}
ignoreAutoHeight={this.props.ignoreAutoHeight}
focus={this.props.focus}
parentActive={this.props.parentActive}
@@ -1122,6 +1122,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
render() {
+ if (this.props.Document[AclSym] && this.props.Document[AclSym] === AclPrivate) return (null);
if (!(this.props.Document instanceof Doc)) return (null);
const backgroundColor = Doc.UserDoc().renderStyle === "comic" ? undefined : StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
const opacity = Cast(this.layoutDoc._opacity, "number", Cast(this.layoutDoc.opacity, "number", Cast(this.Document.opacity, "number", null)));
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 05306c29f..9f1e99c77 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -62,6 +62,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
const lastFrame = Cast(presTargetDoc.lastFrame, "number", null);
const curFrame = NumCast(presTargetDoc.currentFrame);
if (lastFrame !== undefined && curFrame < lastFrame) {
+ presTargetDoc.transition = "all 1s";
+ setTimeout(() => presTargetDoc.transition = undefined, 1010);
presTargetDoc.currentFrame = curFrame + 1;
}
else if (this.childDocs[this.itemIndex + 1] !== undefined) {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a91d4dfd9..c4ab3c9e2 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -411,9 +411,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum
view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
} else if (field instanceof WebField) {
const url = this.layoutDoc.UseCors ? Utils.CorsProxy(field.url.href) : field.url.href;
- view = <iframe className="webBox-iframe" ref={this._iframeRef} src={url} onLoad={this.iframeLoaded} />;
+ view = <iframe className="webBox-iframe" enable-annotation={true} ref={this._iframeRef} src={url} onLoad={this.iframeLoaded} />;
} else {
- view = <iframe className="webBox-iframe" ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} />;
+ view = <iframe className="webBox-iframe" enable-annotation={true} ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} />;
}
return view;
}
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 8541a3149..affffc44e 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -29,6 +29,7 @@
.page {
position: relative;
+ border: unset;
}
.pdfViewer-text-selected {
.textLayer{
@@ -57,6 +58,9 @@
display: inline-block;
width:100%;
}
+ .pdfViewer-overlay {
+ pointer-events: none;
+ }
.pdfViewer-annotationLayer {
position: absolute;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 810ce5aea..5bad248be 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -31,8 +31,10 @@ import Annotation from "./Annotation";
import PDFMenu from "./PDFMenu";
import "./PDFViewer.scss";
import React = require("react");
+import { SnappingManager } from "../../util/SnappingManager";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
const pdfjsLib = require("pdfjs-dist");
+import { Networking } from "../../Network";
export const pageSchema = createSchema({
curPage: "number",
@@ -128,8 +130,17 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
// change the address to be the file address of the PNG version of each page
// file address of the pdf
const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!;
- const addr = Utils.prepend(`/thumbnail${this.props.url.substring("files/pdfs/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.png`);
- this._coverPath = href.startsWith(window.location.origin) ? JSON.parse(await rp.get(addr)) : { width: 100, height: 100, path: "" };
+ const { url: relative } = this.props;
+ const pathComponents = relative.split("/pdfs/")[1].split("/");
+ const coreFilename = pathComponents.pop()!.split(".")[0];
+ const params: any = {
+ coreFilename,
+ pageNum: this.Document.curPage || 1,
+ };
+ if (pathComponents.length) {
+ params.subtree = `${pathComponents.join("/")}/`;
+ }
+ this._coverPath = href.startsWith(window.location.origin) ? await Networking.PostToServer("/thumbnail", params) : { width: 100, height: 100, path: "" };
runInAction(() => this._showWaiting = this._showCover = true);
this.props.startupLive && this.setupPdfJsViewer();
this._searchReactionDisposer = reaction(() => this.Document.searchMatch, search => {
@@ -641,7 +652,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
panelWidth = () => (this.Document.scrollHeight || this.Document._nativeHeight || 0);
panelHeight = () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document._nativeWidth || 0);
@computed get overlayLayer() {
- return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : ""}`} id="overlay"
+ return <div className={`pdfViewer-overlay${InkingControl.Instance.selectedTool !== InkTool.None || SnappingManager.GetIsDragging() ? "-inking" : ""}`} id="overlay"
style={{ transform: `scale(${this._zoomed})` }}>
<CollectionFreeFormView {...this.props}
LibraryPath={this.props.ContainingCollectionView?.props.LibraryPath ?? emptyPath}
@@ -661,6 +672,7 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu
ContentScaling={this.contentZoom}
bringToFront={emptyFunction}
whenActiveChanged={this.whenActiveChanged}
+ childPointerEvents={true}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocument}
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 475fef5b2..caee06d8f 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -178,6 +178,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps, PresDoc
DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
LibraryPath={emptyPath}
fitToBox={true}
+ backgroundColor={this.props.backgroundColor}
rootSelected={returnTrue}
addDocument={returnFalse}
removeDocument={returnFalse}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index eea7b528b..c9d29e485 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -309,9 +309,9 @@ export class SearchBox extends React.Component<SearchProps> {
const baseExpr = "NOT baseProto_b:true";
const includeDeleted = this.getDataStatus() ? "" : " NOT deleted_b:true";
const includeIcons = this.getDataStatus() ? "" : " NOT type_t:fonticonbox";
- const typeExpr = !types ? "" : ` (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})`;
+ // const typeExpr = !types ? "" : ` (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})`; // this line was causing issues for me, check solr logging -syip2
// fq: type_t:collection OR {!join from=id to=proto_i}type_t:collection q:text_t:hello
- const query = [baseExpr, includeDeleted, includeIcons, typeExpr].join(" AND ").replace(/AND $/, "");
+ const query = [baseExpr, includeDeleted, includeIcons].join(" AND ").replace(/AND $/, "");
return query;
}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 1ea686cbb..6891bf652 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -93,13 +93,40 @@ export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
export const DataSym = Symbol("Data");
export const LayoutSym = Symbol("Layout");
+export const AclSym = Symbol("Acl");
+export const AclPrivate = Symbol("AclNoAccess");
+export const AclReadonly = Symbol("AclReadOnly");
+export const AclAddonly = Symbol("AclAddonly");
export const UpdatingFromServer = Symbol("UpdatingFromServer");
const CachedUpdates = Symbol("Cached updates");
function fetchProto(doc: Doc) {
+ if (doc.author !== Doc.CurrentUserEmail) {
+ if (doc.ACL === "noAccess") {
+ doc[AclSym] = AclPrivate;
+ return undefined;
+ } else if (doc.ACL === "readOnly") {
+ doc[AclSym] = AclReadonly;
+ } else if (doc.ACL === "addOnly") {
+ doc[AclSym] = AclAddonly;
+ }
+ }
+
const proto = doc.proto;
if (proto instanceof Promise) {
+ proto.then(proto => {
+ if (proto.author !== Doc.CurrentUserEmail) {
+ if (proto.ACL === "noAccess") {
+ proto[AclSym] = doc[AclSym] = AclPrivate;
+ return undefined;
+ } else if (proto.ACL === "readOnly") {
+ proto[AclSym] = doc[AclSym] = AclReadonly;
+ } else if (proto.ACL === "addOnly") {
+ proto[AclSym] = doc[AclSym] = AclAddonly;
+ }
+ }
+ });
return proto;
}
}
@@ -113,10 +140,10 @@ export class Doc extends RefField {
set: setter,
get: getter,
// getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
- has: (target, key) => key in target.__fields,
+ has: (target, key) => target[AclSym] !== AclPrivate && key in target.__fields,
ownKeys: target => {
const obj = {} as any;
- Object.assign(obj, target.___fields);
+ (target[AclSym] !== AclPrivate) && Object.assign(obj, target.___fields);
runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
return Object.keys(obj);
},
@@ -170,8 +197,11 @@ export class Doc extends RefField {
private [Self] = this;
private [SelfProxy]: any;
+ public [AclSym]: any = undefined;
public [WidthSym] = () => NumCast(this[SelfProxy]._width);
public [HeightSym] = () => NumCast(this[SelfProxy]._height);
+ public [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; }
+ public [ToString]() { return `Doc(${this[AclSym] === AclPrivate ? "-inaccessible-" : this.title})`; }
public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; }
public get [DataSym]() {
const self = this[SelfProxy];
@@ -193,8 +223,6 @@ export class Doc extends RefField {
return undefined;
}
- [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; }
- [ToString]() { return `Doc(${this.title})`; }
private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
public static CurrentUserEmail: string = "";
@@ -794,6 +822,7 @@ export namespace Doc {
}
// don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
export function IsBrushedDegreeUnmemoized(doc: Doc) {
+ if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return 0;
return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0;
}
export function IsBrushedDegree(doc: Doc) {
@@ -802,11 +831,13 @@ export namespace Doc {
})(doc);
}
export function BrushDoc(doc: Doc) {
+ if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc;
brushManager.BrushedDoc.set(doc, true);
brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
return doc;
}
export function UnBrushDoc(doc: Doc) {
+ if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return doc;
brushManager.BrushedDoc.delete(doc);
brushManager.BrushedDoc.delete(Doc.GetProto(doc));
return doc;
@@ -836,6 +867,7 @@ export namespace Doc {
}
const highlightManager = new HighlightBrush();
export function IsHighlighted(doc: Doc) {
+ if (!doc || doc[AclSym] === AclPrivate || Doc.GetProto(doc)[AclSym] === AclPrivate) return false;
return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
}
export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 5192af407..fc7f9ca80 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -155,7 +155,7 @@ export class ComputedField extends ScriptField {
public static MakeInterpolated(fieldKey: string, interpolatorKey: string) {
const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {});
const setField = ScriptField.CompileScript(`(self['${fieldKey}-indexed'])[self.${interpolatorKey}] = value`, { value: "any" }, true, {});
- return getField.compiled && setField.compiled ? new ComputedField(getField, setField) : undefined;
+ return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
}
}
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 32f1b6e6c..e7031cc39 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -15,7 +15,7 @@ export const documentSchema = createSchema({
currentFrame: "number", // current frame of a frame based collection (e.g., a progressive slide)
lastFrame: "number", // last frame of a frame based collection (e.g., a progressive slide)
activeFrame: "number", // the active frame of a frame based animated document
- urrentTimecode: "number", // current play back time of a temporal document (video / audio)
+ currentTimecode: "number", // current play back time of a temporal document (video / audio)
displayTimecode: "number", // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently
x: "number", // x coordinate when in a freeform view
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 024c0f80e..54e7eca28 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,5 +1,5 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym } from "./Doc";
+import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym, AclSym, AclPrivate } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField, PrefetchProxy } from "./Proxy";
import { RefField } from "./RefField";
@@ -106,6 +106,7 @@ const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHe
"LODdisable", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
+ if (target[AclSym]) return true;
if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
if (!prop.startsWith("_")) {
console.log(prop + " is deprecated - switch to _" + prop);
@@ -124,6 +125,8 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
let prop = in_prop;
+ if (in_prop === AclSym) return target[AclSym];
+ if (target[AclSym] === AclPrivate) return undefined;
if (prop === LayoutSym) {
return target.__LAYOUT__;
}
@@ -148,6 +151,9 @@ export function getter(target: any, in_prop: string | symbol | number, receiver:
function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any {
receiver = receiver || target[SelfProxy];
+ if (target === undefined) {
+ console.log("");
+ }
let field = target.__fields[prop];
for (const plugin of getterPlugins) {
const res = plugin(receiver, prop, field);
@@ -160,7 +166,7 @@ function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreP
}
if (field === undefined && !ignoreProto && prop !== "proto") {
const proto = getFieldImpl(target, "proto", receiver, true);//TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
- if (proto instanceof Doc) {
+ if (proto instanceof Doc && proto[AclSym] !== AclPrivate) {
return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
}
return undefined;
diff --git a/src/scraping/buxton/final/BuxtonImporter.ts b/src/scraping/buxton/final/BuxtonImporter.ts
index e55850b29..684c00c0d 100644
--- a/src/scraping/buxton/final/BuxtonImporter.ts
+++ b/src/scraping/buxton/final/BuxtonImporter.ts
@@ -1,4 +1,4 @@
-import { readdirSync, writeFile, mkdirSync } from "fs";
+import { readdirSync, writeFile, mkdirSync, createReadStream, createWriteStream, existsSync, statSync } from "fs";
import * as path from "path";
import { red, cyan, yellow } from "colors";
import { Utils } from "../../../Utils";
@@ -9,6 +9,7 @@ const createImageSizeStream = require("image-size-stream");
import { parseXml } from "libxmljs";
import { strictEqual } from "assert";
import { Readable, PassThrough } from "stream";
+import { Directory, serverPathToFile, pathToDirectory } from "../../../server/ApiManagers/UploadManager";
/**
* This is an arbitrary bundle of data that gets populated
@@ -18,8 +19,7 @@ interface DocumentContents {
body: string;
imageData: ImageData[];
hyperlinks: string[];
- captions: string[];
- embeddedFileNames: string[];
+ tableData: TableData[];
longDescription: string;
}
@@ -40,6 +40,7 @@ export interface DeviceDocument {
secondaryKey: string;
attribute: string;
__images: ImageData[];
+ additionalMedia: ({ [type: string]: string } | undefined)[];
hyperlinks: string[];
captions: string[]; // from the table column
embeddedFileNames: string[]; // from the table column
@@ -255,6 +256,8 @@ const FormatMap = new Map<keyof DeviceDocument, ValueFormatDefinition<any>>([
]);
const sourceDir = path.resolve(__dirname, "source"); // where the Word documents are assumed to be stored
+const assetDir = path.resolve(__dirname, "assets"); // where any additional media content like pdfs will be stored. Each subdirectory of this
+// must follow the enum Directory.<type> naming scheme
const outDir = path.resolve(__dirname, "json"); // where the JSON output of these device documents will be written
const imageDir = path.resolve(__dirname, "../../../server/public/files/images/buxton"); // where, in the server, these images will be written
const successOut = "buxton.json"; // the JSON list representing properly formatted documents
@@ -277,12 +280,13 @@ export default async function executeImport(emitter: ResultCallback, terminator:
rimraf.sync(dir);
mkdirSync(dir);
});
+ await transferAssets();
return parseFiles(wordDocuments, emitter, terminator);
} catch (e) {
const message = [
"Unable to find a source directory.",
- "Please ensure that the following directory exists and is populated with Word documents:",
- `${sourceDir}`
+ "Please ensure that the following directory exists:",
+ `${e.message}`
].join('\n');
console.log(red(message));
return { error: message };
@@ -290,6 +294,32 @@ export default async function executeImport(emitter: ResultCallback, terminator:
}
/**
+ * Builds a mirrored directory structure of all media / asset files
+ * within the server's public directory.
+ */
+async function transferAssets() {
+ for (const assetType of readdirSync(assetDir)) {
+ const subroot = path.resolve(assetDir, assetType);
+ if (!statSync(subroot).isDirectory()) {
+ continue;
+ }
+ const outputSubroot = serverPathToFile(assetType as Directory, "buxton");
+ if (existsSync(outputSubroot)) {
+ continue;
+ } else {
+ mkdirSync(outputSubroot);
+ }
+ for (const fileName of readdirSync(subroot)) {
+ const readStream = createReadStream(path.resolve(subroot, fileName));
+ const writeStream = createWriteStream(path.resolve(outputSubroot, fileName));
+ await new Promise<void>(resolve => {
+ readStream.pipe(writeStream).on("close", resolve);
+ });
+ }
+ }
+}
+
+/**
* Parse every Word document in the directory, notifying any callers as needed
* at each iteration via the emitter.
* @param wordDocuments the string list of Word document names to parse
@@ -356,6 +386,16 @@ const xPaths = {
hyperlinks: '//*[name()="Relationship" and contains(@Type, "hyperlink")]'
};
+interface TableData {
+ fileName: string;
+ caption: string;
+ additionalMedia?: { [type: string]: string };
+}
+
+const SuffixDirectoryMap = new Map<string, Directory>([
+ ["p", Directory.pdfs]
+]);
+
/**
* The meat of the script, images and text content are extracted here
* @param pathToDocument the path to the document relative to the root of the zip
@@ -370,8 +410,7 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont
// get plain text
const body = document.root()?.text() ?? "No body found. Check the import script's XML parser.";
const captions: string[] = [];
- const embeddedFileNames: string[] = [];
-
+ const tableData: TableData[] = [];
// preserve paragraph formatting and line breaks that would otherwise get lost in the plain text parsing
// of the XML hierarchy
const paragraphs = document.find(xPaths.paragraphs).map(node => Utilities.correctSentences(node.text()).transformed!);
@@ -382,7 +421,7 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont
// extract captions from the table cells
const tableRowsFlattened = document.find(xPaths.tableCells).map(node => node.text().trim());
const { length } = tableRowsFlattened;
- const numCols = 3;
+ const numCols = 4;
strictEqual(length > numCols, true, "No captions written."); // first row has the headers, not content
strictEqual(length % numCols === 0, true, "Improper caption formatting.");
@@ -392,8 +431,14 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont
// have been added or reordered since this was written, but follow the same appraoch)
for (let i = numCols; i < tableRowsFlattened.length; i += numCols) {
const row = tableRowsFlattened.slice(i, i + numCols);
- embeddedFileNames.push(row[1]);
- captions.push(row[2]);
+ const entry: TableData = { fileName: row[1], caption: row[2] };
+ const key = SuffixDirectoryMap.get(row[3].toLowerCase());
+ if (key) {
+ const media: any = {};
+ media[key] = `${entry.fileName.split(".")[0]}.pdf`;
+ entry.additionalMedia = media;
+ }
+ tableData.push(entry);
}
// extract all hyperlinks embedded in the document
@@ -409,7 +454,7 @@ async function extractFileContents(pathToDocument: string): Promise<DocumentCont
// cleanup
zip.close();
- return { body, longDescription, imageData, captions, embeddedFileNames, hyperlinks };
+ return { body, longDescription, imageData, tableData, hyperlinks };
}
// zip relative path from root expression / filter used to isolate only media assets
@@ -492,11 +537,12 @@ async function writeImages(zip: any): Promise<ImageData[]> {
* @param contents the data already computed / parsed by extractFileContents
*/
function analyze(fileName: string, contents: DocumentContents): AnalysisResult {
- const { body, imageData, captions, hyperlinks, embeddedFileNames, longDescription } = contents;
+ const { body, imageData, hyperlinks, tableData, longDescription } = contents;
const device: any = {
hyperlinks,
- captions,
- embeddedFileNames,
+ captions: tableData.map(({ caption }) => caption),
+ embeddedFileNames: tableData.map(({ fileName }) => fileName),
+ additionalMedia: tableData.map(({ additionalMedia }) => additionalMedia),
longDescription,
__images: imageData
};
diff --git a/src/scraping/buxton/final/assets/pdfs/3DCad_Brochure.pdf b/src/scraping/buxton/final/assets/pdfs/3DCad_Brochure.pdf
new file mode 100644
index 000000000..4746d2f41
--- /dev/null
+++ b/src/scraping/buxton/final/assets/pdfs/3DCad_Brochure.pdf
Binary files differ
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
index 0136b758e..d2a9e9cce 100644
--- a/src/server/ApiManagers/PDFManager.ts
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -7,54 +7,54 @@ import { createCanvas } from "canvas";
const imageSize = require("probe-image-size");
import * as express from "express";
import * as path from "path";
-import { Directory, serverPathToFile, clientPathToFile } from "./UploadManager";
+import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from "./UploadManager";
import { red } from "colors";
+import { resolve } from "path";
export default class PDFManager extends ApiManager {
protected initialize(register: Registration): void {
register({
- method: Method.GET,
- subscription: new RouteSubscriber("thumbnail").add("filename"),
- secureHandler: ({ req, res }) => getOrCreateThumbnail(req.params.filename, res)
+ method: Method.POST,
+ subscription: new RouteSubscriber("thumbnail"),
+ secureHandler: async ({ req, res }) => {
+ const { coreFilename, pageNum, subtree } = req.body;
+ return getOrCreateThumbnail(coreFilename, pageNum, res, subtree);
+ }
});
}
}
-async function getOrCreateThumbnail(thumbnailName: string, res: express.Response): Promise<void> {
- const noExtension = thumbnailName.substring(0, thumbnailName.length - ".png".length);
- const pageString = noExtension.split('-')[1];
- const pageNumber = parseInt(pageString);
+async function getOrCreateThumbnail(coreFilename: string, pageNum: number, res: express.Response, subtree?: string): Promise<void> {
+ const resolved = `${coreFilename}-${pageNum}.png`;
return new Promise<void>(async resolve => {
- const path = serverPathToFile(Directory.pdf_thumbnails, thumbnailName);
+ const path = serverPathToFile(Directory.pdf_thumbnails, resolved);
if (existsSync(path)) {
const existingThumbnail = createReadStream(path);
const { err, viewport } = await new Promise<any>(resolve => {
imageSize(existingThumbnail, (err: any, viewport: any) => resolve({ err, viewport }));
});
if (err) {
- console.log(red(`In PDF thumbnail response, unable to determine dimensions of ${thumbnailName}:`));
+ console.log(red(`In PDF thumbnail response, unable to determine dimensions of ${resolved}:`));
console.log(err);
return;
}
- dispatchThumbnail(res, viewport, thumbnailName);
+ dispatchThumbnail(res, viewport, resolved);
} else {
- const offset = thumbnailName.length - pageString.length - 5;
- const name = thumbnailName.substring(0, offset) + ".pdf";
- const path = serverPathToFile(Directory.pdfs, name);
- await CreateThumbnail(path, pageNumber, res);
+ await CreateThumbnail(coreFilename, pageNum, res, subtree);
}
resolve();
});
}
-async function CreateThumbnail(file: string, pageNumber: number, res: express.Response) {
- const documentProxy = await Pdfjs.getDocument(file).promise;
+async function CreateThumbnail(coreFilename: string, pageNum: number, res: express.Response, subtree?: string) {
+ const sourcePath = resolve(pathToDirectory(Directory.pdfs), `${subtree ?? ""}${coreFilename}.pdf`);
+ const documentProxy = await Pdfjs.getDocument(sourcePath).promise;
const factory = new NodeCanvasFactory();
- const page = await documentProxy.getPage(pageNumber);
+ const page = await documentProxy.getPage(pageNum);
const viewport = page.getViewport(1 as any);
const { canvas, context } = factory.create(viewport.width, viewport.height);
const renderContext = {
@@ -64,14 +64,13 @@ async function CreateThumbnail(file: string, pageNumber: number, res: express.Re
};
await page.render(renderContext).promise;
const pngStream = canvas.createPNGStream();
- const filenames = path.basename(file).split(".");
- const thumbnailName = `${filenames[0]}-${pageNumber}.png`;
- const pngFile = serverPathToFile(Directory.pdf_thumbnails, thumbnailName);
+ const resolved = `${coreFilename}-${pageNum}.png`;
+ const pngFile = serverPathToFile(Directory.pdf_thumbnails, resolved);
const out = createWriteStream(pngFile);
pngStream.pipe(out);
return new Promise<void>((resolve, reject) => {
out.on("finish", () => {
- dispatchThumbnail(res, viewport, thumbnailName);
+ dispatchThumbnail(res, viewport, resolved);
resolve();
});
out.on("error", error => {