aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
authorMonika Hedman <monika_hedman@brown.edu>2019-02-12 16:26:22 -0500
committerMonika Hedman <monika_hedman@brown.edu>2019-02-12 16:26:22 -0500
commit05a710e2a541a07347d1626489a1811874126c79 (patch)
treefeb751e46036d2104bb81c5bcc4e4ab0b6336c08 /src/client/util
parent6445930e05e8eb81a36930615926712986bc1a9d (diff)
parent7a93f60c9529e5d175e617fc7c07145a9b33e572 (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into hedmanLocal
Diffstat (limited to 'src/client/util')
-rw-r--r--src/client/util/DragManager.ts131
-rw-r--r--src/client/util/Scripting.ts58
-rw-r--r--src/client/util/ScrollBox.tsx21
-rw-r--r--src/client/util/SelectionManager.ts39
-rw-r--r--src/client/util/Transform.ts94
-rw-r--r--src/client/util/TypedEvent.ts42
6 files changed, 385 insertions, 0 deletions
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
new file mode 100644
index 000000000..f4dcce7c8
--- /dev/null
+++ b/src/client/util/DragManager.ts
@@ -0,0 +1,131 @@
+
+export namespace DragManager {
+ export function Root() {
+ const root = document.getElementById("root");
+ if (!root) {
+ throw new Error("No root element found");
+ }
+ return root;
+ }
+
+ let dragDiv: HTMLDivElement;
+
+ export enum DragButtons {
+ Left = 1, Right = 2, Both = Left | Right
+ }
+
+ interface DragOptions {
+ handlers: DragHandlers;
+
+ hideSource: boolean | (() => boolean);
+ }
+
+ export interface DragDropDisposer {
+ (): void;
+ }
+
+ export class DragCompleteEvent {
+ }
+
+ export interface DragHandlers {
+ dragComplete: (e: DragCompleteEvent) => void;
+ }
+
+ export interface DropOptions {
+ handlers: DropHandlers;
+ }
+
+ export class DropEvent {
+ constructor(readonly x: number, readonly y: number, readonly data: { [id: string]: any }) { }
+ }
+
+ export interface DropHandlers {
+ drop: (e: Event, de: DropEvent) => void;
+ }
+
+ export function MakeDropTarget(element: HTMLElement, options: DropOptions): DragDropDisposer {
+ if ("canDrop" in element.dataset) {
+ throw new Error("Element is already droppable, can't make it droppable again");
+ }
+ element.dataset["canDrop"] = "true";
+ const handler = (e: Event) => {
+ const ce = e as CustomEvent<DropEvent>;
+ options.handlers.drop(e, ce.detail);
+ };
+ element.addEventListener("dashOnDrop", handler);
+ return () => {
+ element.removeEventListener("dashOnDrop", handler);
+ delete element.dataset["canDrop"]
+ };
+ }
+
+
+ let _lastPointerX: number = 0;
+ let _lastPointerY: number = 0;
+ export function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options: DragOptions) {
+ if (!dragDiv) {
+ dragDiv = document.createElement("div");
+ DragManager.Root().appendChild(dragDiv);
+ }
+ const w = ele.offsetWidth, h = ele.offsetHeight;
+ const rect = ele.getBoundingClientRect();
+ const scaleX = rect.width / w, scaleY = rect.height / h;
+ let x = rect.left, y = rect.top;
+ // const offsetX = e.x - rect.left, offsetY = e.y - rect.top;
+ let dragElement = ele.cloneNode(true) as HTMLElement;
+ dragElement.style.opacity = "0.7";
+ dragElement.style.position = "absolute";
+ dragElement.style.transformOrigin = "0 0";
+ dragElement.style.zIndex = "1000";
+ dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
+ dragDiv.appendChild(dragElement);
+ _lastPointerX = dragData["xOffset"] + rect.left;
+ _lastPointerY = dragData["yOffset"] + rect.top;
+
+ let hideSource = false;
+ if (typeof options.hideSource === "boolean") {
+ hideSource = options.hideSource;
+ } else {
+ hideSource = options.hideSource();
+ }
+ const wasHidden = ele.hidden;
+ if (hideSource) {
+ ele.hidden = true;
+ }
+
+ const moveHandler = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ x += e.clientX - _lastPointerX; _lastPointerX = e.clientX;
+ y += e.clientY - _lastPointerY; _lastPointerY = e.clientY;
+ dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
+ };
+ const upHandler = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", moveHandler, true);
+ document.removeEventListener("pointerup", upHandler);
+ FinishDrag(dragElement, e, options, dragData);
+ if (hideSource && !wasHidden) {
+ ele.hidden = false;
+ }
+ };
+ document.addEventListener("pointermove", moveHandler, true);
+ document.addEventListener("pointerup", upHandler);
+ }
+
+ function FinishDrag(dragEle: HTMLElement, e: PointerEvent, options: DragOptions, dragData: { [index: string]: any }) {
+ dragDiv.removeChild(dragEle);
+ const target = document.elementFromPoint(e.x, e.y);
+ if (!target) {
+ return;
+ }
+ target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", {
+ bubbles: true,
+ detail: {
+ x: e.x,
+ y: e.y,
+ data: dragData
+ }
+ }));
+ options.handlers.dragComplete({});
+ }
+} \ No newline at end of file
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
new file mode 100644
index 000000000..6bc5fa412
--- /dev/null
+++ b/src/client/util/Scripting.ts
@@ -0,0 +1,58 @@
+// import * as ts from "typescript"
+let ts = (window as any).ts;
+import { Opt, Field } from "../../fields/Field";
+import { Document as DocumentImport } from "../../fields/Document";
+import { NumberField as NumberFieldImport, NumberField } from "../../fields/NumberField";
+import { ImageField as ImageFieldImport } from "../../fields/ImageField";
+import { TextField as TextFieldImport, TextField } from "../../fields/TextField";
+import { RichTextField as RichTextFieldImport } from "../../fields/RichTextField";
+import { KeyStore as KeyStoreImport } from "../../fields/Key";
+
+export interface ExecutableScript {
+ (): any;
+
+ compiled: boolean;
+}
+
+function ExecScript(script: string, diagnostics: Opt<any[]>): ExecutableScript {
+ const compiled = !(diagnostics && diagnostics.some(diag => diag.category == ts.DiagnosticCategory.Error));
+
+ let func: () => Opt<Field>;
+ if (compiled) {
+ func = function (): Opt<Field> {
+ let KeyStore = KeyStoreImport;
+ let Document = DocumentImport;
+ let NumberField = NumberFieldImport;
+ let TextField = TextFieldImport;
+ let ImageField = ImageFieldImport;
+ let RichTextField = RichTextFieldImport;
+ let window = undefined;
+ let document = undefined;
+ let retVal = eval(script);
+
+ return retVal;
+ };
+ } else {
+ func = () => undefined;
+ }
+
+ return Object.assign(func,
+ {
+ compiled
+ });
+}
+
+export function CompileScript(script: string): ExecutableScript {
+ let result = (window as any).ts.transpileModule(script, {})
+
+ return ExecScript(result.outputText, result.diagnostics);
+}
+
+export function ToField(data: any): Opt<Field> {
+ if (typeof data == "string") {
+ return new TextField(data);
+ } else if (typeof data == "number") {
+ return new NumberField(data);
+ }
+ return undefined;
+} \ No newline at end of file
diff --git a/src/client/util/ScrollBox.tsx b/src/client/util/ScrollBox.tsx
new file mode 100644
index 000000000..b6b088170
--- /dev/null
+++ b/src/client/util/ScrollBox.tsx
@@ -0,0 +1,21 @@
+import React = require("react")
+
+export class ScrollBox extends React.Component {
+ onWheel = (e: React.WheelEvent) => {
+ if (e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { // If the element has a scroll bar, then we don't want the containing collection to zoom
+ e.stopPropagation();
+ }
+ }
+
+ render() {
+ return (
+ <div style={{
+ overflow: "auto",
+ width: "100%",
+ height: "100%",
+ }} onWheel={this.onWheel}>
+ {this.props.children}
+ </div>
+ )
+ }
+} \ No newline at end of file
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
new file mode 100644
index 000000000..0759ae110
--- /dev/null
+++ b/src/client/util/SelectionManager.ts
@@ -0,0 +1,39 @@
+import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
+import { observable, action } from "mobx";
+
+export namespace SelectionManager {
+ class Manager {
+ @observable
+ SelectedDocuments: Array<CollectionFreeFormDocumentView> = [];
+
+ @action
+ SelectDoc(doc: CollectionFreeFormDocumentView, ctrlPressed: boolean): void {
+ // if doc is not in SelectedDocuments, add it
+ if (!ctrlPressed) {
+ manager.SelectedDocuments = [];
+ }
+
+ if (manager.SelectedDocuments.indexOf(doc) === -1) {
+ manager.SelectedDocuments.push(doc)
+ }
+ }
+ }
+
+ const manager = new Manager;
+
+ export function SelectDoc(doc: CollectionFreeFormDocumentView, ctrlPressed: boolean): void {
+ manager.SelectDoc(doc, ctrlPressed)
+ }
+
+ export function IsSelected(doc: CollectionFreeFormDocumentView): boolean {
+ return manager.SelectedDocuments.indexOf(doc) !== -1;
+ }
+
+ export function DeselectAll(): void {
+ manager.SelectedDocuments = []
+ }
+
+ export function SelectedDocuments(): Array<CollectionFreeFormDocumentView> {
+ return manager.SelectedDocuments;
+ }
+} \ No newline at end of file
diff --git a/src/client/util/Transform.ts b/src/client/util/Transform.ts
new file mode 100644
index 000000000..7861ed308
--- /dev/null
+++ b/src/client/util/Transform.ts
@@ -0,0 +1,94 @@
+export class Transform {
+ private _translateX: number = 0;
+ private _translateY: number = 0;
+ private _scale: number = 1;
+
+ static get Identity(): Transform {
+ return new Transform(0, 0, 1);
+ }
+
+ constructor(x: number, y: number, scale: number) {
+ this._translateX = x;
+ this._translateY = y;
+ this._scale = scale;
+ }
+
+ translate = (x: number, y: number): Transform => {
+ this._translateX += x;
+ this._translateY += y;
+ return this;
+ }
+
+ translated = (x: number, y: number): Transform => {
+ return this.copy().translate(x, y);
+ }
+
+ preTranslate = (x: number, y: number): Transform => {
+ this._translateX += x * this._scale;
+ this._translateY += y * this._scale;
+ return this;
+ }
+
+ preTranslated = (x: number, y: number): Transform => {
+ return this.copy().preTranslate(x, y);
+ }
+
+ scale = (scale: number): Transform => {
+ this._scale *= scale;
+ return this;
+ }
+
+ scaled = (scale: number): Transform => {
+ return this.copy().scale(scale);
+ }
+
+ preScale = (scale: number): Transform => {
+ this._scale *= scale;
+ this._translateX *= scale;
+ this._translateY *= scale;
+ return this;
+ }
+
+ preScaled = (scale: number): Transform => {
+ return this.copy().preScale(scale);
+ }
+
+ transform = (transform: Transform): Transform => {
+ this._translateX += transform._translateX * this._scale;
+ this._translateY += transform._translateY * this._scale;
+ this._scale *= transform._scale;
+ return this;
+ }
+
+ transformed = (transform: Transform): Transform => {
+ return this.copy().transform(transform);
+ }
+
+ preTransform = (transform: Transform): Transform => {
+ this._translateX = transform._translateX + this._translateX * transform._scale;
+ this._translateY = transform._translateY + this._translateY * transform._scale;
+ this._scale *= transform._scale;
+ return this;
+ }
+
+ preTransformed = (transform: Transform): Transform => {
+ return this.copy().preTransform(transform);
+ }
+
+ transformPoint = (x: number, y: number): [number, number] => {
+ x *= this._scale;
+ x += this._translateX;
+ y *= this._scale;
+ y += this._translateY;
+ return [x, y];
+ }
+
+ inverse = () => {
+ return new Transform(-this._translateX / this._scale, -this._translateY / this._scale, 1 / this._scale)
+ }
+
+ copy = () => {
+ return new Transform(this._translateX, this._translateY, this._scale);
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/TypedEvent.ts b/src/client/util/TypedEvent.ts
new file mode 100644
index 000000000..0714a7f5c
--- /dev/null
+++ b/src/client/util/TypedEvent.ts
@@ -0,0 +1,42 @@
+export interface Listener<T> {
+ (event: T): any;
+}
+
+export interface Disposable {
+ dispose(): void;
+}
+
+/** passes through events as they happen. You will not get events from before you start listening */
+export class TypedEvent<T> {
+ private listeners: Listener<T>[] = [];
+ private listenersOncer: Listener<T>[] = [];
+
+ on = (listener: Listener<T>): Disposable => {
+ this.listeners.push(listener);
+ return {
+ dispose: () => this.off(listener)
+ };
+ }
+
+ once = (listener: Listener<T>): void => {
+ this.listenersOncer.push(listener);
+ }
+
+ off = (listener: Listener<T>) => {
+ var callbackIndex = this.listeners.indexOf(listener);
+ if (callbackIndex > -1) this.listeners.splice(callbackIndex, 1);
+ }
+
+ emit = (event: T) => {
+ /** Update any general listeners */
+ this.listeners.forEach((listener) => listener(event));
+
+ /** Clear the `once` queue */
+ this.listenersOncer.forEach((listener) => listener(event));
+ this.listenersOncer = [];
+ }
+
+ pipe = (te: TypedEvent<T>): Disposable => {
+ return this.on((e) => te.emit(e));
+ }
+} \ No newline at end of file