From ee03fa6e04dd9dba3099f75154de6ffab566ff5c Mon Sep 17 00:00:00 2001
From: Mohammad Amoush <muhammedamoush@gmail.com>
Date: Fri, 13 Sep 2019 16:43:09 -0400
Subject: Trial 5(Most succesful yet)

---
 src/client/views/nodes/DocumentContentsView.tsx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

(limited to 'src/client/views/nodes/DocumentContentsView.tsx')

diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d0e117fe4..df5ff04dd 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -32,6 +32,8 @@ import { Doc } from "../../../new_fields/Doc";
 import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { fromPromise } from "mobx-utils";
+import { DashWebCam } from "../../views/webcam/DashWebCam";
+
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -110,7 +112,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
         return <ObserverJsxParser
             blacklistedAttrs={[]}
-            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox }}
+            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, DashWebCam }}
             bindings={this.CreateBindings()}
             jsx={this.finalLayout}
             showWarnings={true}
-- 
cgit v1.2.3-70-g09d2


From 45b9f489033cd323614463ca9c36f41900bf1965 Mon Sep 17 00:00:00 2001
From: andrewdkim <adkim414@gmail.com>
Date: Tue, 1 Oct 2019 18:14:04 -0400
Subject: trials 2

---
 package.json                                    | 1 +
 src/client/documents/Documents.ts               | 3 ++-
 src/client/views/nodes/DocumentContentsView.tsx | 3 ++-
 src/client/views/webcam/DashWebRTC.tsx          | 9 +++++++--
 4 files changed, 12 insertions(+), 4 deletions(-)

(limited to 'src/client/views/nodes/DocumentContentsView.tsx')

diff --git a/package.json b/package.json
index d0718345f..7ae8c3399 100644
--- a/package.json
+++ b/package.json
@@ -216,6 +216,7 @@
         "typescript-collections": "^1.3.2",
         "url-loader": "^1.1.2",
         "uuid": "^3.3.2",
+        "webrtc-adapter": "^7.3.0",
         "words-to-numbers": "^1.5.1",
         "xoauth2": "^1.2.0",
         "youtube": "^0.1.0"
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 036cc75a0..1f9b575a4 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -47,6 +47,7 @@ import { ProxyField } from "../../new_fields/Proxy";
 import { DocumentType } from "./DocumentTypes";
 import { LinkFollowBox } from "../views/linking/LinkFollowBox";
 import { DashWebCam } from "../views/webcam/DashWebCam";
+import { DashWebRTC } from "../views/webcam/DashWebRTC";
 //import { PresBox } from "../views/nodes/PresBox";
 //import { PresField } from "../../new_fields/PresField";
 var requestImageSize = require('../util/request-image-size');
@@ -179,7 +180,7 @@ export namespace Docs {
                 layout: { view: LinkFollowBox }
             }],
             [DocumentType.WEBCAM, {
-                layout: { view: DashWebCam }
+                layout: { view: DashWebRTC }
             }]
         ]);
 
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index df5ff04dd..2b797eeca 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -33,6 +33,7 @@ import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { fromPromise } from "mobx-utils";
 import { DashWebCam } from "../../views/webcam/DashWebCam";
+import { DashWebRTC } from "../webcam/DashWebRTC";
 
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
@@ -112,7 +113,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
         return <ObserverJsxParser
             blacklistedAttrs={[]}
-            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, DashWebCam }}
+            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, DashWebCam, DashWebRTC }}
             bindings={this.CreateBindings()}
             jsx={this.finalLayout}
             showWarnings={true}
diff --git a/src/client/views/webcam/DashWebRTC.tsx b/src/client/views/webcam/DashWebRTC.tsx
index c04a6d739..203a9df35 100644
--- a/src/client/views/webcam/DashWebRTC.tsx
+++ b/src/client/views/webcam/DashWebRTC.tsx
@@ -6,6 +6,8 @@ import { observable } from "mobx";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
 import "../../views/nodes/WebBox.scss";
+import adapter from 'webrtc-adapter';
+
 
 
 
@@ -63,7 +65,11 @@ export class DashWebRTC extends React.Component<CollectionFreeFormDocumentViewPr
     render() {
         let content =
             <div className="webcam-cont" style={{ width: "100%", height: "100%" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
-                <video autoPlay playsInline ref={(e) => this.videoEl = e!}></video>
+                <video id="localVideo" autoPlay playsInline ref={(e) => this.videoEl = e!}></video>
+                <video id="remoteVideo" autoPlay playsInline></video>
+                <button id="startButton">Start</button>
+                <button id="callButton">Call</button>
+                <button id="hangupButton">Hang Up</button>
             </div>;
 
         let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
@@ -77,7 +83,6 @@ export class DashWebRTC extends React.Component<CollectionFreeFormDocumentViewPr
                 </div>
                 {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
             </>);
-        );
     }
 
 
-- 
cgit v1.2.3-70-g09d2


From a8f14c501cf676f6a2697b73d7f2a162d4100a9e Mon Sep 17 00:00:00 2001
From: Mohammad Amoush <mohammad_amoush@brown.edu>
Date: Wed, 2 Oct 2019 16:52:57 -0400
Subject: Merge conflicts fixed

---
 src/client/documents/Documents.ts               |  16 +-
 src/client/views/MainView.tsx                   |   4 -
 src/client/views/nodes/DocumentContentsView.tsx |  16 +-
 src/client/views/webcam/DashWebRTC.tsx          | 225 ++++++++++++++++++++++--
 4 files changed, 214 insertions(+), 47 deletions(-)

(limited to 'src/client/views/nodes/DocumentContentsView.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 3482033d5..6bd91c3b9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -20,12 +20,8 @@ import { AggregateFunction } from "../northstar/model/idea/idea";
 import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
 import { IconBox } from "../views/nodes/IconBox";
 import { OmitKeys, JSONUtils } from "../../Utils";
-<<<<<<< HEAD
-import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField, WebCamField } from "../../new_fields/URLField";
-=======
 import { Field, Doc, Opt, DocListCastAsync } from "../../new_fields/Doc";
-import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField";
->>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
+import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField, WebCamField } from "../../new_fields/URLField";
 import { HtmlField } from "../../new_fields/HtmlField";
 import { List } from "../../new_fields/List";
 import { Cast, NumCast } from "../../new_fields/Types";
@@ -49,14 +45,9 @@ import { ComputedField } from "../../new_fields/ScriptField";
 import { ProxyField } from "../../new_fields/Proxy";
 import { DocumentType } from "./DocumentTypes";
 import { LinkFollowBox } from "../views/linking/LinkFollowBox";
-<<<<<<< HEAD
 import { DashWebCam } from "../views/webcam/DashWebCam";
 import { DashWebRTC } from "../views/webcam/DashWebRTC";
-//import { PresBox } from "../views/nodes/PresBox";
-//import { PresField } from "../../new_fields/PresField";
-=======
 import { PresElementBox } from "../views/presentationview/PresElementBox";
->>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
 var requestImageSize = require('../util/request-image-size');
 var path = require('path');
 
@@ -186,15 +177,12 @@ export namespace Docs {
             [DocumentType.LINKFOLLOW, {
                 layout: { view: LinkFollowBox }
             }],
-<<<<<<< HEAD
             [DocumentType.WEBCAM, {
                 layout: { view: DashWebRTC }
-            }]
-=======
+            }],
             [DocumentType.PRESELEMENT, {
                 layout: { view: PresElementBox }
             }],
->>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
         ]);
 
         // All document prototypes are initialized with at least these values
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 22d8171c0..7e2b4fa3a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,9 +1,5 @@
 import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-<<<<<<< HEAD
 import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faVideo } from '@fortawesome/free-solid-svg-icons';
-=======
-import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt } from '@fortawesome/free-solid-svg-icons';
->>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index f1c10f2f2..f1678d4f9 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -30,20 +30,12 @@ import { PresElementBox } from "../presentationview/PresElementBox";
 import { VideoBox } from "./VideoBox";
 import { WebBox } from "./WebBox";
 import React = require("react");
-<<<<<<< HEAD
-import { FieldViewProps } from "./FieldView";
-import { Without, OmitKeys } from "../../../Utils";
-import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
+import { StrCast, NumCast } from "../../../new_fields/Types";
 import { List } from "../../../new_fields/List";
-import { Doc } from "../../../new_fields/Doc";
-import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
-import { ScriptField } from "../../../new_fields/ScriptField";
 import { fromPromise } from "mobx-utils";
 import { DashWebCam } from "../../views/webcam/DashWebCam";
 import { DashWebRTC } from "../webcam/DashWebRTC";
 
-=======
->>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -115,11 +107,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         if (!this.layout && this.props.layoutKey !== "overlayLayout") return (null);
         return <ObserverJsxParser
             blacklistedAttrs={[]}
-<<<<<<< HEAD
-            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, DashWebCam, DashWebRTC }}
-=======
-            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox }}
->>>>>>> 69e4a936c4eb0cc2e35e4e7f3258aed1f72b8da7
+            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, DashWebCam, DashWebRTC }}
             bindings={this.CreateBindings()}
             jsx={this.finalLayout}
             showWarnings={true}
diff --git a/src/client/views/webcam/DashWebRTC.tsx b/src/client/views/webcam/DashWebRTC.tsx
index 9c289b40f..64ddb318f 100644
--- a/src/client/views/webcam/DashWebRTC.tsx
+++ b/src/client/views/webcam/DashWebRTC.tsx
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
 import React = require("react");
 import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView";
 import { FieldViewProps, FieldView } from "../nodes/FieldView";
-import { observable, trace } from "mobx";
+import { observable } from "mobx";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
 import "../../views/nodes/WebBox.scss";
@@ -11,7 +11,7 @@ import adapter from 'webrtc-adapter';
 
 
 
-const mediaStreamConstaints = {
+const mediaStreamConstraints = {
     video: true,
 };
 
@@ -26,7 +26,7 @@ export class DashWebRTC extends React.Component<CollectionFreeFormDocumentViewPr
     @observable private localVideoEl: HTMLVideoElement | undefined;
     @observable private peerVideoEl: HTMLVideoElement | undefined;
     @observable private localStream: MediaStream | undefined;
-    @observable private startTime = null;
+    @observable private startTime: any = null;
     @observable private remoteStream: MediaStream | undefined;
     @observable private localPeerConnection: any;
     @observable private remotePeerConnection: any;
@@ -38,7 +38,7 @@ export class DashWebRTC extends React.Component<CollectionFreeFormDocumentViewPr
     componentDidMount() {
         this.callButton!.disabled = true;
         this.hangupButton!.disabled = true;
-        navigator.mediaDevices.getUserMedia(mediaStreamConstaints).then(this.gotLocalMediaStream).catch(this.handleLocalMediaStreamError);
+        navigator.mediaDevices.getUserMedia(mediaStreamConstraints).then(this.gotLocalMediaStream).catch(this.handleLocalMediaStreamError);
         this.localVideoEl!.addEventListener('loadedmetadata', this.logVideoLoaded);
         this.peerVideoEl!.addEventListener('loadedmetadata', this.logVideoLoaded);
         this.peerVideoEl!.addEventListener('onresize', this.logResizedVideo);
@@ -50,7 +50,7 @@ export class DashWebRTC extends React.Component<CollectionFreeFormDocumentViewPr
         if (this.localVideoEl) {
             this.localVideoEl.srcObject = mediaStream;
         }
-        trace('Received local stream.');
+        this.trace('Received local stream.');
         this.callButton!.disabled = false;
 
     }
@@ -64,27 +64,222 @@ export class DashWebRTC extends React.Component<CollectionFreeFormDocumentViewPr
 
     handleLocalMediaStreamError = (error: string) => {
         //console.log("navigator.getUserMedia error: ", error);
-        trace(`navigator.getUserMedia error: ${error.toString()}.`);
+        this.trace(`navigator.getUserMedia error: ${error.toString()}.`);
 
     }
 
-    logVideoLoaded(event: any) {
+    logVideoLoaded = (event: any) => {
         let video = event.target;
-        trace(`${video!.id} videoWidth: ${video!.videoWidth}px, ` +
-            `videoHeight: ${video!.videoHeight}px.`);
+        this.trace(`${video.id} videoWidth: ${video.videoWidth}px, ` +
+            `videoHeight: ${video.videoHeight}px.`);
     }
 
-    logResizedVideo(event: any) {
+    logResizedVideo = (event: any) => {
         this.logVideoLoaded(event);
 
         if (this.startTime) {
-            let elapsedTime = window.performance.now() - this.startTime!;
+            let elapsedTime = window.performance.now() - this.startTime;
             this.startTime = null;
-            trace(`Setup time: ${elapsedTime.toFixed(3)}ms.`);
+            this.trace(`Setup time: ${elapsedTime.toFixed(3)}ms.`);
         }
 
     }
 
+    handleConnection = (event: any) => {
+        let peerConnection = event.target;
+        let iceCandidate = event.candidate;
+
+        if (iceCandidate) {
+            let newIceCandidate: RTCIceCandidate = new RTCIceCandidate(iceCandidate);
+            let otherPeer: any = this.getOtherPeer(peerConnection);
+
+            otherPeer.addIceCandidate(newIceCandidate).then(() => {
+                this.handleConnectionSuccess(peerConnection);
+            }).catch((error: any) => {
+                this.handleConnectionFailure(peerConnection, error);
+            });
+
+            this.trace(`${this.getPeerName(peerConnection)} ICE candidate:\n` +
+                `${event.candidate.candidate}.`);
+
+        }
+    }
+
+    // Logs that the connection succeeded.
+    handleConnectionSuccess = (peerConnection: any) => {
+        this.trace(`${this.getPeerName(peerConnection)} addIceCandidate success.`);
+    }
+
+    handleConnectionFailure = (peerConnection: any, error: any) => {
+        this.trace(`${this.getPeerName(peerConnection)} failed to add ICE Candidate:\n` +
+            `${error.toString()}.`);
+    }
+
+    // Logs changes to the connection state.
+    handleConnectionChange = (event: any) => {
+        let peerConnection = event.target;
+        console.log('ICE state change event: ', event);
+        this.trace(`${this.getPeerName(peerConnection)} ICE state: ` +
+            `${peerConnection.iceConnectionState}.`);
+    }
+
+    // Logs error when setting session description fails.
+    setSessionDescriptionError = (error: any) => {
+        this.trace(`Failed to create session description: ${error.toString()}.`);
+    }
+
+    // Logs success when setting session description.
+    setDescriptionSuccess = (peerConnection: any, functionName: any) => {
+        let peerName = this.getPeerName(peerConnection);
+        this.trace(`${peerName} ${functionName} complete.`);
+    }
+
+
+    // Logs success when localDescription is set.
+    setLocalDescriptionSuccess = (peerConnection: any) => {
+        this.setDescriptionSuccess(peerConnection, 'setLocalDescription');
+    }
+
+    // Logs success when remoteDescription is set.
+    setRemoteDescriptionSuccess = (peerConnection: any) => {
+        this.setDescriptionSuccess(peerConnection, 'setRemoteDescription');
+    }
+
+    createdOffer = (description: any) => {
+        this.trace(`Offer from localPeerConnection:\n${description.sdp}`);
+        this.trace('localPeerConnection setLocalDescription start.');
+
+        this.localPeerConnection.setLocalDescription(description).then(() => {
+            this.setLocalDescriptionSuccess(this.localPeerConnection);
+        }).catch(this.setSessionDescriptionError);
+
+
+        this.trace('remotePeerConnection setRemoteDescription start.');
+        this.remotePeerConnection.setRemoteDescription(description)
+            .then(() => {
+                this.setRemoteDescriptionSuccess(this.remotePeerConnection);
+            }).catch(this.setSessionDescriptionError);
+
+        this.trace('remotePeerConnection createAnswer start.');
+        this.remotePeerConnection.createAnswer()
+            .then(this.createdAnswer)
+            .catch(this.setSessionDescriptionError);
+
+    }
+
+    createdAnswer = (description: any) => {
+        this.trace(`Answer from remotePeerConnection:\n${description.sdp}.`);
+
+        this.trace('remotePeerConnection setLocalDescription start.');
+        this.remotePeerConnection.setLocalDescription(description)
+            .then(() => {
+                this.setLocalDescriptionSuccess(this.remotePeerConnection);
+            }).catch(this.setSessionDescriptionError);
+
+        this.trace('localPeerConnection setRemoteDescription start.');
+        this.localPeerConnection.setRemoteDescription(description)
+            .then(() => {
+                this.setRemoteDescriptionSuccess(this.localPeerConnection);
+            }).catch(this.setSessionDescriptionError);
+    }
+
+
+    startAction = () => {
+        this.startButton!.disabled = true;
+        navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
+            .then(this.gotLocalMediaStream).catch(this.handleLocalMediaStreamError);
+        this.trace('Requesting local stream.');
+    }
+
+
+    // Handles call button action: creates peer connection.
+    callAction = () => {
+        this.callButton!.disabled = true;
+        this.hangupButton!.disabled = false;
+
+        this.trace('Starting call.');
+        this.startTime = window.performance.now();
+
+        // Get local media stream tracks.
+        const videoTracks = this.localStream!.getVideoTracks();
+        const audioTracks = this.localStream!.getAudioTracks();
+        if (videoTracks.length > 0) {
+            this.trace(`Using video device: ${videoTracks[0].label}.`);
+        }
+        if (audioTracks.length > 0) {
+            this.trace(`Using audio device: ${audioTracks[0].label}.`);
+        }
+
+        let servers: RTCConfiguration | undefined = undefined;  // Allows for RTC server configuration.
+
+        // Create peer connections and add behavior.
+        this.localPeerConnection = new RTCPeerConnection(servers);
+        this.trace('Created local peer connection object localPeerConnection.');
+
+        this.localPeerConnection.addEventListener('icecandidate', this.handleConnection);
+        this.localPeerConnection.addEventListener(
+            'iceconnectionstatechange', this.handleConnectionChange);
+
+        this.remotePeerConnection = new RTCPeerConnection(servers);
+        this.trace('Created remote peer connection object remotePeerConnection.');
+
+        this.remotePeerConnection.addEventListener('icecandidate', this.handleConnection);
+        this.remotePeerConnection.addEventListener(
+            'iceconnectionstatechange', this.handleConnectionChange);
+        this.remotePeerConnection.addEventListener('addstream', this.gotRemoteMediaStream);
+
+        // Add local stream to connection and create offer to connect.
+        this.localPeerConnection.addStream(this.localStream);
+        this.trace('Added local stream to localPeerConnection.');
+
+        this.trace('localPeerConnection createOffer start.');
+        this.localPeerConnection.createOffer(offerOptions)
+            .then(this.createdOffer).catch(this.setSessionDescriptionError);
+    }
+
+
+    // Handles hangup action: ends up call, closes connections and resets peers.
+    hangupAction = () => {
+        this.localPeerConnection.close();
+        this.remotePeerConnection.close();
+        this.localPeerConnection = null;
+        this.remotePeerConnection = null;
+        this.hangupButton!.disabled = true;
+        this.callButton!.disabled = false;
+        this.trace('Ending call.');
+    }
+
+    // Gets the "other" peer connection.
+    getOtherPeer = (peerConnection: any) => {
+        return (peerConnection === this.localPeerConnection) ?
+            this.remotePeerConnection : this.localPeerConnection;
+    }
+
+    // Gets the name of a certain peer connection.
+    getPeerName = (peerConnection: any) => {
+        return (peerConnection === this.localPeerConnection) ?
+            'localPeerConnection' : 'remotePeerConnection';
+    }
+
+    // Logs an action (text) and the time when it happened on the console.
+    trace = (text: string) => {
+        text = text.trim();
+        const now = (window.performance.now() / 1000).toFixed(3);
+
+        console.log(now, text);
+    }
+
+
+
+
+
+
+
+
+
+
+
+
 
 
 
@@ -116,9 +311,9 @@ export class DashWebRTC extends React.Component<CollectionFreeFormDocumentViewPr
             <div className="webcam-cont" style={{ width: "100%", height: "100%" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
                 <video id="localVideo" autoPlay playsInline ref={(e) => this.localVideoEl = e!}></video>
                 <video id="remoteVideo" autoPlay playsInline ref={(e) => this.peerVideoEl = e!}></video>
-                <button id="startButton" ref={(e) => this.startButton = e!}>Start</button>
-                <button id="callButton" ref={(e) => this.callButton = e!}>Call</button>
-                <button id="hangupButton" ref={(e) => this.hangupButton = e!}>Hang Up</button>
+                <button id="startButton" ref={(e) => this.startButton = e!} onClick={this.startAction}>Start</button>
+                <button id="callButton" ref={(e) => this.callButton = e!} onClick={this.callAction}>Call</button>
+                <button id="hangupButton" ref={(e) => this.hangupButton = e!} onClick={this.hangupAction}>Hang Up</button>
             </div>;
 
         let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
-- 
cgit v1.2.3-70-g09d2


From d97091421468238d32edf55c240bfb3b021e9e05 Mon Sep 17 00:00:00 2001
From: Mohammad Amoush <mohammad_amoush@brown.edu>
Date: Tue, 29 Oct 2019 18:52:26 -0400
Subject: couple fixes

---
 src/client/views/nodes/DocumentContentsView.tsx | 4 ++--
 src/client/views/webcam/DashWebRTCVideo.tsx     | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

(limited to 'src/client/views/nodes/DocumentContentsView.tsx')

diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index f1678d4f9..ee58e4152 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -34,7 +34,7 @@ import { StrCast, NumCast } from "../../../new_fields/Types";
 import { List } from "../../../new_fields/List";
 import { fromPromise } from "mobx-utils";
 import { DashWebCam } from "../../views/webcam/DashWebCam";
-import { DashWebRTC } from "../webcam/DashWebRTC";
+import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo";
 
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
@@ -107,7 +107,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         if (!this.layout && this.props.layoutKey !== "overlayLayout") return (null);
         return <ObserverJsxParser
             blacklistedAttrs={[]}
-            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, DashWebCam, DashWebRTC }}
+            components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, DashWebCam, DashWebRTCVideo }}
             bindings={this.CreateBindings()}
             jsx={this.finalLayout}
             showWarnings={true}
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
index db9c922fc..5bb7cef6f 100644
--- a/src/client/views/webcam/DashWebRTCVideo.tsx
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -8,7 +8,7 @@ import { InkingControl } from "../InkingControl";
 import "../../views/nodes/WebBox.scss";
 import "./DashWebRTC.scss";
 import adapter from 'webrtc-adapter';
-import { DashWebRTC } from "./DashWebRTC";
+//import { DashWebRTC } from "./DashWebRTC";
 
 
 
@@ -37,7 +37,7 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV
     private hangupButton: HTMLButtonElement | undefined;
 
     componentDidMount() {
-        DashWebRTC.setVideoObjects(this.localVideoEl!, this.peerVideoEl!);
+        //DashWebRTC.setVideoObjects(this.localVideoEl!, this.peerVideoEl!);
     }
 
 
-- 
cgit v1.2.3-70-g09d2


From 96b2f5f5334fb475180a095905e19e45a0414233 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sun, 9 Feb 2020 17:02:43 -0500
Subject: from added range sliders

---
 package-lock.json                                  |  78 ++++---
 package.json                                       |   1 +
 src/client/documents/DocumentTypes.ts              |   1 +
 src/client/documents/Documents.ts                  |   8 +
 .../views/collections/CollectionTimeView.tsx       |  57 ++++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  15 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   4 +-
 src/client/views/nodes/FormattedTextBox.tsx        |   2 +-
 src/client/views/nodes/KeyValueBox.scss            |   4 +-
 src/client/views/nodes/SliderBox-components.tsx    | 256 +++++++++++++++++++++
 src/client/views/nodes/SliderBox-tooltip.css       |  33 +++
 src/client/views/nodes/SliderBox.scss              |   8 +
 src/client/views/nodes/SliderBox.tsx               | 128 +++++++++++
 src/new_fields/Doc.ts                              |  22 +-
 14 files changed, 564 insertions(+), 53 deletions(-)
 create mode 100644 src/client/views/nodes/SliderBox-components.tsx
 create mode 100644 src/client/views/nodes/SliderBox-tooltip.css
 create mode 100644 src/client/views/nodes/SliderBox.scss
 create mode 100644 src/client/views/nodes/SliderBox.tsx

(limited to 'src/client/views/nodes/DocumentContentsView.tsx')

diff --git a/package-lock.json b/package-lock.json
index 379cd3337..d1c169bed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -647,7 +647,7 @@
         },
         "@types/passport": {
             "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.2.tgz",
+            "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.0.tgz",
             "integrity": "sha512-Pf39AYKf8q+YoONym3150cEwfUD66dtwHJWvbeOzKxnA0GZZ/vAXhNWv9vMhKyRQBQZiQyWQnhYBEBlKW6G8wg==",
             "requires": {
                 "@types/express": "*"
@@ -3776,6 +3776,11 @@
                 "type": "^1.0.1"
             }
         },
+        "d3-array": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
+            "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
+        },
         "d3-format": {
             "version": "1.4.3",
             "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.3.tgz",
@@ -5391,8 +5396,7 @@
                 },
                 "ansi-regex": {
                     "version": "2.1.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "aproba": {
                     "version": "1.2.0",
@@ -5410,13 +5414,11 @@
                 },
                 "balanced-match": {
                     "version": "1.0.0",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "brace-expansion": {
                     "version": "1.1.11",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "balanced-match": "^1.0.0",
                         "concat-map": "0.0.1"
@@ -5429,18 +5431,15 @@
                 },
                 "code-point-at": {
                     "version": "1.1.0",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "concat-map": {
                     "version": "0.0.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "console-control-strings": {
                     "version": "1.1.0",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "core-util-is": {
                     "version": "1.0.2",
@@ -5543,8 +5542,7 @@
                 },
                 "inherits": {
                     "version": "2.0.4",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "ini": {
                     "version": "1.3.5",
@@ -5554,7 +5552,6 @@
                 "is-fullwidth-code-point": {
                     "version": "1.0.0",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "number-is-nan": "^1.0.0"
                     }
@@ -5567,20 +5564,17 @@
                 "minimatch": {
                     "version": "3.0.4",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "brace-expansion": "^1.1.7"
                     }
                 },
                 "minimist": {
                     "version": "0.0.8",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "minipass": {
                     "version": "2.9.0",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "safe-buffer": "^5.1.2",
                         "yallist": "^3.0.0"
@@ -5597,7 +5591,6 @@
                 "mkdirp": {
                     "version": "0.5.1",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "minimist": "0.0.8"
                     }
@@ -5678,8 +5671,7 @@
                 },
                 "number-is-nan": {
                     "version": "1.0.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "object-assign": {
                     "version": "4.1.1",
@@ -5689,7 +5681,6 @@
                 "once": {
                     "version": "1.4.0",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "wrappy": "1"
                     }
@@ -5765,8 +5756,7 @@
                 },
                 "safe-buffer": {
                     "version": "5.1.2",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "safer-buffer": {
                     "version": "2.1.2",
@@ -5796,7 +5786,6 @@
                 "string-width": {
                     "version": "1.0.2",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "code-point-at": "^1.0.0",
                         "is-fullwidth-code-point": "^1.0.0",
@@ -5814,7 +5803,6 @@
                 "strip-ansi": {
                     "version": "3.0.1",
                     "bundled": true,
-                    "optional": true,
                     "requires": {
                         "ansi-regex": "^2.0.0"
                     }
@@ -5853,13 +5841,11 @@
                 },
                 "wrappy": {
                     "version": "1.0.2",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 },
                 "yallist": {
                     "version": "3.1.1",
-                    "bundled": true,
-                    "optional": true
+                    "bundled": true
                 }
             }
         },
@@ -13518,6 +13504,27 @@
                 "tinycolor2": "^1.4.1"
             }
         },
+        "react-compound-slider": {
+            "version": "2.5.0",
+            "resolved": "https://registry.npmjs.org/react-compound-slider/-/react-compound-slider-2.5.0.tgz",
+            "integrity": "sha512-T84FtSI0bkQPmH5GaaHbL+2McOyIR6M5sqS80dqw/bHc5r2UKLYY64BWTbsL+XO0jlx7REuJJnZUBqo4eSRl7g==",
+            "requires": {
+                "@babel/runtime": "^7.7.7",
+                "d3-array": "^1.2.4",
+                "prop-types": "^15.7.2",
+                "warning": "^3.0.0"
+            },
+            "dependencies": {
+                "@babel/runtime": {
+                    "version": "7.8.4",
+                    "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz",
+                    "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==",
+                    "requires": {
+                        "regenerator-runtime": "^0.13.2"
+                    }
+                }
+            }
+        },
         "react-dimensions": {
             "version": "1.3.1",
             "resolved": "https://registry.npmjs.org/react-dimensions/-/react-dimensions-1.3.1.tgz",
@@ -13804,7 +13811,7 @@
         },
         "readable-stream": {
             "version": "2.3.6",
-            "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
             "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
             "requires": {
                 "core-util-is": "~1.0.0",
@@ -14244,7 +14251,8 @@
                 "fsevents": {
                     "version": "2.1.2",
                     "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
-                    "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA=="
+                    "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
+                    "optional": true
                 },
                 "get-caller-file": {
                     "version": "2.0.5",
@@ -15722,7 +15730,7 @@
         },
         "strip-ansi": {
             "version": "3.0.1",
-            "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
             "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
             "requires": {
                 "ansi-regex": "^2.0.0"
@@ -17949,7 +17957,7 @@
         },
         "wrap-ansi": {
             "version": "2.1.0",
-            "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
             "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
             "requires": {
                 "string-width": "^1.0.1",
diff --git a/package.json b/package.json
index 3b87a9a47..c71d321f4 100644
--- a/package.json
+++ b/package.json
@@ -205,6 +205,7 @@
         "react-bootstrap": "^1.0.0-beta.16",
         "react-bootstrap-dropdown-menu": "^1.1.15",
         "react-color": "^2.18.0",
+        "react-compound-slider": "^2.5.0",
         "react-dimensions": "^1.3.1",
         "react-dom": "^16.12.0",
         "react-golden-layout": "^1.0.6",
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 8f96b2fa6..6ef23ef08 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -14,6 +14,7 @@ export enum DocumentType {
     LINK = "link",
     LINKDOC = "linkdoc",
     BUTTON = "button",
+    SLIDER = "slider",
     TEMPLATE = "template",
     EXTENSION = "extension",
     YOUTUBE = "youtube",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d647b34e6..3d43c6eb2 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -37,6 +37,7 @@ import { DocumentManager } from "../util/DocumentManager";
 import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
 import { Scripting } from "../util/Scripting";
 import { ButtonBox } from "../views/nodes/ButtonBox";
+import { SliderBox } from "../views/nodes/SliderBox";
 import { FontIconBox } from "../views/nodes/FontIconBox";
 import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
 import { PresBox } from "../views/nodes/PresBox";
@@ -230,6 +231,9 @@ export namespace Docs {
             [DocumentType.BUTTON, {
                 layout: { view: ButtonBox, dataField: data },
             }],
+            [DocumentType.SLIDER, {
+                layout: { view: SliderBox, dataField: data },
+            }],
             [DocumentType.PRES, {
                 layout: { view: PresBox, dataField: data },
                 options: {}
@@ -559,6 +563,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}) });
         }
 
+        export function SliderDocument(options?: DocumentOptions) {
+            return InstanceFromProto(Prototypes.get(DocumentType.SLIDER), undefined, { ...(options || {}) });
+        }
+
 
         export function FontIconDocument(options?: DocumentOptions) {
             return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) });
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 4983acbc2..2a45c91dd 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -2,7 +2,6 @@ import { faEdit } from "@fortawesome/free-solid-svg-icons";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, trace, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Set } from "typescript-collections";
 import { Doc, DocListCast, Field } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { RichTextField } from "../../../new_fields/RichTextField";
@@ -54,7 +53,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
         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();
+        return Array.from(facets);
     }
 
     /**
@@ -73,12 +72,52 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
                         docFilter.splice(index, 3);
                     }
                 }
+                const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"));
+                if (docRangeFilters) {
+                    let index: number;
+                    while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) {
+                        docRangeFilters.splice(index, 3);
+                    }
+                }
             } else {
-                const newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true });
-                const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc };
-                const params = { layoutDoc: Doc.name, dataDoc: Doc.name, };
-                newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables);
-                Doc.AddDocToList(facetCollection, "data", newFacet);
+                const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]);
+                const facetValues = Array.from(allCollectionDocs.reduce((set, child) =>
+                    set.add(Field.toString(child[facetHeader] as Field)), new Set<string>()));
+
+                let nonNumbers = 0;
+                let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE;
+                facetValues.map(val => {
+                    const num = Number(val);
+                    if (Number.isNaN(num)) {
+                        nonNumbers++;
+                    } else {
+                        minVal = Math.min(num, minVal);
+                        maxVal = Math.max(num, maxVal);
+                    }
+                });
+                if (nonNumbers / allCollectionDocs.length < .1) {
+                    const newFacet = Docs.Create.SliderDocument({ title: facetHeader });
+                    newFacet.treeViewExpandedView = "layout";
+                    newFacet.treeViewOpen = true;
+                    newFacet._sliderMin = minVal;
+                    newFacet._sliderMax = maxVal;
+                    newFacet._sliderMinThumb = minVal;
+                    newFacet._sliderMaxThumb = maxVal;
+                    newFacet.target = this.props.Document;
+                    const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`;
+                    newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" });
+
+                    // const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc };
+                    // const params = { layoutDoc: Doc.name, dataDoc: Doc.name, };
+                    // newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables);
+                    Doc.AddDocToList(facetCollection, "data", newFacet);
+                } else {
+                    const newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true });
+                    const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc };
+                    const params = { layoutDoc: Doc.name, dataDoc: Doc.name, };
+                    newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables);
+                    Doc.AddDocToList(facetCollection, "data", newFacet);
+                }
             }
         }
     }
@@ -120,7 +159,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
             pair.layout[fieldKey] instanceof RichTextField ||
             typeof (pair.layout[fieldKey]) === "number" ||
             typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey)));
-        keySet.toArray().map(fieldKey =>
+        Array.from(keySet).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" })
         ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
@@ -227,7 +266,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) {
                 </Flyout>
             </div>
             <div className="collectionTimeView-tree" key="tree">
-                <CollectionTreeView {...this.props} Document={facetCollection} />
+                <CollectionTreeView {...this.props} PanelWidth={() => this._facetWidth} Document={facetCollection} />
             </div>
         </div>;
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2518a4a55..799627969 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -828,6 +828,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @computed get filterDocs() {
         const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []);
+        const docRangeFilters = Cast(this.props.Document._docRangeFilters, 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);
@@ -853,7 +854,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             }
             return true;
         }) : this.childDocs;
-        return filteredDocs;
+        const rangeFilteredDocs = docRangeFilters.length ? filteredDocs.filter(d => {
+            for (let i = 0; i < docRangeFilters.length; i += 3) {
+                const key = docRangeFilters[i];
+                const min = Number(docRangeFilters[i + 1]);
+                const max = Number(docRangeFilters[i + 2]);
+                const val = Cast(d[key], "number", null);
+                if (val !== undefined && (val < min || val > max)) {
+                    return false;
+                }
+            }
+            return true;
+        }) : this.childDocs;
+        return rangeFilteredDocs;
     }
     get doLayoutComputation() {
         const { newPool, computedElementData } = this.doInternalLayoutComputation;
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 3b9015994..284600bb4 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,7 +1,6 @@
 import { computed } from "mobx";
 import { observer } from "mobx-react";
 import { Doc } from "../../../new_fields/Doc";
-import { ScriptField } from "../../../new_fields/ScriptField";
 import { Cast, StrCast } from "../../../new_fields/Types";
 import { OmitKeys, Without } from "../../../Utils";
 import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
@@ -14,6 +13,7 @@ import { LinkFollowBox } from "../linking/LinkFollowBox";
 import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
 import { AudioBox } from "./AudioBox";
 import { ButtonBox } from "./ButtonBox";
+import { SliderBox } from "./SliderBox";
 import { DocumentBox } from "./DocumentBox";
 import { DocumentViewProps } from "./DocumentView";
 import "./DocumentView.scss";
@@ -104,7 +104,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
             <ObserverJsxParser
                 blacklistedAttrs={[]}
                 components={{
-                    FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView,
+                    FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, SliderBox, FieldView,
                     CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
                     PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox,
                     ColorBox, DocuLinkBox, InkingStroke, DocumentBox
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 9370d3745..449dca3a1 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -574,7 +574,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
                     if (ret.frag.size > 2 && ret.start >= 0) {
                         let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
                         if (ret.frag.firstChild) {
-                            selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
+                            selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
                         }
                         editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
                         const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 6e8a36c6a..a26880c9e 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -74,7 +74,7 @@ $header-height: 30px;
 
 .keyValueBox-evenRow {
     position: relative;
-    display: inline-block;
+    display: flex;
     width:100%;
     height:$header-height;
     background: $light-color;
@@ -114,7 +114,7 @@ $header-height: 30px;
 
 .keyValueBox-oddRow {
     position: relative;
-    display: inline-block;
+    display: flex;
     width:100%;
     height:30px;
     background: $light-color-secondary;
diff --git a/src/client/views/nodes/SliderBox-components.tsx b/src/client/views/nodes/SliderBox-components.tsx
new file mode 100644
index 000000000..a38cad459
--- /dev/null
+++ b/src/client/views/nodes/SliderBox-components.tsx
@@ -0,0 +1,256 @@
+import * as React from "react";
+import { SliderItem } from "react-compound-slider";
+import "./SliderBox-tooltip.css";
+
+const { Component, Fragment } = React;
+
+// *******************************************************
+// TOOLTIP RAIL
+// *******************************************************
+const railStyle: React.CSSProperties = {
+    position: "absolute",
+    width: "100%",
+    height: 40,
+    top: -13,
+    borderRadius: 7,
+    cursor: "pointer",
+    opacity: 0.3,
+    zIndex: 300,
+    border: "1px solid grey"
+};
+
+const railCenterStyle: React.CSSProperties = {
+    position: "absolute",
+    width: "100%",
+    height: 14,
+    borderRadius: 7,
+    cursor: "pointer",
+    pointerEvents: "none",
+    backgroundColor: "rgb(155,155,155)"
+};
+
+interface TooltipRailProps {
+    activeHandleID: string;
+    getRailProps: (props: object) => object;
+    getEventData: (e: Event) => object;
+}
+
+export class TooltipRail extends Component<TooltipRailProps> {
+    state = {
+        value: null,
+        percent: null
+    };
+
+    static defaultProps = {
+        disabled: false
+    };
+
+    onMouseEnter = () => {
+        document.addEventListener("mousemove", this.onMouseMove);
+    };
+
+    onMouseLeave = () => {
+        this.setState({ value: null, percent: null });
+        document.removeEventListener("mousemove", this.onMouseMove);
+    };
+
+    onMouseMove = (e: Event) => {
+        const { activeHandleID, getEventData } = this.props;
+
+        if (activeHandleID) {
+            this.setState({ value: null, percent: null });
+        } else {
+            this.setState(getEventData(e));
+        }
+    };
+
+    render() {
+        const { value, percent } = this.state;
+        const { activeHandleID, getRailProps } = this.props;
+
+        return (
+            <Fragment>
+                {!activeHandleID && value ? (
+                    <div
+                        style={{
+                            left: `${percent}%`,
+                            position: "absolute",
+                            marginLeft: "-11px",
+                            marginTop: "-35px"
+                        }}
+                    >
+                        <div className="tooltip">
+                            <span className="tooltiptext">Value: {value}</span>
+                        </div>
+                    </div>
+                ) : null}
+                <div
+                    style={railStyle}
+                    {...getRailProps({
+                        onMouseEnter: this.onMouseEnter,
+                        onMouseLeave: this.onMouseLeave
+                    })}
+                />
+                <div style={railCenterStyle} />
+            </Fragment>
+        );
+    }
+}
+
+// *******************************************************
+// HANDLE COMPONENT
+// *******************************************************
+interface HandleProps {
+    key: string;
+    handle: SliderItem;
+    isActive: Boolean;
+    disabled?: Boolean;
+    domain: number[];
+    getHandleProps: (id: string, config: object) => object;
+}
+
+export class Handle extends Component<HandleProps> {
+    static defaultProps = {
+        disabled: false
+    };
+
+    state = {
+        mouseOver: false
+    };
+
+    onMouseEnter = () => {
+        this.setState({ mouseOver: true });
+    };
+
+    onMouseLeave = () => {
+        this.setState({ mouseOver: false });
+    };
+
+    render() {
+        const {
+            domain: [min, max],
+            handle: { id, value, percent },
+            isActive,
+            disabled,
+            getHandleProps
+        } = this.props;
+        const { mouseOver } = this.state;
+
+        return (
+            <Fragment>
+                {(mouseOver || isActive) && !disabled ? (
+                    <div
+                        style={{
+                            left: `${percent}%`,
+                            position: "absolute",
+                            marginLeft: "-11px",
+                            marginTop: "-35px"
+                        }}
+                    >
+                        <div className="tooltip">
+                            <span className="tooltiptext">Value: {value}</span>
+                        </div>
+                    </div>
+                ) : null}
+                <div
+                    role="slider"
+                    aria-valuemin={min}
+                    aria-valuemax={max}
+                    aria-valuenow={value}
+                    style={{
+                        left: `${percent}%`,
+                        position: "absolute",
+                        marginLeft: "-11px",
+                        marginTop: "-6px",
+                        zIndex: 400,
+                        width: 24,
+                        height: 24,
+                        cursor: "pointer",
+                        border: 0,
+                        borderRadius: "50%",
+                        boxShadow: "1px 1px 1px 1px rgba(0, 0, 0, 0.4)",
+                        backgroundColor: disabled ? "#666" : "#3e1db3"
+                    }}
+                    {...getHandleProps(id, {
+                        onMouseEnter: this.onMouseEnter,
+                        onMouseLeave: this.onMouseLeave
+                    })}
+                />
+            </Fragment>
+        );
+    }
+}
+
+// *******************************************************
+// TRACK COMPONENT
+// *******************************************************
+interface TrackProps {
+    source: SliderItem;
+    target: SliderItem;
+    disabled: Boolean;
+    getTrackProps: () => object;
+}
+
+export function Track({
+    source,
+    target,
+    getTrackProps,
+    disabled = false
+}: TrackProps) {
+    return (
+        <div
+            style={{
+                position: "absolute",
+                height: 14,
+                zIndex: 1,
+                backgroundColor: disabled ? "#999" : "#3e1db3",
+                borderRadius: 7,
+                cursor: "pointer",
+                left: `${source.percent}%`,
+                width: `${target.percent - source.percent}%`
+            }}
+            {...getTrackProps()}
+        />
+    );
+}
+
+// *******************************************************
+// TICK COMPONENT
+// *******************************************************
+interface TickProps {
+    tick: SliderItem;
+    count: number;
+    format: (val: number) => string;
+}
+
+const defaultFormat = (d: number) => `d`;
+
+export function Tick({ tick, count, format = defaultFormat }: TickProps) {
+    return (
+        <div>
+            <div
+                style={{
+                    position: "absolute",
+                    marginTop: 17,
+                    width: 1,
+                    height: 5,
+                    backgroundColor: "rgb(200,200,200)",
+                    left: `${tick.percent}%`
+                }}
+            />
+            <div
+                style={{
+                    position: "absolute",
+                    marginTop: 25,
+                    fontSize: 10,
+                    textAlign: "center",
+                    marginLeft: `${-(100 / count) / 2}%`,
+                    width: `${100 / count}%`,
+                    left: `${tick.percent}%`
+                }}
+            >
+                {format(tick.value)}
+            </div>
+        </div>
+    );
+}
diff --git a/src/client/views/nodes/SliderBox-tooltip.css b/src/client/views/nodes/SliderBox-tooltip.css
new file mode 100644
index 000000000..8afde8eb5
--- /dev/null
+++ b/src/client/views/nodes/SliderBox-tooltip.css
@@ -0,0 +1,33 @@
+.tooltip {
+    position: relative;
+    display: inline-block;
+    border-bottom: 1px dotted #222;
+    margin-left: 22px;
+  }
+  
+  .tooltip .tooltiptext {
+    width: 100px;
+    background-color: #222;
+    color: #fff;
+    opacity: 0.8;
+    text-align: center;
+    border-radius: 6px;
+    padding: 5px 0;
+    position: absolute;
+    z-index: 1;
+    bottom: 150%;
+    left: 50%;
+    margin-left: -60px;
+  }
+  
+  .tooltip .tooltiptext::after {
+    content: "";
+    position: absolute;
+    top: 100%;
+    left: 50%;
+    margin-left: -5px;
+    border-width: 5px;
+    border-style: solid;
+    border-color: #222 transparent transparent transparent;
+  }
+  
\ No newline at end of file
diff --git a/src/client/views/nodes/SliderBox.scss b/src/client/views/nodes/SliderBox.scss
new file mode 100644
index 000000000..4ef277d8c
--- /dev/null
+++ b/src/client/views/nodes/SliderBox.scss
@@ -0,0 +1,8 @@
+.sliderBox-outerDiv {
+    width: 100%;
+    height: 100%;
+    pointer-events: all;
+    border-radius: inherit;
+    display: flex;
+    flex-direction: column;
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx
new file mode 100644
index 000000000..00d3baf7c
--- /dev/null
+++ b/src/client/views/nodes/SliderBox.tsx
@@ -0,0 +1,128 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit } from '@fortawesome/free-regular-svg-icons';
+import { computed } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Handles, Rail, Slider, Tracks, Ticks } from 'react-compound-slider';
+import { Doc } from '../../../new_fields/Doc';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
+import { ScriptField } from '../../../new_fields/ScriptField';
+import { BoolCast, FieldValue, StrCast, NumCast, Cast } from '../../../new_fields/Types';
+import { DragManager } from '../../util/DragManager';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocComponent } from '../DocComponent';
+import './SliderBox.scss';
+import { Handle, TooltipRail, Track, Tick } from './SliderBox-components';
+import { FieldView, FieldViewProps } from './FieldView';
+
+
+library.add(faEdit as any);
+
+const ButtonSchema = createSchema({
+    onClick: ScriptField,
+    buttonParams: listSpec("string"),
+    text: "string"
+});
+
+type SliderDocument = makeInterface<[typeof ButtonSchema, typeof documentSchema]>;
+const SliderDocument = makeInterface(ButtonSchema, documentSchema);
+
+@observer
+export class SliderBox extends DocComponent<FieldViewProps, SliderDocument>(SliderDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SliderBox, fieldKey); }
+    private dropDisposer?: DragManager.DragDropDisposer;
+
+    @computed get dataDoc() {
+        return this.props.DataDoc &&
+            (this.Document.isTemplateForField || BoolCast(this.props.DataDoc.isTemplateForField) ||
+                this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document);
+    }
+
+    specificContextMenu = (e: React.MouseEvent): void => {
+        const funcs: ContextMenuProps[] = [];
+        funcs.push({
+            description: "Clear Script Params", event: () => {
+                const params = FieldValue(this.Document.buttonParams);
+                params && params.map(p => this.props.Document[p] = undefined);
+            }, icon: "trash"
+        });
+
+        ContextMenu.Instance.addItem({ description: "OnClick...", subitems: funcs, icon: "asterisk" });
+    }
+    onChange = (values: readonly number[]) => {
+        Cast(this.props.Document.onThumbChanged, ScriptField, null)?.script.run({ range: values, this: this.props.Document })
+    }
+
+    render() {
+        const domain = [NumCast(this.props.Document._sliderMin), NumCast(this.props.Document._sliderMax)]
+        const defaultValues = [NumCast(this.props.Document._sliderMinThumb), NumCast(this.props.Document._sliderMaxThumb)];
+        return (
+            <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()}
+                style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
+                <div className="sliderBox-mainButton" style={{
+                    background: this.Document.backgroundColor, color: this.Document.color || "black",
+                    fontSize: this.Document.fontSize, letterSpacing: this.Document.letterSpacing || ""
+                }} >
+                    <Slider
+                        mode={2}
+                        step={1}
+                        domain={domain}
+                        rootStyle={{ position: "relative", width: "100%" }}
+                        // onUpdate={this.onUpdate}
+                        onChange={this.onChange}
+                        values={defaultValues}
+                    >
+
+                        <Rail>{railProps => <TooltipRail {...railProps} />}</Rail>
+                        <Handles>
+                            {({ handles, activeHandleID, getHandleProps }) => (
+                                <div className="slider-handles">
+                                    {handles.map(handle => (
+                                        <Handle
+                                            key={handle.id}
+                                            handle={handle}
+                                            domain={domain}
+                                            isActive={handle.id === activeHandleID}
+                                            getHandleProps={getHandleProps}
+                                        />
+                                    ))}
+                                </div>
+                            )}
+                        </Handles>
+                        <Tracks left={false} right={false}>
+                            {({ tracks, getTrackProps }) => (
+                                <div className="slider-tracks">
+                                    {tracks.map(({ id, source, target }) => (
+                                        <Track
+                                            key={id}
+                                            source={source}
+                                            target={target}
+                                            disabled={false}
+                                            getTrackProps={getTrackProps}
+                                        />
+                                    ))}
+                                </div>
+                            )}
+                        </Tracks>
+                        <Ticks count={5}>
+                            {({ ticks }) => (
+                                <div className="slider-tracks">
+                                    {ticks.map((tick) => (
+                                        <Tick
+                                            key={tick.id}
+                                            tick={tick}
+                                            count={ticks.length}
+                                            format={(val: number) => val.toString()}
+                                        />
+                                    ))}
+                                </div>
+                            )}
+                        </Ticks>
+                    </Slider>
+                </div>
+            </div>
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index b1c1fda05..3ccf4c49b 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -804,7 +804,22 @@ export namespace Doc {
         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) {
+    export function setDocFilterRange(container: Doc, key: string, range?: number[]) {
+        const docFilters = Cast(container._docRangeFilters, listSpec("string"), []);
+        for (let i = 0; i < docFilters.length; i += 3) {
+            if (docFilters[i] === key) {
+                docFilters.splice(i, 3);
+                break;
+            }
+        }
+        if (range !== undefined) {
+            docFilters.push(key);
+            docFilters.push(range[0].toString());
+            docFilters.push(range[1].toString());
+            container._docRangeFilters = new List<string>(docFilters);
+        }
+    }
+    export function setDocFilter(container: Doc, key: string, value: any, modifiers?: string | number) {
         const docFilters = Cast(container._docFilter, listSpec("string"), []);
         for (let i = 0; i < docFilters.length; i += 3) {
             if (docFilters[i] === key && docFilters[i + 1] === value) {
@@ -812,7 +827,7 @@ export namespace Doc {
                 break;
             }
         }
-        if (modifiers !== undefined) {
+        if (typeof modifiers === "string") {
             docFilters.push(key);
             docFilters.push(value);
             docFilters.push(modifiers);
@@ -843,4 +858,5 @@ Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: bo
     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) { Doc.setDocFilter(container, key, value, modifiers); });
\ No newline at end of file
+Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers?: string) { Doc.setDocFilter(container, key, value, modifiers); });
+Scripting.addGlobal(function setDocFilterRange(container: Doc, key: string, min: number, max: number) { Doc.setDocFilterRange(container, key, min, max); });
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2

