aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/Utils.ts2
-rw-r--r--src/client/documents/Documents.ts9
-rw-r--r--src/client/util/DocumentManager.ts4
-rw-r--r--src/client/util/DragManager.ts5
-rw-r--r--src/client/util/RichTextRules.ts20
-rw-r--r--src/client/util/SelectionManager.ts2
-rw-r--r--src/client/views/DocumentButtonBar.tsx28
-rw-r--r--src/client/views/DocumentDecorations.tsx3
-rw-r--r--src/client/views/MainView.tsx4
-rw-r--r--src/client/views/OverlayView.tsx2
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx5
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx68
-rw-r--r--src/client/views/collections/CollectionStackingView.scss1
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx24
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx2
-rw-r--r--src/client/views/collections/CollectionSubView.tsx30
-rw-r--r--src/client/views/collections/CollectionTimeView.scss8
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx152
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx47
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx191
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx61
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss5
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx81
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss5
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx58
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx22
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx22
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.tsx83
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.scss5
-rw-r--r--src/client/views/nodes/ImageBox.tsx3
-rw-r--r--src/client/views/nodes/PresBox.scss19
-rw-r--r--src/client/views/nodes/PresBox.tsx138
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx4
-rw-r--r--src/new_fields/Doc.ts59
-rw-r--r--src/new_fields/ScriptField.ts4
-rw-r--r--src/new_fields/documentSchemas.ts4
-rw-r--r--src/new_fields/util.ts2
-rw-r--r--src/scraping/buxton/.idea/buxton.iml8
-rw-r--r--src/scraping/buxton/.idea/inspectionProfiles/profiles_settings.xml6
-rw-r--r--src/scraping/buxton/.idea/misc.xml4
-rw-r--r--src/scraping/buxton/.idea/modules.xml8
-rw-r--r--src/scraping/buxton/.idea/vcs.xml6
-rw-r--r--src/scraping/buxton/.idea/workspace.xml141
-rw-r--r--src/scraping/buxton/json/buxton.json260
-rw-r--r--src/scraping/buxton/json/incomplete.json563
-rw-r--r--src/scraping/buxton/jsonifier.py231
-rw-r--r--src/scraping/buxton/node_scraper.ts226
-rw-r--r--src/server/authentication/models/current_user_utils.ts53
50 files changed, 2097 insertions, 595 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 9e3db9e06..0fa33dcb7 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -363,7 +363,7 @@ export function percent2frac(percent: string) {
return Number(percent.substr(0, percent.length - 1)) / 100;
}
-export function numberRange(num: number) { return Array.from(Array(num)).map((v, i) => i); }
+export function numberRange(num: number) { return num > 0 && num < 1000 ? Array.from(Array(num)).map((v, i) => i) : []; }
export function returnTransparent() { return "transparent"; }
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5eb4dc5fb..64dc0d8b7 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -67,7 +67,7 @@ export interface DocumentOptions {
_fitWidth?: boolean;
_fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents
_LODdisable?: boolean;
- _dropAction?: dropActionType;
+ dropAction?: dropActionType;
_chromeStatus?: string;
_viewType?: number;
_gridGap?: number; // gap between items in masonry view
@@ -106,6 +106,7 @@ export interface DocumentOptions {
documentText?: string;
borderRounding?: string;
boxShadow?: string;
+ showTitle?: string;
sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views
schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
@@ -339,8 +340,8 @@ export namespace Docs {
*/
export namespace Create {
- const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "_dropAction", "_annotationOn",
- "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar"];
+ const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "dropAction", "_annotationOn",
+ "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "showTitle"];
/**
* This function receives the relevant document prototype and uses
@@ -744,7 +745,7 @@ export namespace Docs {
}
if (type.indexOf("excel") !== -1) {
ctor = Docs.Create.DBDocument;
- options._dropAction = "copy";
+ options.dropAction = "copy";
}
if (type.indexOf("html") !== -1) {
if (path.includes(window.location.hostname)) {
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 9fff8faa7..60bb25272 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -93,9 +93,9 @@ export class DocumentManager {
const toReturn: DocumentView[] = [];
DocumentManager.Instance.DocumentViews.map(view =>
- view.props.Document === toFind && toReturn.push(view));
+ view.props.Document.presBox === undefined && view.props.Document === toFind && toReturn.push(view));
DocumentManager.Instance.DocumentViews.map(view =>
- view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
+ view.props.Document.presBox === undefined && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
return toReturn;
}
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c05a2de96..e572f0fcb 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -197,7 +197,10 @@ export namespace DragManager {
dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d)
);
e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) =>
- Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => drop[prop] = undefined));
+ Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => {
+ drop[prop] = undefined;
+ })
+ );
};
dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded
StartDrag(eles, dragData, downX, downY, options, finishDrag);
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 77b111412..3b30b5b3f 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -79,13 +79,16 @@ export const inpRules = {
const fieldKey = match[1];
const docid = match[2]?.substring(1);
if (!fieldKey) {
- DocServer.GetRefField(docid).then(docx => {
- const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid);
- DocUtils.Publish(target, docid, returnFalse, returnFalse);
- DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", "");
- });
- const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid });
- return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link);
+ if (docid) {
+ DocServer.GetRefField(docid).then(docx => {
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid);
+ DocUtils.Publish(target, docid, returnFalse, returnFalse);
+ DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", "");
+ });
+ const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid });
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link);
+ }
+ return state.tr;
}
const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
return state.tr.deleteRange(start, end).insert(start, fieldView);
@@ -96,7 +99,8 @@ export const inpRules = {
(state, match, start, end) => {
const fieldKey = match[1];
const docid = match[2]?.substring(1);
- DocServer.GetRefField(docid).then(docx => {
+ if (!fieldKey && !docid) return state.tr;
+ docid && DocServer.GetRefField(docid).then(docx => {
if (!(docx instanceof Doc && docx)) {
const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid);
DocUtils.Publish(docx, docid, returnFalse, returnFalse);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 86a7a620e..4612f10f4 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -3,8 +3,6 @@ import { Doc } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
import { computedFn } from "mobx-utils";
import { List } from "../../new_fields/List";
-import { DocumentDecorations } from "../views/DocumentDecorations";
-import RichTextMenu from "./RichTextMenu";
export namespace SelectionManager {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 65d1ade2a..963d97d59 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -11,7 +11,7 @@ import { emptyFunction } from "../../Utils";
import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
import RichTextMenu from '../util/RichTextMenu';
import { UndoManager } from "../util/UndoManager";
-import { CollectionDockingView } from './collections/CollectionDockingView';
+import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView';
import { ParentDocSelector } from './collections/ParentDocumentSelector';
import './collections/ParentDocumentSelector.scss';
import './DocumentButtonBar.scss';
@@ -23,6 +23,7 @@ import { Template, Templates } from "./Templates";
import React = require("react");
import { DragManager } from '../util/DragManager';
import { MetadataEntryMenu } from './MetadataEntryMenu';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -197,6 +198,27 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
/>
</div>;
}
+ @computed
+ get pinButton() {
+ const targetDoc = this.view0?.props.Document;
+ const isPinned = targetDoc && CurrentUserUtils.IsDocPinned(targetDoc);
+ return !targetDoc ? (null) : <div className="documentButtonBar-linker"
+ title={CurrentUserUtils.IsDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}
+ style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }}
+
+ onClick={e => {
+ if (isPinned) {
+ DockedFrameRenderer.UnpinDoc(targetDoc);
+ }
+ else {
+ targetDoc.sourceContext = this.view0?.props.ContainingCollectionDoc; // bcz: !! Shouldn't need this ... use search to lookup contexts dynamically
+ DockedFrameRenderer.PinDoc(targetDoc);
+ }
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin"
+ />
+ </div>;
+ }
@computed
get linkButton() {
@@ -294,6 +316,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
const isText = this.view0.props.Document.data instanceof RichTextField; // bcz: Todo - can't assume layout is using the 'data' field. need to add fieldKey to DocumentView
const considerPull = isText && this.considerGoogleDocsPull;
const considerPush = isText && this.considerGoogleDocsPush;
+ Doc.UserDoc().pr
return <div className="documentButtonBar">
<div className="documentButtonBar-button">
{this.linkButton}
@@ -307,6 +330,9 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
<div className="documentButtonBar-button">
{this.contextButton}
</div>
+ <div className="documentButtonBar-button">
+ {this.pinButton}
+ </div>
<div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}>
{this.considerGoogleDocsPush}
</div>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index c5034b901..a0ba16ea4 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -265,8 +265,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const layoutKey = Cast(dv.props.Document.layoutKey, "string", null);
const collapse = layoutKey !== "layout_icon";
if (collapse) {
- if (layoutKey && layoutKey !== "layout") dv.props.Document.deiconifyLayout = layoutKey.replace("layout_", "");
dv.setCustomView(collapse, "icon");
+ if (layoutKey && layoutKey !== "layout") dv.props.Document.deiconifyLayout = layoutKey.replace("layout_", "");
} else {
const deiconifyLayout = Cast(dv.props.Document.deiconifyLayout, "string", null);
dv.setCustomView(deiconifyLayout ? true : false, deiconifyLayout);
@@ -274,6 +274,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
});
}
+ SelectionManager.DeselectAll();
}
@action
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 87a81504c..a839f9fd2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,6 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import {
- faArrowDown, faBullseye, faFilter, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
+ faFileAlt, faStickyNote, faArrowDown, faBullseye, faFilter, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -101,6 +101,8 @@ export class MainView extends React.Component {
}
}
+ library.add(faFileAlt);
+ library.add(faStickyNote);
library.add(faFont);
library.add(faExclamation);
library.add(faPortrait);
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 295cd7c6e..7a99bf0ae 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -144,7 +144,7 @@ export class OverlayView extends React.Component {
return (null);
}
return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => {
- d.inOverlay = true;
+ setTimeout(() => d.inOverlay = true, 0);
let offsetx = 0, offsety = 0;
const onPointerMove = action((e: PointerEvent) => {
if (e.buttons === 1) {
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 0933d5924..00edf71dd 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -13,9 +13,6 @@ import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { Doc } from '../../../new_fields/Doc';
import { FormattedTextBox } from '../nodes/FormattedTextBox';
-
-
-
type CarouselDocument = makeInterface<[typeof documentSchema,]>;
const CarouselDocument = makeInterface(documentSchema);
@@ -74,7 +71,7 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
</>;
}
render() {
- return <div className="collectionCarouselView-outer">
+ return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget}>
{this.content}
{this.buttons}
</div>;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e691788ad..cb413b3e3 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -177,35 +177,30 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
@undoBatch
@action
- public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]): boolean {
- if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance;
- const newItemStackConfig = {
- type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)]
- };
-
- const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
-
+ public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[], addToSplit?: boolean): boolean {
+ if (!CollectionDockingView.Instance) return false;
+ const instance = CollectionDockingView.Instance;
let retVal = false;
if (instance._goldenLayout.root.contentItems[0].isRow) {
retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
- DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanle) {
- child.contentItems[0].remove();
- child.addChild(newContentItem, undefined, true);
+ DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanel) {
+ const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath);
+ child.addChild(newItemStackConfig, undefined);
+ !addToSplit && child.contentItems[0].remove();
instance.layoutChanged(document);
return true;
- } else {
- Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
- if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) {
- child.contentItems[j].remove();
- child.addChild(newContentItem, undefined, true);
- return true;
- }
- return false;
- });
}
- return false;
+ return Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
+ if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) {
+ const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath);
+ child.addChild(newItemStackConfig, undefined);
+ !addToSplit && child.contentItems[j].remove();
+ instance.layoutChanged(document);
+ return true;
+ }
+ return false;
+ });
});
}
if (retVal) {
@@ -255,9 +250,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
//
@undoBatch
@action
- public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) {
+ public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[], shiftKey?: boolean) {
document.isDisplayPanel = true;
- if (!CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath)) {
+ if (shiftKey || !CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath, shiftKey)) {
CollectionDockingView.AddRightSplit(document, dataDoc, libraryPath);
}
}
@@ -473,7 +468,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
onPointerDown={e => {
e.preventDefault();
e.stopPropagation();
- DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY);
+ const dragData = new DragManager.DocumentDragData([doc]);
+ dragData.dropAction = doc.dropAction === "alias" ? "alias" : doc.dropAction === "copy" ? "copy" : undefined;
+ DragManager.StartDocumentDrag([dragSpan], dragData, e.clientX, e.clientY);
}}>
<FontAwesomeIcon icon="file" size="lg" />
</span>, dragSpan);
@@ -640,7 +637,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
**/
@undoBatch
@action
- public PinDoc(doc: Doc) {
+ public static PinDoc(doc: Doc) {
//add this new doc to props.Document
const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
if (curPres) {
@@ -648,10 +645,23 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
pinDoc.presentationTargetDoc = doc;
Doc.AddDocToList(curPres, "data", pinDoc);
if (!DocumentManager.Instance.getDocumentView(curPres)) {
- this.addDocTab(curPres, undefined, "onRight");
+ CollectionDockingView.AddRightSplit(curPres, undefined);
}
}
}
+ /**
+ * Adds a document to the presentation view
+ **/
+ @undoBatch
+ @action
+ public static UnpinDoc(doc: Doc) {
+ //add this new doc to props.Document
+ const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
+ if (curPres) {
+ const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc));
+ ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]);
+ }
+ }
componentDidMount() {
const observer = new _global.ResizeObserver(action((entries: any) => {
@@ -748,7 +758,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
focus={emptyFunction}
backgroundColor={returnEmptyString}
addDocTab={this.addDocTab}
- pinToPres={this.PinDoc}
+ pinToPres={DockedFrameRenderer.PinDoc}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
zoomToScale={emptyFunction}
@@ -768,4 +778,4 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); });
-Scripting.addGlobal(function useRightSplit(doc: any) { CollectionDockingView.UseRightSplit(doc, undefined); });
+Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, undefined, shiftKey); });
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 843c743db..293dc5414 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -19,6 +19,7 @@
position: absolute;
top: 0;
overflow-y: auto;
+ overflow-x: hidden;
flex-wrap: wrap;
transition: top .5s;
>div {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 91c7ca76e..9e8600193 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -11,7 +11,6 @@ import { listSpec } from "../../../new_fields/Schema";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../new_fields/Types";
import { emptyFunction, Utils } from "../../../Utils";
-import { DocumentType } from "../../documents/DocumentTypes";
import { DragManager } from "../../util/DragManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
@@ -41,7 +40,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
@computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); }
@computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * this.gridGap); }
- @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 2 * this.gridGap)); }
+ @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 0)); } // 2 * this.gridGap)); }
@computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); }
@computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
@computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
@@ -52,11 +51,11 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
@computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; }
- children(docs: Doc[]) {
+ children(docs: Doc[], columns?: number) {
this._docXfs.length = 0;
return docs.map((d, i) => {
- const width = () => Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
const height = () => this.getDocHeight(d);
+ const width = () => this.widthScale * Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
const dref = React.createRef<HTMLDivElement>();
const dxf = () => this.getDocTransform(d, dref.current!);
this._docXfs.push({ dxf: dxf, width: width, height: height });
@@ -377,6 +376,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
return sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]));
}
+ @computed get contentScale() {
+ const heightExtra = this.heightPercent > 1 ? this.heightPercent : 1;
+ return Math.max(this.props.Document[WidthSym]() / this.props.PanelWidth(), heightExtra * this.props.Document[HeightSym]() / this.props.PanelHeight());
+ }
+ @computed get widthScale() {
+ return this.heightPercent < 1 ? Math.max(1, this.contentScale) : 1;
+ }
+ @computed get heightPercent() {
+ return this.props.PanelHeight() / this.layoutDoc[HeightSym]();
+ }
render() {
TraceMobx();
const editableViewProps = {
@@ -390,9 +399,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
ref={this.createRef}
style={{
overflowY: this.props.active() ? "auto" : "hidden",
- transform: `scale(${Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]())})`,
- height: `${Math.max(100, 100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]()))}%`,
- transformOrigin: "top"
+ transform: `scale(${Math.min(1, this.heightPercent)})`,
+ height: `${100 * Math.max(1, this.contentScale)}%`,
+ width: `${100 * this.widthScale}%`,
+ transformOrigin: "top left",
}}
onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
onDrop={this.onDrop.bind(this)}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index b81b1f31d..21982f1ca 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -407,7 +407,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
gridTemplateColumns: singleColumn ? undefined : templatecols,
gridAutoRows: singleColumn ? undefined : "0px"
}}>
- {this.props.parent.children(this.props.docList)}
+ {this.props.parent.children(this.props.docList, uniqueHeadings.length)}
{singleColumn ? (null) : this.props.parent.columnDragger}
</div>
{(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 9cdd48089..e0e99d635 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -43,6 +43,7 @@ export interface SubCollectionViewProps extends CollectionViewProps {
children?: never | (() => JSX.Element[]) | React.ReactNode;
isAnnotationOverlay?: boolean;
annotationsKey: string;
+ layoutEngine?: () => string;
}
export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@@ -110,34 +111,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
get childDocs() {
const docs = DocListCast(this.dataField);
const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
- const viewedDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
- const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []);
- const clusters: { [key: string]: { [value: string]: string } } = {};
- for (let i = 0; i < docFilters.length; i += 3) {
- const [key, value, modifiers] = docFilters.slice(i, i + 3);
- const cluster = clusters[key];
- if (!cluster) {
- const child: { [value: string]: string } = {};
- child[value] = modifiers;
- clusters[key] = child;
- } else {
- cluster[value] = modifiers;
- }
- }
- const filteredDocs = docFilters.length ? viewedDocs.filter(d => {
- for (const key of Object.keys(clusters)) {
- const cluster = clusters[key];
- const satisfiesFacet = Object.keys(cluster).some(inner => {
- const modifier = cluster[inner];
- return (modifier === "x") !== Doc.matchFieldValue(d, key, inner);
- });
- if (!satisfiesFacet) {
- return false;
- }
- }
- return true;
- }) : viewedDocs;
- return filteredDocs;
+ return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
}
@action
diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss
index 3aac2bc75..a5ce73a92 100644
--- a/src/client/views/collections/CollectionTimeView.scss
+++ b/src/client/views/collections/CollectionTimeView.scss
@@ -5,9 +5,17 @@
height: 100%;
width: 100%;
overflow: hidden;
+ .collectionTimeView-backBtn {
+ background: green;
+ display: inline;
+ margin-right: 20px;
+ }
.collectionFreeform-customText {
text-align: left;
}
+ .collectionFreeform-customDiv {
+ position: absolute;
+ }
.collectionTimeView-thumb {
position: absolute;
width: 30px;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 253dfa890..0c1f93829 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,29 +1,34 @@
import { faEdit } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable } from "mobx";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { Set } from "typescript-collections";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Field } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
+import { RichTextField } from "../../../new_fields/RichTextField";
import { listSpec } from "../../../new_fields/Schema";
import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { Docs } from "../../documents/Documents";
+import { Scripting } from "../../util/Scripting";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
import { EditableView } from "../EditableView";
import { anchorPoints, Flyout } from "../TemplateMenu";
+import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines";
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import "./CollectionTimeView.scss";
import { CollectionSubView } from "./CollectionSubView";
-import { CollectionTreeView } from "./CollectionTreeView";
+import "./CollectionTimeView.scss";
import React = require("react");
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { RichTextField } from "../../../new_fields/RichTextField";
+import { CollectionTreeView } from "./CollectionTreeView";
+import { ObjectField } from "../../../new_fields/ObjectField";
@observer
export class CollectionTimeView extends CollectionSubView(doc => doc) {
+ _changing = false;
+ @observable _layoutEngine = "pivot";
+
componentDidMount() {
- this.props.Document._freeformLayoutEngine = "timeline";
const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked;
if (!this.props.Document._facetCollection) {
const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true });
@@ -31,19 +36,24 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilter"]);
const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)";
- const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailed'); useRightSplit(alias); ";
+ const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailView'); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "boolean", checked: "boolean", containingTreeView: Doc.name });
- this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name });
+ this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name, shiftKey: "boolean" });
this.props.Document._facetCollection = facetCollection;
this.props.Document._fitToBox = true;
}
+ if (!this.props.Document.onViewDefClick) {
+ this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" })
+ }
}
+
bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth;
getTransform = () => this.props.ScreenToLocalTransform().translate(-this._facetWidth, 0);
@computed get _allFacets() {
const facets = new Set<string>();
this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
+ Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.forEach(child => Object.keys(child).forEach(key => facets.add(key)));
return facets.toArray();
}
@@ -106,14 +116,13 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
const docItems: ContextMenuProps[] = [];
const keySet: Set<string> = new Set();
- this.childLayoutPairs.map(pair =>
- Array.from(Object.keys(Doc.GetProto(pair.layout))).filter(fieldKey =>
- pair.layout[fieldKey] instanceof RichTextField ||
- typeof (pair.layout[fieldKey]) === "number" ||
- typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey)));
+ this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey =>
+ pair.layout[fieldKey] instanceof RichTextField ||
+ typeof (pair.layout[fieldKey]) === "number" ||
+ typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey)));
keySet.toArray().map(fieldKey =>
- docItems.push({ description: ":" + fieldKey, event: () => this.props.Document.pivotField = fieldKey, icon: "compress-arrows-alt" }));
- docItems.push({ description: ":(null)", event: () => this.props.Document.pivotField = undefined, icon: "compress-arrows-alt" })
+ docItems.push({ description: ":" + fieldKey, event: () => this.props.Document._pivotField = fieldKey, icon: "compress-arrows-alt" }));
+ docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" })
ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
ContextMenu.Instance.displayMenu(x, y, ":");
@@ -190,14 +199,14 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
document.removeEventListener("pointermove", this.onMidUp);
}
+ layoutEngine = () => this._layoutEngine;
@computed get contents() {
return <div className="collectionTimeView-innards" key="timeline" style={{ width: this.bodyPanelWidth() }}>
- <CollectionFreeFormView {...this.props} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
+ <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
</div>;
}
-
- _changing = false;
- render() {
+ @computed get filterView() {
+ trace();
const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
const flyout = (
<div className="collectionTimeView-flyout" style={{ width: `${this._facetWidth}` }}>
@@ -208,68 +217,98 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
</label>)}
</div>
);
+ return <div className="collectionTimeView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
+ <div className="collectionTimeView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
+ <div className="collectionTimeView-button">
+ <span className="collectionTimeView-span">Facet Filters</span>
+ <FontAwesomeIcon icon={faEdit} size={"lg"} />
+ </div>
+ </Flyout>
+ </div>
+ <div className="collectionTimeView-tree" key="tree">
+ <CollectionTreeView {...this.props} Document={facetCollection} />
+ </div>
+ </div>;
+ }
+
+ public static SyncTimelineToPresentation(doc: Doc) {
+ const fieldKey = Doc.LayoutFieldKey(doc);
+ doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(curPresentationItem()[this._pivotField || 'year'] || 0)");
+ }
+ specificMenu = (e: React.MouseEvent) => {
+ const layoutItems: ContextMenuProps[] = [];
+ const doc = this.props.Document;
+
+ layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline" }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot" }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" });
+
+ ContextMenu.Instance.addItem({ description: "Pivot/Time Options ...", subitems: layoutItems, icon: "eye" });
+ }
+
+ render() {
const newEditableViewProps = {
GetValue: () => "",
SetValue: (value: any) => {
if (value?.length) {
- this.props.Document.pivotField = value;
+ this.props.Document._pivotField = value;
return true;
}
return false;
},
showMenuOnLoad: true,
- contents: ":" + StrCast(this.props.Document.pivotField),
+ contents: ":" + StrCast(this.props.Document._pivotField),
toggle: this.toggleVisibility,
color: "#f1efeb" // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
};
let nonNumbers = 0;
this.childDocs.map(doc => {
- const num = NumCast(doc[StrCast(this.props.Document.pivotField)], Number(StrCast(doc[StrCast(this.props.Document.pivotField)])));
+ const num = NumCast(doc[StrCast(this.props.Document._pivotField)], Number(StrCast(doc[StrCast(this.props.Document._pivotField)])));
if (Number.isNaN(num)) {
nonNumbers++;
}
});
- const doTimeline = nonNumbers / this.childDocs.length < 0.1;
- if (doTimeline !== (this.props.Document._freeformLayoutEngine === "timeline")) {
+ const forceLayout = StrCast(this.props.Document._forceRenderEngine);
+ const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
+ if (doTimeline !== (this._layoutEngine === "timeline")) {
if (!this._changing) {
this._changing = true;
- setTimeout(() => {
- if (nonNumbers / this.childDocs.length > 0.1) {
- this.childDocs.map(child => child.isMinimized = false);
- this.props.Document._freeformLayoutEngine = "pivot";
- } else {
- this.props.Document._freeformLayoutEngine = "timeline";
- }
+ setTimeout(action(() => {
+ this._layoutEngine = doTimeline ? "timeline" : "pivot";
this._changing = false;
- }, 0);
+ }), 0);
}
- return (null);
}
+
+ const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
return !facetCollection ? (null) :
- <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
+ <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu}
+ style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
<div className={"pivotKeyEntry"}>
- <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} />
+ <button className="collectionTimeView-backBtn" style={{ width: 50, height: 20, background: "green" }}
+ onClick={action(() => {
+ let pfilterIndex = NumCast(this.props.Document._pfilterIndex);
+ if (pfilterIndex > 0) {
+ this.props.Document._docFilter = ObjectField.MakeCopy(this.props.Document["_pfilter" + --pfilterIndex] as ObjectField);
+ this.props.Document._pfilterIndex = pfilterIndex;
+ } else {
+ this.props.Document._docFilter = new List([]);
+ }
+ })}>
+ back
+ </button>
+ <EditableView {...newEditableViewProps} display={"inline"} menuCallback={this.menuCallback} />
</div>
{!this.props.isSelected() || this.props.PanelHeight() < 100 ? (null) :
<div className="collectionTimeView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this._facetWidth}px, 0px)` }} >
<span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} />
</div>
}
- <div className="collectionTimeView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
- <div className="collectionTimeView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
- <div className="collectionTimeView-button">
- <span className="collectionTimeView-span">Facet Filters</span>
- <FontAwesomeIcon icon={faEdit} size={"lg"} />
- </div>
- </Flyout>
- </div>
- <div className="collectionTimeView-tree" key="tree">
- <CollectionTreeView {...this.props} Document={facetCollection} />
- </div>
- </div>
+ {this.filterView}
{this.contents}
{!this.props.isSelected() || !doTimeline ? (null) : <>
<div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
@@ -278,4 +317,13 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
</>}
</div>;
}
-} \ No newline at end of file
+}
+
+Scripting.addGlobal(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
+ let pfilterIndex = NumCast(pivotDoc._pfilterIndex);
+ pivotDoc["_pfilter" + pfilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilter as ObjectField);
+ pivotDoc._pfilterIndex = ++pfilterIndex;
+ pivotDoc._docFilter = new List();
+ (bounds.payload as string[]).map(filterVal =>
+ Doc.setDocFilter(pivotDoc, StrCast(pivotDoc._pivotField), filterVal, "check"));
+}); \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 631870866..09f6a96d7 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -626,6 +626,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
const layoutItems: ContextMenuProps[] = [];
layoutItems.push({ description: (this.props.Document.preventTreeViewOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.preventTreeViewOpen = !this.props.Document.preventTreeViewOpen, icon: "paint-brush" });
layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" });
+ layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" });
ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" });
}
ContextMenu.Instance.addItem({
@@ -639,28 +640,46 @@ export class CollectionTreeView extends CollectionSubView(Document) {
d.captions = undefined;
});
});
- const { TextDocument, ImageDocument, CarouselDocument } = Docs.Create;
+ const { TextDocument, ImageDocument, CarouselDocument, TreeDocument } = Docs.Create;
const { Document } = this.props;
const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif";
- const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "short_description" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`;
+ const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`;
const textDoc = TextDocument("", { title: "details", _autoHeight: true });
- const detailedLayout = Docs.Create.StackingDocument([
+ const detailView = Docs.Create.StackingDocument([
CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" }),
textDoc,
- ], { _chromeStatus: "disabled", title: "detailed layout stack" });
- textDoc.data = new RichTextField(detailedTemplate, "short_description year company");
- detailedLayout.isTemplateDoc = makeTemplate(detailedLayout);
+ TextDocument("", { title: "short_description", _autoHeight: true }),
+ TreeDocument([], { title: "narratives", _height: 75, treeViewHideTitle: true })
+ ], { _chromeStatus: "disabled", _width: 300, _height: 300, _autoHeight: true, title: "detailView" });
+ textDoc.data = new RichTextField(detailedTemplate, "year company");
+ detailView.isTemplateDoc = makeTemplate(detailView);
- const cardLayout = ImageDocument(fallbackImg, { title: "cardLayout", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work?
- cardLayout.proto!.layout = ImageBox.LayoutString("hero");
- cardLayout.showTitle = "title";
- cardLayout.showTitleHover = "titlehover";
- Document.childLayout = cardLayout;
- Document.childDetailed = detailedLayout;
+ const heroView = ImageDocument(fallbackImg, { title: "heroView", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work?
+ heroView.proto!.layout = ImageBox.LayoutString("hero");
+ heroView.showTitle = "title";
+ heroView.showTitleHover = "titlehover";
+
+ Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data",
+ Docs.Create.FontIconDocument({
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ dragFactory: heroView, removeDropProperties: new List<string>(["dropAction"]), title: "hero view", icon: "portrait"
+ }));
+
+ Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data",
+ Docs.Create.FontIconDocument({
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ dragFactory: detailView, removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "file-alt"
+ }));
+
+
+ Document.childLayout = heroView;
+ Document.childDetailed = detailView;
Document._viewType = CollectionViewType.Time;
- Document.pivotField = "company";
+ Document._forceActive = true;
+ Document._pivotField = "company";
+ Document.childDropAction = "alias";
}
});
const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
@@ -684,7 +703,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
render() {
- const dropAction = StrCast(this.props.Document._dropAction) as dropActionType;
+ const dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
return !this.childDocs ? (null) : (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 63bcc68e5..95f7794bb 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -1,29 +1,18 @@
-import { Doc, Field, FieldResult, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { NumCast, StrCast, Cast, DateCast, BoolCast } from "../../../../new_fields/Types";
+import { Doc, Field, FieldResult } from "../../../../new_fields/Doc";
+import { NumCast, StrCast, Cast } from "../../../../new_fields/Types";
import { ScriptBox } from "../../ScriptBox";
import { CompileScript } from "../../../util/Scripting";
import { ScriptField } from "../../../../new_fields/ScriptField";
import { OverlayView, OverlayElementOptions } from "../../OverlayView";
import { emptyFunction, aggregateBounds } from "../../../../Utils";
import React = require("react");
-import { ObservableMap, runInAction } from "mobx";
import { Id, ToString } from "../../../../new_fields/FieldSymbols";
import { ObjectField } from "../../../../new_fields/ObjectField";
import { RefField } from "../../../../new_fields/RefField";
-interface PivotData {
- type: string;
- text: string;
- x: number;
- y: number;
- zIndex?: number;
- width?: number;
- height?: number;
- fontSize: number;
- color?: string;
-}
-
export interface ViewDefBounds {
+ type: string;
+ text?: string;
x: number;
y: number;
z?: number;
@@ -31,7 +20,10 @@ export interface ViewDefBounds {
width?: number;
height?: number;
transition?: string;
+ fontSize?: number;
highlight?: boolean;
+ color?: string;
+ payload: any;
}
export interface PoolData {
@@ -44,7 +36,6 @@ export interface PoolData {
color?: string,
transition?: string,
highlight?: boolean,
- state?: any
}
export interface ViewDefResult {
@@ -57,49 +48,99 @@ function toLabel(target: FieldResult<Field>) {
return target[ToString]();
}
return String(target);
+}/**
+ * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
+ *
+ * @param {String} text The text to be rendered.
+ * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
+ *
+ * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
+ */
+function getTextWidth(text: string, font: string): number {
+ // re-use canvas object for better performance
+ var canvas = (getTextWidth as any).canvas || ((getTextWidth as any).canvas = document.createElement("canvas"));
+ var context = canvas.getContext("2d");
+ context.font = font;
+ var metrics = context.measureText(text);
+ return metrics.width;
+}
+
+interface pivotColumn {
+ docs: Doc[],
+ filters: string[]
}
+
export function computePivotLayout(
poolData: Map<string, PoolData>,
pivotDoc: Doc,
childDocs: Doc[],
+ filterDocs: Doc[],
childPairs: { layout: Doc, data?: Doc }[],
panelDim: number[],
- viewDefsToJSX: (views: any) => ViewDefResult[]
+ viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[]
) {
- console.log("PIVOT " + pivotDoc[HeightSym]());
const fieldKey = "data";
- const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
- const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3));
+ const pivotColumnGroups = new Map<FieldResult<Field>, pivotColumn>();
- let maxInColumn = 1;
- const pivotFieldKey = toLabel(pivotDoc.pivotField);
- for (const doc of childDocs) {
+ const pivotFieldKey = toLabel(pivotDoc._pivotField);
+ for (const doc of filterDocs) {
const val = Field.toString(doc[pivotFieldKey] as Field);
if (val) {
- !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []);
- pivotColumnGroups.get(val)!.push(doc);
- maxInColumn = Math.max(maxInColumn, pivotColumnGroups.get(val)?.length || 0);
+ !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val] });
+ pivotColumnGroups.get(val)!.docs.push(doc);
}
}
+ if (pivotColumnGroups.size > 10) {
+ const arrayofKeys = Array.from(pivotColumnGroups.keys());
+ const sortedKeys = arrayofKeys.sort();
+ const clusterSize = Math.ceil(pivotColumnGroups.size / 10);
+ const numClusters = Math.ceil(sortedKeys.length / clusterSize);
+ for (let i = 0; i < numClusters; i++) {
+ for (let j = i * clusterSize + 1; j < Math.min(sortedKeys.length, (i + 1) * clusterSize); j++) {
+ const curgrp = pivotColumnGroups.get(sortedKeys[i * clusterSize])!;
+ const newgrp = pivotColumnGroups.get(sortedKeys[j])!;
+ curgrp.docs.push(...newgrp.docs);
+ curgrp.filters.push(...newgrp.filters);
+ pivotColumnGroups.delete(sortedKeys[j]);
+ }
+ }
+ }
+ const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3));
+ const desc = `${fontSize}px ${getComputedStyle(document.body).fontFamily}`;
+ const textlen = Array.from(pivotColumnGroups.keys()).map(c => getTextWidth(toLabel(c), desc)).reduce((p, c) => Math.max(p, c), 0 as number);
+ const max_text = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2);
+ let maxInColumn = Array.from(pivotColumnGroups.values()).reduce((p, s) => Math.max(p, s.docs.length), 1);
const colWidth = panelDim[0] / pivotColumnGroups.size;
- const colHeight = panelDim[1];
- const pivotAxisWidth = Math.sqrt(colWidth * colHeight / maxInColumn);
- const numCols = Math.max(Math.round(colWidth / pivotAxisWidth), 1);
+ const colHeight = panelDim[1] - max_text;
+ let numCols = 0;
+ let bestArea = 0;
+ let pivotAxisWidth = 0;
+ for (let i = 1; i < 10; i++) {
+ const numInCol = Math.ceil(maxInColumn / i);
+ const hd = colHeight / numInCol;
+ const wd = colWidth / i;
+ const dim = Math.min(hd, wd);
+ if (dim > bestArea) {
+ bestArea = dim;
+ numCols = i;
+ pivotAxisWidth = dim;
+ }
+ }
const docMap = new Map<Doc, ViewDefBounds>();
- const groupNames: PivotData[] = [];;
+ const groupNames: ViewDefBounds[] = [];
const expander = 1.05;
const gap = .15;
let x = 0;
- let max_text = 60;
- pivotColumnGroups.forEach((val, key) => {
+ const sortedPivotKeys = Array.from(pivotColumnGroups.keys()).sort();
+ sortedPivotKeys.forEach(key => {
+ const val = pivotColumnGroups.get(key)!;
let y = 0;
let xCount = 0;
const text = toLabel(key);
- max_text = Math.max(max_text, Math.min(500, text.length));
groupNames.push({
type: "text",
text,
@@ -107,9 +148,10 @@ export function computePivotLayout(
y: pivotAxisWidth,
width: pivotAxisWidth * expander * numCols,
height: max_text,
- fontSize
+ fontSize,
+ payload: val
});
- for (const doc of val) {
+ for (const doc of val.docs) {
const layoutDoc = Doc.Layout(doc);
let wid = pivotAxisWidth;
let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
@@ -118,10 +160,12 @@ export function computePivotLayout(
wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
}
docMap.set(doc, {
- x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.length < numCols ? (numCols - val.length) * pivotAxisWidth / 2 : 0),
+ type: "doc",
+ x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.docs.length < numCols ? (numCols - val.docs.length) * pivotAxisWidth / 2 : 0),
y: -y + (pivotAxisWidth - hgt) / 2,
width: wid,
- height: hgt
+ height: hgt,
+ payload: undefined
});
xCount++;
if (xCount >= numCols) {
@@ -132,24 +176,32 @@ export function computePivotLayout(
x += pivotAxisWidth * (numCols * expander + gap);
});
- return normalizeResults(panelDim, max_text, childPairs, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
+ const maxColHeight = pivotAxisWidth * expander * Math.ceil(maxInColumn / numCols);
+ const dividers = sortedPivotKeys.map((key, i) =>
+ ({ type: "div", color: "lightGray", x: i * pivotAxisWidth * (numCols * expander + gap), y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, height: maxColHeight, payload: pivotColumnGroups.get(key)!.filters }));
+ groupNames.push(...dividers);
+ return normalizeResults(panelDim, max_text, childPairs, docMap, poolData, viewDefsToJSX, groupNames, 0, [], childDocs.filter(c => !filterDocs.includes(c)));
}
+function toNumber(val: FieldResult<Field>) {
+ return val === undefined ? undefined : NumCast(val, Number(StrCast(val)));
+}
export function computeTimelineLayout(
poolData: Map<string, PoolData>,
pivotDoc: Doc,
childDocs: Doc[],
+ filterDocs: Doc[],
childPairs: { layout: Doc, data?: Doc }[],
panelDim: number[],
- viewDefsToJSX: (views: any) => ViewDefResult[]
+ viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[]
) {
const fieldKey = "data";
const pivotDateGroups = new Map<number, Doc[]>();
const docMap = new Map<Doc, ViewDefBounds>();
- const groupNames: PivotData[] = [];
- const timelineFieldKey = Field.toString(pivotDoc.pivotField as Field);
- const curTime = Cast(pivotDoc[fieldKey + "-timelineCur"], "number", null);
+ const groupNames: ViewDefBounds[] = [];
+ const timelineFieldKey = Field.toString(pivotDoc._pivotField as Field);
+ const curTime = toNumber(pivotDoc[fieldKey + "-timelineCur"]);
const curTimeSpan = Cast(pivotDoc[fieldKey + "-timelineSpan"], "number", null);
const minTimeReq = curTime === undefined ? Cast(pivotDoc[fieldKey + "-timelineMinReq"], "number", null) : curTimeSpan && (curTime - curTimeSpan);
const maxTimeReq = curTime === undefined ? Cast(pivotDoc[fieldKey + "-timelineMaxReq"], "number", null) : curTimeSpan && (curTime + curTimeSpan);
@@ -161,12 +213,10 @@ export function computeTimelineLayout(
}
let minTime = Number.MAX_VALUE;
- let maxTime = Number.MIN_VALUE;
- childDocs.map(doc => {
+ let maxTime = -Number.MAX_VALUE;
+ filterDocs.map(doc => {
const num = NumCast(doc[timelineFieldKey], Number(StrCast(doc[timelineFieldKey])));
- if (Number.isNaN(num) || (minTimeReq && num < minTimeReq) || (maxTimeReq && num > maxTimeReq)) {
- doc.isMinimized = true;
- } else {
+ if (!(Number.isNaN(num) || (minTimeReq && num < minTimeReq) || (maxTimeReq && num > maxTimeReq))) {
!pivotDateGroups.get(num) && pivotDateGroups.set(num, []);
pivotDateGroups.get(num)!.push(doc);
minTime = Math.min(num, minTime);
@@ -180,8 +230,14 @@ export function computeTimelineLayout(
minTime = curTime - (maxTime - curTime);
}
}
- pivotDoc[fieldKey + "-timelineMin"] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime;
- pivotDoc[fieldKey + "-timelineMax"] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime;
+ setTimeout(() => {
+ pivotDoc[fieldKey + "-timelineMin"] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime;
+ pivotDoc[fieldKey + "-timelineMax"] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime;
+ }, 0);
+
+ if (maxTime === minTime) {
+ maxTime = minTime + 1;
+ }
const arrayofKeys = Array.from(pivotDateGroups.keys());
const sortedKeys = arrayofKeys.sort((n1, n2) => n1 - n2);
@@ -190,10 +246,10 @@ export function computeTimelineLayout(
let prevKey = Math.floor(minTime);
if (sortedKeys.length && scaling * (sortedKeys[0] - prevKey) > 25) {
- groupNames.push({ type: "text", text: prevKey.toString(), x: x, y: 0, height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: prevKey.toString(), x: x, y: 0, height: fontHeight, fontSize, payload: undefined });
}
if (!sortedKeys.length && curTime !== undefined) {
- groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, zIndex: 1000, color: "orange", y: 0, height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, zIndex: 1000, color: "orange", y: 0, height: fontHeight, fontSize, payload: undefined });
}
const pivotAxisWidth = NumCast(pivotDoc.pivotTimeWidth, panelDim[1] / 2.5);
@@ -201,26 +257,27 @@ export function computeTimelineLayout(
let zind = 0;
sortedKeys.forEach(key => {
if (curTime !== undefined && curTime > prevKey && curTime <= key) {
- groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: curTime.toString(), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: key });
}
const keyDocs = pivotDateGroups.get(key)!;
- keyDocs.forEach(d => d.isMinimized = false);
x += scaling * (key - prevKey);
const stack = findStack(x, stacking);
prevKey = key;
- !stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth) && groupNames.push({ type: "text", text: key.toString(), x: x, y: stack * 25, height: fontHeight, fontSize });
+ if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
+ groupNames.push({ type: "text", text: key.toString(), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
+ }
layoutDocsAtTime(keyDocs, key);
});
- if (sortedKeys.length && curTime > sortedKeys[sortedKeys.length - 1]) {
+ if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
x = (curTime - minTime) * scaling;
- groupNames.push({ type: "text", text: curTime.toString(), x: x, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: curTime.toString(), x: x, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: undefined });
}
if (Math.ceil(maxTime - minTime) * scaling > x + 25) {
- groupNames.push({ type: "text", text: Math.ceil(maxTime).toString(), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize });
+ groupNames.push({ type: "text", text: Math.ceil(maxTime).toString(), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined });
}
- const divider = { type: "div", color: "black", x: 0, y: 0, width: panelDim[0], height: 1 } as any;
- return normalizeResults(panelDim, fontHeight, childPairs, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider]);
+ const divider = { type: "div", color: "black", x: 0, y: 0, width: panelDim[0], height: 1, payload: undefined };
+ return normalizeResults(panelDim, fontHeight, childPairs, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider], childDocs.filter(c => !filterDocs.includes(c)));
function layoutDocsAtTime(keyDocs: Doc[], key: number) {
keyDocs.forEach(doc => {
@@ -233,8 +290,9 @@ export function computeTimelineLayout(
wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
}
docMap.set(doc, {
+ type: "doc",
x: x, y: -Math.sqrt(stack) * pivotAxisWidth / 2 - pivotAxisWidth + (pivotAxisWidth - hgt) / 2,
- zIndex: (curTime === key ? 1000 : zind++), highlight: curTime === key, width: wid / (Math.max(stack, 1)), height: hgt
+ zIndex: (curTime === key ? 1000 : zind++), highlight: curTime === key, width: wid / (Math.max(stack, 1)), height: hgt, payload: undefined
});
stacking[stack] = x + pivotAxisWidth;
});
@@ -242,17 +300,18 @@ export function computeTimelineLayout(
}
function normalizeResults(panelDim: number[], fontHeight: number, childPairs: { data?: Doc, layout: Doc }[], docMap: Map<Doc, ViewDefBounds>,
- poolData: Map<string, PoolData>, viewDefsToJSX: (views: any) => ViewDefResult[], groupNames: PivotData[], minWidth: number, extras: PivotData[]) {
+ poolData: Map<string, PoolData>, viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], groupNames: ViewDefBounds[], minWidth: number, extras: ViewDefBounds[],
+ extraDocs: Doc[]) {
- const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, height: gn.height }) as PivotData);
- const docEles = childPairs.filter(d => !d.layout.isMinimized).map(pair => docMap.get(pair.layout) as PivotData);
+ const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds);
+ const docEles = childPairs.filter(d => docMap.get(d.layout)).map(pair => docMap.get(pair.layout) as ViewDefBounds);
const aggBounds = aggregateBounds(docEles.concat(grpEles), 0, 0);
aggBounds.r = Math.max(minWidth, aggBounds.r - aggBounds.x);
const wscale = panelDim[0] / (aggBounds.r - aggBounds.x);
let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale;
if (Number.isNaN(scale)) scale = 1;
- childPairs.filter(d => !d.layout.isMinimized).map(pair => {
+ childPairs.filter(d => docMap.get(d.layout)).map(pair => {
const newPosRaw = docMap.get(pair.layout);
if (newPosRaw) {
const newPos = {
@@ -267,6 +326,7 @@ function normalizeResults(panelDim: number[], fontHeight: number, childPairs: {
poolData.set(pair.layout[Id], { transition: "transform 1s", ...newPos });
}
});
+ extraDocs.map(ed => poolData.set(ed[Id], { x: 0, y: 0, zIndex: -99 }));
return {
elements: viewDefsToJSX(extras.concat(groupNames.map(gname => ({
@@ -277,7 +337,8 @@ function normalizeResults(panelDim: number[], fontHeight: number, childPairs: {
color: gname.color,
width: gname.width === undefined ? undefined : gname.width * scale,
height: Math.max(fontHeight, (gname.height || 0) * scale),
- fontSize: gname.fontSize
+ fontSize: gname.fontSize,
+ payload: gname.payload
}))))
};
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 63544a637..2518a4a55 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -7,7 +7,7 @@ import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync, Field } f
import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
import { Id } from "../../../../new_fields/FieldSymbols";
import { InkTool, InkField, InkData } from "../../../../new_fields/InkField";
-import { createSchema, makeInterface } from "../../../../new_fields/Schema";
+import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
@@ -32,7 +32,7 @@ import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
import PDFMenu from "../../pdf/PDFMenu";
import { CollectionSubView } from "../CollectionSubView";
-import { computePivotLayout, ViewDefResult, computeTimelineLayout, PoolData } from "./CollectionFreeFormLayoutEngines";
+import { computePivotLayout, ViewDefResult, computeTimelineLayout, PoolData, ViewDefBounds } from "./CollectionFreeFormLayoutEngines";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
@@ -126,6 +126,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
+ if (this.props.Document.isBackground) return false;
const xf = this.getTransform();
const xfo = this.getTransformOverlay();
const [xp, yp] = xf.transformPoint(de.x, de.y);
@@ -434,13 +435,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let y = this.Document._panY || 0;
const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout);
const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
- if (!this.isAnnotationOverlay && docs.length) {
+ if (!this.isAnnotationOverlay && docs.length && this.childDataProvider(docs[0])) {
PDFMenu.Instance.fadeOut(true);
const minx = this.childDataProvider(docs[0]).x;//docs.length ? NumCast(docs[0].x) : 0;
const miny = this.childDataProvider(docs[0]).y;//docs.length ? NumCast(docs[0].y) : 0;
const maxx = this.childDataProvider(docs[0]).width + minx;//docs.length ? NumCast(docs[0].width) + minx : minx;
const maxy = this.childDataProvider(docs[0]).height + miny;//docs.length ? NumCast(docs[0].height) + miny : miny;
- const ranges = docs.filter(doc => doc).reduce((range, doc) => {
+ const ranges = docs.filter(doc => doc).filter(doc => this.childDataProvider(doc)).reduce((range, doc) => {
const x = this.childDataProvider(doc).x;//NumCast(doc.x);
const y = this.childDataProvider(doc).y;//NumCast(doc.y);
const xe = this.childDataProvider(doc).width + x;//x + NumCast(layoutDoc.width);
@@ -747,11 +748,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
};
}
- viewDefsToJSX = (views: any[]) => {
+ viewDefsToJSX = (views: ViewDefBounds[]) => {
return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!);
}
- private viewDefToJSX(viewDef: any): Opt<ViewDefResult> {
+ onViewDefDivClick = (e: React.MouseEvent, payload: any) => {
+ (this.props.Document.onViewDefDivClick as ScriptField)?.script.run({ this: this.props.Document, payload });
+ }
+ private viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> {
const x = Cast(viewDef.x, "number");
const y = Cast(viewDef.y, "number");
const z = Cast(viewDef.z, "number");
@@ -769,15 +773,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
style={{ width, height, color, fontSize, transform: `translate(${x}px, ${y}px)` }}>
{text}
</div>,
- bounds: { x: x!, y: y!, z, zIndex, width, height }
+ bounds: viewDef
};
} else if (viewDef.type === "div") {
const backgroundColor = Cast(viewDef.color, "string");
return [x, y].some(val => val === undefined) ? undefined :
{
- ele: <div className="collectionFreeform-customDiv" key={"div" + x + y + z}
+ ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z} onClick={e => this.onViewDefDivClick(e, viewDef)}
style={{ width, height, backgroundColor, transform: `translate(${x}px, ${y}px)` }} />,
- bounds: { x: x!, y: y!, z, zIndex, width, height }
+ bounds: viewDef
};
}
}
@@ -790,12 +794,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}.bind(this));
doTimelineLayout(poolData: Map<string, any>) {
- return computeTimelineLayout(poolData, this.props.Document, this.childDocs,
+ return computeTimelineLayout(poolData, this.props.Document, this.childDocs, this.filterDocs,
this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
}
doPivotLayout(poolData: Map<string, any>) {
- return computePivotLayout(poolData, this.props.Document, this.childDocs,
+ return computePivotLayout(poolData, this.props.Document, this.childDocs, this.filterDocs,
this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
}
@@ -808,7 +812,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
- state = pos.state === undefined ? state : pos.state;
poolData.set(pair.layout[Id], pos);
});
return { elements: elements };
@@ -816,12 +819,42 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed get doInternalLayoutComputation() {
const newPool = new Map<string, any>();
- switch (this.Document._freeformLayoutEngine) {
+ switch (this.props.layoutEngine?.()) {
case "timeline": return { newPool, computedElementData: this.doTimelineLayout(newPool) };
case "pivot": return { newPool, computedElementData: this.doPivotLayout(newPool) };
}
return { newPool, computedElementData: this.doFreeformLayout(newPool) };
}
+
+ @computed get filterDocs() {
+ const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []);
+ const clusters: { [key: string]: { [value: string]: string } } = {};
+ for (let i = 0; i < docFilters.length; i += 3) {
+ const [key, value, modifiers] = docFilters.slice(i, i + 3);
+ const cluster = clusters[key];
+ if (!cluster) {
+ const child: { [value: string]: string } = {};
+ child[value] = modifiers;
+ clusters[key] = child;
+ } else {
+ cluster[value] = modifiers;
+ }
+ }
+ const filteredDocs = docFilters.length ? this.childDocs.filter(d => {
+ for (const key of Object.keys(clusters)) {
+ const cluster = clusters[key];
+ const satisfiesFacet = Object.keys(cluster).some(inner => {
+ const modifier = cluster[inner];
+ return (modifier === "x") !== Doc.matchFieldValue(d, key, inner);
+ });
+ if (!satisfiesFacet) {
+ return false;
+ }
+ }
+ return true;
+ }) : this.childDocs;
+ return filteredDocs;
+ }
get doLayoutComputation() {
const { newPool, computedElementData } = this.doInternalLayoutComputation;
runInAction(() =>
@@ -839,7 +872,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)}
dataProvider={this.childDataProvider}
jitterRotation={NumCast(this.props.Document.jitterRotation)}
- fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine !== undefined} />,
+ fitToBox={this.props.fitToBox || this.props.layoutEngine !== undefined} />,
bounds: this.childDataProvider(pair.layout)
}));
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
index f57ba438a..0c74b8ddb 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -7,6 +7,7 @@
.document-wrapper {
display: flex;
flex-direction: column;
+ width: 100%;
.label-wrapper {
display: flex;
@@ -17,13 +18,13 @@
}
- .resizer {
+ .multiColumnResizer {
cursor: ew-resize;
transition: 0.5s opacity ease;
display: flex;
flex-direction: column;
- .internal {
+ .multiColumnResizer-hdl {
width: 100%;
height: 100%;
transition: 0.5s background-color ease;
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 65862f34f..7d8de0db4 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -1,19 +1,18 @@
+import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
-import { makeInterface } from '../../../../new_fields/Schema';
-import { documentSchema } from '../../../../new_fields/documentSchemas';
-import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
import * as React from "react";
import { Doc } from '../../../../new_fields/Doc';
-import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types';
+import { documentSchema } from '../../../../new_fields/documentSchemas';
+import { makeInterface } from '../../../../new_fields/Schema';
+import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../new_fields/Types';
+import { DragManager } from '../../../util/DragManager';
+import { Transform } from '../../../util/Transform';
+import { undoBatch } from '../../../util/UndoManager';
import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
-import { Utils } from '../../../../Utils';
+import { CollectionSubView } from '../CollectionSubView';
import "./collectionMulticolumnView.scss";
-import { computed, trace, observable, action } from 'mobx';
-import { Transform } from '../../../util/Transform';
-import WidthLabel from './MulticolumnWidthLabel';
import ResizeBar from './MulticolumnResizer';
-import { undoBatch } from '../../../util/UndoManager';
-import { DragManager } from '../../../util/DragManager';
+import WidthLabel from './MulticolumnWidthLabel';
type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
const MulticolumnDocument = makeInterface(documentSchema);
@@ -34,7 +33,7 @@ export const DimUnit = {
};
const resolvedUnits = Object.values(DimUnit);
-const resizerWidth = 4;
+const resizerWidth = 8;
@observer
export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
@@ -45,7 +44,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
*/
@computed
private get ratioDefinedDocs() {
- return this.childLayoutPairs.map(({ layout }) => layout).filter(({ dimUnit }) => StrCast(dimUnit) === DimUnit.Ratio);
+ return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio);
}
/**
@@ -60,9 +59,9 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
private get resolvedLayoutInformation(): LayoutData {
let starSum = 0;
const widthSpecifiers: WidthSpecifier[] = [];
- this.childLayoutPairs.map(({ layout: { dimUnit, dimMagnitude } }) => {
- const unit = StrCast(dimUnit);
- const magnitude = NumCast(dimMagnitude);
+ this.childLayoutPairs.map(pair => {
+ const unit = StrCast(pair.layout.dimUnit, "*");
+ const magnitude = NumCast(pair.layout.dimMagnitude, 1);
if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
(unit === DimUnit.Ratio) && (starSum += magnitude);
widthSpecifiers.push({ magnitude, unit });
@@ -82,9 +81,9 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
setTimeout(() => {
const { ratioDefinedDocs } = this;
if (this.childLayoutPairs.length) {
- const minimum = Math.min(...ratioDefinedDocs.map(({ dimMagnitude }) => NumCast(dimMagnitude)));
+ const minimum = Math.min(...ratioDefinedDocs.map(doc => NumCast(doc.dimMagnitude, 1)));
if (minimum !== 0) {
- ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude) / minimum);
+ ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum, 1);
}
}
});
@@ -119,7 +118,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
private get totalRatioAllocation(): number | undefined {
const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length;
if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
- return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1));
+ return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)) - 2 * NumCast(this.props.Document._xMargin);
}
}
@@ -160,8 +159,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
if (columnUnitLength === undefined) {
return 0; // we're still waiting on promises to resolve
}
- let width = NumCast(layout.dimMagnitude);
- if (StrCast(layout.dimUnit) === DimUnit.Ratio) {
+ let width = NumCast(layout.dimMagnitude, 1);
+ if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) {
width *= columnUnitLength;
}
return width;
@@ -203,6 +202,19 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
+ return <ContentFittingDocumentView
+ {...this.props}
+ Document={layout}
+ DataDocument={layout.resolvedDataDoc as Doc}
+ CollectionDoc={this.props.Document}
+ PanelWidth={width}
+ PanelHeight={height}
+ getTransform={dxf}
+ onClick={this.onChildClickHandler}
+ renderDepth={this.props.renderDepth + 1}
+ />
+ }
/**
* @returns the resolved list of rendered child documents, displayed
* at their resolved pixel widths, each separated by a resizer.
@@ -214,21 +226,14 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
const collector: JSX.Element[] = [];
for (let i = 0; i < childLayoutPairs.length; i++) {
const { layout } = childLayoutPairs[i];
+ const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin));
+ const width = () => this.lookupPixels(layout);
+ const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0);
collector.push(
- <div
- className={"document-wrapper"}
- key={Utils.GenerateGuid()}
- >
- <ContentFittingDocumentView
- {...this.props}
- Document={layout}
- DataDocument={layout.resolvedDataDoc as Doc}
- CollectionDoc={this.props.Document}
- PanelWidth={() => this.lookupPixels(layout)}
- PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)}
- getTransform={() => this.lookupIndividualTransform(layout)}
- onClick={this.onChildClickHandler}
- />
+ <div className={"document-wrapper"}
+ key={"wrapper" + i}
+ style={{ width: width() }} >
+ {this.getDisplayDoc(layout, dxf, width, height)}
<WidthLabel
layout={layout}
collectionDoc={Document}
@@ -236,7 +241,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
</div>,
<ResizeBar
width={resizerWidth}
- key={Utils.GenerateGuid()}
+ key={"resizer" + i}
columnUnitLength={this.getColumnUnitLength}
toLeft={layout}
toRight={childLayoutPairs[i + 1]?.layout}
@@ -249,7 +254,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
render(): JSX.Element {
return (
- <div className={"collectionMulticolumnView_contents"} ref={this.createDashEventsTarget}>
+ <div className={"collectionMulticolumnView_contents"}
+ style={{
+ marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin),
+ marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin)
+ }} ref={this.createDashEventsTarget}>
{this.contents}
</div>
);
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
index ef4b4a19c..64f607680 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
@@ -8,6 +8,7 @@
.document-wrapper {
display: flex;
flex-direction: row;
+ height: 100%;
.label-wrapper {
display: flex;
@@ -18,13 +19,13 @@
}
- .resizer {
+ .multiRowResizer {
cursor: ew-resize;
transition: 0.5s opacity ease;
display: flex;
flex-direction: row;
- .internal {
+ .multiRowResizer-hdl {
width: 100%;
height: 100%;
transition: 0.5s background-color ease;
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index aa440b677..ff7c4998f 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -34,7 +34,7 @@ export const DimUnit = {
};
const resolvedUnits = Object.values(DimUnit);
-const resizerHeight = 4;
+const resizerHeight = 8;
@observer
export class CollectionMultirowView extends CollectionSubView(MultirowDocument) {
@@ -45,7 +45,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
*/
@computed
private get ratioDefinedDocs() {
- return this.childLayoutPairs.map(({ layout }) => layout).filter(({ dimUnit }) => StrCast(dimUnit) === DimUnit.Ratio);
+ return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio);
}
/**
@@ -60,9 +60,9 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
private get resolvedLayoutInformation(): LayoutData {
let starSum = 0;
const heightSpecifiers: HeightSpecifier[] = [];
- this.childLayoutPairs.map(({ layout: { dimUnit, dimMagnitude } }) => {
- const unit = StrCast(dimUnit);
- const magnitude = NumCast(dimMagnitude);
+ this.childLayoutPairs.map(pair => {
+ const unit = StrCast(pair.layout.dimUnit, "*");
+ const magnitude = NumCast(pair.layout.dimMagnitude, 1);
if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
(unit === DimUnit.Ratio) && (starSum += magnitude);
heightSpecifiers.push({ magnitude, unit });
@@ -82,9 +82,9 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
setTimeout(() => {
const { ratioDefinedDocs } = this;
if (this.childLayoutPairs.length) {
- const minimum = Math.min(...ratioDefinedDocs.map(({ dimMagnitude }) => NumCast(dimMagnitude)));
+ const minimum = Math.min(...ratioDefinedDocs.map(layout => NumCast(layout.dimMagnitude, 1)));
if (minimum !== 0) {
- ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude) / minimum);
+ ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum);
}
}
});
@@ -119,7 +119,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
private get totalRatioAllocation(): number | undefined {
const layoutInfoLen = this.resolvedLayoutInformation.heightSpecifiers.length;
if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
- return this.props.PanelHeight() - (this.totalFixedAllocation + resizerHeight * (layoutInfoLen - 1));
+ return this.props.PanelHeight() - (this.totalFixedAllocation + resizerHeight * (layoutInfoLen - 1)) - 2 * NumCast(this.props.Document._yMargin);
}
}
@@ -160,8 +160,8 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
if (rowUnitLength === undefined) {
return 0; // we're still waiting on promises to resolve
}
- let height = NumCast(layout.dimMagnitude);
- if (StrCast(layout.dimUnit) === DimUnit.Ratio) {
+ let height = NumCast(layout.dimMagnitude, 1);
+ if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) {
height *= rowUnitLength;
}
return height;
@@ -203,6 +203,19 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
+ return <ContentFittingDocumentView
+ {...this.props}
+ Document={layout}
+ DataDocument={layout.resolvedDataDoc as Doc}
+ CollectionDoc={this.props.Document}
+ PanelWidth={width}
+ PanelHeight={height}
+ getTransform={dxf}
+ onClick={this.onChildClickHandler}
+ renderDepth={this.props.renderDepth + 1}
+ />
+ }
/**
* @returns the resolved list of rendered child documents, displayed
* at their resolved pixel widths, each separated by a resizer.
@@ -214,22 +227,15 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
const collector: JSX.Element[] = [];
for (let i = 0; i < childLayoutPairs.length; i++) {
const { layout } = childLayoutPairs[i];
+ const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin));
+ const height = () => this.lookupPixels(layout);
+ const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0);
collector.push(
<div
className={"document-wrapper"}
- key={Utils.GenerateGuid()}
+ key={"wrapper" + i}
>
- <ContentFittingDocumentView
- {...this.props}
- Document={layout}
- DataDocument={layout.resolvedDataDoc as Doc}
- CollectionDoc={this.props.Document}
- PanelHeight={() => this.lookupPixels(layout)}
- PanelWidth={() => PanelWidth() - (BoolCast(Document.showHeightLabels) ? 20 : 0)}
- getTransform={() => this.lookupIndividualTransform(layout)}
- onClick={this.onChildClickHandler}
- renderDepth={this.props.renderDepth + 1}
- />
+ {this.getDisplayDoc(layout, dxf, width, height)}
<HeightLabel
layout={layout}
collectionDoc={Document}
@@ -237,7 +243,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
</div>,
<ResizeBar
height={resizerHeight}
- key={Utils.GenerateGuid()}
+ key={"resizer" + i}
columnUnitLength={this.getRowUnitLength}
toTop={layout}
toBottom={childLayoutPairs[i + 1]?.layout}
@@ -250,7 +256,11 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
render(): JSX.Element {
return (
- <div className={"collectionMultirowView_contents"} ref={this.createDashEventsTarget}>
+ <div className={"collectionMultirowView_contents"}
+ style={{
+ marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin),
+ marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin)
+ }} ref={this.createDashEventsTarget}>
{this.contents}
</div>
);
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 46c39d817..6b89402e6 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -46,14 +46,12 @@ export default class ResizeBar extends React.Component<ResizerProps> {
const unitLength = columnUnitLength();
if (unitLength) {
if (toNarrow) {
- const { dimUnit, dimMagnitude } = toNarrow;
- const scale = dimUnit === DimUnit.Ratio ? unitLength : 1;
- toNarrow.dimMagnitude = NumCast(dimMagnitude) - Math.abs(movementX) / scale;
+ const scale = StrCast(toNarrow.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ toNarrow.dimMagnitude = Math.max(0.05, NumCast(toNarrow.dimMagnitude, 1) - Math.abs(movementX) / scale);
}
if (this.resizeMode === ResizeMode.Pinned && toWiden) {
- const { dimUnit, dimMagnitude } = toWiden;
- const scale = dimUnit === DimUnit.Ratio ? unitLength : 1;
- toWiden.dimMagnitude = NumCast(dimMagnitude) + Math.abs(movementX) / scale;
+ const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementX) / scale);
}
}
}
@@ -61,17 +59,17 @@ export default class ResizeBar extends React.Component<ResizerProps> {
private get isActivated() {
const { toLeft, toRight } = this.props;
if (toLeft && toRight) {
- if (StrCast(toLeft.dimUnit) === DimUnit.Pixel && StrCast(toRight.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel && StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toLeft) {
- if (StrCast(toLeft.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toRight) {
- if (StrCast(toRight.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
@@ -91,7 +89,7 @@ export default class ResizeBar extends React.Component<ResizerProps> {
render() {
return (
<div
- className={"resizer"}
+ className={"multiColumnResizer"}
style={{
width: this.props.width,
opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0
@@ -100,12 +98,12 @@ export default class ResizeBar extends React.Component<ResizerProps> {
onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}
>
<div
- className={"internal"}
+ className={"multiColumnResizer-hdl"}
onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)}
style={{ backgroundColor: this.resizeMode }}
/>
<div
- className={"internal"}
+ className={"multiColumnResizer-hdl"}
onPointerDown={e => this.registerResizing(e, ResizeMode.Global)}
style={{ backgroundColor: this.resizeMode }}
/>
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 4f58f3fa8..d00939b26 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -46,14 +46,12 @@ export default class ResizeBar extends React.Component<ResizerProps> {
const unitLength = columnUnitLength();
if (unitLength) {
if (toNarrow) {
- const { dimUnit, dimMagnitude } = toNarrow;
- const scale = dimUnit === DimUnit.Ratio ? unitLength : 1;
- toNarrow.dimMagnitude = NumCast(dimMagnitude) - Math.abs(movementY) / scale;
+ const scale = StrCast(toNarrow.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ toNarrow.dimMagnitude = Math.max(0.05, NumCast(toNarrow.dimMagnitude, 1) - Math.abs(movementY) / scale);
}
if (this.resizeMode === ResizeMode.Pinned && toWiden) {
- const { dimUnit, dimMagnitude } = toWiden;
- const scale = dimUnit === DimUnit.Ratio ? unitLength : 1;
- toWiden.dimMagnitude = NumCast(dimMagnitude) + Math.abs(movementY) / scale;
+ const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementY) / scale);
}
}
}
@@ -61,17 +59,17 @@ export default class ResizeBar extends React.Component<ResizerProps> {
private get isActivated() {
const { toTop, toBottom } = this.props;
if (toTop && toBottom) {
- if (StrCast(toTop.dimUnit) === DimUnit.Pixel && StrCast(toBottom.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel && StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toTop) {
- if (StrCast(toTop.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toBottom) {
- if (StrCast(toBottom.dimUnit) === DimUnit.Pixel) {
+ if (StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
@@ -91,7 +89,7 @@ export default class ResizeBar extends React.Component<ResizerProps> {
render() {
return (
<div
- className={"resizer"}
+ className={"multiRowResizer"}
style={{
height: this.props.height,
opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0
@@ -100,12 +98,12 @@ export default class ResizeBar extends React.Component<ResizerProps> {
onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}
>
<div
- className={"internal"}
+ className={"multiRowResizer-hdl"}
onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)}
style={{ backgroundColor: this.resizeMode }}
/>
<div
- className={"internal"}
+ className={"multiRowResizer-hdl"}
onPointerDown={e => this.registerResizing(e, ResizeMode.Global)}
style={{ backgroundColor: this.resizeMode }}
/>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2a11267d4..3bceec45f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -88,6 +88,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
width: this.width,
height: this.height,
zIndex: this.ZInd,
+ display: this.ZInd === -99 ? "none" : undefined,
+ pointerEvents: this.props.Document.isBackground ? "none" : undefined
}} >
{!this.props.fitToBox ? <DocumentView {...this.props}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 0f517197a..dcdce5fce 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -50,6 +50,7 @@ import { RadialMenuProps } from './RadialMenuItem';
import { CollectionStackingView } from '../collections/CollectionStackingView';
import { RichTextField } from '../../../new_fields/RichTextField';
+import { HistoryUtil } from '../../util/History';
library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
@@ -110,7 +111,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get topMost() { return this.props.renderDepth === 0; }
@computed get nativeWidth() { return this.layoutDoc._nativeWidth || 0; }
@computed get nativeHeight() { return this.layoutDoc._nativeHeight || 0; }
- @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
+ @computed get onClickHandler() { return this.props.onClick || this.layoutDoc.onClick || this.Document.onClick; }
@computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; }
@computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; }
@@ -209,6 +210,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
dragData.moveDocument = this.props.moveDocument;// this.Document.onDragStart ? undefined : this.props.moveDocument;
dragData.applyAsTemplate = applyAsTemplate;
dragData.dragDivName = this.props.dragDivName;
+ this.props.Document.sourceContext = this.props.ContainingCollectionDoc; // bcz: !! shouldn't need this ... use search find the document's context dynamically
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.Document.onDragStart });
}
}
@@ -253,7 +255,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
- onClick = async (e: React.MouseEvent | React.PointerEvent) => {
+ onClick = undoBatch((e: React.MouseEvent | React.PointerEvent) => {
if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
e.stopPropagation();
@@ -267,7 +269,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
SelectionManager.DeselectAll();
Doc.UnBrushDoc(this.props.Document);
} else if (this.onClickHandler && this.onClickHandler.script) {
- this.onClickHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, containingCollection: this.props.ContainingCollectionDoc }, console.log);
+ this.onClickHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, containingCollection: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey }, console.log);
} else if (this.Document.type === DocumentType.BUTTON) {
ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
} else if (this.props.Document.isButton === "Selector") { // this should be moved to an OnClick script
@@ -282,7 +284,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
preventDefault && e.preventDefault();
}
- }
+ })
buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
const maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs);
@@ -346,7 +348,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) {
if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) {
this.cleanUpInteractions();
- this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+ this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
}
}
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -454,7 +456,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
return;
}
- if (!e.nativeEvent.cancelBubble || this.Document.onClick || this.Document.onDragStart) {
+ if (!e.nativeEvent.cancelBubble || this.onClickHandler || this.Document.onDragStart) {
this._downX = e.clientX;
this._downY = e.clientY;
this._hitTemplateDrag = false;
@@ -465,7 +467,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._hitTemplateDrag = true;
}
}
- if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
+ if ((this.active || this.Document.onDragStart || this.onClickHandler) &&
+ !e.ctrlKey &&
+ (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
+ !this.Document.lockedPosition &&
+ !this.Document.inOverlay) {
+ e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
+ }
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -482,12 +490,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (e.cancelBubble && this.active) {
document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
}
- else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
+ else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.onClickHandler) && !this.Document.lockedPosition && !this.Document.inOverlay) {
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
+ if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+ this.startDragging(this._downX, this._downY, this.props.ContainingCollectionDoc?.childDropAction ? this.props.ContainingCollectionDoc?.childDropAction : this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
}
}
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -517,13 +525,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@undoBatch
- deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
+ deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument?.(this.props.Document); }
- static makeNativeViewClicked = (doc: Doc, prevLayout: string) => {
- undoBatch(() => {
- if (StrCast(doc.title).endsWith("_" + prevLayout)) doc.title = StrCast(doc.title).replace("_" + prevLayout, "");
- doc.layoutKey = "layout";
- })();
+ static makeNativeViewClicked = (doc: Doc) => {
+ undoBatch(() => Doc.setNativeView(doc))();
}
static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>, creator: (documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc, name: string = "custom", docLayoutTemplate?: Doc) => {
@@ -533,7 +538,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (doc[customName] === undefined) {
const _width = NumCast(doc._width);
const _height = NumCast(doc._height);
- const options = { title: "data", _width, x: -_width / 2, y: - _height / 2, };
+ const options = { title: "data", _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
const field = doc.data;
let fieldTemplate: Opt<Doc>;
@@ -599,7 +604,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
`Link from ${StrCast(de.complete.annoDragData.annotationDocument.title)}`);
}
if (de.complete.docDragData && de.complete.docDragData.applyAsTemplate) {
- Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom");
+ Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom", undefined);
e.stopPropagation();
}
if (de.complete.linkDragData) {
@@ -611,18 +616,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
- @action
- onDrop = (e: React.DragEvent) => {
- const text = e.dataTransfer.getData("text/plain");
- if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
- const oldLayout = this.Document.layout || "";
- const layout = text.replace("{layout}", oldLayout);
- this.Document.layout = layout;
- e.stopPropagation();
- e.preventDefault();
- }
- }
-
@undoBatch
@action
freezeNativeDimensions = (): void => {
@@ -652,21 +645,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@action
setCustomView =
(custom: boolean, layout: string): void => {
- if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) {
- Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document);
- } else if (custom) {
- DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]);
-
- let foundLayout: Opt<Doc> = undefined;
- DocListCast(Cast(CurrentUserUtils.UserDocument.expandingButtons, Doc, null)?.data)?.map(btnDoc => {
- if (StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title) === layout) {
- foundLayout = btnDoc.dragFactory as Doc;
- }
- })
+ // if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) {
+ // Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document);
+ // } else
+ if (custom) {
+ DocumentView.makeNativeViewClicked(this.props.Document);
+
+ let foundLayout: Opt<Doc>;
+ DocListCast(Cast(Doc.UserDoc().expandingButtons, Doc, null)?.data)?.concat([Cast(Doc.UserDoc().iconView, Doc, null)]).
+ map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc).forEach(tempDoc => {
+ if (StrCast(tempDoc.title) === layout) {
+ foundLayout = tempDoc;
+ }
+ })
DocumentView.
makeCustomViewClicked(this.props.Document, this.props.DataDoc, Docs.Create.StackingDocument, layout, foundLayout);
} else {
- DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]);
+ DocumentView.makeNativeViewClicked(this.props.Document);
}
}
@@ -903,7 +898,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const showTitle = StrCast(this.getLayoutPropStr("showTitle"));
const showTitleHover = StrCast(this.getLayoutPropStr("showTitleHover"));
const showCaption = this.getLayoutPropStr("showCaption");
- const showTextTitle = showTitle && StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined;
+ const showTextTitle = showTitle && (StrCast(this.layoutDoc.layout).indexOf("PresBox") !== -1 || StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1) ? showTitle : undefined;
const searchHighlight = (!this.Document.searchFields ? (null) :
<div className="documentView-searchHighlight">
{this.Document.searchFields}
@@ -974,7 +969,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear;
highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
- onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
+ onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
style={{
transition: this.Document.isAnimating ? ".5s linear" : StrCast(this.Document.transition),
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 969df7c5d..9370d3745 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1101,7 +1101,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
<div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} ref={this._scrollRef}>
<div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
</div>
- {this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
+ {!this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
<div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
<div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 43f4a0ba9..7bbf4a368 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -8,7 +8,7 @@
transform-origin: top left;
.imageBox-fader {
- pointer-events: all;
+ pointer-events: inherit;
}
}
@@ -34,13 +34,12 @@
height: 100%;
max-width: 100%;
max-height: 100%;
- pointer-events: none;
+ pointer-events: inherit;
background: transparent;
img {
height: auto;
width: 100%;
- pointer-events: all;
}
}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 66b0e5a15..1951fcc1a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -416,7 +416,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
style={{
transform: `scale(${this.props.ContentScaling()})`,
width: `${100 / this.props.ContentScaling()}%`,
- height: `${100 / this.props.ContentScaling()}%`
+ height: `${100 / this.props.ContentScaling()}%`,
+ pointerEvents: this.props.Document.isBackground ? "none" : undefined
}} >
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss
index 01e7f4834..7618aa7e3 100644
--- a/src/client/views/nodes/PresBox.scss
+++ b/src/client/views/nodes/PresBox.scss
@@ -6,7 +6,7 @@
top: 0;
bottom: 0;
width: 100%;
- min-width: 100px;
+ min-width: 120px;
height: 100%;
min-height: 50px;
letter-spacing: 2px;
@@ -14,10 +14,6 @@
transition: 0.7s opacity ease;
pointer-events: all;
- .presBox-listCont {
- position: relative;
- }
-
.presBox-buttons {
padding: 10px;
width: 100%;
@@ -28,4 +24,17 @@
border-radius: 5px;
}
}
+ .presBox-backward, .presBox-forward {
+ width: 25px;
+ border-radius: 5px;
+ top:50%;
+ position: absolute;
+ display: inline-block;
+ }
+ .presBox-backward {
+ left:5;
+ }
+ .presBox-forward {
+ right:5;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 06d8e688b..6c4cbba12 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -2,11 +2,10 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { listSpec } from "../../../new_fields/Schema";
-import { ComputedField } from "../../../new_fields/ScriptField";
+import { listSpec, makeInterface } from "../../../new_fields/Schema";
import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { Docs } from "../../documents/Documents";
@@ -17,7 +16,13 @@ import { CollectionView, CollectionViewType } from "../collections/CollectionVie
import { ContextMenu } from "../ContextMenu";
import { FieldView, FieldViewProps } from './FieldView';
import "./PresBox.scss";
-import { presSchema } from "../presentationview/PresElementBox";
+import { CollectionCarouselView } from "../collections/CollectionCarouselView";
+import { returnFalse } from "../../../Utils";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { CollectionTimeView } from "../collections/CollectionTimeView";
+import { documentSchema } from "../../../new_fields/documentSchemas";
+import { InkingControl } from "../InkingControl";
+import { InkTool } from "../../../new_fields/InkField";
library.add(faArrowLeft);
library.add(faArrowRight);
@@ -31,24 +36,41 @@ library.add(faEdit);
@observer
export class PresBox extends React.Component<FieldViewProps> {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
+ _childReaction: IReactionDisposer | undefined;
+ _slideshowReaction: IReactionDisposer | undefined;
+ @observable _isChildActive = false;
+
componentDidMount() {
const userDoc = CurrentUserUtils.UserDocument;
- let presTemp = Cast(userDoc.presentationTemplate, Doc);
- if (presTemp instanceof Promise) {
- presTemp.then(presTemp => this.props.Document.childLayout = presTemp);
- }
- else if (presTemp === undefined) {
- presTemp = userDoc.presentationTemplate = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent", _xMargin: 5, isTemplateDoc: true, isTemplateForField: "data" });
- }
- else {
- this.props.Document.childLayout = presTemp;
- }
+ this._slideshowReaction = reaction(() => this.props.Document._slideshow,
+ (slideshow) => {
+ if (!slideshow) {
+ let presTemp = Cast(userDoc.presentationTemplate, Doc);
+ if (presTemp instanceof Promise) {
+ presTemp.then(presTemp => this.props.Document.childLayout = presTemp);
+ }
+ else if (presTemp === undefined) {
+ presTemp = userDoc.presentationTemplate = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent", _xMargin: 5, isTemplateDoc: true, isTemplateForField: "data" });
+ }
+ else {
+ this.props.Document.childLayout = presTemp;
+ }
+ } else {
+ this.props.Document.childLayout = undefined;
+ }
+ }, { fireImmediately: true });
+ this._childReaction = reaction(() => this.childDocs.slice(), (children) => children.forEach((child, i) => child.presentationIndex = i), { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ this._childReaction?.();
+ this._slideshowReaction?.();
}
@computed get childDocs() { return DocListCast(this.props.Document[this.props.fieldKey]); }
next = async () => {
- const current = NumCast(this.props.Document.selectedDoc);
+ runInAction(() => Doc.UserDoc().curPresentation = this.props.Document);
+ const current = NumCast(this.props.Document._itemIndex);
//asking to get document at current index
const docAtCurrentNext = await this.getDocAtIndex(current + 1);
if (docAtCurrentNext !== undefined) {
@@ -65,7 +87,8 @@ export class PresBox extends React.Component<FieldViewProps> {
}
}
back = async () => {
- const current = NumCast(this.props.Document.selectedDoc);
+ action(() => Doc.UserDoc().curPresentation = this.props.Document);
+ const current = NumCast(this.props.Document._itemIndex);
//requesting for the doc at current index
const docAtCurrent = await this.getDocAtIndex(current);
if (docAtCurrent !== undefined) {
@@ -104,12 +127,17 @@ export class PresBox extends React.Component<FieldViewProps> {
}
}
+ whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
+ active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
+ (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
+
/**
* This is the method that checks for the actions that need to be performed
* after the document has been presented, which involves 3 button options:
* Hide Until Presented, Hide After Presented, Fade After Presented
*/
showAfterPresented = (index: number) => {
+ action(() => Doc.UserDoc().curPresentation = this.props.Document);
this.childDocs.forEach((doc, ind) => {
//the order of cases is aligned based on priority
if (doc.hideTillShownButton && ind <= index) {
@@ -130,6 +158,7 @@ export class PresBox extends React.Component<FieldViewProps> {
* Hide Until Presented, Hide After Presented, Fade After Presented
*/
hideIfNotPresented = (index: number) => {
+ action(() => Doc.UserDoc().curPresentation = this.props.Document);
this.childDocs.forEach((key, ind) => {
//the order of cases is aligned based on priority
@@ -151,6 +180,7 @@ export class PresBox extends React.Component<FieldViewProps> {
* te option open, navigates to that element.
*/
navigateToElement = async (curDoc: Doc, fromDocIndex: number) => {
+ action(() => Doc.UserDoc().curPresentation = this.props.Document);
const fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc;
let docToJump = curDoc;
let willZoom = false;
@@ -177,15 +207,17 @@ export class PresBox extends React.Component<FieldViewProps> {
});
//docToJump stayed same meaning, it was not in the group or was the last element in the group
+ const aliasOf = await Cast(docToJump.aliasOf, Doc);
+ const srcContext = aliasOf && await Cast(aliasOf.sourceContext, Doc);
if (docToJump === curDoc) {
//checking if curDoc has navigation open
- const target = await curDoc.presentationTargetDoc as Doc;
- if (curDoc.navButton) {
- DocumentManager.Instance.jumpToDocument(target, false);
- } else if (curDoc.showButton) {
+ const target = await Cast(curDoc.presentationTargetDoc, Doc);
+ if (curDoc.navButton && target) {
+ DocumentManager.Instance.jumpToDocument(target, false, undefined, srcContext);
+ } else if (curDoc.showButton && target) {
const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
//awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(target, true);
+ await DocumentManager.Instance.jumpToDocument(target, true, undefined, srcContext);
curDoc.viewScale = DocumentManager.Instance.getScaleOfDocView(target);
//saving the scale user was on before zooming in
@@ -199,7 +231,8 @@ export class PresBox extends React.Component<FieldViewProps> {
const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
//awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(await docToJump.presentationTargetDoc as Doc, willZoom);
+ const presTargetDoc = await docToJump.presentationTargetDoc as Doc;
+ await DocumentManager.Instance.jumpToDocument(presTargetDoc, willZoom, undefined, srcContext);
const newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc);
curDoc.viewScale = newScale;
//saving the scale that user was on
@@ -215,7 +248,7 @@ export class PresBox extends React.Component<FieldViewProps> {
getDocAtIndex = async (index: number) => {
const list = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
if (list && index >= 0 && index < list.length) {
- this.props.Document.selectedDoc = index;
+ this.props.Document._itemIndex = index;
//awaiting async call to finish to get Doc instance
return list[index];
}
@@ -238,12 +271,12 @@ export class PresBox extends React.Component<FieldViewProps> {
//The function that is called when a document is clicked or reached through next or back.
//it'll also execute the necessary actions if presentation is playing.
- @action
public gotoDocument = async (index: number, fromDoc: number) => {
+ action(() => Doc.UserDoc().curPresentation = this.props.Document);
Doc.UnBrushAllDocs();
const list = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
if (list && index >= 0 && index < list.length) {
- this.props.Document.selectedDoc = index;
+ this.props.Document._itemIndex = index;
if (!this.props.Document.presStatus) {
this.props.Document.presStatus = true;
@@ -260,14 +293,14 @@ export class PresBox extends React.Component<FieldViewProps> {
}
//The function that starts or resets presentaton functionally, depending on status flag.
- @action
startOrResetPres = () => {
+ action(() => Doc.UserDoc().curPresentation = this.props.Document);
if (this.props.Document.presStatus) {
this.resetPresentation();
} else {
this.props.Document.presStatus = true;
this.startPresentation(0);
- this.gotoDocument(0, NumCast(this.props.Document.selectedDoc));
+ this.gotoDocument(0, NumCast(this.props.Document._itemIndex));
}
}
@@ -280,13 +313,13 @@ export class PresBox extends React.Component<FieldViewProps> {
//The function that resets the presentation by removing every action done by it. It also
//stops the presentaton.
- @action
resetPresentation = () => {
+ action(() => Doc.UserDoc().curPresentation = this.props.Document);
this.childDocs.forEach((doc: Doc) => {
doc.opacity = 1;
doc.viewScale = 1;
});
- this.props.Document.selectedDoc = 0;
+ this.props.Document._itemIndex = 0;
this.props.Document.presStatus = false;
if (this.childDocs.length !== 0) {
DocumentManager.Instance.zoomIntoScale(this.childDocs[0], 1);
@@ -296,6 +329,7 @@ export class PresBox extends React.Component<FieldViewProps> {
//The function that starts the presentation, also checking if actions should be applied
//directly at start.
startPresentation = (startIndex: number) => {
+ action(() => Doc.UserDoc().curPresentation = this.props.Document);
this.childDocs.map(doc => {
if (doc.hideTillShownButton && this.childDocs.indexOf(doc) > startIndex) {
doc.opacity = 0;
@@ -323,14 +357,17 @@ export class PresBox extends React.Component<FieldViewProps> {
}));
specificContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: "Make Current Presentation", event: action(() => Doc.UserDoc().curPresentation = this.props.Document), icon: "asterisk" });
+ const funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Show as Slideshow", event: action(() => this.props.Document._slideshow = "slideshow"), icon: "asterisk" });
+ funcs.push({ description: "Show as Timeline", event: action(() => this.props.Document._slideshow = "timeline"), icon: "asterisk" });
+ funcs.push({ description: "Show as List", event: action(() => this.props.Document._slideshow = undefined), icon: "asterisk" });
+ ContextMenu.Instance.addItem({ description: "Presentation Funcs...", subitems: funcs, icon: "asterisk" });
}
/**
* Initially every document starts with a viewScale 1, which means
* that they will be displayed in a canvas with scale 1.
*/
- @action
initializeScaleViews = (docList: Doc[], viewtype: number) => {
this.props.Document._chromeStatus = "disabled";
const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 46;
@@ -346,19 +383,37 @@ export class PresBox extends React.Component<FieldViewProps> {
});
}
-
selectElement = (doc: Doc) => {
const index = DocListCast(this.props.Document[this.props.fieldKey]).indexOf(doc);
- index !== -1 && this.gotoDocument(index, NumCast(this.props.Document.selectedDoc));
+ index !== -1 && this.gotoDocument(index, NumCast(this.props.Document._itemIndex));
}
getTransform = () => {
return this.props.ScreenToLocalTransform().translate(0, -50);// listBox padding-left and pres-box-cont minHeight
}
+ panelHeight = () => {
+ return this.props.PanelHeight() - 20;
+ }
render() {
this.initializeScaleViews(this.childDocs, NumCast(this.props.Document._viewType));
- return (
- <div className="presBox-cont" onContextMenu={this.specificContextMenu}>
+ return (this.props.Document._slideshow ?
+ <div className="presBox-cont" onContextMenu={this.specificContextMenu} style={{ pointerEvents: this.active() ? "all" : "none" }} >
+ {this.props.Document.inOverlay ? (null) :
+ <div className="presBox-listCont" >
+ {this.props.Document._slideshow === "slideshow" ?
+ <CollectionCarouselView {...this.props} PanelHeight={this.panelHeight} chromeCollapsed={true} annotationsKey={""} CollectionView={undefined}
+ moveDocument={returnFalse}
+ addDocument={this.addDocument} removeDocument={returnFalse} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
+ :
+ <CollectionTimeView {...this.props} PanelHeight={this.panelHeight} chromeCollapsed={true} annotationsKey={""} CollectionView={undefined}
+ moveDocument={returnFalse}
+ addDocument={this.addDocument} removeDocument={returnFalse} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
+ }
+ </div>}
+ <button className="presBox-backward" title="Back" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+ <button className="presBox-forward" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ </div>
+ : <div className="presBox-cont" onContextMenu={this.specificContextMenu}>
<div className="presBox-buttons">
<button className="presBox-button" title="Back" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
<button className="presBox-button" title={"Reset Presentation" + this.props.Document.presStatus ? "" : " From Start"} onClick={this.startOrResetPres}>
@@ -366,13 +421,10 @@ export class PresBox extends React.Component<FieldViewProps> {
</button>
<button className="presBox-button" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
<button className="presBox-button" title={this.props.Document.inOverlay ? "Expand" : "Minimize"} onClick={this.toggleMinimize}><FontAwesomeIcon icon={"eye"} /></button>
- </div>
- {this.props.Document.inOverlay ? (null) :
- <div className="presBox-listCont" >
- <CollectionView {...this.props} addDocument={this.addDocument} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
- </div>
- }
- </div>
- );
+ {this.props.Document.inOverlay ? (null) :
+ <div className="presBox-listCont">
+ <CollectionView {...this.props} whenActiveChanged={this.whenActiveChanged} PanelHeight={this.panelHeight} addDocument={this.addDocument} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
+ </div>}
+ </div></div>);
}
} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index dcc5247d5..52773d466 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -50,11 +50,11 @@ export class PresElementBox extends DocExtendableComponent<FieldViewProps, PresD
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
_heightDisposer: IReactionDisposer | undefined;
- @computed get indexInPres() { return this.originalLayout?.presBoxKey ? DocListCast(this.presentationDoc[StrCast(this.originalLayout?.presBoxKey)]).indexOf(this.originalLayout) : 0; }
+ @computed get indexInPres() { return NumCast(this.originalLayout?.presentationIndex); }
@computed get presentationDoc() { return Cast(this.originalLayout?.presBox, Doc) as Doc; }
@computed get originalLayout() { return this.props.Document.expandedTemplate as Doc; }
@computed get targetDoc() { return this.originalLayout?.presentationTargetDoc as Doc; }
- @computed get currentIndex() { return NumCast(this.presentationDoc?.selectedDoc); }
+ @computed get currentIndex() { return NumCast(this.presentationDoc?._itemIndex); }
componentDidMount() {
this._heightDisposer = reaction(() => [this.originalLayout.embedOpen, this.originalLayout.collapsedHeight],
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 7033b23f1..69717e612 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -17,6 +17,7 @@ import { intersectRect } from "../Utils";
import { UndoManager } from "../client/util/UndoManager";
import { computedFn } from "mobx-utils";
import { RichTextField } from "./RichTextField";
+import { Script } from "vm";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -439,6 +440,7 @@ export namespace Doc {
if (layout instanceof Doc && layout !== alias && layout === Doc.Layout(alias)) {
Doc.SetLayout(alias, Doc.MakeAlias(layout));
}
+ alias.aliasOf = doc;
alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`);
return alias;
}
@@ -449,7 +451,7 @@ export namespace Doc {
// the lyouatDoc's layout is layout string (not a document)
//
export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
- return layoutDoc.isTemplateForField && dataDoc && layoutDoc !== dataDoc && !(Doc.LayoutField(layoutDoc) instanceof Doc);
+ return (layoutDoc.isTemplateForField || layoutDoc.isTemplateDoc) && dataDoc && layoutDoc !== dataDoc && !(Doc.LayoutField(layoutDoc) instanceof Doc);
}
//
@@ -466,7 +468,8 @@ export namespace Doc {
// If it doesn't find the expanded layout, then it makes a delegate of the template layout and
// saves it on the data doc indexed by the template layout's id.
//
- const expandedLayoutFieldKey = templateField + "-layout[" + templateLayoutDoc[Id] + "]";
+ const layoutFielddKey = Doc.LayoutFieldKey(templateLayoutDoc);
+ const expandedLayoutFieldKey = (templateField || layoutFielddKey) + "-layout[" + templateLayoutDoc[Id] + "]";
let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
@@ -493,7 +496,7 @@ export namespace Doc {
// if the childDoc is a template for a field, then this will return the expanded layout with its data doc.
// otherwise, it just returns the childDoc
export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) {
- const resolvedDataDoc = containerDataDoc === containerDoc || !containerDataDoc ? undefined : containerDataDoc;
+ const resolvedDataDoc = containerDataDoc === containerDoc || !containerDataDoc || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc;
return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
}
export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
@@ -553,7 +556,7 @@ export namespace Doc {
} else if (cfield instanceof ComputedField) {
copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
} else if (field instanceof ObjectField) {
- copy[key] = ObjectField.MakeCopy(field);
+ copy[key] = key.includes("layout[") && doc[key] instanceof Doc ? Doc.MakeCopy(doc[key] as Doc, false) : ObjectField.MakeCopy(field);
} else if (field instanceof Promise) {
debugger; //This shouldn't happend...
} else {
@@ -580,7 +583,7 @@ export namespace Doc {
let _applyCount: number = 0;
export function ApplyTemplate(templateDoc: Doc) {
if (templateDoc) {
- const applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), "layoutCustom", templateDoc.title + "(..." + _applyCount++ + ")");
+ const applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), "layout", templateDoc.title + "(..." + _applyCount++ + ")");
applied && (Doc.GetProto(applied).layout = applied.layout);
return applied;
}
@@ -625,7 +628,7 @@ export namespace Doc {
Cast(templateField.data, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc));
(Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateField.data));
}
- if (templateField.data instanceof RichTextField && templateField.data.Text) {
+ if (templateField.data instanceof RichTextField && (templateField.data.Text || templateField.data.Data.toString().includes("dashField"))) {
templateField._textTemplate = ComputedField.MakeFunction(`copyField(this.${metadataFieldKey})`, { this: Doc.name });
}
@@ -792,6 +795,29 @@ export namespace Doc {
const fieldStr = Field.toString(fieldVal as Field);
return fieldStr === value;
}
+
+ export function setNativeView(doc: any) {
+ const prevLayout = StrCast(doc.layoutKey).split("_")[1];
+ const deiconify = prevLayout === "icon" && StrCast(doc.deiconifyLayout) ? "layout_" + StrCast(doc.deiconifyLayout) : "";
+ doc.deiconifyLayout = undefined;
+ if (StrCast(doc.title).endsWith("_" + prevLayout)) doc.title = StrCast(doc.title).replace("_" + prevLayout, "");
+ doc.layoutKey = deiconify || "layout";
+ }
+ export function setDocFilter(container: Doc, key: string, value: any, modifiers?: string) {
+ const docFilters = Cast(container._docFilter, listSpec("string"), []);
+ for (let i = 0; i < docFilters.length; i += 3) {
+ if (docFilters[i] === key && docFilters[i + 1] === value) {
+ docFilters.splice(i, 3);
+ break;
+ }
+ }
+ if (modifiers !== undefined) {
+ docFilters.push(key);
+ docFilters.push(value);
+ docFilters.push(modifiers);
+ container._docFilter = new List<string>(docFilters);
+ }
+ }
}
Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
@@ -804,25 +830,16 @@ Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy
Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); });
Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); });
+Scripting.addGlobal(function setNativeView(doc: any) { Doc.setNativeView(doc); });
Scripting.addGlobal(function undo() { return UndoManager.Undo(); });
Scripting.addGlobal(function redo() { return UndoManager.Redo(); });
+Scripting.addGlobal(function curPresentationItem() {
+ const curPres = Doc.UserDoc().curPresentation as Doc;
+ return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)];
+})
Scripting.addGlobal(function selectDoc(doc: any) { Doc.UserDoc().SelectedDocs = new List([doc]); });
Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) {
const docs = DocListCast(Doc.UserDoc().SelectedDocs).filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCUMENT && d.type !== DocumentType.KVP && (!excludeCollections || !Cast(d.data, listSpec(Doc), null)));
return docs.length ? new List(docs) : prevValue;
});
-Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers?: string) {
- const docFilters = Cast(container._docFilter, listSpec("string"), []);
- for (let i = 0; i < docFilters.length; i += 3) {
- if (docFilters[i] === key && docFilters[i + 1] === value) {
- docFilters.splice(i, 3);
- break;
- }
- }
- if (modifiers !== undefined) {
- docFilters.push(key);
- docFilters.push(value);
- docFilters.push(modifiers);
- container._docFilter = new List<string>(docFilters);
- }
-}); \ No newline at end of file
+Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers?: string) { Doc.setDocFilter(container, key, value, modifiers); }); \ No newline at end of file
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
index f8a8d1226..4c78ea3aa 100644
--- a/src/new_fields/ScriptField.ts
+++ b/src/new_fields/ScriptField.ts
@@ -114,8 +114,8 @@ export class ScriptField extends ObjectField {
});
return compiled;
}
- public static MakeFunction(script: string, params: object = {}) {
- const compiled = ScriptField.CompileScript(script, params, true);
+ public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) {
+ const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
return compiled.compiled ? new ScriptField(compiled) : undefined;
}
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 0b3bd6338..cb35c0681 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -8,13 +8,15 @@ export const documentSchema = createSchema({
layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
layout_custom: Doc, // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey')
title: "string", // document title (can be on either data document or layout)
- _dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy")
+ dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy")
+ childDropAction: "string", // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
_nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
_nativeHeight: "number", // "
_width: "number", // width of document in its container's coordinate system
_height: "number", // "
_freeformLayoutEngine: "string",// the string ID for the layout engine to use to layout freeform view documents
_LODdisable: "boolean", // whether to disbale LOD switching for CollectionFreeFormViews
+ _pivotField: "string", // specifies which field should be used as the timeline/pivot axis
color: "string", // foreground color of document
backgroundColor: "string", // background color of document
opacity: "number", // opacity of document
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 1641ecc8a..26c10525e 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -101,7 +101,7 @@ export function makeEditable() {
}
let layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
- "LODdisable", "dropAction", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
+ "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 (typeof prop === "string" && prop !== "__id" && prop !== "__LAYOUT__" && prop !== "__fields" &&
diff --git a/src/scraping/buxton/.idea/buxton.iml b/src/scraping/buxton/.idea/buxton.iml
new file mode 100644
index 000000000..d0876a78d
--- /dev/null
+++ b/src/scraping/buxton/.idea/buxton.iml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+ <component name="NewModuleRootManager">
+ <content url="file://$MODULE_DIR$" />
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/inspectionProfiles/profiles_settings.xml b/src/scraping/buxton/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 000000000..105ce2da2
--- /dev/null
+++ b/src/scraping/buxton/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+ <settings>
+ <option name="USE_PROJECT_PROFILE" value="false" />
+ <version value="1.0" />
+ </settings>
+</component> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/misc.xml b/src/scraping/buxton/.idea/misc.xml
new file mode 100644
index 000000000..a2e120dcc
--- /dev/null
+++ b/src/scraping/buxton/.idea/misc.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
+</project> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/modules.xml b/src/scraping/buxton/.idea/modules.xml
new file mode 100644
index 000000000..5bbca8f01
--- /dev/null
+++ b/src/scraping/buxton/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectModuleManager">
+ <modules>
+ <module fileurl="file://$PROJECT_DIR$/.idea/buxton.iml" filepath="$PROJECT_DIR$/.idea/buxton.iml" />
+ </modules>
+ </component>
+</project> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/vcs.xml b/src/scraping/buxton/.idea/vcs.xml
new file mode 100644
index 000000000..c2365ab11
--- /dev/null
+++ b/src/scraping/buxton/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/workspace.xml b/src/scraping/buxton/.idea/workspace.xml
index 6f1ae3814..c1db7a75b 100644
--- a/src/scraping/buxton/.idea/workspace.xml
+++ b/src/scraping/buxton/.idea/workspace.xml
@@ -2,107 +2,7 @@
<project version="4">
<component name="ChangeListManager">
<list default="true" id="693c6819-edcc-46d6-8260-3f51ec080a46" name="Default Changelist" comment="">
- <change afterPath="$PROJECT_DIR$/new_scraper.py" afterDir="false" />
- <change beforePath="$PROJECT_DIR$/../../client/views/collections/CollectionSubView.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../../client/views/collections/CollectionSubView.tsx" afterDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image1.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image10.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image11.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image12.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image13.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image14.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image15.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image16.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image2.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image3.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image4.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image5.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image6.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image7.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image8.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Apple_ADB_Mouse/image9.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image1.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image10.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image11.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image2.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image3.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image4.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image5.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image6.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image7.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image8.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Casio_CZ-101/image9.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image1.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image10.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image11.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image12.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image13.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image14.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image15.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image16.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image17.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image18.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image2.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image3.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image4.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image5.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image6.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image7.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image8.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Contour_UniTrap/image9.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image1.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image10.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image11.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image12.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image13.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image14.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image15.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image16.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image17.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image18.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image19.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image2.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image20.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image21.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image3.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image4.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image5.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image6.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image7.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image8.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Dymo_MK-6/image9.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image1.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image10.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image11.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image12.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image13.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image14.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image15.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image16.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image2.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image3.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image4.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image5.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image6.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image7.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image8.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_Helios-Klimax/image9.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image1.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image10.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image11.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image12.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image13.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image14.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image15.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image2.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image3.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image4.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image5.png" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image6.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image7.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image8.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/extracted_images/Bill_Notes_IBM_PS2_Mouse/image9.jpeg" beforeDir="false" />
- <change beforePath="$PROJECT_DIR$/jsonifier.py" beforeDir="false" afterPath="$PROJECT_DIR$/jsonifier.py" afterDir="false" />
- <change beforePath="$PROJECT_DIR$/scraper.py" beforeDir="false" afterPath="$PROJECT_DIR$/scraper.py" afterDir="false" />
+ <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -122,7 +22,6 @@
<component name="PropertiesComponent">
<property name="ASKED_SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
- <property name="SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
<property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" />
</component>
@@ -192,8 +91,8 @@
</configuration>
<list>
<item itemvalue="Python.jsonifier" />
- <item itemvalue="Python.scraper" />
<item itemvalue="Python.narratives" />
+ <item itemvalue="Python.scraper" />
</list>
</component>
<component name="SvnConfiguration">
@@ -218,38 +117,38 @@
<screen x="0" y="23" width="1440" height="836" />
</state>
<state x="483" y="152" key="#xdebugger.evaluate/0.23.1440.836@0.23.1440.836" timestamp="1580601059439" />
- <state width="1419" height="216" key="GridCell.Tab.0.bottom" timestamp="1580656997013">
+ <state width="1419" height="268" key="GridCell.Tab.0.bottom" timestamp="1580786975290">
<screen x="0" y="23" width="1440" height="836" />
</state>
- <state width="1419" height="216" key="GridCell.Tab.0.bottom/0.23.1440.836@0.23.1440.836" timestamp="1580656997013" />
- <state width="1419" height="216" key="GridCell.Tab.0.center" timestamp="1580656997012">
+ <state width="1419" height="268" key="GridCell.Tab.0.bottom/0.23.1440.836@0.23.1440.836" timestamp="1580786975289" />
+ <state width="1419" height="268" key="GridCell.Tab.0.center" timestamp="1580786975289">
<screen x="0" y="23" width="1440" height="836" />
</state>
- <state width="1419" height="216" key="GridCell.Tab.0.center/0.23.1440.836@0.23.1440.836" timestamp="1580656997012" />
- <state width="1419" height="216" key="GridCell.Tab.0.left" timestamp="1580656997012">
+ <state width="1419" height="268" key="GridCell.Tab.0.center/0.23.1440.836@0.23.1440.836" timestamp="1580786975289" />
+ <state width="1419" height="268" key="GridCell.Tab.0.left" timestamp="1580786975289">
<screen x="0" y="23" width="1440" height="836" />
</state>
- <state width="1419" height="216" key="GridCell.Tab.0.left/0.23.1440.836@0.23.1440.836" timestamp="1580656997012" />
- <state width="1419" height="216" key="GridCell.Tab.0.right" timestamp="1580656997012">
+ <state width="1419" height="268" key="GridCell.Tab.0.left/0.23.1440.836@0.23.1440.836" timestamp="1580786975289" />
+ <state width="1419" height="268" key="GridCell.Tab.0.right" timestamp="1580786975289">
<screen x="0" y="23" width="1440" height="836" />
</state>
- <state width="1419" height="216" key="GridCell.Tab.0.right/0.23.1440.836@0.23.1440.836" timestamp="1580656997012" />
- <state width="1419" height="268" key="GridCell.Tab.1.bottom" timestamp="1580610405283">
+ <state width="1419" height="268" key="GridCell.Tab.0.right/0.23.1440.836@0.23.1440.836" timestamp="1580786975289" />
+ <state width="1419" height="268" key="GridCell.Tab.1.bottom" timestamp="1580786975292">
<screen x="0" y="23" width="1440" height="836" />
</state>
- <state width="1419" height="268" key="GridCell.Tab.1.bottom/0.23.1440.836@0.23.1440.836" timestamp="1580610405283" />
- <state width="1419" height="268" key="GridCell.Tab.1.center" timestamp="1580610405282">
+ <state width="1419" height="268" key="GridCell.Tab.1.bottom/0.23.1440.836@0.23.1440.836" timestamp="1580786975292" />
+ <state width="1419" height="268" key="GridCell.Tab.1.center" timestamp="1580786975291">
<screen x="0" y="23" width="1440" height="836" />
</state>
- <state width="1419" height="268" key="GridCell.Tab.1.center/0.23.1440.836@0.23.1440.836" timestamp="1580610405282" />
- <state width="1419" height="268" key="GridCell.Tab.1.left" timestamp="1580610405282">
+ <state width="1419" height="268" key="GridCell.Tab.1.center/0.23.1440.836@0.23.1440.836" timestamp="1580786975291" />
+ <state width="1419" height="268" key="GridCell.Tab.1.left" timestamp="1580786975290">
<screen x="0" y="23" width="1440" height="836" />
</state>
- <state width="1419" height="268" key="GridCell.Tab.1.left/0.23.1440.836@0.23.1440.836" timestamp="1580610405282" />
- <state width="1419" height="268" key="GridCell.Tab.1.right" timestamp="1580610405282">
+ <state width="1419" height="268" key="GridCell.Tab.1.left/0.23.1440.836@0.23.1440.836" timestamp="1580786975290" />
+ <state width="1419" height="268" key="GridCell.Tab.1.right" timestamp="1580786975292">
<screen x="0" y="23" width="1440" height="836" />
</state>
- <state width="1419" height="268" key="GridCell.Tab.1.right/0.23.1440.836@0.23.1440.836" timestamp="1580610405282" />
+ <state width="1419" height="268" key="GridCell.Tab.1.right/0.23.1440.836@0.23.1440.836" timestamp="1580786975292" />
<state x="229" y="80" key="SettingsEditor" timestamp="1580610123068">
<screen x="0" y="23" width="1440" height="836" />
</state>
@@ -258,6 +157,10 @@
<screen x="0" y="23" width="1440" height="836" />
</state>
<state width="720" height="417" key="XDebugger.FullValuePopup/0.23.1440.836@0.23.1440.836" timestamp="1580584300118" />
+ <state x="399" y="273" key="com.intellij.ide.util.TipDialog" timestamp="1580799621511">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state x="399" y="273" key="com.intellij.ide.util.TipDialog/0.23.1440.836@0.23.1440.836" timestamp="1580799621511" />
<state x="515" y="128" key="com.intellij.openapi.editor.actions.MultiplePasteAction$ClipboardContentChooser" timestamp="1580582281665">
<screen x="0" y="23" width="1440" height="836" />
</state>
diff --git a/src/scraping/buxton/json/buxton.json b/src/scraping/buxton/json/buxton.json
new file mode 100644
index 000000000..d26cc3a46
--- /dev/null
+++ b/src/scraping/buxton/json/buxton.json
@@ -0,0 +1,260 @@
+[{
+ "title": "3Dconnexion CadMan 3D Motion Controller",
+ "company": "3Dconnexion",
+ "year": 2003,
+ "primaryKey": [
+ "Joystick"
+ ],
+ "secondaryKey": [
+ "Isometric",
+ "Joystick"
+ ],
+ "originalPrice": 399,
+ "degreesOfFreedom": 6,
+ "dimensions": {
+ "length": 175,
+ "width": 122,
+ "height": 43,
+ "unit": "mm"
+ },
+ "shortDescription": "The CadMan is a 6 degree of freedom (DOF) joystick controller. It represented a significant step towards making this class of is controller affordable. It was mainly directed at 3D modelling and animation and was a “next generation” of the Magellan controller, which is also in the collection.",
+ "longDescription": "The CadMan is a 6 degree of freedom (DOF) joystick controller. It represented a significant step towards making this class of is controller more affordable. It was mainly directed at 3D modelling and animation and was a “next generation” of the Magellan/SpaceMouse controller, which is also in the collection. Like the Magellan, this is an isometric rate-control joystick. That is, it rests in a neutral central position, not sending and signal. When a force is applied to it, it emits a signal indicating the direction and strength of that force. This signal can then be mapped to a parameter of a selected object, such as a sphere, and – for example – cause that sphere to rotate for as long as, and as fast as, and in the direction determined by, the duration, force, and direction of the applied force. When released, it springs back to neutral position. Note that the force does not need to be directed along a single DOF. In fact, a core feature of the device is that one can simultaneously and independently apply force that asserts control over more than one DOF, and furthermore, vary those forces dynamically. As an aid to understanding, let me walk through some of the underlying concepts at play here by using a more familiar device: a computer mouse. If you move a mouse in a forward/backward direction, the mouse pointer on the screen moves between the screen’s top and bottom. If you think of the screen as a piece of graph paper, that corresponds to moving along the “Y” axis. That is one degree of freedom. On the other hand, you could move the mouse left and right, which causes the mouse to move between the left and right side of the screen. That would correspond to moving along the graph paper’s “X” axis – a second degree of freedom. Yet, you can also move the mouse diagonally. This is an example of independently controlling two degrees of freedom. Now imagine that if you lifted your mouse off your desktop, that your computer could dynamically sense its height as you did so. This would constitute a “flying mouse” (the literal translation of the German word for a “Bat”, which Canadian colleague, Colin Ware, applied to just such a mouse which he built in 1988). If you moved your Bat vertically up and down, perpendicular to the desktop, you would be controlling movement along the “Z” axis - a third degree of freedom. Having already seen that we can move a mouse diagonally, we have established that we need not be constrained to only moving along a single axis. That extends to the movement of our Bat and movement along the “Z” axis. We can control our hand movement in dependently in any or all directions in 3D space. But how does one reconcile the fact that we call the CadMan a “3D controller, and yet also describe it as having 6 degrees of freedom? After all, the example this far demonstrates that our Bat, as described thus far, has freedom on movement in 3 Dimensions. While true, we can extend our example to prove that that freedom to move in 3D is also highly constrained. To demonstrate this, move your hand in 3D space on and above your desktop. However, do so keeping your palm flat, parallel to the desktop with your fingers pointing directly forward. In so doing, you are still moving in 3D. Now, while moving, twist your wrist, while moving the hand, such that your palm is alternatively exposed to the left and right side. This constitutes rotation around the “Y” axis. A fourth DOF. Now add a waving motion to your hand, as if it were a paper airplane diving up and down, while also rocking left and right. But keep your fingers pointing forward. You have now added a fifth DOF, rotation around the “X” axis. Finally, add a twist to your wrist so that your fingers are no longer constrained to pointing forward. This is the sixth degree of freedom, rotation around the “Z” axis. Now don’t be fooled, this exercise could continue. We are not restricted to even six DOF. Imagine doing the above, but where the movement and rotations are measured relative to the Bat’s position and orientation, rather than to the holding/controlling hand, per se. One could imagine the Bat having a scroll wheel, like the one on most mice today. Furthermore, while flying your Bat around in 3D, that wheel could easily be rolled in either forward or backward, and thereby control the size of whatever was being controlled. Hence, with one hand we could assert simultaneous and independent control over 7 DOF in 3D space. This exercise has two intended take-aways. The first is a better working understanding between the notion of Degree of Freedom (DOF) and Dimension in space. Hopefully, the confusion frequently encountered when 3D and 6DOF are used in close context, can now be eliminated. Second, is that, with appropriate sensing, the human hand is capable of exercising control over far more degrees of freedom that six. And if we use the two hands together, the potential number of DOF that one can control goes even further. Finally, it is important to add one more take-away – one which both emerges from, and is frequently encountered when discussing, the previous two. That is, do not equate exercising simultaneous control over a high number of DOF with consciously doing the same number of different things all at once. The example that used to be thrown at me when I started talking about coordinated simultaneously bi-manual action went along the lines of, “Psychology tells us that we cannot do multiple things at once, for example, simultaneously tapping your head and rubbing your stomach. ”Well, first, I can tap my head with one hand while rubbing my stomach with the other. But that is not the point. The whole essence of skill – motor-sensory and cognitive – is “chunking” or task integration. When one appears to be doing many different things at once, if they are skilled, they are consciously doing only one thing. Playing a chord on the piano, for example, or skiing down the hill. Likewise, in flying your imaginary BAT in the previous exercise with the scroll wheel, were you doing 7 things at once, or one thing with 7 DOF? And if you had a Bat in each hand, does that mean you are now doing 14 things at once, or are you doing one thing with 14 DOF? Let me provide a different way of answering this question: if you have ever played air guitar, or “conducted” the orchestra that you are listening to on the radio, you are exercising control over more than 14 DOF. And you are doing exactly what I just said, “playing air guitar” or “conducting an orchestra”. One thing – at the conscious level, which is what matters – despite almost any one thing being able to be deconstructed into hundreds of sub-tasks. As I said the essence of skill: aggregation, or chunking. What is most important for both tool designers and users to be mindful of, is the overwhelming influence that our choice and design of tools impacts the degree to which such integration or chunking can take place. The degree to which the tool matches both the skills that we have already acquired through a lifetime of living in the everyday world, and the demands of the intended task, the more seamless that task can be performed, the more “natural” it will feel, and the less learning will be required. In my experience, it brought particular value when used bimanually, in combination with a mouse, where the preferred hand performed conventional pointing, selection and dragging tasks, while the non-preferred hand could manipulate the parameters of the thing being selected. First variation of the since the 2001 formation of 3Dconnextion. The CadMan came in 5 colours: smoke, orange, red, blue and green. See the notes for the LogiCad3D Magellan for more details on this class of device. It is the “parent” of the CadMan, and despite the change in company name, it comes from the same team."
+ },
+ {
+ "title": "Adesso ACK-540UB USB Mini-Touch Keyboard with Touchpad",
+ "company": "Adesso",
+ "year": 2005,
+ "primaryKey": [
+ "Keyboard"
+ ],
+ "secondaryKey": [
+ "Pad",
+ "Touch"
+ ],
+ "originalPrice": 59.95,
+ "degreesOfFreedom": 2,
+ "dimensions": {
+ "length": 287,
+ "width": 140,
+ "height": 35.5,
+ "unit": "mm"
+ },
+ "shortDescription": "The Mini-Touch Keyboard is a surprisingly rare device: a laptop-style, small-footprint keyboard with a centrally mounted touch-pad. .",
+ "longDescription": "First released in 2003 with a PS/2 connector (ACK-540PW &amp; ACK-540PB). USB version released in 2006 in either black (ACK-540UB) or white (ACK-540UW). Marketed under different brands, including SolidTek: http: //www. tigerdirect. com/applications/searchtools/item-details. asp? EdpNo=1472243https: //acecaddigital. com/index. php/products/keyboards/mini-keyboards/kb-540 Deltaco: https: //www. digitalimpuls. no/logitech/116652/deltaco-minitastatur-med-touchpad-usb"
+ },
+ {
+ "title": "Braun AG T3 Transistor Radio",
+ "company": "Braun AG",
+ "year": 1958,
+ "primaryKey": [
+ "Radio"
+ ],
+ "secondaryKey": [
+ "Handheld",
+ "Object",
+ "Reference"
+ ],
+ "originalPrice": 28.57,
+ "degreesOfFreedom": 2,
+ "dimensions": {
+ "length": 152,
+ "width": 41,
+ "height": 83,
+ "unit": "mm"
+ },
+ "shortDescription": "The 1958 Braun T3 transistor radio, designed by Dieter Rams Dieter Rams in conjunction with the Ulm Hochschüle fur Gestaltung (School of Design). An excellent example of the international style of design of the mid-20th century, the T3 radio was the inspiration for the design language of the Apple iPod Classic.",
+ "longDescription": "The 1958 Braun T3 transistor radio is a classic of the international design style prevalent in the mid-20th century. By its sparse clean lines, it shares characteristics of the style seen in another familiar example, the font Helvetic, which was designed the previous year. The T3 was designed by Dieter Rams, recruited by Braun in 1955, in collaboration with the Ulm Hochschüle fur Gestaltun. . Its design language had a strong influence on that of the original Apple iPod Classic. The connection is made more obvious if one views the radio rotated 90° clockwise, as in one of the accompanying photographs. Here one can easily see the the similarity of proportions, uniformity of colour, angle of corners, location of display (audio versus visual), and the use of a flush rotary wheel controller."
+ },
+ {
+ "title": "Casio CZ-101 Digital Synthesizer",
+ "company": "Casio",
+ "year": 1984,
+ "primaryKey": [
+ "Synthesizer"
+ ],
+ "secondaryKey": [
+ "Chord",
+ "Keyboard",
+ "Object",
+ "Reference",
+ "Wheel"
+ ],
+ "originalPrice": 499,
+ "degreesOfFreedom": 1,
+ "dimensions": {
+ "length": 20,
+ "width": 65.7,
+ "height": 58,
+ "unit": "mm"
+ },
+ "shortDescription": "One of the first programable polyphonic (8 simultaneous voices) digital synthesizers for less than $500. 00. Used a form of digital synthesis known as Phase Distortion to obtain a rich variety of dynamic timbres. Could be used with batteries or plugged in to power. This one was given to me at the product launch.",
+ "longDescription": "One of the first programable polyphonic (8 simultaneous voices) digital synthesizers for less than $500. 00. Used a form of digital synthesis known as Phase Distortion to obtain a rich variety of dynamic timbres. Could be used with batteries or plugged in to power. This one was given to me at the product launch. The inclusion of this synthesizer in the collection is as a small reminder of the diversity of keyboard types, and especially, as an example to shed light on chord keyboards. In entering text, for example, chord keyboards are those where more than one key must be simultaneously pressed to enter a single character. Technically, this includes any keyboard with a SHIFT key. Interestingly, piano-type like keyboards like that on the Casio-CZ-101 probably don’t conform to this definition of chording, despite its ability to play musical chords. On the other hand, flutes and trumpets definitely do fall within the definition. Why? With piano-like keyboards, each unique note has a single unique key dedicated to it. When one plays a chord, i. e. , simultaneously presses multiple keys, the result is a chord of notes – the note associated with each depressed key sounds. On the other hand, with trumpet valves or flute keys, only one note is produced at a time. It is the combination of keys pressed (coupled with breath) which determines the pitch of that single note. This is far closer to entering text with a chord keyboard, where each chord enters a single unique character."
+ },
+ {
+ "title": "Contour Design UniTrap ",
+ "company": "Contour Design",
+ "year": 1999,
+ "primaryKey": [
+ "Re-skin"
+ ],
+ "secondaryKey": [
+ "Mouse"
+ ],
+ "originalPrice": 14.99,
+ "degreesOfFreedom": 2,
+ "dimensions": {
+ "length": 130.5,
+ "width": 75.7,
+ "height": 43,
+ "unit": "mm"
+ },
+ "shortDescription": "This is a plastic shell within which the round Apple iMac G3 “Hockey Puck” mouse can be fit. While the G3 Mouse worked well mechanically, when gripped its round shape gave few cues as to its orientation. Hence, if you moved your hand up, the screen pointer may well have moved diagonally. By reskinning it with the inexpensive Contour UniTrap, the problem went away without the need to buy a whole new mouse.",
+ "longDescription": "Also add back pointers from devices re-skinned"
+ },
+ {
+ "title": "Depraz Swiss Mouse",
+ "company": "Depraz",
+ "year": 1980,
+ "primaryKey": [
+ "Mouse"
+ ],
+ "secondaryKey": [
+ "Ball",
+ "Chord",
+ "Keyboard",
+ "Mouse"
+ ],
+ "originalPrice": 295,
+ "degreesOfFreedom": 2,
+ "dimensions": {
+ "length": 50.8,
+ "width": 76.2,
+ "height": 114.3,
+ "unit": "mm"
+ },
+ "shortDescription": "This mouse is one of the first commercially available mice to be sold publicly. It is known as the Swiss mouse, and yes, the roller mechanism was designed by a Swiss watchmaker. Coincidentally, the company that made it, Depraz, is based in Apples, Switzerland. Their success in selling this mouse is what caused Logitech to switch from a software development shop to one of the world’s leading suppliers of mice and other input devices.",
+ "longDescription": "DePraz began manufacturing in 1980, but following design built in 1979. Logitech started selling it in 1982. It was one of the first mass produced mice, one of the first available ball mice, as well as to have an optical shaft encoder – thereby improving linearity. An interesting fact, given its Swiss heritage, is that its designer, André Guignard, was trained as a Swiss watch maker. Unlike most modern mice, the DePraz, or “Swiss” mouse had a quasi-hemispherical shape. Hence, it was held in a so-called “power-grip”, much as one would grip a horizontally held ball – the thumb and small finger applying pressure on each side, with added support from the weight/friction of the palm on the back of the mouse. In this posture, the three middle fingers naturally positioning themselves over the three buttons mounted at the lower edge of the front. Largely freed of grip pressure, by grace of thumb and little finger, the middle fingers had essentially freedom of motion to independently operate the buttons. Each having a dedicated finger, the buttons could be easily pushed independently or in any combination. Like the three valves on a trumpet, this ability to “chord” extended the three physical buttons to have the power of seven. The down-side of this “turtle shell” form factor is that it placed the hand in a posture in which mouse movement relied more of the larger muscle groups of the arm to wrist, rather than wrist to fingers – the latter being the approach taken in most subsequent mice. The original Swiss Mouse was developed at École Polytechnique Fédérale de Lausanne by a project led by Jean-Daniel Nicoud, who was also responsible for the development of its optical shaft encoder. To augment their revenue stream, Logitech, then a software and hardware consulting company for the publishing industry, acquired marketing rights for North America. Mouse revenue quickly overshadowed that from software. In 1983, Logitech acquired DePraz, named the Swiss Mouse the “P4”, and grew to become one of the largest input device manufacturer in the world. One curious coincidence is that they were founded in the town of Apples, Switzerland."
+ },
+ {
+ "title": "FingerWorks TouchStream LP",
+ "company": "FingerWorks",
+ "year": 2002,
+ "primaryKey": [
+ "Keyboard"
+ ],
+ "secondaryKey": [
+ "Foldable",
+ "Gesture",
+ "Keyboard",
+ "Multi-touch",
+ "Reskin",
+ "Touchpad"
+ ],
+ "originalPrice": 339,
+ "degreesOfFreedom": 2,
+ "dimensions": {
+ "length": 180,
+ "width": 140,
+ "height": 9,
+ "unit": "mm"
+ },
+ "shortDescription": "The TouchStream is a keyboard based on a pair of multi-touch pads. These can sense key taps and finger gestures. The “keys” are graphic. They are flush with the pad and have no mechanical movement. There are however, small raised points to help position the hands on the keyboard eyes-free typing, but these still allow the fingers slide easily on the surface when gesturing, such as when emulating a mouse. . The keyboard is independent of the base. It can be folded in half for compact portability. It can also be placed conveniently over a laptop’s keyboard as a replacement which then also enables the gesture enhancements to be used on the road. Although not obvious to the eye, this is the core technology which, after being acquired by Apple, evolved into the iPhone’s multi-touch capability.",
+ "longDescription": "Named FingerBoard during development, this product was relabeled TouchStream in October 2001 as the release date approached. when finally shipped, was renamed TouchStreamThe very rare original stand for this device was a gift from Sean Gerety, Atlanta, GA."
+ },
+ {
+ "title": "One Laptop Per Child (OLPC) XO-1",
+ "company": "One Laptop Per Child (OLPC)",
+ "year": 2007,
+ "primaryKey": [
+ "Computer"
+ ],
+ "secondaryKey": [
+ "Keyboard",
+ "Laptop",
+ "Pad",
+ "Slate",
+ "Touch"
+ ],
+ "originalPrice": 199,
+ "degreesOfFreedom": 2,
+ "dimensions": {
+ "length": 242,
+ "width": 228,
+ "height": 30,
+ "unit": "mm"
+ },
+ "shortDescription": "The OLPC XO-1 is very innovative device that nevertheless raises serious issues about technology and social responsibility. It is included in the collection primarily as a warning against technological hubris, and the fact that no technologies are neutral from a social-cultural perspective.",
+ "longDescription": "IntroductionI have this computer in my collection as a reminder of the delicate relationship between object and purpose, and how no matter how well one does on the former, it will likely have no impact on making a wanting concept achieve the stated (and even valid) purpose any better. I include it in the collection as a cautionary tale of how the object may help sell a concept, regardless how ill-conceived – even to those who should know better, had they applied the most basic critical thinking. For consumers, investors and designers, its story serves as a cautionary reminder to the importance of cultivating and retaining a critical mind and questioning perspective, regardless of how intrinsically seductive or well-intentioned a technology may be. From the perspective of hardware and software, what the One Laptop Per Child (OLPC) project was able to accomplish is impressive. In general, the team delivered a computer that could be produced at a remarkably low price – even if about double that which was targeted. Specifically, the display, for example, is innovative, and stands out due to its ability to work both in the bright sun (reflective) as well as in poorly lit spaces (emissive) – something that goes beyond pretty much anything else that is available on today’s (2017) slate computers or e-readers. In short, some excellent work went into this machine, something that is even more impressive, given the nature of the organization from which it emerged. The industrial design was equally impressive. Undertaken by Yves Behar’s FuseprojectUltimately, however, the machine was a means to an end, not the end itself. Rather than a device, the actual mission of the OLPC project was: … to empower the world's poorest children through education. Yet, as described by in their materials, the computer was intended to play a key role in this: With access to this type of tool [the computer], children are engaged in their own education, and learn, share, and create together. They become connected to each other, to the world and to a brighter future. Hence, making a suitable computer suitable to that purpose and the conditions where it would be used, at a price point that would enable broad distribution, was a key part of the project. The Underlying Belief System of the OLPC ProjectSince they are key to the thinking behind the OLPC project, I believe if fair to frame my discussion around the following four questions: Will giving computers to kids in the developing world improve their education? Will having a thus better-educated youth help bring a society out of poverty? Can that educational improvement be accomplished by giving the computers to the kids, with no special training for teachers? Should this be attempted on a global scale without any advance field trials or pilot studies? From the perspective of the OLPC project, the answer to every one of these questions is an unequivocal “yes”. In fact, as we shall see, any suggestion to the contrary is typically answered by condescension and/or mockery. The answers appear to be viewed as self-evident and not worth even questioning. Those who have not subscribed to this doctrine might call such a viewpoint hubris. What staggers me is how the project got so far without the basic assumptions being more broadly questioned, much less such questions being seriously addressed by the proponents. How did seemingly otherwise people commit to the project, through their labour or financial investment, given the apparently naïve and utopian approach that it took? Does the desire to do good cloud judgment that much? Are we that dazzled by a cool technology or big hairy audacious goal? Or by a charismatic personality? To explain my concern, and what this artifact represents to me, let me just touch on the four assumptions on which the project was founded. Will giving computers to kids in the developing world improve education? The literature on this question is, at best, mixed. What is clear is that one cannot make any assumption that such improvements will occur, regardless of whether one is talking about the developing world or suburban USA. For example, in January 2011, The World Bank published the following study: Can Computers Help Students Learn? From Evidence to Policy, January 2011, Number 4, The World Bank. A public-private partnership in Colombia, called Computers for Education, was created in 2002 to increase the availability of computers in public schools for use in education. Since starting, the program has installed more than 73, 000 computers in over 6, 300 public schools in more than 1, 000 municipalities. By 2008, over 2 million students and 83, 000 teachers had taken part. This document reports on a two-year study to determine the impact of the program on student performance. Students in schools that received the computers and teacher training did not do measurably better on tests than students in the control group. Nor was there a positive effect on other measures of learning. Researchers did not find any difference in test scores when they looked at specific components of math and language studies, such as algebra and geometry, and grammar and paraphrase ability in Spanish. But report also notes that results of such studies are mixed: Studies on the relationship between using computers in the classroom and improved test scores in developing countries give mixed results: A review of Israel’s Tomorrow-98 program in the mid-1990s, which put computers in schools across the country, did not find any impact on math and Hebrew language scores. But in India, a study of a computer-assisted learning program showed a significant positive impact on math scores. One thing researchers agree on, more work is needed in this field. Before moving on, a search of the literature will show that these results are consistent with those that were available in the literature at the time that the project was started. The point that I am making is not that the OLPC project could not be made to work; rather, that it was wrong to assume that it would do so without spending at least as much time designing the process to bring that about, as was expended designing the computer itself. Risk is fine, and something that can be mitigated. But diving in under the assumption that it would just work is not calculated risk, it is gambling - with other people’s lives, education and money. Will a better educated population help bring a society out of poverty? I am largely going to punt on this question. The fact is, I would be hard pressed to argue against education. But let us grant that improving education in the developing world is a good thing. The appropriate question is: is the approach of the OLPC project a reasonable or responsible way to disburse the limited resources that are available to address the educational challenges of the developing world? At the very least, I would suggest that this is a topic worthy of debate. An a priori assumption that giving computers is the right solution is akin to the, “If you build it they will come” approach seen in the movie, Field of Dreams. The problem here is that this is not a movie. There are real lives and futures that are at stake here – lives of those who cannot afford to see the movie, much less have precious resources spent on projects that are not well thought through. Can that improvement be accomplished by just giving the computers to the kids without training teachers? Remarkably, the OLPC Project’s answer is an explicit, “Yes”. In a TED talk filmed in December 2007, the founder of the OLPC initiative, Nicholas Negroponte states: “When people tell me, you know, who’s going to teach the teachers to teach the kids, I say to myself, “What planet do you come from? ” Okay, there’s not a person in this room [the TED Conference], I don’t care how techy you are, there’s not a person in this room that doesn’t give their laptop or cell phone to a kid to help them debug it. Okay, we all need help, even those of us who are very seasoned. ”Let us leave aside the naïvete of this statement stemming from the lack of distinction between ability to use applications and devices versus the ability to create and shape them. A failure of logic remains in that those unseasoned kids are part of “us”, as in “we all need help”. Where do the kids go for help? To other kids? What if they don’t know? Often they won’t. After all, the question may well have to do with a concept in calculus, rather than how to use the computer. What then? No answer is offered. Rather, those who dare raise the serious and legitimate concerns regarding teacher preparation are mockingly dismissed as coming from another planet! Well, perhaps they are. But in that case, there should at least be some debate as to who lives on which planet. Is it the people raising the question or the one dismissing the concern that lives in the real world of responsible thought and action? Can this all be accomplished without any advance field trials? Should one just immediately commit to international deployment of the program? As recently as September 2009, Negroponte took part in a panel discussion where he spoke on this matter. He states: I'd like you to imagine that I told you \"I have a technology that is going to change the quality of life. \" And then I tell you \"Really the right thing to do is to set up a pilot project to test my technology. And then the second thing to do is, once the pilot has been running for some period of time, is to go and measure very carefully the benefits of that technology. \"And then I am to tell you that what we are going to is very scientifically evaluate this technology, with control groups - giving it to some, giving it to others. And this all is very reasonable until I tell you the technology is electricity. And you say \"Wait, you don't have to do that!\"But you don't have to do that with laptops and learning either. And the fact that somebody in the room would say the impact is unclear is to me amazing - unbelievably amazing. There's not a person in this room who hasn't bought a laptop for their child, if they could afford it. And you don't know somebody who hasn't done it, if they can afford it. So there's only one question on the table and that's, “How to afford it? ” That's the only question. There is no other question - it's just the economics. And so, when One Laptop Per Child started, I didn't have the picture quite as clear as that, but we did focus on trying to get the price down. We did focus on those things. Unfortunately, Negroponte demonstrates his lack of understanding of both the history of electricity and education in this example. His historical mistake is this: yes, it was pretty obvious that electricity could bring many benefits to society. But what happened when Edison did exactly what Negroponte advocates? He almost lost his company due to his complete (but mistaken) conviction that DC, rather the AC was the correct technology to pursue. As with electricity, yes, it is rather obvious that education could bring significant benefits to the developing world. But in order to avoid making the same kind of expensive mistake that Edison did, perhaps one might want to do one’s best to make sure that the chosen technology is the AC, rather than DC, of education. A little more research, and a little less hubris might have put the investments in Edison and the OLPC to much better use. But the larger question is this: in what way is it responsible for the wealthy western world to advocate an untested and expensive (in every sense) technological solution on the poorest nations in the world? If history has taught us anything, it has taught us that just because our intentions are good, the same is not necessarily true for consequences of our actions. Later in his presentation, Negroponte states: … our problems are swimming against very naïve views of education. With this, I have to agree. It is just whose views on education are naïve, and how can such views emerge from MIT, no less, much less pass with so little critical scrutiny by the public, the press, participants, and funders? In an interview with Paul Marks, published in the New Scientist in December 2008, we see the how the techno-centric aspect of the project plays into the ostensible human centric purpose of the project. Negroponte’s retort regarding some of the initial skepticism that the project provoked was this: “When we first said we could build a laptop for $100 it was viewed as unrealistic and so 'anti-market' and so 'anti' the current laptops which at the time were around $1000 each, \" Negroponte said. \"It was viewed as pure bravado - but look what happened: the netbook market has developed in our wake. \" The project's demands for cheaper components such as keyboards, and processors nudged the industry into finding ways to cut costs, he says. \"What started off as a revolution became a culture. \"Surprise, yes, computers get smaller, faster, and cheaper over the course of time, and yes, one can even grant that the OLPC project may have accelerated that inevitable move. And, I have already stated my admiration and respect for the quality of the technology that was developed. But in the context of the overall objectives of the project, the best that one can say is, “Congratulations on meeting a milestone. ” However, by the same token, one might also legitimately question if starting with the hardware was not an instance of putting the cart before the horse. Yes, it is obviously necessary to have portable computers in the first place, before one can introduce them into the classroom, home, and donate them to children in the developing world. But it is also the case that small portable computers were already in existence and at the time that the project was initiated. While a factor of ten more expensive than the eventual target price, they were both available and adequate to support limited preliminary testing of the underlying premises of the project in an affordable manner. That is, before launching into a major - albeit well-intentioned – hardware development project, it may have been prudent to have tested the underlying premises of its motivation. Here we have to return to the raison d’être of the initiative: … to empower the world's poorest children through educationHence, the extent to which this is achieved from a given investment must be the primary metric of success, as well as the driving force of the project. Yet, that is clearly not what happened. Driven by a blind Edisonian belief in their un-tested premise, the project’s investments were overwhelmingly on the side of technology rather than pedagogy. Perhaps the nature and extent of the naïve (but well-meaning) utopian dream underlying the project is captured in the last part of the interview, above: Negroponte believes that empowering children and their parents with the educational resources offered by computers and the Internet will lead to informed decisions that improve democracy. Indeed, it has led to some gentle ribbing between himself and his brother: John Negroponte - currently deputy secretary of state in the outgoing Bush administration and the first ever director of national intelligence at the National Security Agency. \"I often joke with John that he can bring democracy his way - and I'll bring it mine, \" he says. Apparently providing inexpensive laptops to children in the developing world is not only going to raise educational standards, eradicate poverty, it is also going to bring democracy! All that, with no mention of the numerous poor non-democratic countries that have literacy levels equal to or higher than the USA (Cuba might be one reasonable example). The words naïve technological-utopianism come to mind. I began by admitting that I was conflicted in terms of this project. From the purely technological perspective, there is much to admire in the project’s accomplishments. Sadly, that was not the project’s primary objective. What appears to be missing throughout is an inability to distinguish between the technology and the purpose to which is was intended to serve. My concern in this regard is reflected in a paper by Warschauer &amp; Ames(2010). The analysis reveals that provision of individual laptops is a utopian vision for the children in the poorest countries, whose educational and social futures could be more effectively improved if the same investments were instead made on more sustainable and proven interventions. Middle- and high-income countries may have a stronger rationale for providing individual laptops to children, but will still want to eschew OLPC’s technocentric vision. In summary, OLPC represents the latest in a long line of technologically utopian development schemes that have unsuccessfully attempted to solve complex social problems with overly simplistic solutions. There is a delicate relationship between technology and society, culture, ethics, and values. What this case study reflects is the fact that technologies are not neutral. They never are. Hence, technological initiatives must be accompanied by appropriate social, cultural and ethical considerations – especially in projects such as this where the technologies are being introduced into particularly vulnerable societies. That did not happen here, The fact that this project got the support that it did, and has gone as far as it has, given the way it was approached, is why this reminder – in the form of this device – is included in the collection. And if anyone ever wonders why I am so vocal about the need for public discourse around technology, one need look no further than the OLPC project."
+ },
+ {
+ "title": "TASA Model 55 ASCII Keyboard",
+ "company": "TASA (Touch Activated Switch Arrays)",
+ "year": 1979,
+ "primaryKey": [
+ "Keyboard"
+ ],
+ "secondaryKey": [
+ "Pad",
+ "Touch"
+ ],
+ "originalPrice": 80,
+ "degreesOfFreedom": 0,
+ "dimensions": {
+ "length": 382.27,
+ "width": 158.75,
+ "height": 8.255,
+ "unit": "mm"
+ },
+ "shortDescription": "This touch-sensitive keyboard is especially suited for super clean environments, such as hospitals, and those which are just the opposite. The reason is that, being completely flat, there are no crack or gaps where dirt or bacteria can accumulate. This same property enables it to be easily cleaned. However, the reason that I got this keyboard because it was silent – there are no mechanical key-clicks. Hence, for example, it enabled me to soundlessly enter data to my digital musical instrument during a concert or while recording.",
+ "longDescription": "This is a solid-state touch-sensitive keyboard with no moving parts. Because its surface is flat, the only way one knows that it is a QWERTY keyboard is by the graphical representation on its surface. One types by placing one’s fingers on pictures of keys, rather than physical/mechanical keycaps. Because of the lack of the tactile feedback associated with conventional keyboards, as expected, typing speed and/or accuracy will be compromised with this keyboard. And yet, this keyboard brings real value in certain situations, and in so doing, it provides a good example of the rule: Everything is best for something and worst for something else. Because the is especially suited for super clean environments, such as hospitals, and those which are just the opposite. The reason is that, being completely flat, there are no crack or gaps where dirt or bacteria can accumulate. This same property enables it to be easily cleaned. However, the reason that I got this keyboard because it was silent – there are no mechanical key-clicks. Hence, for example, it enabled me to soundlessly enter data to my digital musical instrument during a concert or while recording. This is one of a number of capacitive touch-sensing input devices produced in the period around 1981 by Touch Activated Switch Arrays (TASA). The others included a touch-sensitive linear controller, the Ferinstat, which could function as a linear slider/fader, for applications such as audio or process control. These came in two lengths and are included in the collection. There were also the Model 16 Micro Proximity Keyboards, which were 16-button keyboards, arranged in a 4x4 array of touch-sensitive buttons that included a touch-sensitive numerical keypad. They also demonstrated a small, capacitive touch-sensitive touch pad, not unlike what one sees on today’s laptops, for example."
+ },
+ {
+ "title": "HandyKey (TekGear) Twiddler ",
+ "company": "HandyKey (TekGear)",
+ "year": 1991,
+ "primaryKey": [
+ "Chord",
+ "Keyboard"
+ ],
+ "secondaryKey": [
+ "Gesture",
+ "Joystick",
+ "Keyboard",
+ "Reality",
+ "Virtual",
+ "Vr",
+ "Wearable"
+ ],
+ "originalPrice": 199,
+ "degreesOfFreedom": 2,
+ "dimensions": {
+ "length": 128,
+ "width": 45,
+ "height": 50,
+ "unit": "mm"
+ },
+ "shortDescription": "The Twiddler is a one-hand chord keyboard with integrated pointing capability, which can control the cursor in a joystick-like manner. This was a favourite device of the early Cyborg wearable-computer community.",
+ "longDescription": "……. . Note: Lyons, et al. abstract: An experienced user of the Twiddler, a one--handed chording keyboard, averages speeds of 60 words per minute with letter--by--letter typing of standard test phrases. This fast typing rate coupled with the Twiddler's 3x4 button design, similar to that of a standard mobile telephone, makes it a potential alternative to multi--tap for text entry on mobile phones. Despite this similarity, there is very little data on the Twiddler's performance and learnability. We present a longitudinal study of novice users' learning rates on the Twiddler. Ten participants typed for 20 sessions using two different methods. Each session is composed of 20 minutes of typing with multi--tap and 20 minutes of one--handed chording on the Twiddler. We found that users initially have a faster average typing rate with multi--tap; however, after four sessions the difference becomes negligible, and by the eighth session participants type faster with chording on the Twiddler. Furthermore, after 20 sessions typing rates for the Twiddler are still increasing."
+ },
+ {
+ "title": "Blue Orb Inc. OrbiTouch",
+ "company": "Blue Orb Inc",
+ "year": 2002,
+ "primaryKey": [
+ "Joystick"
+ ],
+ "secondaryKey": [
+ "Keyboard"
+ ],
+ "originalPrice": 695,
+ "degreesOfFreedom": 4,
+ "dimensions": {
+ "length": 482.6,
+ "width": 228.6,
+ "height": 74.2,
+ "unit": "mm"
+ },
+ "shortDescription": "On the one hand, this device has the overall footprint of a keyboard, and it is used to enter text. And yet, it is two wide, flat, spring-loaded, self-returning joysticks, which are used to enter characters, rather than the keys that we typically employ. To add to the unconventional nature of this device, one enters text via these two joysticks by means of something called radial menus, one for each hand. And, in keeping with many keyboards, such as those with an integrated touch pad, the OrbiTouch also enables mouse like capabilities, such as pointing and selecting, also by means of one of the joysticks.",
+ "longDescription": "Keyboards, Joysticks and Hierarchic Radial MenusIntroductionWhen you first look at this device, you might guess that it is some kind of keyboard. It even says so on the box and on the device itself. The keyboard-like footprint might reinforce this notion, as might the alphanumeric characters in the grey ring around the circular orb on the right-hand. On the other hand, if this is a keyboard, where are the keys? Reading the labels more carefully sheds light on the paradox: there are none. This is a “keyless keyboard. ” Yes, this is a contradiction in terms. But it is just such curiosities that make devices like this potentially interesting. Hence, we shall take a reasonably deep dive to see what might be revealed. Let’s start by trying to understand what the rationale was for landing on this particular design. The orbiTouch was developed by an industrial engineering doctoral student at the University of Central Florida, Peter McAlindon. His goal was to develop a means of text entry that minimized hand and wrist motion. The intent was to reduce the incidence of repetitive stress injury. A fair bit of research was undertaken between initial concept and commercial release. This can be accessed online, and doing so is a worthwhile exercise. Let us now turn our eye to the physical device in order to get a sense of where all of this landed. The Physical DeviceThe orbiTouch is dominated by two large circular “orbs. ” To my eye, their form initially practically screamed out, “I am a rotary control - Turn me!” However, appearances can be deceptive. Rather than dials, the orbs turn out to be a pair of a joysticks of a particular type. Rather than the stick-tilting motion typical of most, these “joysticks” are operated by moving them along the horizontal plane. In this they are a close cousins of the Altra Felix and KA Design Turbo Puck, both also in the collection. However, in contrast with the Felix and Turbo Puck, whose handles are “floating” (if you let go, they remain in the position where you released your grip), the orbs are “self-centering. ” That is, when released, internal springs return the orbs to their neutral central “home” position. In this, they behave much like the Gravis joystick in the collection, for example. At a finer level of detail, the orbs are specific class of joystick: “8-way joy-switches”. The term”8-way” indicates that only movement along the 8 main axes of the compass are sensed. As to the word “switch”, think of each orb as 8 switches, any one of which can be turned on by moving the orb in one of the 8 directions. (Conversely, they are turned off when the orb is released and returns to home position). Unlike an analogue joystick, such switches do not, and cannot, report how far or fast the orb has moved in any particular direction, nor how much pressure might be applied in the process. While limited, joy-switches provide a less complex and lower cost solution that are appropriate in situations where this additional data is not needed. There are several examples of joy-switches in the collection, especially video game controllers. One of the most iconic examples is the Atari CX-40 controller, which is a 4-way joy-switch. To recap, the orbiTouch is a bi-manual device for entering text by means of two orb-shaped planer-moving 8-way self-centering joy-switches. Having swallowed that mouth-full, let us now explore how text is entered using such a transducer. Entering TextIn general, a character or function is input by moving the two orbs. Which character or function depends on the direction (if any) each of the orbs has moved. For example, if both the left and right orb move west (left), the character “a” is entered. On the other hand, if the right orb again moves west, but the left one east (right), then the character input is “e”. How or why this is the case can be explained with the help of some images. For easier reading, the figure below shows the labels around the orbs in an exploded view. Notice that for both orbs, there is a label segment for each of its 8 directions. Since the example discussed entering an “a” and an “e”, each of which involved the right orb moving west (left) let’s look at the associated label segment in even more detail. Like all of the label segments for the right orb, this one consists of six areas containing text, each with a distinct background colour: red, yellow, green, orange and blue for the letters A through E, respectively, and black for the region containing “BACKSPACE”. Now look again at previous image and notice that each of these colours matches the label associated with one of the directions of the left orb. Text is entered using a two part process. Moving the right orb to the left/west specifies that you are going to enter one of: a, b, c, d, e, or BACKSPACE. (Like most keyboards, despite the labels on the key-caps being upper case, lower-case characters are entered unless the shift key is depressed. )Moving the left orb in the direction whose label corresponds to the background colour of the desired character causes that character to be entered. Hence, with the right orb held in the left/west position, one can enter the sequence, “abcde”, followed by a Backspace, by sequentially moving the left orb west (red), north-west (yellow), north (green), north-east (orange), east (blue) and south (black). The same technique can then be used to access all the characters and commands found in the right orb’s labels. Special ModesThere is one thing to add at this point: While entering printing characters always requires the use of both orbs, some actions can be performed using the left orb only. This can be inferred by the text that accompanies some of the left orb’s labels. For example, moving the left orb north (green) in quick succession (analogous to a double-click on a mouse), indicates that SHIFT will apply to the next character entered. Likewise, doing the same thing in the south-west (grey) direction applies the Caps Lock mode, i. e. , SHIFT will be applied to all subsequent entries until the mode is cancelled. These one-handed special modes/functions are summarized in the image below. Of these, the only one that I want to discuss at the moment is the ability of the orbiTouch to switch from entering text to controlling the screen cursor. This is done by moving the left orb south (black) twice in quick succession. When this is done, the right orb controls the cursor movement – the cursor moves continuously in the direction that you move the orb. In this, any doubts that you had about me characterizing the orbs as joysticks should disappear, since this cursor control is classic joystick behaviour. One issue of note is that the label describes this as “mouse” not “joystick”, which while understandable, is incorrect. Finally, before moving on to the next topic, note that while the right orb controls the movement of the screen cursor in mouse mode, movement of the left or left/west or right/east is taken as a left and right mouse button press, respectively. Remembering that the premise here is that the hands don’t have to move from the orbiTouch in order switch between typing and pointing tasks. But that doesn’t mean that the overhead in switching between the tasks is removed. One type of overhead is just substituted for another. And, the moded nature of the orbiTouch means that the option of parallel pointing-typing actions are eliminated. Rather than criticism, I mention these points to indicate the need to be mindful of the trade-offs and consequences of different design decisions - consequences that the designer should be aware of. Going Meta: What’s Really Going On? I want to approach doing so by stepping back, and approaching the underlying method of “typing” by going “meta”. That is, I want to jump up a lever of abstraction, beyond the physical device (for the moment), and explain what is going on at the conceptual level. The rest of the text is in much rougher form …. What will be revealed, if we do so, is that text is entered by means of the parallel use of two 8-direction radial menus. So what is a radial menu? These are the neglected cousins of the linear menus that populate conventional graphical user interfaces. The difference is that one makes a selection by the direction of movement, rather than the distance (as in the case with linear menus). It turns out that people can learn these quickly if the directions correspond to the 8 main points of the compass. For example, in a program menu, moving up (North) might mean Print, down (South) could mean Save, and moving down to the right (South East), Save As. Like linear menus, these menus can also be hierarchic. So, for example, after moving South East in order to specify Save As, a stroke to the left (West) might mean that it should be saved as a PDF file, whereas it would be saved as a Plain Text file if the secondary connected stroke was to the right (East). The reason for this brief tutorial on radial menus is that they pretty much define at the conceptual level how text is entered using the orbiTouch. The eight directions that you can move the orbs defines the menu item selected. And, by having the actual output depending on the combination of the selection made by each of the two orbs, the device can perhaps be best described as entering text using a two-level hierarchic radial menu, where menu selections are made using two planar moving 8-way joy switches. That is quite a mouth-full, and it has taken all of the text above to bring us to the point where there is a reasonable chance that it makes sense. And we still haven’t gotten into the details! it uses hierarchic (2-level) radial menus, but where the hierarchy is space multiplexed, rather than time multiplexed. That is, rather than doing one menu selection after the other, you do them simultaneously, by using a different hand to articulate the selection from each of the two menus. (While the text on the description is sparse still, look at the training cards, etc. and the photos on the page. )At the level of the mental model, there is no question in my mind (actually, I shouldn’t say that, because I am supposed to be an objective researcher who needs empirical data to inform decisions, but what the hell!) that you could give someone who knew how to use this device two isotonic joysticks, such as used with a video game controller, and they would be able to enter text just as fast as with this device. Furthermore, I am sure that if one had a slate capable of sensing both touch and stylus simultaneously, I am certain that the skill would transfer equally to using a touch radial gesture in the non-dominant hand, and stylus (or touch) radial gesture with the other. At the basic level, it is a 2-level radial menu, but where each level is operated independently and quasi-simultaneously by a different one of the operator’s two hands. Level 1: Right HandThis lets the operator select one of eight regionsThe label for each region consists of 6 characters (5 printing and one “special)In selecting one of the regions, one is not selecting any one of the characters of that region; rather, they are just indicating that the character that they want is one of the six in that regionEach of the characters in a region has a different background colour: blue, orange, green, yellow, red and black. Level 2: Left HandThis lets the operator select one of eight regionsEach region is labeled by a single colourAmong the colours that label the eight regions are the same ones used as character background colours in the regions of the right-hand control: blue, orange, green, yellow, red and blackBy the left hand selecting one of these six colours, one indicates which character is to be entered from among the six characters in the region indicated by the right hand – the selected character being the one whose background colour corresponds to the colour selected by the left hand. Hence, there are two 8-way, single level radial menus used. I believe it fair to say that it is, nevertheless, a 2 level radial menu, since both need to be used in order to enter one token. In actual fact, things are more complex, since none of the above covers issues such as all of the special character, punctuation, etc. , that do not appear on the labels of the right hand. To keep things brief, this is why only 6 of the left-hand menu options are used in what is discussed above. The other two options are needed to fill in the gaps. And, even then, the device resorts to something like double-clicks to get special modes and capabilities. For example, double clicking the black (south) region of the left hand turns the right-hand dome into a pointing device, i. e. , a mouse substitute for pointing, etc. I went through the – as it turned out – interesting exercise of translating the two parallel depth-1 radial menus of the orbiTouch UI into two different depth-2, breadth-8 hierarchic radial menus. You can see them in the attached images. The one assumes that the LH “dome” as the first-level selection, and then make the second-level selection with the right-hand dome. The other does the opposite, i. e. , the right-hand dome selection is the first level. It is interesting to compare the two with each other, as well as with both the labeling on the orbiTouch and the Quickstart documentation: The RH level-1 version seems easier to get rudimentary understanding compared to the LH due to clustering of letters and numbers on outer menus. Likewise, for the special characters that are the upper case of the numbersThe physical device is fine for letting you hunt-and-peck, so to speak, for characters, but it is useless for numbers, and most special characters. The documentation provided with the Quick Start (attached is not especially useful in terms of providing heuristics for memorization. While the orbiTouch certainly uses radial menus, it decidedly does not employ marking menus. One of the key things missing is the ability to check and correct before committing to an input, and the lack of ability to backtrack to the start, and therefore abort without entering anything. One thing that I have learned from this exercise is the difference that results due to having self-returning joysticks. Gestures don’t have that attribute. It matters esp w. r. t. the last point. What I like about this story, is how looking at something seemingly very different at the right level of abstraction, teaches us/me something new about something I was supposed to be an expert in. That is, that 2-level hierarchic marking menus can be achieved by two simultaneous single-level MMs. This is why I have the collection, and why I love what I do. There is still delight, despite being a 63-year-old geezer grandfather. The orbiTouch Keyless Keyboard was first known as the Keybowl, and the company was formerly known as Keybowl Inc. , and then Blue Orb Inc."
+ }
+] \ No newline at end of file
diff --git a/src/scraping/buxton/json/incomplete.json b/src/scraping/buxton/json/incomplete.json
new file mode 100644
index 000000000..595412e56
--- /dev/null
+++ b/src/scraping/buxton/json/incomplete.json
@@ -0,0 +1,563 @@
+[
+ {
+ "filename": "3DMag.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "3DPlus.docx",
+ "year": "ERR__YEAR__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "3DSpace.docx",
+ "year": "ERR__YEAR__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "3Dconnexion_SpaceNavigator.docx",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "3MErgo.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "ADB2.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "AWrock.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Abaton.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Active.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "AlphaSmart_Pro.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "Amazon_Kindle_Keyboard.docx",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Apple_ADB_Mouse.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured."
+ },
+ {
+ "filename": "Apple_Adj_Keyboard.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Apple_Mac_Portable-Katy’s MacBook Air-2.docx",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Apple_Mac_Portable-Katy’s MacBook Air.docx",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Apple_Mac_Portable.docx",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Apple_Scroll_Mouse.docx",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Apple_iPhone.docx",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "BAT.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Bill_Notes_CyKey.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "Brailler.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Brewster_Stereoscope.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "CasioC801.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "CasioTC500.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Casio_Mini.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Citizen_LC_909.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Citizen_LC_913.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured."
+ },
+ {
+ "filename": "Citizen_LCl_914.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "CoolPix.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Cross.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Dymo_MK-6.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Emotiv.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Explorer.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Falcon.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "FingerWorks_Prototype.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Freeboard.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match was captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "FrogPad.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "FujitsuPalm.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "FujitsuTouch.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "GRiD1550-Katy’s MacBook Air-2.docx",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "GRiD1550-Katy’s MacBook Air.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "GRiD1550.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Gavilan_SC.docx",
+ "company": "ERR__COMPANY__: outer match wasn't captured.",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Genius_Ring_Mouse.docx",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Grandjean_Stenotype.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "HTC_Touch.docx",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured."
+ },
+ {
+ "filename": "Helios-Klimax.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Honeywell_T86.docx",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "IBMTrack.docx",
+ "year": "ERR__YEAR__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "IBM_Convertable-Katy’s MacBook Air-2.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "IBM_Convertable-Katy’s MacBook Air.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "IBM_Convertable.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "IBM_PS2_Mouse.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "IBM_Simon.docx",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value."
+ },
+ {
+ "filename": "IDEO.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Joyboard.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Kensington_SB_TB-Mouse.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Kindle_3G_lighted_cover.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "Leatherman_Tread.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "M1.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured."
+ },
+ {
+ "filename": "MS-1_Stereoscope.docx",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "MWB_Braille_Writer.docx",
+ "company": "ERR__COMPANY__: outer match wasn't captured.",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "MaltronLH.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "Marine_Band_Harmonica.docx",
+ "company": "ERR__COMPANY__: outer match was captured.",
+ "year": "ERR__YEAR__: outer match was captured.",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Matrox.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "Metaphor_Kbd.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Metaphor_Mouse.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Microwriter.docx",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Motorola_DynaTAC.docx",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "MousePen.docx",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "NB75D.docx",
+ "year": "ERR__YEAR__: outer match was captured.",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "NewO.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Newton120.docx",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Nikon_Coolpix-100.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Numonics_Mgr_Mouse.docx",
+ "company": "ERR__COMPANY__: outer match was captured.",
+ "year": "ERR__YEAR__: outer match was captured.",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "PARCkbd.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured."
+ },
+ {
+ "filename": "PadMouse.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Philco_Mystery_Control.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "PowerTrack.docx",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "ProAgio.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "Pulsar_time_Computer.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "Ring.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "SafeType_Kbd.docx",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "Samsung_SPH-A500.docx",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "SurfMouse.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured."
+ },
+ {
+ "filename": "TPARCtab.docx",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "The_Tap.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "Thumbelina.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured."
+ },
+ {
+ "filename": "adecm.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "eMate.docx",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "gravis.docx",
+ "year": "ERR__YEAR__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "iGesture.docx",
+ "year": "__ERR__YEAR__TRANSFORM__: NaN cannot be parsed to a numeric value.",
+ "primaryKey": "ERR__PRIMARYKEY__: outer match was captured.",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "iGrip.docx",
+ "shortDescription": "ERR__SHORTDESCRIPTION__: outer match was captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ },
+ {
+ "filename": "iLiad.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "degreesOfFreedom": "ERR__DEGREESOFFREEDOM__: outer match wasn't captured."
+ },
+ {
+ "filename": "round.docx",
+ "secondaryKey": "ERR__SECONDARYKEY__: outer match wasn't captured.",
+ "originalPrice": "ERR__ORIGINALPRICE__: outer match wasn't captured.",
+ "dimensions": "ERR__DIMENSIONS__: outer match wasn't captured.",
+ "longDescription": "ERR__LONGDESCRIPTION__: outer match was captured."
+ }
+] \ No newline at end of file
diff --git a/src/scraping/buxton/jsonifier.py b/src/scraping/buxton/jsonifier.py
new file mode 100644
index 000000000..a315d49c0
--- /dev/null
+++ b/src/scraping/buxton/jsonifier.py
@@ -0,0 +1,231 @@
+import os
+import docx2txt
+from docx import Document
+from docx.opc.constants import RELATIONSHIP_TYPE as RT
+import re
+import shutil
+import uuid
+import json
+import base64
+from shutil import copyfile
+from PIL import Image
+
+files_path = "../../server/public/files"
+source_path = "./source"
+temp_images_path = "./extracted_images"
+server_images_path = f"{files_path}/images/buxton"
+json_path = "./json"
+
+
+# noinspection PyProtectedMember
+def extract_links(file):
+ links = []
+ doc = Document(file)
+ rels = doc.part.rels
+ for rel in rels:
+ item = rels[rel]
+ if item.reltype == RT.HYPERLINK and ".aspx" not in item._target:
+ links.append(item._target)
+ return links
+
+
+def extract_value(kv_string):
+ pieces = kv_string.split(":")
+ return (pieces[1] if len(pieces) > 1 else kv_string).strip()
+
+
+def mkdir_if_absent(path):
+ try:
+ if not os.path.exists(path):
+ os.mkdir(path)
+ except OSError:
+ print("failed to create the appropriate directory structures for %s" % file_name)
+
+
+def guid():
+ return str(uuid.uuid4())
+
+
+def encode_image(folder: str, name: str):
+ with open(f"{temp_images_path}/{folder}/{name}", "rb") as image:
+ encoded = base64.b64encode(image.read())
+ return encoded.decode("utf-8")
+
+
+def parse_document(name: str):
+ print(f"parsing {name}...")
+ pure_name = name.split(".")[0]
+
+ result = {}
+
+ saved_device_images_dir = server_images_path + "/" + pure_name
+ temp_device_images_dir = temp_images_path + "/" + pure_name
+ mkdir_if_absent(temp_device_images_dir)
+ mkdir_if_absent(saved_device_images_dir)
+
+ raw = str(docx2txt.process(source_path +
+ "/" + name, temp_device_images_dir))
+
+ extracted_images = []
+ for image in os.listdir(temp_device_images_dir):
+ temp = f"{temp_device_images_dir}/{image}"
+ native_width, native_height = Image.open(temp).size
+ if abs(native_width - native_height) < 10:
+ continue
+ original = saved_device_images_dir + "/" + image.replace(".", "_o.", 1)
+ medium = saved_device_images_dir + "/" + image.replace(".", "_m.", 1)
+ copyfile(temp, original)
+ copyfile(temp, medium)
+ server_path = f"http://localhost:1050/files/images/buxton/{pure_name}/{image}"
+ extracted_images.append(server_path)
+ result["extracted_images"] = extracted_images
+
+ def sanitize(line): return re.sub("[\n\t]+", "", line).replace(u"\u00A0", " ").replace(
+ u"\u2013", "-").replace(u"\u201c", '''"''').replace(u"\u201d", '''"''').strip()
+
+ def sanitize_price(raw_price: str):
+ raw_price = raw_price.replace(",", "")
+ start = raw_price.find("$")
+ if "x" in raw_price.lower():
+ return None
+ if start > -1:
+ i = start + 1
+ while i < len(raw_price) and re.match(r"[0-9.]", raw_price[i]):
+ i += 1
+ price = raw_price[start + 1: i + 1]
+ return float(price)
+ elif raw_price.lower().find("nfs"):
+ return -1
+ else:
+ return None
+
+ def remove_empty(line): return len(line) > 1
+
+ def try_parse(to_parse: int):
+ try:
+ value = int(to_parse)
+ return value
+ except ValueError:
+ value = None
+ return value
+
+ lines = list(map(sanitize, raw.split("\n")))
+ lines = list(filter(remove_empty, lines))
+
+ result["title"] = lines[2].strip()
+ result["short_description"] = lines[3].strip().replace(
+ "Short Description: ", "")
+
+ cur = 5
+ notes = ""
+ while lines[cur] != "Device Details":
+ notes += lines[cur] + " "
+ cur += 1
+ result["buxton_notes"] = notes.strip()
+
+ cur += 1
+ clean = list(
+ map(lambda data: data.strip().split(":"), lines[cur].split("|")))
+ result["company"] = clean[0][len(clean[0]) - 1].strip()
+
+ result["year"] = try_parse(clean[1][len(clean[1]) - 1].strip())
+ result["original_price"] = sanitize_price(
+ clean[2][len(clean[2]) - 1].strip())
+
+ cur += 1
+
+ result["degrees_of_freedom"] = try_parse(extract_value(
+ lines[cur]).replace("NA", "N/A"))
+ cur += 1
+
+ dimensions = lines[cur].lower()
+ if dimensions.startswith("dimensions"):
+ dim_concat = dimensions[11:].strip()
+ cur += 1
+ while lines[cur] != "Key Words":
+ dim_concat += (" " + lines[cur].strip())
+ cur += 1
+ result["dimensions"] = dim_concat
+ else:
+ result["dimensions"] = "N/A"
+
+ cur += 1
+ result["primary_key"] = extract_value(lines[cur])
+ cur += 1
+ result["secondary_key"] = extract_value(lines[cur])
+
+ while lines[cur] != "Links":
+ result["secondary_key"] += (" " + extract_value(lines[cur]).strip())
+ cur += 1
+
+ cur += 1
+ link_descriptions = []
+ while lines[cur] != "Image":
+ description = lines[cur].strip().lower()
+ valid = True
+ for ignored in ["powerpoint", "vimeo", "xxx"]:
+ if ignored in description:
+ valid = False
+ break
+ if valid:
+ link_descriptions.append(description)
+ cur += 1
+ result["link_descriptions"] = link_descriptions
+
+ result["hyperlinks"] = extract_links(source_path + "/" + name)
+
+ images = []
+ captions = []
+ cur += 3
+ while cur + 1 < len(lines) and lines[cur] != "NOTES:":
+ name = lines[cur]
+ if "full document" not in name.lower():
+ images.append(name)
+ captions.append(lines[cur + 1])
+ cur += 2
+ result["table_image_names"] = images
+
+ result["captions"] = captions
+
+ notes = []
+ if cur < len(lines) and lines[cur] == "NOTES:":
+ cur += 1
+ while cur < len(lines):
+ notes.append(lines[cur])
+ cur += 1
+ if len(notes) > 0:
+ result["notes"] = notes
+
+ return result
+
+
+if os.path.exists(server_images_path):
+ shutil.rmtree(server_images_path)
+while os.path.exists(server_images_path):
+ pass
+os.mkdir(server_images_path)
+
+mkdir_if_absent(source_path)
+mkdir_if_absent(json_path)
+mkdir_if_absent(temp_images_path)
+
+results = []
+
+candidates = 0
+for file_name in os.listdir(source_path):
+ if file_name.endswith('.docx') or file_name.endswith(".doc"):
+ candidates += 1
+ results.append(parse_document(file_name))
+
+
+with open(f"./json/buxton_collection.json", "w", encoding="utf-8") as out:
+ json.dump(results, out, ensure_ascii=False, indent=4)
+
+print(f"\nSuccessfully parsed {candidates} candidates.")
+
+print("\nrewriting .gitignore...")
+entries = ['*', '!.gitignore']
+with open(files_path + "/.gitignore", 'w') as f:
+ f.write('\n'.join(entries))
+
+shutil.rmtree(temp_images_path)
diff --git a/src/scraping/buxton/node_scraper.ts b/src/scraping/buxton/node_scraper.ts
index ef1d989d4..c1d4088b9 100644
--- a/src/scraping/buxton/node_scraper.ts
+++ b/src/scraping/buxton/node_scraper.ts
@@ -1,8 +1,114 @@
-import { readdirSync } from "fs";
-import { resolve } from "path";
-
+import { readdirSync, writeFile, existsSync, mkdirSync } from "fs";
+import * as path from "path";
+import { red, cyan, yellow } from "colors";
const StreamZip = require('node-stream-zip');
+export interface DeviceDocument {
+ title: string;
+ shortDescription: string;
+ longDescription: string;
+ company: string;
+ year: number;
+ originalPrice: number;
+ degreesOfFreedom: number;
+ dimensions: string;
+ primaryKey: string;
+ secondaryKey: string;
+}
+
+type Converter<T> = (raw: string) => { transformed?: T, error?: string };
+
+interface Processor<T> {
+ exp: RegExp;
+ matchIndex?: number;
+ transformer?: Converter<T>;
+}
+
+const RegexMap = new Map<keyof DeviceDocument, Processor<any>>([
+ ["title", {
+ exp: /contact\s+(.*)Short Description:/
+ }],
+ ["company", {
+ exp: /Company:\s+([^\|]*)\s+\|/,
+ transformer: (raw: string) => ({ transformed: raw.replace(/\./g, "") })
+ }],
+ ["year", {
+ exp: /Year:\s+([^\|]*)\s+\|/,
+ transformer: numberValue
+ }],
+ ["primaryKey", {
+ exp: /Primary:\s+(.*)(Secondary|Additional):/,
+ transformer: collectUniqueTokens
+ }],
+ ["secondaryKey", {
+ exp: /(Secondary|Additional):\s+([^\{\}]*)Links/,
+ transformer: collectUniqueTokens,
+ matchIndex: 2
+ }],
+ ["originalPrice", {
+ exp: /Original Price \(USD\)\:\s+\$([0-9\.]+)/,
+ transformer: numberValue
+ }],
+ ["degreesOfFreedom", {
+ exp: /Degrees of Freedom:\s+([0-9]+)/,
+ transformer: numberValue
+ }],
+ ["dimensions", {
+ exp: /Dimensions\s+\(L x W x H\):\s+([0-9\.]+\s+x\s+[0-9\.]+\s+x\s+[0-9\.]+\s\([A-Za-z]+\))/,
+ transformer: (raw: string) => {
+ const [length, width, group] = raw.split(" x ");
+ const [height, unit] = group.split(" ");
+ return {
+ transformed: {
+ length: Number(length),
+ width: Number(width),
+ height: Number(height),
+ unit: unit.replace(/[\(\)]+/g, "")
+ }
+ };
+ }
+ }],
+ ["shortDescription", {
+ exp: /Short Description:\s+(.*)Bill Buxton[’']s Notes/,
+ transformer: correctWordParagraphs
+ }],
+ ["longDescription", {
+ exp: /Bill Buxton[’']s Notes(.*)Device Details/,
+ transformer: correctWordParagraphs
+ }],
+]);
+
+function numberValue(raw: string) {
+ const transformed = Number(raw);
+ if (isNaN(transformed)) {
+ return { error: `${transformed} cannot be parsed to a numeric value.` };
+ }
+ return { transformed };
+}
+
+function collectUniqueTokens(raw: string) {
+ return { transformed: Array.from(new Set(raw.replace(/,|\s+and\s+/g, " ").split(/\s+/).map(token => token.toLowerCase().trim()))).map(capitalize).sort() };
+}
+
+function correctWordParagraphs(raw: string) {
+ raw = raw.replace(/\./g, ". ").replace(/\:/g, ": ").replace(/\,/g, ", ").replace(/\?/g, "? ").trimRight();
+ raw = raw.replace(/\s{2,}/g, " ");
+ return { transformed: raw };
+}
+
+const outDir = path.resolve(__dirname, "json");
+const successOut = "buxton.json";
+const failOut = "incomplete.json";
+const deviceKeys = Array.from(RegexMap.keys());
+
+function printEntries(zip: any) {
+ console.log("READY!", zip.entriesCount);
+ for (const entry of Object.values(zip.entries()) as any[]) {
+ const desc = entry.isDirectory ? 'directory' : `${entry.size} bytes`;
+ console.log(`Entry ${entry.name}: ${desc}`);
+ }
+}
+
export async function open(path: string) {
const zip = new StreamZip({
file: path,
@@ -10,11 +116,6 @@ export async function open(path: string) {
});
return new Promise<string>((resolve, reject) => {
zip.on('ready', () => {
- console.log("READY!", zip.entriesCount);
- for (const entry of Object.values(zip.entries()) as any[]) {
- const desc = entry.isDirectory ? 'directory' : `${entry.size} bytes`;
- console.log(`Entry ${entry.name}: ${desc}`);
- }
let body = "";
zip.stream("word/document.xml", (error: any, stream: any) => {
if (error) {
@@ -36,22 +137,115 @@ export async function extract(path: string) {
const components = contents.toString().split('<w:t');
for (const component of components) {
const tags = component.split('>');
- console.log(tags[1]);
const content = tags[1].replace(/<.*$/, "");
body += content;
}
return body;
}
-async function parse(): Promise<string[]> {
- const sourceDirectory = resolve(`${__dirname}/source`);
+function tryGetValidCapture(matches: RegExpExecArray | null, matchIndex: number) {
+ let captured: string;
+ if (!matches || !(captured = matches[matchIndex])) {
+ return undefined;
+ }
+ const lower = captured.toLowerCase();
+ if (/to come/.test(lower)) {
+ return undefined;
+ }
+ if (lower.includes("xxx")) {
+ return undefined;
+ }
+ if (!captured.toLowerCase().replace(/[….\s]+/g, "").length) {
+ return undefined;
+ }
+ return captured;
+}
+
+function capitalize(word: string) {
+ const clean = word.trim();
+ if (!clean.length) {
+ return word;
+ }
+ return word.charAt(0).toUpperCase() + word.slice(1);
+}
+
+export function analyze(path: string, body: string): { device?: DeviceDocument, errors?: any } {
+ const device: any = {};
+
+ const segments = path.split("/");
+ const filename = segments[segments.length - 1].replace("Bill_Notes_", "");
+
+ const errors: any = { filename };
+
+ for (const key of deviceKeys) {
+ const { exp, transformer, matchIndex } = RegexMap.get(key)!;
+ const matches = exp.exec(body);
+
+ let captured = tryGetValidCapture(matches, matchIndex ?? 1);
+ if (!captured) {
+ errors[key] = `ERR__${key.toUpperCase()}__: outer match ${matches === null ? "wasn't" : "was"} captured.`;
+ continue;
+ }
+
+ captured = captured.replace(/\s{2,}/g, " ");
+ if (transformer) {
+ const { error, transformed } = transformer(captured);
+ if (error) {
+ errors[key] = `__ERR__${key.toUpperCase()}__TRANSFORM__: ${error}`;
+ continue;
+ }
+ captured = transformed;
+ }
+
+ device[key] = captured;
+ }
+
+ const errorKeys = Object.keys(errors);
+ if (errorKeys.length > 1) {
+ console.log(red(`\n@ ${cyan(filename.toUpperCase())}...`));
+ errorKeys.forEach(key => key !== "filename" && console.log(red(errors[key])));
+ return { errors };
+ }
+
+ return { device };
+}
+
+async function parse() {
+ const sourceDirectory = path.resolve(`${__dirname}/source`);
const candidates = readdirSync(sourceDirectory).filter(file => file.endsWith(".doc") || file.endsWith(".docx")).map(file => `${sourceDirectory}/${file}`);
- await extract(candidates[0]);
- try {
- return Promise.all(candidates.map(extract));
- } catch {
- return [];
+ const imported = await Promise.all(candidates.map(async path => ({ path, body: await extract(path) })));
+ // const imported = [{ path: candidates[10], body: await extract(candidates[10]) }];
+ const data = imported.map(({ path, body }) => analyze(path, body));
+ const masterdevices: DeviceDocument[] = [];
+ const masterErrors: any[] = [];
+ data.forEach(({ device, errors }) => {
+ if (device) {
+ masterdevices.push(device);
+ } else {
+ masterErrors.push(errors);
+ }
+ });
+ const total = candidates.length;
+ if (masterdevices.length + masterErrors.length !== total) {
+ throw new Error(`Encountered a ${masterdevices.length} to ${masterErrors.length} mismatch in device / error split!`);
}
+ console.log();
+ await writeOutputFile(successOut, masterdevices, total, true);
+ await writeOutputFile(failOut, masterErrors, total, false);
+ console.log();
+}
+
+async function writeOutputFile(relativePath: string, data: any[], total: number, success: boolean) {
+ console.log(yellow(`Encountered ${data.length} ${success ? "valid" : "invalid"} documents out of ${total} candidates. Writing ${relativePath}...`));
+ return new Promise<void>((resolve, reject) => {
+ const destination = path.resolve(outDir, relativePath);
+ const contents = JSON.stringify(data, undefined, 4);
+ writeFile(destination, contents, err => err ? reject(err) : resolve());
+ });
+}
+
+if (!existsSync(outDir)) {
+ mkdirSync(outDir);
}
parse(); \ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 6bfc729de..8b760db00 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -15,6 +15,9 @@ import { nullAudio } from "../../../new_fields/URLField";
import { DragManager } from "../../../client/util/DragManager";
import { InkingControl } from "../../../client/views/InkingControl";
import { CollectionViewType } from "../../../client/views/collections/CollectionView";
+import { makeTemplate } from "../../../client/util/DropConverter";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { PrefetchProxy } from "../../../new_fields/Proxy";
export class CurrentUserUtils {
private static curr_id: string;
@@ -45,17 +48,19 @@ export class CurrentUserUtils {
// setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
static setupCreatorButtons(doc: Doc, buttons?: string[]) {
const notes = CurrentUserUtils.setupNoteTypes(doc);
+ const emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, showTitle: "title", boxShadow: "0 0" });
+ const emptyCollection = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" });
doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", _height: 75 });
doc.activePen = doc;
const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
- { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" })' },
+ { title: "collection", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: emptyCollection },
{ title: "preview", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,true,[_last_])?.[0]"), { _width: 250, _height: 250, title: "container" })' },
{ title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
{ title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", {_width: 300, _height: 300, title: "New Webpage" })' },
{ title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 200, title: "an image of a cat" })' },
{ title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
{ title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: "Button" })' },
- { title: "presentation", icon: "tv", ignoreClick: true, drag: `Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { _width: 200, _height: 500, _xMargin:5, _viewType: ${CollectionViewType.Stacking}, title: "a presentation trail" })` },
+ { title: "presentation", icon: "tv", click: 'openOnRight(Doc.UserDoc().curPresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().curPresentation = getCopy(this.dragFactory,true)`, dragFactory: emptyPresentation },
{ title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
{ title: "mobile view", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
{ title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
@@ -66,7 +71,7 @@ export class CurrentUserUtils {
{ title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
];
return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
- _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
@@ -104,7 +109,7 @@ export class CurrentUserUtils {
{ title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
];
return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
- _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
@@ -120,7 +125,7 @@ export class CurrentUserUtils {
{ title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
];
return docProtoData.map(data => Docs.Create.FontIconDocument({
- _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, _dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
clipboard: data.clipboard,
onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
@@ -153,7 +158,7 @@ export class CurrentUserUtils {
});
// setup a color picker
const color = Docs.Create.ColorDocument({
- title: "color picker", _width: 300, _dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
+ title: "color picker", _width: 300, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
});
return Docs.Create.ButtonDocument({
@@ -186,7 +191,7 @@ export class CurrentUserUtils {
_width: 50, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Library", fontSize: 10,
letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
- title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, _dropAction: "alias", lockedPosition: true, boxShadow: "0 0",
+ title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true, boxShadow: "0 0",
}),
targetContainer: sidebarContainer,
onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;")
@@ -227,14 +232,29 @@ export class CurrentUserUtils {
/// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
static setupExpandingButtons(doc: Doc) {
+ const slideTemplate = Docs.Create.StackingDocument(
+ [
+ Docs.Create.MulticolumnDocument([], { title: "images", _height: 200, _xMargin: 10, _yMargin: 10 }),
+ Docs.Create.TextDocument("", { title: "contents", _height: 100 })
+ ],
+ { _width: 400, _height: 300, title: "slide", _chromeStatus: "disabled", backgroundColor: "lightGray", _autoHeight: true });
+ slideTemplate.isTemplateDoc = makeTemplate(slideTemplate);
+
+ const iconDoc = Docs.Create.TextDocument("", { title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onClick: ScriptField.MakeScript("setNativeView(this)") });
+ Doc.GetProto(iconDoc).data = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', "");
+ doc.isTemplateDoc = makeTemplate(iconDoc);
+ doc.iconView = new PrefetchProxy(iconDoc);
+
doc.undoBtn = Docs.Create.FontIconDocument(
- { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List<string>(["dropAction"]), title: "undo button", icon: "undo-alt" });
+ { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List<string>(["dropAction"]), title: "undo button", icon: "undo-alt" });
doc.redoBtn = Docs.Create.FontIconDocument(
- { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), removeDropProperties: new List<string>(["dropAction"]), title: "redo button", icon: "redo-alt" });
-
- doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], {
+ { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), removeDropProperties: new List<string>(["dropAction"]), title: "redo button", icon: "redo-alt" });
+ doc.slidesBtn = Docs.Create.FontIconDocument(
+ { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: slideTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "sticky-note" });
+ doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc, doc.slidesBtn as Doc], {
title: "expanding buttons", _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0",
backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true,
+
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
});
}
@@ -248,7 +268,7 @@ export class CurrentUserUtils {
// the initial presentation Doc to use
static setupDefaultPresentation(doc: Doc) {
- doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, boxShadow: "0 0" });
+ doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, showTitle: "title", boxShadow: "0 0" });
}
static setupMobileUploads(doc: Doc) {
@@ -286,6 +306,15 @@ export class CurrentUserUtils {
return doc;
}
+ public static IsDocPinned(doc: Doc) {
+ //add this new doc to props.Document
+ const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
+ if (curPres) {
+ return DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)) !== -1;
+ }
+ return false;
+ }
+
public static async loadCurrentUser() {
return rp.get(Utils.prepend("/getCurrentUser")).then(response => {
if (response) {