aboutsummaryrefslogtreecommitdiff
path: root/src/client/util
diff options
context:
space:
mode:
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/TypedEvent.ts42
5 files changed, 291 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/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